LetsNote(CF-S10) + Ubuntu環境で無線LAN接続できるようにする

背景

  • 2010年頃発売のLetsNote(CF-S10)を使っている

    • 無線LANアダプタ(Intel(R) Centrino(R) Advanced-N + WiMAX 6250 AGN)が内蔵されていて、前面にON/OFFを切り替えるスイッチがある
    • さすがに15年前の製品のためか、Windows10を使うのが体感的に厳しくなってきたので、Linuxに入れ替えることにした
  • それに加えて、USB接続の無線LANアダプタ(AC600 wireless Realtek RTL8811AU)も併用している

    • Windowsをバージョンアップ(たしか7→10)したあたりで、本体内蔵の無線LANアダプタが使えなくなったため
  • Linux(Ubuntu)をインストールして、USB接続の無線LANアダプタをインストール

現象

NetworkManager上はアダプタを認識しているにもかかわらず、WiFiをオンにできない

回避策

BIOS設定にて、ノートPC搭載の無線LANを無効化する。
無効化すると、USB接続の無線LANアダプタ側のみが認識され、WiFiに接続できるようになる(WiFiをオンにできるようになる)

dmesgログを見ると、本体搭載の無線LANアダプタに起因して、RF_KILLでWiFiがブロックされてしまっているように見える。
(詳しいところはよくわからない)

~$ dmesg | grep wifi -i
...
[    5.745448] iwlwifi 0000:03:00.0: Detected Intel(R) Centrino(R) Advanced-N + WiMAX 6250 AGN, REV=0x84
[    5.751227] iwlwifi 0000:03:00.0: reporting RF_KILL (radio disabled)
[    5.751252] iwlwifi 0000:03:00.0: RF_KILL bit toggled to disable radio.

BIOS設定により、内蔵無線LANを無効化することにより、Linux側にデバイスとして認識されなくなるため、上記のブロックが発生しなくなる。 結果、USB接続の無線LANアダプタの方でWiFi接続できるようになる(という理解)

GUIアプリケーションから、コンソールアプリケーションを起動し、stdin/stdoutでやり取りする

背景

GUIアプリケーションから、(コンソールウインドウを作成することなく)コンソールアプリケーションを起動したい
親プロセスと子プロセス間のやり取りをstdin/stdoutを通じて行いたい

解決方法

名前なしパイプを作成する(CreatePipe)
疑似コンソールを通じて名前なしパイプを子プロセスに渡す(CreatePseudoConsole)
疑似コンソールを渡して子プロセスを作成する(CreateProcess)

あとは、名前なしパイプを通じて子プロセスのstdinにデータを送ったり、stdoutからデータを読み出すことができる。

詳細

#include <windows.h>
#include <vector>

int sample()
{
    // 子プロセスとやり取りする用の名前なしパイプを作成する
    HANDLE hLocalReader = nullptr, hRemoteWriter = nullptr;
    CreatePipe(&hLocalReader, &hRemoteWriter, nullptr, 0);
    HANDLE hRemoteReader = nullptr, hLocalWriter = nullptr;
    CreatePipe(&hRemoteReader, &hLocalWriter, nullptr, 0);

    // 疑似コンソールを作成する
    COORD size = { 80, 25 };
    HPCON hPC = nullptr;
    CreatePseudoConsole(size, hRemoteReader, hRemoteWriter, 0, &hPC);

    // 子プロセスを作成するための準備
    STARTUPINFOEX siex = {};
    STARTUPINFO& si = siex.StartupInfo;
    si.cb = sizeof(siex);

    // lpAttributeListのサイズを得る
    SIZE_T attrSize;
    InitializeProcThreadAttributeList(nullptr, 1, 0, &attrSize);

    // lpAttributeListの領域を割り当てる
    std::vector<uint8_t> buf(attrSize);
    siex.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)buf.data();      // お手軽にvectorを使っているが、お好みの方法で割り当てる
    // lpAttributeListを作成し、更新
    InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &attrSize);
    // 疑似コンソールのハンドルを設定する
    UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hPC, sizeof(HPCON), NULL, NULL);

    // 子プロセスを起動(ここではsshをを起動するものとする)
    wchar_t param[256];
    wsprintf(param, L"c:/windows/system32/openssh/ssh.exe hostname");    // 例えば ssh でどこかに接続する
    PROCESS_INFORMATION pi = {};

    BOOL isOK = CreateProcessW(nullptr, param, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &si, &pi); 

    // 不要なハンドルを閉じる(親側で使わない)
    CloseHandle(hRemoteReader);
    CloseHandle(hRemoteWriter);

    if (isOK == FALSE) {
        printf("failed to create process\n");
        return 1;
    }

    // パスワードを入力する
    const char* sshCommand = "password\n";
    DWORD written = 0;
    WriteFile(hLocalWriter, sshCommand, strlen(sshCommand), &written, nullptr);

    // レスポンスを読み取る
    DWORD read = 0;
    char buff[256];
    ReadFile(hLocalReader, buff, 256, &read, nullptr);

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    // 残りのハンドルhLocalReader/hLocalWriter/hPCは不要になったら適宜後始末する

    return 0;
}

