dullwhaleのメモ帳

何度も同じことを調べなくてよいように...

OpenAPIで記述したAPI仕様をRedocを使ってsingle fileのHTMLにする

いくつも方法があるものの、新しめの手法に中々たどり着けなかったからメモする。 この方法も(開発段階という意味で)previewらしいから変化する可能性が高い。注意が必要。

ライブプレビューする

ライブプレビューにはいくつか方法がある。 ここでは、ファイルをウォッチして自動で再ビルド、サーブしてくれる方法を説明する。 APIの定義があるファイルをapi.yamlとする。

npx @redocly/cli preview-docs api.yaml

デフォルトではlocalhost:8080でプレビューをサーブする。 WSLなどの環境で便利。 ただ、現時点では構文が間違っていると例外を吐いて勝手に終了してしまう問題がある。

single fileのHTMLを生成する

CIや他人との共有に備えて、一つのファイルで完結するHTMLを生成することもできる。

npx @redocly/cli build-docs api.yaml

Windows App SDK(WinUI3)でNotifyIconを持つプログラムを作成する

この記事は、Windows App SDK(WinUI3)でNotifyIconからウィンドウを開く方法について説明する。 NotifyIconの実装にはライブラリH.NotifyIconを用いる。

NotifyIconとその使われ方

Windowsにおいて、普段はデーモンのように動くが時々GUIで対話するアプリケーションを作りたい。 そのようなアプリケーションのGUIを起動する手段として、NotifyIconが使われることがある。 NotifyIconはsystem tray iconやtaskbar iconとも言われ、デフォルトでタスクバーの右に配置される。 次の図にNotifyIconの例を示す。 赤い枠で囲まれた部分に各アプリケーションや機能のNotifyIconが格納されている。

図: Windows 11におけるsystem tray中のNotifyIconの例

アイコンの右クリックメニューでいくつかの簡単な(GUIの用語としての)コマンドを実行でき、複雑な操作に備えてウィンドウを開けるようになっているものが多い。

対象

事前作業 プロジェクトを作成しUnpackagedにする

この節では事前作業としてVisual Studio 2022でソリューション・プロジェクトを作成し、アプリケーションのベース部分を作成する。 配布は行わないから、Unpackagedアプリとして作成する。

詳細は公式ドキュメントに記載されているから、ここでは要点だけ記述する。

1 . プロジェクト作成時のテンプレートでは「C#」、「Windows」、「WinUI」でフィルタし、「空のアプリ、パッケージ (WinUI 3 in Desktop)」を選択する。

2 . *.csprojファイルを開き、<PropertyGroup>の下の階層に以下の記述を追加する。

<WindowsPackageType>None</WindowsPackageType>

3 . Visual Studioからの実行の構成がデフォルトで「${プロジェクト名} (Packaged)」になっているから、「${プロジェクト名} (Unpackaged)」に変更したうえで実行してみる。

4 . ビルド、実行が行われ、起動したウィンドウ中央の「Click Me」ボタンを押して「Clicked」に変化すれば、この節手順は完了。

続く節ではサードパーティーライブラリH.NotifyIconの導入理由と手順を示す。

なぜサードパーティーのライブラリを使うのか

この記事を書いた時点においては、NotifyIconの機能はWindows App SDKで提供されていないようだ。 Win32APIを直接コールするか、Windows FormsのAPIを呼び出す必要がある。 どちらの方法を取っても、記述量が増えバグのリスクも高まる。 特にWin32APIはより低レベルのインターフェイスを提供する分、危険である。

対して、H.NotifyIconはごく少ないコード量でNotifyIconを利用でき、危険なコードになりにくい。 同様の機能を提供し、かつ開発が継続しているライブラリが他に見当たらないからこれを使う。

H.NotifyIconの導入

Windows App SDKと一緒に使うことを想定したNuGetパッケージH.NotifyIcon.WinUIをインストールする。 H.NotifyIconではないことに注意せよ。 さらに、他のGUIフレームワーク向けのよく似たパッケージが複数用意されているから混同に注意せよ。 インストールはメニューの「プロジェクト(P)」>「NuGet パッケージの管理(N)」から行うのが簡単だろう。

NotifyIconをクリックしてウィンドウを開けるようにする

この節では、常駐アプリとして稼働し、必要なときだけNotifyIconをクリックすることでウィンドウを開くアプリケーションを作成する。 記述するコードは少ないものの、コード量に対して考えるべきことが多いから、以降いくつかの節に分けて説明する。

ウィンドウが全て閉じられてもアプリケーションが動き続けるようにする

