tech.guitarrapc.cóm

Technical updates

ConsoleAppFrameworkでasync voidは使えない

ConsoleAppFrameworkでasync voidなコマンドは実行しても正常に完了できません。async Taskを使いましょう。以上です。

どのような挙動になるかを説明しておきます。

ConsoleAppFrameworkのCommand

ConsoleAppFrameworkのコマンドの返り値は次のいずれかを期待しています。エラー時は例外にするならvoidasync Task、ExitCodeを調整したいときはintasync Task<int>を使うという使い分けです。

  • void
  • async Task
  • int
  • async Task<int>

たとえば同期処理しかないコマンドは次のように書けます。

[Command("hello")]
public void Hello(string name)
{
    Console.WriteLine($"Hello {name}!");
}
$ dotnet run ConsoleApp1 -- hello foo
Hello foo!

たとえば非同期処理を含むコマンドなら次のように書けます。

[Command("hello-async")]
public async Task AsyncTask(string name)
{
    var ts = TimeProvider.System.GetTimestamp();
    Console.WriteLine($"Hello {name}!");
    await Task.Delay(1000);
    Console.WriteLine($"Waited {TimeProvider.System.GetElapsedTime(ts)}");
}
$ dotnet run ConsoleApp1 -- hello-async foo
Hello foo!
Waited 00:00:01.0050441

async voidの問題

async Taskと書くつもりで間違えてasync voidにすることはまれに時々あります。1 ではasync voidにしたコマンドはasync Taskとどのように挙動が違うか見てみましょう。

// async Taskをasync voidに変更しただけ
[Command("hello-async-void")]
public async void AsyncVoid(string name)
{
    Console.WriteLine($"Hello {name}!");
    await Task.Delay(1000);
    Console.WriteLine($"Waited");
}

async voidだと非同期処理が完了する前にコマンドが終了してしまいます。このため、await Task.Delay(1000)を待つことなくコマンドがそこで終了するのでWaited...が表示されません。

$ dotnet run ConsoleApp1 -- hello-async-void foo
Hello foo!

IDEやビルド時に気づければよいのですが、5.3.3まではasync voidなコマンドでも何事もなくビルド、実行できるので気づくのに遅れるでしょう。

image

また、デバッグ実行してもawaitの行でステップ実行を進めるとデバッガーが終了します。これはasync voidあるあるですね。

対策

async Taskにしましょう。

また、2025/1/16にリリースされた5.3.4async voidコマンドはアナライザーがエラーを出してビルドできくなりました。2今ConsoleAppFramework v5を使っている人は、このバージョンにアップグレードしておきましょう。

image


  1. voidなコマンドをasyncつけて、というのは頭が動いていない時にうっかりすることはゼロじゃないです。
  2. #157でどのような対応がされたかわかります

WindowsエクスプローラーのコンテキストメニューからOpen with Visual Studioを消す

Visual Studioを使っていて、フォルダのコンテキストメニュー(右クリックメニュー)に「Open with Visual Studio」が出て邪魔だなと思ったことはありませんか?Visual Studiは起動パスに.vs/を作るのですが、これで差分ができて悲しい思いを何度かしています。 事故らないようにコンテキストメニューから消してみましょう。1

image

Open with Visual Studioのメニュー実体

コンテキストメニューはレジストリに設定が登録されています。今回はフォルダの右クリックなので鍵になるのはHKEY_CLASSES_ROOT\Directory\Shellです。

image

含まれるデータは次の通りです。

$ Get-Item "registry::HKEY_CLASSES_ROOT\Directory\Background\shell\AnyCode"

    Hive: HKEY_CLASSES_ROOT\Directory\Background\shell

Name                           Property
----                           --------
AnyCode                        (default) : @C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\1033\VSLauncherUI.dll,-1002
                               Icon      : C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\VSLauncher.exe,-105
$ Get-Item "registry::HKEY_CLASSES_ROOT\Directory\shell\AnyCode"

    Hive: HKEY_CLASSES_ROOT\Directory\shell