注意点など

- 実行するプログラムや環境の組み合わせなども影響しそうであるが、出力にマルチバイト文字を含む場合、文字化けすることがあるようだ
- 例: Windows11日本語版でsshdを動かした状態で、そのホストに対し、ssh <接続先ホスト> dirと実行したところ、出力が文字化けしていた。
- sshからの出力はエスケープシーケンスが混ざるので、端末としてのテキスト表示部分だけを取り出したい場合、エスケープシーケンスを考慮した端末エミュレート処理が必要

spdlog.hを利用したソースコードをVisualStudioでコンパイルすると「error C2338: static_assert failed: 'Unicode support requires compiling with /utf-8'」になる

個人で制作してるツールの中でspdlogというライブラリを利用しているが、 このツールのリポジトリにpushしたときに実行するCIワークフローの中でビルドする際に、 ある時期から以下のspdlog関連のエラーがでるようになった。

C:\path\to\spdlog\include\spdlog\fmt\bundled\base.h(458,28): error C2338: static_assert failed: 'Unicode support requires compiling with /utf-8'

エラー行近辺の処理やspdlog側の変更履歴を見る感じ、どうも下記の修正が影響している様子。

https://github.com/gabime/spdlog/pull/3236

この修正により、VisualStudioでビルドする際はビルドオプションとして /utf-8 の指定が必須になっているようだ。

これ、/utf-8が必須になることにより、 日本語版(cp932)のWindows環境でマルチバイトキャラセット(MBCS)構成でビルドしている既存のプロジェクトが困るケースがあるのでは、という気がする。

/utf-8を指定することにより、文字列リテラルで使っているマルチバイト文字がUTF-8として扱われることになるので、 実行時の文字コードとして従来はCP932(ShiftJIS)を想定して動いていたものがUTF-8になったことによりバグにつながる、 みたいなケースがありそう。

まあ、
文字セットがMBCSである かつ 文字列リテラルの中で日本語を使っている かつ 文字コードをShiftJISで想定している
なんてのはコードベースがわりと古いものだろうし、そういうプロジェクトでspdlogを使う、なんてのは相当レアケースの様な気もするので、現実的に困ることはそんなにないのかも (修正前のspdlogを使うことで回避することもできるし)

ただ、spdlogをこれから使ってみようという人がVisualStudio環境でお試しで使ってみて、エラーになったからそこで引き返してしまう、みたいなことはありそう。

Win32ネイティブアプリからトースト通知を表示する

自アプリ(Win32ネイティブアプリ)でトースト表示するにあたり、手順を調べたので以下にメモしておく。

なお、本記事に張り付けたコード辺は自アプリの関連処理を切り出したものだけど、切り出したものに対する検証(ビルド、実行)はしていないため、何か不備はあるかも・・

アプリに対してAppUserModelIdを割り当てる

AppUserModelIdはGUID。guidgen.exeを使って自アプリ用のAppUserModelIdを作っておく。 トーストを表示する際、このAppUserModelIdを指定する必要がある。