よくある常駐アプリのように、バックグラウンドで稼働し続け、必要なときだけウィンドウを表示できるよう実装する。 これは実装の視点から見ると、全てのウィンドウが閉じられてもアプリケーションが起動し続ける必要がある。 これを実現するためにClosedイベントをキャッチしてデフォルトのハンドラからイベントを隠ぺいする。 詳しくは次の記事を参照せよ。

Windows App SDK(WinUI3)でウィンドウを閉じられてもアプリケーションを動かし続ける - dullwhaleのメモ帳

ここでは、Closedイベントの隠ぺいに加えてウィンドウを非表示にする処理を加える。 デフォルトで作成されるMainWindowのコンストラクタ内でイベントハンドラを追加する。 MainWindow.xaml.cs

public MainWindow()
{
    // 中略
    // ウィンドウが閉じられたイベントに対するイベントハンドラを追加
    Closed += (sender, args) =>
    {
        // イベントを処理済みとしてマークして、デフォルトのハンドラへの伝播を阻止する。
        // デフォルトのハンドラはアクティブなウィンドウが1つもないとアプリケーションを終了させる。
        args.Handled = true;
        // ウィンドウを閉じる代わりに隠す。
        this.Hide();
    };
}

NotifyIconをXAMLで作成する

H.NotifyIconのREADMEにあるサンプルを流用して、最低限のNotifyIconを実装する。 左クリック時のイベントハンドラを指定している部分だけが元のサンプルとの違いである。

LeftClickCommand="{x:Bind IconClicked}"

MainWindow.xaml

<!-- 中略 -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
    <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
    <tb:TaskbarIcon LeftClickCommand="{x:Bind IconClicked}">
        <tb:TaskbarIcon.IconSource>
            <tb:GeneratedIconSource
        Text="❤️"
        Foreground="Red"
        />
        </tb:TaskbarIcon.IconSource>
    </tb:TaskbarIcon>
</StackPanel>
<!-- 中略 -->

非表示をウィンドウを表示するための下準備をする

テンプレートのコードを少し修正し、続く節でイベントハンドラからウィンドウを表示できるよう準備をする。 既存のAppクラスの一部を以下のように書き換え、staticメンバを作成して簡易的なシングルトンとして機能させる。

App.xaml.cs

protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
    MainWindow = new MainWindow();
    MainWindow.Activate();
}

public static Window MainWindow;

NotifyIcon左クリック時にウィンドウを表示する

上で指定したイベントハンドラIconClickedを実装する。 このイベントハンドラの指定には癖がある。 H.NotifyIconのコントロールではメソッドを直接指定できず、System.Windows.Input.ICommand型の変数をx:Bindする必要がある。 この原因は恐らく、H.NotifyIconがWPFwindows presentation foundation)をターゲットとして作成され、後からWindows App SDKに対応させたからだと思われる。

まずはinterface ICommandを実装するclassを実装する。 ICommandの設計思想は置いておき、定義されている3つのものを次のように実装すればよい。

MainWindow.xaml.cs

using System.Windows.Input;

// 中略

public class IconClickedCommand : ICommand
{
    // 使わないからnullにしておく
    public event EventHandler CanExecuteChanged = null;
    public bool CanExecute(object parameter)
    {
        // この単純な実装では常にtrueを返せばよい。
        return true;
    }

    // 処理の実体をここに記述する
    public void Execute(object parameter)
    {
        App.MainWindow.Activate();
    }
}

そして、このインスタンスをメンバに登録する。

MainWindow.xaml.cs

public sealed partial class MainWindow : Window
{
    private readonly IconClickedCommand IconClicked;
    public MainWindow()
    {
        this.InitializeComponent();

        IconClicked = new IconClickedCommand();
        // 中略 イベントハンドラ追加の処理
    }
}

これで、先のXAMLでの指定が機能する。

LeftClickCommand="{x:Bind IconClicked}"

プログラムの実行と停止

Unpackagedであることを確認してVisual Studioからデバッグでビルドと実行を行う。 最初に起動したウィンドウを閉じた後、NotifyIconを左クリックして再びウィンドウを表示できれば動作テストは完了。

Closedイベントに対するデフォルトの動作変えたから、アプリを停止させるにはVisual Studioから「デバッグの停止」を押すか、プロセスを直接killする必要がある。

成果物

https://github.com/dullwhale/winui3-notifyicon-test

認可の文脈におけるgroupとroleの違い

認可における普遍的な概念としてのgroupとroleは紛らわしい。 権限管理のフレームワークやライブラリにおいて、groupとroleの両方が同時に利用可能な場合、どのように使い分ければよいのか混乱するから自分の理解をメモしておく。 困ったことに、どちらを使っても同じ目的を達成できることが多く、人間が概念を理解し運用設計をしなければ使いこなせない。

