私PowerShellだけどスクリプトだけでサービス登録したい


# 3行で要約

- PowerShellスクリプト(ps1ファイル)のみでサービスを実装する方法の紹介と解説

- サービス制御マネージャー (SCM) への登録はexeファイルが必須のため、SCMとps1スクリプトを仲介する仕組みが必要

- MS公式ではPowerShellスクリプトからexeを出力する方法で解決していた


## ネタ元

[PowerShell での Windows サービスの作成](https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2016/may/windows-powershell-writing-windows-services-in-powershell)

PowerShellとSCMの知識がある人はこの記事を読む必要ありません。上記サイトを読めば実装できると思います。


## はじめに

PowerShellでタスクトレイに常駐するスクリプトを[こちら](https://qiita.com/magiclib/items/cc2de9169c781642e52d)で紹介していますが、ログイン中のみしか動作しません。
Windows OSが起動したらログインしていなくても常駐するサービスが必要になったので調査してみたところ上記ネタ元の記事がヒットしました。
ソースが長くわかりにくいので、自分の理解を整理するために記事として残しておきます。


## サービスとは？

Linuxで常駐するプロセスはcronやsystemdで管理していますが、
Windowsだとサービス制御マネージャー (SCM)が管理しています。

[お作法](https://learn.microsoft.com/ja-jp/dotnet/framework/windows-services/walkthrough-creating-a-windows-service-application-in-the-component-designer)に従ってSCMと対話できる仕組みを持った実行ファイル(exeファイル)を用意する必要があります。

PowerShellのps1スクリプトでは直接SCMと対話できないので、SMCとps1を仲介するexeファイルが必要となります。


## ネタ元のスクリプトを動作確認

ネタ元の記事にサービスとして動くPowerShellスクリプトがあるので動きを確認しましょう。

記事ページ内のスクリプトのリンクが切れているので、記事の作者がgithubで公開しているスクリプトを入手しましょう。

[PSService.ps1](https://github.com/JFLarvoire/SysToolsLib/blob/master/PowerShell/PSService.ps1)


- 注意

  -  ダウンロードしたファイルそのままだと実行が制限されるので一度エディタでPSService.ps1を開いてから保存しなおしましょう。

  - セキュリティでエラーが出る場合は[実行ポリシー](https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3)を設定してください。
 

### サービス登録＋開始

PSService.ps1を任意のパスに配置して管理者で起動したpowershellから以下のコマンドを実行しましょう。

```
ステータス取得(未登録)
> .\PSService.ps1 -Status
Not Installed

サービス登録
> .\PSService.ps1 -Setup

ステータス取得(登録済：停止中)
> .\PSService.ps1 -Status
Stopped

サービス開始（開始されるまで数秒待機が必要です）
> .\PSService.ps1 -Start
警告: サービス 'A Sample PowerShell Service (PSService)' の開始を待っています...
警告: サービス 'A Sample PowerShell Service (PSService)' の開始を待っています...
警告: サービス 'A Sample PowerShell Service (PSService)' の開始を待っています...
警告: サービス 'A Sample PowerShell Service (PSService)' の開始を待っています...
```

### PSServiceサービス解説


#### サービス登録情報


「サービス」画面で登録情報を確認します。
コンピュータの管理からサービスを選択するか、
「ファイル名を指定して実行」で「services.msc」を起動します。
開始、停止、再起動はコマンドだけではなくこちらのUIでも操作可能です。

「A Sample PowerShell Service」として登録されています。


![service1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264524/93d192d3-0934-86af-455f-02e812bfdfd4.png)
![service2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/264524/c13df063-75f7-c386-882a-efc5628e0867.png)



PSService.ps1の60行目あたりの文字列で登録されていることが確認できます。


```powershell
  $serviceDisplayName = "A Sample PowerShell Service"
  $ServiceDescription = "Shows how a service can be written in PowerShell"
```


実行ファイルのパスに「C:\Windows\System32\PSService.exe」が設定されています。これがSCMとPSService.ps1を仲介しています。


#### PSService.exeの解説


setupコマンドを実行されたらサービス用のexeを生成する動きになっています。


「exeが無いならビルドすればいいじゃない？」


いや、そうなんだけどsetup時にexeを生成するというアイディアを短いコードで実現してるのがすごい。。。



仕組みとしてはPSService.ps1の355行に次のコメントがあります。
```powershell
#   Description     C# source of the PSService.exe stub    
```

ps1のスクリプトにC#のソースを持ち、PSService.exeというstubを生成しています。
中身は難しくないC#なので読めばわかりますが、
SCMと対話するためのOnStart(),OnStop()を実装し、それぞれイベントでPSService.ps1に引数を指定してキックする動作になっています。

```powershell
        Process p = new Process();
        // Redirect the output stream of the child process.
        p.StartInfo.UseShellExecute = false;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.FileName = "PowerShell.exe";
        p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes.
        p.Start();
```


#### サービスの定期実行をログで確認

サービスとして動作していることはログで確認できます。
C:\Windows\Logs\PSService.log
にログが出力されています。

10秒毎にログを出力する動きになっているようです。
476行目でログ出してる処理あるので定期的な実行処理をカスタムする場合はこのあたりのソースを修正すれば良さそうです。


```powershell
"TimerTick" { # Example. Periodic event generated for this example
          Log "$scriptName -Service # Timer ticked"
        }
```



### サービス停止＋解除

停止、解除もコマンドで行います。

```
サービス停止（停止されるまで数秒待機が必要です）
> .\PSService.ps1 -Stop
警告: サービス 'A Sample PowerShell Service (PSService)' の停止を待っています...
警告: サービス 'A Sample PowerShell Service (PSService)' の停止を待っています...
警告: サービス 'A Sample PowerShell Service (PSService)' の停止を待っています...
警告: サービス 'A Sample PowerShell Service (PSService)' の停止を待っています...

サービス解除
> .\PSService.ps1 -Remove
```


ここまでの解説が理解できればPSService.ps1をカスタムして任意の処理をサービス実行することは難しくないと思います。

仕組みは理解できなくても、
708行目に以下コメントがありますので、このあたりに自作コードを追加すればそれっぽく実装できると思います。


```powershell
######### TO DO: Implement your own service code here. ##########
```


### 補足：サービス開発でよくあるハマるポイント


- サービスはSYSTEMユーザーで実行されるので、ログインしたユーザーと権限や環境変数が違います。ログインして動作確認したスクリプトでもSYSTEMユーザーでは動作しない場合がある。

- デスクトップ対話はOFFのため画面を出すことはできません。画面を出したい場合はログインしているユーザーのプロセスとして別に起動する必要があります。サービスと通信するためにプロセス間通信の知識が必要になります。

- デバッグ環境が用意できないです。地道にログに出力しながらデバッグする必要があります。Visual studio Code とかでデバッグできると楽なのですが、セッションIDが違うプロセスに対してVisual studio Codeがアタッチできるのか不明。

- デバッグ中にウィルスチェッカーで検出されがちなので、駆除されてファイルが消えても驚かない。

## おわりに

PSService.exeの仕組みは、スクリプトを仲介するexeを作るの面倒がらずにちゃんと作ってて素晴らしいです。逆に言うと手抜きでサービス実装することは難しいということでもありますが。

setup時に自動生成する点も使いやすくステキですね。

PSService.exeの仕組みを流用すればpythonなどのPowerShell以外のスクリプトをサービス化できそうです。

### 追記(2023/06/19)

コピペで動作するシンプルなスクリプトを作りました。

[エディタとPowerShellで開発しよう！君だけのWindowsサービス](https://qiita.com/magiclib/items/0d5fb158010a9df6b24b)


# 私PowerShellだけど…シリーズ

[私PowerShellだけど、君のタスクトレイで暮らしたい](https://qiita.com/magiclib/items/cc2de9169c781642e52d)
[私PowerShellだけど「送る」からファイルを受け取りたい（コンテキストメニュー登録もあるよ）](https://qiita.com/magiclib/items/7529557f756af9c8d21b)
[私powershellだけどタスクトレイの片隅でアイを叫ぶ](https://qiita.com/magiclib/items/9d0cd35814b6fcff2e44)
[私PowerShellだけど子を持つ親になるのはいろいろ大変そう](https://qiita.com/magiclib/items/ef0ab5c1fd8e0260b39d)
[私PowerShellだけどあなたにトーストを届けたい（プログレスバー付）](https://qiita.com/magiclib/items/12e2a9e1e1e823a7fa5c)
[私Powershellだけど日付とサイズでログを切り替えたい(log4net)](https://qiita.com/magiclib/items/4fe5bab3e879dd8de951)
[私PowerShellだけどスクリーンショットを撮影したい](https://qiita.com/magiclib/items/91f7d32cbd766b39b293)
[私PowerShellだけどマウスカーソル付きのスクリーンショットを撮影したい](https://qiita.com/magiclib/items/c867525aa83db5a00ebb)
[私PowerShellだけどスリープ抑制したい](https://qiita.com/magiclib/items/352e46f86d3d0e2e9c7b#%E7%A7%81powershell%E3%81%A0%E3%81%91%E3%81%A9%E3%82%B9%E3%83%AA%E3%83%BC%E3%83%97%E6%8A%91%E5%88%B6%E3%81%97%E3%81%9F%E3%81%84)