AppUserModelIDを登録する(スタートメニューにアプリのショートカットを作成する)

スタートメニューにショートカットを作り、そのショートカットメニューのプロパティとして PKEY_AppUserModel_IDキーに紐づける形で前ステップで生成したAppUserModelIDを割り当てる。

以下のような形でショートカットを作成しておけばよい。

#include <propkey.h>
#include <propvarutil.h>
// 上記のほかに、CComPtrを使うための準備をしておく(関連ヘッダのinclude,CoInitializeなど)

// 事前に確保しておく↓
constexpr LPCWSTR MYAPP_USERMODEL_ID = L"{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}";

// pathToSave : ショートカットキー保存先パス
// exePath : ショートカットのリンク先(実行ファイル)のパス
bool registerShortcut(LPCWSTR pathToSave, LPCWSTR exePath)
{
    CComPtr<IShellLink> shellLinkPtr;
    HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,IID_IShellLink,(void**)&shellLinkPtr);
    if (FAILED(hr)){
        return false;
    }


    // 実行ファイルへのパス
    shellLinkPtr->SetPath(exePath);

    // 引数、カレントディレクトリなどは必要に応じて設定する
    shellLinkPtr->SetArguments(L"");
    shellLinkPtr->SetWorkingDirectory(L"");

    // AppUserModelIdをショートカットに設定する
    CComPtr<IPropertyStore> propStore;
    shellLinkPtr->QueryInterface(IID_IPropertyStore, (void**)&propStore);

    // AppUserModelIdをappIdPropVarに設定し、キーに書き込む
    PROPVARIANT appIdPropVar;
    InitPropVariantFromString(MYAPP_USERMODEL_ID, &appIdPropVar);
    propStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);

    propStore->Commit();

    PropVariantClear(&appIdPropVar);

    CComPtr<IPersistFile> persistFilePtr;
    hr = shellLinkPtr->QueryInterface(IID_IPersistFile, (void**)&persistFilePtr);
    if(FAILED(hr)){
        return false;
    }
    // ショートカットキーをファイルとして保存する
    hr = persistFilePtr->Save(pathToSave, TRUE);
    if(FAILED(hr)){
        return false;
    }

    return true;
}

トーストを表示する

ToastNotificationインスタンスを作成し、これをToastNotificationManagerに対して渡すとトーストを表示することができる。

トーストに表示する内容はXMLベースのデータをこしらえて定義する。
テキストのほかに画像をおいたりハイパーリンクを設置したり、ボタンを置いたりなどできる。 また、クリック時の処理などいろいろ定義することができる。 ただし、クリック時の処理を定義しようとすると、そのための準備がいろいろ必要になるがここでは触れていない。

#include <winrt/Windows.UI.Notifications.h>
#include <winrt/Windows.Data.Xml.Dom.h>

using namespace winrt;
using namespace winrt::Windows::UI::Notifications;
using namespace winrt::Windows::Data::Xml::Dom;

void ShowToast()
{
    XmlDocument doc;
    doc.LoadXml(L"
          <toast>\
          <visual>\
          <binding template=\"ToastGeneric\">\
          <text></text>\
          <text></text>\
          </binding>\
          </visual>\
          </toast>");

    doc.DocumentElement().SetAttribute(L"launch", L"action=xxx&message=yyy");  // コールバック時にここで指定した文字列がえられる


    doc.SelectSingleNode(L"//text[1]").InnerText(L"こんにちは");
    doc.SelectSingleNode(L"//text[2]").InnerText(L"テストです");

    // トーストを表示する
    winrt::Windows::UI::Notifications::ToastNotification notif(doc);

    winrt::Windows::UI::Notifications::ToastNotificationManager toastManager;

    // CreateToastNotifierの引数に自アプリのAppUserModelIdを指定することにより、
    // 自アプリの通知としてトースト表示される。
    ToastNotifier toastNotifier(toastManager.CreateToastNotifier(MYAPP_USERMODEL_ID));
    toastNotifier.Show(notif);

}