Name                           Property
----                           --------
AnyCode                        (default) : @C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\1033\VSLauncherUI.dll,-1002
                               Icon      : C:\Program Files (x86)\Common Files\Microsoft Shared\MSEnv\VSLauncher.exe,-105

いろいろな削除方法

regeditで削除する方法と、C#やPowerShellで削除する方法を紹介します。

regeditで削除する

以下の方法でregeditから削除できます。

  1. regeditを起動
  2. HKEY_CLASSES_ROOT\Directory\Background\shellを開く
  3. AnyCodeキーを削除
  4. HKEY_CLASSES_ROOT\Directory\shell\を開く
  5. AnyCodeキーを削除

C#で削除する

以下のコードを実行すると、AnyCodeキーを削除できます。管理者権限から実行する必要があるので注意です。

// Program.cs (要管理者権限)
using Microsoft.Win32;

string[] keys = ["Directory\\Shell", "Directory\\Background\\shell"];
foreach (var key in keys)
{
    using var reg = Registry.ClassesRoot.OpenSubKey(key, writable: true);
    if (reg is not null)
    {
        reg.DeleteSubKeyTree("AnyCode", throwOnMissingSubKey: true);
    }
}

PowerShellで削除する

PowerShellからの削除はサブツリー削除に難があるので、C#コードをそのまま持ってくるのが安定です。

# 要管理者権限
$keys = @("Directory\shell", "Directory\Background\shell")
foreach ($key in $keys) {
  $reg = [Microsoft.Win32.Registry]::ClassesRoot.OpenSubKey($key, $true)
  if ($null -ne $reg) {
    $reg.DeleteSubKeyTree("AnyCode", $true)
  }
}

削除できない例

PowerShellはレジストリPS Providerからアクセスが可能なので、ネイティブなコマンドレットで削除すると次のようになりますが、肝心のAnyCodeキーのサブキー削除がうまくいかないようです。残念ながら、これ対処がない感じです。

# 要管理者権限
$keys = @("registry::HKEY_CLASSES_ROOT\Directory\shell\AnyCode", "registry::HKEY_CLASSES_ROOT\Directory\Background\shell\AnyCode")
foreach ($key in $keys) {
  if (Test-Path "$key") {
    foreach ($sub in $(Get-ChildItem "$key").Name) {
      foreach ($prop in (Get-Item "$key").Property) {
          echo "Deleting $prop in $key"
          Remove-ItemProperty "$key" -Name "$prop"
      }
    }
    Remove-Item "$key" -Force -Recurse
  }
}
Line |
   9 |      Remove-Item "$key" -Force -Recurse
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Cannot delete a subkey tree because the subkey does not exist.

削除後

無事に削除できましたね。

image

Explorerも再起動不要です。

image

メニューを戻す

Visual Studio Installerを起動してRepair(修復)を実行すると、コンテキストメニューにOpen with Visual Studioが戻ります。

image

Repiarの実行後は再起動不要です。

image


  1. 私がVisual Studioを起動するときは、.slnから起動StreamDeckから起動タスクバーにピンしたVisual Studioから起動のいずれかを使っています。

WingetでPowerShell(pwsh)をインストールする

PowerShell 7以降をWingetでインストールする方法を紹介します。対象OSはWindowsです。

Microsoftが推奨するインストール方法

MicrosoftはPowerShellをWingetでインストールすることを推奨しています。WingetはWindows11標準で利用できるパッケージマネージャです。何も導入をすることなくコマンドラインからサクッとインストールできます。これは近年のWindowsにおける劇的な体験改善だと感じます。1

Installing PowerShell on Windows | Microsoft Learn

インストール可能なPowerShell

インストール可能なPowerShellは、PowerShellとPowerShell Previewです。

$ winget search Microsoft.PowerShell
Name               Id                           Version   Source
-----------------------------------------------------------------
PowerShell         Microsoft.PowerShell         7.4.6.0   winget
PowerShell Preview Microsoft.PowerShell.Preview 7.5.0.101 winget

6.0.0.0~最新版までインストール可能なバージョンとして登録されています。検索時は--exact(あるいは-e)オプションをつけると厳密名で検索できておすすめです。

