【PowerShell】PowerShell版sudoを用いてタッチパッドのON/OFFを切り替えるコマンド

# 背景
ノートPCのタッチパッドの調子が悪く、
勝手にタッチの判定がされるなどの不具合事象が起きる端末がありました。

この状態になると、GUIでポインタを操作してパッドをオフにしてという操作をすることも中々難しくなる。
勿論マウスを持っていればいいですが、自分はマウスは持ち歩かないタイプの人なのです...

Winodowsさんは中々優秀で、ポインティングデバイスがなくてもキーボードだけで結構操作できてしまう。
それこそブラウザ操作もVimiumのようなプラグインを使えば全然操作できる(なんならマウスで操作するよりも快適)

ということで、こんな事象が起きたときに備えてコマンドでタッチパッドをOFFにできたらいいなということで、それ用のコマンドを作成しました。


# 事前まとめ
- `Get-PnpDevice`というコマンドを用いてデバイス情報を取得する
- `Disable-PnpDevice`というコマンドを用いて対象のデバイスをオフにする(ONの場合は`Enable-PnpDevice`)
- `Disable-PnpDevice`や`Enable-PnpDevice`を実行するには管理者権限で実行する必要がある。
- `Start-Process`を使い、一時的に管理者権限のプロセスを立ち上げその上でコマンドを実行させる必要がある。

## 対象のデバイスの情報を取得する。
今回では対象を「タッチパッド」とします。
以下のコマンドにより取得可能です。

```powershell:タッチパッドPnpDeviceオブジェクトを取得
Get-PnpDevice -FriendlyName "HID 準拠タッチ パッド" 
```

```powershell:結果
Status     Class           FriendlyName              InstanceId
------     -----           ------------              ----------
OK         HIDClass        HID 準拠タッチ パッド       HID\DELL...
```

HIDは「Human Interface Device(ヒューマン・インターフェイス・デバイス)」の略です。
このFriendLyNameはWindowsに備わっている標準ドライバのひとつと思われるので、どのWindows環境でも同様と感がます(少なくとも私の別端末でも同様の名前で取得することができた)

# 必要となる操作
以下、タッチパッドのON/OFFを操作するためには、どのような操作を必要とするのか、
ひとつずつ見ていきます。

## 対象のデバイスを無効にする

```powershell:無効化
Get-PnpDevice -FriendlyName "HID 準拠タッチ パッド" | Disable-PnpDevice

```

```powershell:結果
発生場所 行:1 文字:47
+ Get-PnpDevice -FriendlyName "HID 準拠タッチ パッド" | Disable-PnpDevice
+                                               ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (Win32_PnPEntity...E10349&0&0001"):ROOT\cimv2\Win32_PnPEntity) [Disable-PnpDevice], CimException
    + FullyQualifiedErrorId : HRESULT 0x80041001,Disable-PnpDevice
```

この情報だけだと何が要因でエラーが起きているのかを示す程に十分な情報がありませんが、
同様のコマンドを管理者権限のプロセス上で実行すると問題なく実行することができました。

```powershell:管理者権限での実行例
Get-PnpDevice -FriendlyName "HID 準拠タッチ パッド" | Disable-PnpDevice

確認
この操作を実行しますか?
対象 "Win32_PnPEntity: HID 準拠タッチ パッド (DeviceID = "HID\XXXXXXXXXXXX\XXXXXXXXXXX")" に対して操作
"Disable" を実行しています。
[Y] はい(Y)  [A] すべて続行(A)  [N] いいえ(N)  [L] すべて無視(L)  [S] 中断(S)  [?] ヘルプ (既定値は "Y"): 

```

実行確認のダイアログメッセージが表示されました。

# 課題と対策
上記の操作をコマンドとしてひとまとめにするには、
以下のような仕様を盛り込む必要がありそうです。

- コマンド実行時に自動で管理者権限に移行し対象の操作を行う。
- 実行確認のダイアログをスキップさせる

## PowerShellでsudoのように一時的に管理者権限に移行して実行する
Unix系の環境であれば、``sudo``や``su -c``などが思い浮かびますが、
これをPowerShell上で実現するにはどうすればいいのか？