その他メモ

  • Windows10/11環境では、Shell_NotifyIconを使って、トースト表示をすることもできる

    • ただし、以下のような制約があるようだ
      • クリックしたときの処理を定義できない
      • 見た目の細かなカスタマイズはできない(せいぜいアイコンを指定するくらい)
      • 通知センターに通知が残らない、
  • 自前でトースト表示する場合、トーストをクリックしたときの処理を自アプリで処理することができるが、その場合はコールバックを登録したり、コールバックを用意する必要がある

  • 他アプリが登録したUserAppModeIdを流用することで、自アプリでAppUserModeIdを登録しなくてもトースト表示はできる

    • ただし、流用元アプリとしてトースト表示が行われる

既存のAppUserModelIdを調べる

  • Excplorerのアドレスバーにshell:AppsFolderと打つ
  • 何もない領域を右クリック(Win11環境であれば、シフトキー押しながら右クリック)し、グループで表示>その他を選択する
  • 「詳細表示の設定」ダイアログが表示されるので、AppUserModelIdをチェックしてOKボタンを押下する
  • 再度、 何もない領域を右クリックして、表示>詳細を選択する

リスト表示にAppUserModelIdという列が表示されるので、ここからアプリごとのAppUserModelIdを確認することができる

LibreOfficeで開いている表計算ドキュメントのシート一覧を列挙する

最近作っている自分用ツール から、LibreOfficeで開いている表計算ドキュメントのシート名一覧を列挙して、それを選択したら、そのシートをアクティブにしたうえで、ウインドウを前面に出す、みたいなことができたいかと思って調べたので以下に自分用のメモ

Excelでは同様のことが実現できているが、自宅ではLibreOfficeを使っているので同じことできないかと思って調べたらできそうであることがわかった。

とりあえず、JScriptで書いているが、あとでC++のCOMで使う形に書き換える

var factory = new ActiveXObject("com.sun.star.ServiceManager");
var loader = factory.createInstance("com.sun.star.frame.Desktop");

var docs = loader.getComponents();
var e = docs.createEnumeration();
while(e.hasMoreElements()) {
    var doc = e.nextElement();

    // ドキュメントのパス(URL)を取得
    WScript.Echo(doc.getURL());

    // シートオブジェクトを取得
    var sheets = doc.getSheets();

    // シート数を得る
    var sheetCount = sheets.getCount();
    WScript.Echo(sheetCount);

    // シート名を列挙する
    for (var i = 0; i < sheetCount; ++i) {
        var sheet = sheets.getByIndex(i);
        WScript.Echo(sheet.name);
    }

    // アクティブなワークシートを変える
    var controller = doc.getCurrentController();
    if (sheetCount >= 4) {
        // 例えば、4番目のワークシートをアクティブにする
        controller.setActiveSheet(sheets.getByIndex(3));
    }

    var frame = controller.getFrame();

    // ウインドウを前面に出す
    // ※ ただし、今日の環境だと、タスクバー上のCalcのアイコンが点滅するだけになってしまう。
    frame.getContainerWindow().toFront();
}
  • 多分ここで列挙されるドキュメントは表計算以外のドキュメントも含まれる形になるはず
    • 種類を判別する方法はあるはずだけど調べていない。とりあえずドキュメントパスの拡張子でも判断できそうな気も
  • ウインドウを前面に出すのはtoFront()を使うのではなく、ウインドウハンドルを探して、AttachThreadInput()SetForegroundWindow()でいけるはず
    • 確実じゃないかもしれないけど、ドキュメントパスとウインドウタイトルを見れば見つけられそうな気がしている

コントロールパネルの一覧を列挙・項目を実行(表示)する手順についてのメモ

自作ツールでコントロールパネルの項目を列挙し、任意の項目を実行するにあたり方法を調べたので以下にメモ