# × これはいや
$ winget show --id Microsoft.PowerShell --versions
# 〇 おすすめ
$ winget show -e --id Microsoft.PowerShell --versions
Found PowerShell [Microsoft.PowerShell]
Version
--------
7.4.6.0
7.4.5.0
7.4.4.0
7.4.3.0
7.4.2.0
7.4.1.0
7.4.0.0
7.3.11.0
...省略
6.0.4.0
6.0.3.0
6.0.2.0
6.0.1.0
6.0.0.0

PowerShellをインストール

WingetでPowerShellを追加します。PowerShellはMicrosoft.Powershellという名前で登録されていいます。今のバージョンは7.4です。

$ winget install --id Microsoft.PowerShell --source winget

指定したバージョンをインストールする場合は、--versionオプションを指定します。

# 7.4.6.0を指定
$ winget install --id Microsoft.PowerShell --source winget --version 7.4.6.0

Winget実行を非管理者ターミナルで実行するとUACのセキュリティプロンプトが表示されます。実行中のUACプロンプトを防ぐ場合、Windows 11 24H2で入ったsudoのインライン実行を使うといいでしょう。2

# sudoを使って非管理者ターミナルでも管理者昇格する
$ sudo winget install --id Microsoft.PowerShell --source winget --version 7.4.6.0
Found PowerShell [Microsoft.PowerShell] Version 7.4.6.0
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Successfully verified installer hash
Starting package install...
Successfully installed

プレビューバージョンのインストール

プレビューバージョンのPowerShellをインストールする場合は、Microsoft.PowerShell.Previewを指定します。今のバージョンは7.5です。

$ winget install --id Microsoft.PowerShell.Preview --source winget

アンインストール

アンインストールは次のコマンドで行います。

$ sudo winget uninstall --id Microsoft.Powershell
$ sudo winget uninstall --id Microsoft.PowerShell.Preview

まとめ

WingetでPowerShellのインストールも簡単にできます。Windows標準からできる意味でapt同様に使えるので、個人的にもおすすめです。


  1. ScoopやChocolateyを導入していない人に対して導入を促すより、Wingetを活用できたほうがいいでしょう。
  2. Sudo for Windowsの詳細は別記事で。Sudo for Windows | Microsoft Learn

scoopでPowerShell(pwsh)をインストールする

PowerShell 7以降をscoopでインストールする方法を紹介します。対象OSはWindowsです。

Microsoftが推奨するインストール方法

MicrosoftはPowerShellをWingetでインストールすることを推奨しています。ただWingetを使うとシステムに1つのPowerShellしかインストールできません。複数のバージョンを使う方法として公式はZipパッケージを示していますが、scoopを使うとZipを意識せずにインストールできます。

Installing PowerShell on Windows | Microsoft Learn

インストール可能なPowerShell

scoopでインストール可能なPowerShellは、PowerShell(mainバケット)とPowerShell Preview(versionsバケット)です。PowerShell Previewはpwsh-betaとなっています。

$ scoop search pwsh
Results from local buckets...

Name      Version    Source   Binaries
----      -------    ------   --------
pwsh      7.4.6      main
pwsh-beta 7.5.0-rc.1 versions

PowerShellをインストール

scoopでPowerShellを追加します。PowerShell 7はpwshという名前でMainバケットに登録されていいます。

$ scoop install pwsh

mainバケットはデフォルトで利用できますが、もし追加する場合は次のコマンドを実行してください。

$ scoop bucket add main

プレビューバージョンのインストール

プレビューバージョンのPowerShellをインストールする場合は、versionsバケットを追加してからインストールします。

$ scoop bucket add versions
Checking repo... OK
The versions bucket was added successfully.
$ scoop install versions/pwsh-beta

アンインストール

アンインストールは次のコマンドで行います。

$ scoop uninstall pwsh
$ scoop uninstall versions/pwsh-beta

注意

pwshとpwsh-betaの同時インストールは避けたほうがいい

pwshのインストールとpwsh-betaのインストールはshimsで競合するため、pwshをアンインストールしてからpwsh-betaをインストールしてください。ちょっといやな挙動です。

pwsh-betaをインストール後にscoop uninstall pwshができない