以下の方法で近しいことが可能です。
```powershell:一時的に管理者権限に移行してコマンドを実行
Start-Process powershell.exe -Verb runAs -ArgumentList ("コマンド")
```

``Start-Process``を用いているように、
PowerShell自体には``su``や``sudo``と言った**直接的に管理者権限に移行できるコマンドは無い**模様。
上記のコマンドで行っていることは、新たなプロセスとして管理者権限でPowerShellのプロセスを立ち上げて、``-ArgumentList``にコマンドを渡すことで、結果的にpowershell.exeが引数の文字列(コマンド)を解釈して実行するという流れになっています。

この時、管理者権限で実行されたシェルが新規ウィンドウとして立ち上がるため、Unix系に慣れている人からすると違和感が覚えると思います...

## 実行確認のダイアログをスキップさせる
こちらは簡単。
``Disable-PnpDevice``に以下のオプションを付与することで実現可能です。

```powershell:確認ダイアログのスキップ
Disable-PnpDevice -Confirm:$false
```

# コマンドを作る
以下作成したコマンドです。
無効にするコマンドと有効にするコマンドで2つ作っています。

## ソースコード

### タッチパッドを無効にするコマンド(Disable-TouchPad)
```powershell:Disable-TouchPad
function Disable-TouchPad(){
    #状態判定処理
    $obj = Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド"
    if($obj.Status -eq "Error"){
        Write-Host "タッチパッドは既に無効です。"
        return
    }    

    #ArgumentListに渡すコマンドを事前に定義。
    $command1 = 'Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド" '
    $command2 = 'Disable-PnpDevice -Confirm:$false'

    Start-Process powershell.exe -Verb runAs -ArgumentList (" $command1 | $command2 ")
     
    #処理結果判定

    $status = (Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド").Status
    $limit = 10
    #$count = 0
    while($true){
        #状態判定
        if(!($Status -eq "Error")){
            Start-Sleep -Seconds 0.5
            $status = (Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド").Status
            $count += 0.5
            #$count
            }else{
                Write-Host "Disable-TouchPad:Success"
                return
            }
        #ループbreak判定
        if($count -eq $limit){
            Write-Host "Disable-TouchPad:failure"
            throw "Error:Coudn't Disable TouchPad"
        }
    }
}
```

### タッチパッドを無効にするコマンド(Enable-TouchPad)
```powershell:Enable-TouchPad
function Enable-TouchPad() {
    #状態判定処理
    $obj = Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド"
    if($obj.Status -eq "OK"){
        Write-Host "タッチパッドは既に有効です。"
        return
    }
    
    #ArgumentListに渡すコマンド事前に定義
    $command1 = 'Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド" '
    $command2 = 'Enable-PnpDevice -Confirm:$false'

    #有効化処理
    Start-Process powershell.exe -Verb runAs -ArgumentList (" $command1 | $command2 ")

    #処理結果判定
    $status = (Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド").Status
    $limit = 10

    while($true){
        #状態判定
        if(!($Status -eq "OK")){
            Start-Sleep -Seconds 0.5
            $status = (Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド").Status
            $count += 0.5
           #$count
        }else{
            Write-Host "Enable-TouchPad:Success"
            return
        }
        #ループbreak判定
        if($count -eq $limit){
            Write-Host "Enable-TouchPad:failure"
            throw "Error:Coudn't Enable TouchPad"
        }
    }
}
```

## 作成時の備忘

### -ArgumentListが思うように展開されない問題

ソースコード内では``-FriendlyName``の指定を``"HID?準拠タッチ?パッド"``としている。
これは文字列リテラル内にスペースが存在すると問題が生じたためでだ。

例えば以下のコードはコマンドがうまく通らない
```powershell:-ArgumentList失敗例
$command = 'Get-PnpDevice -FriendlyName "HID 準拠タッチ パッド"  | Disable-PnpDevice; pause'
Start-Process powershell.exe -Verb runAs -ArgumentList ($command)
```