列挙する

  1. 以下のレジストリキー直下にあるキーを列挙する
    HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel\NameSpace

  2. 直下にあるキー名がコントロールパネルの項目を表すCLSIDなので、これを使って HKEY_CLASSES_ROOT\CLSID 直下にのサブキーを参照する
    例: HKEY_CLASSES_ROOT\CLSID\{87D66A43-7B11-4A28-9811-C86EE395ACF7}

  3. 各キーにLocalizedString値(種類はREG_EXPAND_SZ型)があるので、これを取得する
    例: @%SystemRoot%\System32\srchadmin.dll,-601#immutable1

  4. 取得した値SHLoadIndirectString関数を用いて変換する
    @で始まる文字列を上記APIを使うと実際の文字列に置き換えることができる

その他の情報

  • アイコンはHKEY_CLASSES_ROOT/CLSID/{xxxx...}/DefaultIcon の(既定)にパスが入っている
    • (アイコンリソースを持つモジュールのパス),(インデックス or 識別子) の形式で格納されている

      • (インデックス or 識別子)が正の値の場合はインデックス、負の値の場合はリソース識別子
    • この情報をもとに、WIN32 APIのExtractIconExでアイコンハンドルを取得できるが、-1の時にうまく取れなかった。

      • APIリファレンスをみると、ExtractIcon系のAPIは、indexに-1を指定したとき、アイコンリソース数を返す仕様になっている。
        じかし、ExtractIconExであれば、indexが-1でもラージアイコンorスモールアイコンを受け取る配列を指定していれば、
        アイコンリソース数を返すのでなく、ハンドルをロードする、と読めるような記載があるが、 手元の環境で動かしてみる感じ、アイコンリソース数と思われる数が返ってくる...
    • これは解決できなかったのでしかたなく、indexが-1の時は別途、LoadLibraryEx -> FindResource -> LoadResource -> LockResource -> CreateIconFromResourceで取得する形にした
      • ただ、この方法で取得したアイコンはサイズか色が違ってそうで、このアイコンを描画するとなんか見た目がガビガビなので、もっとよい方法がある気がしてるがとりあえず放置

コントロールパネルの項目を実行(表示)する

HKEY_CLASSES_ROOT/CLSID/{xxxx...}キー内のSystem.ApplicationName値を取得し、 これを %SystemRoot%\System32\control.exe /name <取得した値> とすると、 そのコントロールパネル項目を表示できる。

Alt-Tabでのウインドウ選択操作をHJKLキーでできるようにするためのyamy設定(Windows11向け)

背景

Alt-Tabキー押下でウインドウを切り替える際の選択操作をHJKLキーで選択できるよう、 yamyの設定ファイルでキー割り当てを定義している。

Windows10では、Alt-Tabキー押下中に表示されるあの切り替えウインドウのウインドウクラス名はMultitaskingViewFrameだったので、 下記のように記載していた。

# タスク切り替えウインドウ(Win10)
window TaskSwitchWindowForWin10 /.*:MultitaskingViewFrame/ : Global
 key A-H = A-Left
 key A-L = A-Right
 key A-J = A-Down
 key A-K = A-Up

Windows11(21H2)ではAlt-Tabキー押下時に表示されるウインドウが別物になってしまったようで、 上記の定義ではヒットしなくなり、HJKLキーで移動できなくなってしまった。

この環境での、Alt-Tabキー押下時に出てくるウインドウクラス名を確認したところ、 XamlExplorerHostIslandWindowという名前になっていたので、 yamy設定ファイルに下記のような定義を追加したところ、Windows11環境でもHJKLキーで選択できるようになった。

Windows11での設定

# タスク切り替えウインドウ(Win11)
window TaskSwitchWindowForWin11 ( /.*:XamlExplorerHostIslandWindow/ && /タスクの切り替え/ ) : Global
 key A-H = A-Left
 key A-L = A-Right
 key A-J = A-Down
 key A-K = A-Up

なお、ウインドウクラス名が切り替えウインドウ専用の名前っぽくないので、ウインドウタイトルも見ることにした。 システムが出すウインドウのタイトルを見て判断するのは、システムの言語設定の影響を受けそうでいやではあるが、どうせ日本語以外の環境で使うことないしな・・