原則

基本の考え方は次の3つに集約される。

  • groupはuserの集合である
  • roleはpermissionの集合である
  • permissionの付与は原則roleで単位で行う

この定義から、次のような2つの運用上の原則が導かれる。

  • 大多数に当てはまる通常の権限付与ではuserをgroupでまとめ、groupにroleを付与する。もしroleを付与できない場合はpermissionを付与する。
  • 例外的な権限付与は個別のroleをuserに追加する。

ここで、roleは必要に応じて追加されうるという点に注意せよ。 追加は永続的なものかもしれないし、一時的なものかもしれない。

理由

以降は理由や例を記載する。

大多数をカバーする非例外的、通常の権限付与はgroupを介して行うべきである。 具体的にはgroupを作成し、そのgroupに複数のuserを追加し、groupにロールを付与する。 もしシステム側の制約でroleを付与できない場合は代わりにpermissionを追加する。 このgroupは現実世界の役職などに合わせることが適切だろう。

stock_managerグループを作成し、そこに在庫管理する複数のuserを追加する。
stock_managerに在庫を管理するpermissionが定義されたroleを付与する。

この方法の利点は、運用上の手間が少ないことである。 改修などでpermissionなどが増えた場合、groupに紐づくroleを1回だけ修正すれば良い。

例外な権限付与の場合はgroupとは無関係に、必要なuserにだけroleを追加する。

ユーザAliceは、viewerのgroupに入っているから基本的には読み取りしかできないが、Aliceに個別のroleを付与して書き込み権限を追加で与える。

また、よりリスクが高い操作を行うために期限付きで一時的にroleを付与する使い方も考えられる。

普段は自分の認証に関わる情報を読み取ることができないが、二要素目の認証を行うことで一時的に読み取るためのroleを付与する。

次のstackoverflowのやりとりが参考になる。

https://stackoverflow.com/questions/7770728/group-vs-role-any-real-difference

Windows App SDK(WinUI3)でウィンドウを閉じられてもアプリケーションを動かし続ける

Window.Closedにイベントハンドラを追加し、そのイベントハンドラ内で引数として渡されたイベントのHandledプロパティをtrueに設定すればよい。

Windows App SDKのイベントの仕組み

Windows App SDKのイベント処理の仕組みは、Webフロントエンドのそれと似ている。

ここでは説明のため、一旦JavaScriptを考える。 同じイベントをlistenする複数のイベントハンドラが登録されている状況を想像せよ。 例えば<div>要素の中に<button>があり、それぞれclickイベントに対するハンドラが設定されているとする。 この状況でbuttonを押すと、通常は次の順番で両方のハンドラが呼び出される。

  1. buttonのclickイベントのハンドラ
  2. divのclickイベントのハンドラ

ところが、buttonのイベントハンドラ内でイベントを完全にconsumeして、div要素へのイベントの伝播を阻止することもできる。 詳しくはイベントバブリングやイベントプロパゲーションで調べよ。

話をWindows App SDKに戻す。 恐らくWindowベースclassのClosedイベントに反応するハンドラとして、「他にactiveなウィンドウが存在しなければアプリケーションを終了する」という処理が登録されている。 これを阻止するためには、同様にClosedイベントへ反応する優先度の高いハンドラを追加し、そのハンドラ内でイベントの伝播を止めてしまえばよい。 例えば、以下のようなコードが考えられる。

// Closedイベントに反応するイベントハンドラを追加
Window.Closed += (sender, args) =>
{
  // trueを設定することで、イベントの伝播を阻止。
  args.Handled = true;
};
コード中の`Window`は実際には継承したクラス/インスタンス名になる。
テンプレートから名前を変更していない場合は`MainWindow`だろう。

Debian 12にpyenvでPython 3.12をインストールする

Pythonで開発を行おうとすると、OSに付属のものとは切り離されたバージョンを利用したくなる。 複数バージョンのPythonを管理し、切り替えることができるツールの一つとしてpyenvがある。 この記事ではDebian 12にpyenvをインストールし、pyenvを用いてPython 3.12.3をインストールするまでの手順について残す。 Python 3.12.3は、この記事を書いた時点での最新の安定板である。

手順

最初にpyenvをインストールする。 シェルはbashとする。

# pyenvのインストールスクリプトをダウンロードして、そのまま実行する。
curl https://pyenv.run | bash

# PATHを通して`pyenv`を使えるようにする。
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc

# ~/.profileが存在する場合
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.profile
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.profile
echo 'eval "$(pyenv init -)"' >> ~/.profile

# ~/.bash_profileが存在する場合
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