```powershell:結果
Get-PnpDevice : 引数 '準拠タッチ' を受け入れる位置指定パラメータ
ーが見つかりません。
発生場所 行:1 文字:1
+ Get-PnpDevice -FriendlyName HID 準拠タッチ パッド | Disable-Pnp
Device; pause
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Get-PnpDevice]、ParameterBindingException
    + FullyQualifiedErrorId : PositionalParameterNotFound,Get-Pn
   pDevice

続行するには、Enter キーを押してください...:
```

「``準拠タッチ``を受け入れる位置指定パラメータがみつからない」というエラー内容から、
本来文字列リテラルとしてひとまとめで渡されるべき値がスペース区切りでそれぞれ引き渡されているということが推察できる。
つまり、以下のような形で渡されていると考えられる。
```powershell
Get-PnpDevice -FriendlyName HID 準拠タッチ パッド
#-FriendlyName:HID
#args1:準拠タッチ
#args2:パッド
```
この対策としてスペースを作らないように書き換えたのが以下だ
```powershell:ワイルドカードでスペースを埋める
$command1 = 'Get-PnpDevice -FriendlyName "HID?準拠タッチ?パッド" '
```

また以下のように全体をダブルクォーテーションで囲み、``-FriendLyName``のパラメータをシングルクォーテーションで囲む方法でも可能だった。

```powershell:シングルクォーテーションで全体を囲む
$command = "Get-PnpDevice -FriendlyName 'HID 準拠タッチ パッド' | Disable-PnpDevice; pause"
```

これに関してはシングルクォーテーションとダブルクォーテーションの展開の仕様に違いが起因していることは間違いないが、明快な説明ができるほどあまりよくわかっていない。

ただ、シングルクォーテーションの方が、厳格な文字列リテラル表記となるため、恐らくダブルクォーテーションでは``-ArumentList``のパラメータとして渡した値が展開され、結果クォーテーションが外れてしまうのだと考えられる。

### 状態判定が早すぎると失敗する
コードの中で切り替え後にステータスが変わっているか判定する処理を入れている。
先のコードでは、while文により無限ループを作り、``$limit``で定義した時間まで判定処理を継続させるように作ってある。
元々はシンプルに実行後にif文で判定を処理を行うだけだったのだが、このやり方では必ず判定処理で失敗した。
ただし実際にはタッチパッドの有効/無効の切り替えは問題なく完了されているのであった。
結果、原因はステータスの反映までに時間が掛かるためであった。
0.5秒間隔で判定処理を行っているが、自分の環境では5回(2.5秒)分まで目的のステータスに遷移しなかった。

### -ArgumentList内で指定するコマンドを変数に格納可能。さらにパイプで繋ぐことも可能。
これはtipsのような備忘
``-ArgumentList``のパラメータとして長々とコマンドラインを記述するのはかなり可読性を低下させるため、
事前に変数に格納して実行する仕様にした。
このとき``Invoke-Expression``を使わずともそのまま実行可能であった。
また複数のコマンドをパイプ処理させることも可能であった。

## 留意点
このコマンドはOSからデバイスを外しているようなイメージ
Windowsの設定内のタッチパッドの項目にはタッチパッドのオン/オフをスイッチすることができるが、
これをコマンドで実現している訳ではない。
設定からのON/OFFの場合はあくまでデバイス自体は有効化されたまま、タッチパッドの機能をオフにしている。

CLI上でデバイスを有効化させたまま、タッチパッドだけをOFFにする操作が調べても見当たらなかったため、
デバイス自体を無効化する手段を取った。

## 余談
Get-PnpDeviceで取得できるデバイスはタッチパッドに限らないため、
他のデバイスを付け外しさせる際にも汎用的に使えるはず。
※PnP⇒プラグアンドプレイの略と考えられるため、このコマンドレットで取得できるデバイスはOS動作中に外しても問題ないデバイスのはずだ。

# 参考
https://learn.microsoft.com/en-us/powershell/module/pnpdevice/get-pnpdevice?view=windowsserver2022-ps

https://learn.microsoft.com/ja-jp/windows/win32/cimwin32prov/win32-pnpdevice