pwsh-betaが入っているときにscoop uninstall pwshするとpwsh-betaが呼ばれてプロセス利用中のためアンインストールできなくなります。1

$ scoop uninstall versions/pwsh-beta
Uninstalling 'pwsh-beta' (7.5.0-rc.1).
ERROR The following instances of "pwsh-beta" are still running. Close them and try again.

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     67    50.74     109.63       0.48   24392  18 pwsh

アンインストールする場合は、%USERPROFILE%\scoop\shims\pwsh.shims|exe%USERPROFILE%\scoop\apps\pwsh-betaの両方を削除します。

if (Test-Path "${env:USERPROFILE}\scoop\shims\pwsh.shims") {
    Remove-Item "${env:USERPROFILE}\scoop\shims\pwsh.shims"
}
if (Test-Path "${env:USERPROFILE}\scoop\shims\pwsh.exe") {
    Remove-Item "${env:USERPROFILE}\scoop\shims\pwsh.exe"
}
if (Test-Path "${env:USERPROFILE}\scoop\apps\pwsh-beta") {
    Remove-Item "${env:USERPROFILE}\scoop\apps\pwsh-beta" -Recurse
}

いやな挙動ですが、いったんしょうがないか...

まとめ

scoopで異なるバージョンの利用にversionsバケットを使うのはpwsh-betaでも同様ですが、挙動がちょっと嫌な感じでですね。 scoopにこだわらずWingetを素直に使ってもいい気もします。


  1. Windows PowerShellからのscoop uninstall pwshでも同様の挙動になるのは解せない

aquaでPowerShell(pwsh)をインストールする

PowerShell 7以降をaquaでインストールする方法を紹介します。

必要条件

PowerShellがaquaレジストリに追加されたのは先日なので、4.292.0以上のバージョンが必要です。

registries:
  - type: standard
    ref: v4.295.1 # renovate: depName=aquaproj/aqua-registry

PowerShellをインストール

aqua.yamlにPowerShellを追加します。aqua gでパッケージ名を検索してもいいですし、パッケージを指定できます。--select-versionあるいは-sは、指定したパッケージのバージョンを指定できるパラメーターで指定しない場合は最新バージョンがインストールされます。指定するとデフォルトで30バージョンを選べます。

# パッケージ名を検索、aqua.yamlには追加しない
$ aqua g
# PowerShellパッケージを指定、aqua.yamlに追加。バージョンは最新
$ aqua g -i PowerShell/PowerShell
# PowerShellパッケージを指定、aqua.yamlに追加。バージョンは候補から選ぶ
$ aqua g -i -s PowerShell/PowerShell
# 追加結果を確認
$ cat aqua.yaml
... 省略
packages:
  - name: PowerShell/PowerShell@v7.4.6

リリース一覧を見るとPowerShellのリリースペースは早いですね。どうでもいいですが、aquaのリリース一覧はバージョン順ではなく、リリース日付順になっています。日頃バージョンを扱っててバージョン順序はライブラリの思想出るなぁと思っているのですが、aquaはGitHub APIから取得したまま表示しているようです。1

aquaのパッケージバージョンの表示

Github Releaseの表示

アンインストール

アンインストールは次のコマンドで行います。

$ aqua rm -m lp PowerShell/PowerShell

まとめ

aquaを使ったPowerShellインストールは、aptと違ってパスごとのバージョン管理できるのが便利です。シェルなので単一でもいいというのはありますが、いろいろなバージョンを試すにはきわめて有用です。

あと、apt経由のPowerShellインストールはパッケージリポジトリを導入する必要があり面倒なんですよね。PowerShellはUbuntuに限らずディストリビューションの標準パッケージに配信していないのでインストール面倒なのはちょっと困ったものです。Windowsですら配信していない一貫した姿勢なのは好きです。


  1. aquaのGitHubReleaseClient.ListReleasesでバージョンを一覧取得するのですが、その実装はgo-githubを使うようです。go-githubRepositoriesService.ListReleasesメソッドが処理の実体で、GitHub APIのhttpエンドポイントList releasesを特にいじることなく使っています。