# 設定を反映
exec "$SHELL"

続いて、Pythonのインストールに必要な前提のライブラリをインストールする。 さもないと続くpyenv installPython本体のインストールに失敗する。 最低限必要なライブラリはlibssl-devのみであり、他はオプション機能である。 ただしオプションといえども、あとからモジュールが利用できないといった問題で時間を取られないよう、初めからすべてインストールしておくべきだ。

sudo apt install libssl-dev libbz2-dev libffi-dev liblzma-dev libreadline-dev libsqlite3-dev tk-dev

最後にpyenvを用いてPythonをインストールする

# pyenvを使って指定バージョンのPythonをインストール
pyenv install 3.12.3
# インストールしたバージョンのPythonを有効化
pyenv global 3.12.3
# Pythonバージョンがちゃんと切り替わっているか確認
python -V

備考

pyenvが提供する機能は「異なるバージョンのPythonを簡単に切り替えられるようにする」だけである。 プロジェクトごとに利用するパッケージを分離するには別の手段を使う必要がある。 この要求を満たすものとして手軽なものはPythonに付属のvenvだろう。

治安の悪いgitリポジトリ内の文字コードをシェルからBOMなしUTF-8改行コードLFに統一する

他人から受け取ったファイルは文字エンコードShift_JISだったり、BOMがついていたり、改行コードがCRLFだったりする。 そのままファイルの編集を続けると、システムに繋げたり乗っけたりするタイミングで問題が発生しやすいから、BOMなしUTF-8 改行コードLFに統一したい。

通常の開発であればIDEやエディタに.editorconfigを追加すればよい。 ただし、そういった方法が取れない/取るまでもない状況もある。 そのような場合に使えるワンライナーを作ったからメモしておく。

事前にnkfコマンドのインストールが必要。

find -type f -not -path './.git/*' | xargs nkf --guess | grep -vE ": (BINARY|(ASCII|UTF-8) \(LF\))$" | rev | cut -d: -f2- | rev | xargs nkf -wLu --overwrite

revで2回反転させている箇所とか無駄が多いから、もっと効率の良い方法は考えられそう。 あまり大量のファイルを処理することは想定していない。

Debian12 bookwormにPythonをインストールしてグローバルに使うPythonパッケージをインストールする

この記事ではDebian 12にPythonをインストールし、コマンドのようにどこでも使うPythonパッケージをインストールする方法について記述する。

コマンドと解説

# Python本体とpipxをインストールする
sudo apt install python-is-python3 pipx
# パスを通す
pipx ensurepath
# 無事にインストールできたか確認
python -V

# 目的のPythonパッケージをインストール
pipx install ${PythonPackageName}

python-is-python3pythonを一々python3とタイプすることなく、pythonでも起動できるようにするパッケージである。 普通にPythonパッケージをaptからインストールすると、デフォルトでpython3でしか起動できない。 自分でシンボリックリンクエイリアスを設定してもよいが、既存パッケージがあるので活用する。 さらに、同パッケージは依存パッケージとしてpythonが設定されているから、Python本体のインストールも行われる。

pipxPython環境の隔離機能付きのパッケージマネージャーだと思えばいい。 詳細は公式のドキュメントを参照せよ。 わざわざpipxをインストールする目的はPEP 668の回避にある。

2023-01-30にリリースされたpipバージョン23.0以降、PEP 668に準拠した挙動が実装されている。 同バージョン以降のpipで仮想環境Pythonパッケージをインストールしようとすると、次のようなエラーメッセージが出力される。

error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.
    
    If you wish to install a non-Debian-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip. Make
    sure you have python3-full installed.
    
    If you wish to install a non-Debian packaged Python application,
    it may be easiest to use pipx install xyz, which will manage a
    virtual environment for you. Make sure you have pipx installed.
    
    See /usr/share/doc/python3.11/README.venv for more information.

note: If you believe this is a mistake, please contact your Python installation 
or OS distribution provider. You can override this, at the risk of breaking your
 Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

PEP 668は簡単に言うと「グローバルのPythonパッケージと仮想環境内のPythonパッケージが衝突する問題などを避けるため、何らかの仮想環境内以外でpip installを行うな」と主張している。

cf. https://pip.pypa.io/en/stable/news/#v23-0

対応策として、エラーメッセージにもあるようにpipxが推奨されているから、これに従う。

it may be easiest to use pipx install xyz, which will manage a
virtual environment for you. Make sure you have pipx installed.

このために、最初のsudo apt installで一緒にpipxをインストールしている。

なお、開発などプロジェクトでだけ使うパッケージについてはこれまで通りvenvなどを使えばよい。