エディタとPowerShellで開発しよう！君だけのWindowsサービス

# ３行で要約

- PowerShellスクリプトだけで動作するサービスのテンプレート（150行程度）

- Visual Studioでのコンパイル不要、エディタだけで開発できます

- 簡易的なサービス提供や、サービスの調査、学習用途にお使いください


## はじめに

[こちら](https://qiita.com/magiclib/items/b4e2e919f02af1b04b58)の記事でPowerShellスクリプトでサービス実装する方法の解説を書きました。

上記記事で紹介したスクリプトは軽い気持ちで使うにはハードルが高いので、不要な機能を削除して利用しやすいようなテンプレートにまとめました。


## サービステンプレート


### ソースコード

以下にソースを貼ります。


```PowerShell:ServiceTemplate.ps1
[CmdletBinding(DefaultParameterSetName = 'Status')]
Param(
  [Parameter(ParameterSetName = 'Start', Mandatory = $true)]
  [Switch]$Start,
  [Parameter(ParameterSetName = 'Stop', Mandatory = $true)]
  [Switch]$Stop,
  [Parameter(ParameterSetName = 'Status', Mandatory = $false)]
  [Switch]$Status,
  [Parameter(ParameterSetName = 'Setup', Mandatory = $true)]
  [Switch]$Setup,
  [Parameter(ParameterSetName = 'Remove', Mandatory = $true)]
  [Switch]$Remove,
  # 以下は内部呼び出し用
  [Parameter(ParameterSetName = 'Service', Mandatory = $true)]
  [Switch]$Service,
  [Parameter(ParameterSetName = 'SCMStart', Mandatory = $true)]
  [Switch]$SCMStart,
  [Parameter(ParameterSetName = 'SCMStop', Mandatory = $true)]
  [Switch]$SCMStop
)

$ps1Info = Get-Item $MyInvocation.MyCommand.Definition
$scriptPath = $ps1Info.fullname
$scriptDir = $ps1Info.DirectoryName
$serviceName = $ps1Info.basename  # TODO サービス名
$serviceDisplayName = "PowerShell Service Template" # TODO サービスの説明
$exePath = "$scriptDir\$serviceName.exe"
$logPath = "$scriptDir\$serviceName.log"
$exitEventName = "Global\Event_ServiceTemplate_exit"  # TODO 複数サービス登録する場合は重複しないように書き換える

#-----------------------------------------------------------------------------#
# CSharp - Service Source Code 
$execScriptPath = $scriptPath -replace "\\", "\\"
$serviceSource_cs = @"
using System;
using System.Diagnostics;
using System.ServiceProcess;

public class ServiceTemplate : ServiceBase {   
  private void ExecuteProcess(string executePath , string param){
    Process p = new Process();
    p.StartInfo.UseShellExecute = false;
    p.StartInfo.RedirectStandardOutput = false;
    p.StartInfo.FileName = executePath;
    p.StartInfo.Arguments = param;
    p.Start();
    p.WaitForExit();
  }
  protected override void OnStart(string [] args) {
      ExecuteProcess("PowerShell.exe", "-ExecutionPolicy Bypass -c & '$execScriptPath' -SCMStart");
  }
  protected override void OnStop() {
      ExecuteProcess("PowerShell.exe", "-ExecutionPolicy Bypass -c & '$execScriptPath' -SCMStop");
  }
  public static void Main() {
      ServiceBase.Run(new ServiceTemplate());
  }
}
"@

#-----------------------------------------------------------------------------#
# event functions
Function Send-ServiceEvent () { 
  Param(
    [Parameter(Mandatory = $true)]
    [String]$EventName
  )
  [System.Threading.EventWaitHandle]::OpenExisting($EventName).set()
}

Function wait-ServiceEvent () {
  Param(
    [Parameter(Mandatory = $true)]
    [String]$EventName,
    [Parameter(Mandatory = $false)]
    [String]$Timeout = -1
  )
  $serviceEvent = New-Object -TypeName System.Threading.EventWaitHandle -ArgumentList $false, 0, $EventName
  $serviceEvent.WaitOne($Timeout, $false)
}

#-----------------------------------------------------------------------------#
# Service Script Main
$Status = ($PSCmdlet.ParameterSetName -eq 'Status')
if ($Start) {
  Start-Service $serviceName
}
if ($Stop) {
  Stop-Service $serviceName
}
if ($Status) {
  try {
    $sv = Get-Service $serviceName -ea stop
    $sv.Status
  }
  catch {
    "Not Installed"
  }
}
if ($Setup) {
  try {
    Get-Service $serviceName -ea stop > $null
    exit 0
  }
  catch {
  }
  try {
    Add-Type -TypeDefinition $serviceSource_cs -Language CSharp -OutputAssembly $exePath -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false
    New-Service $serviceName $exePath -DisplayName $serviceDisplayName -StartupType Automatic > $null
  }
  catch {
    $_.Exception.Message
  }
}
if ($Remove) {
  try {
    Get-Service $serviceName -ea stop > $null
    Stop-Service $serviceName
    sc.exe delete $serviceName > $null
  }
  catch {
    $_.Exception.Message
  }
}
if ($SCMStart) {
  Start-Process PowerShell.exe -ArgumentList ("-c & '$scriptPath' -Service")
}
if ($SCMStop) {
  Send-ServiceEvent $exitEventName
}
if ($Service) {
  try {
    $timeout = 10 * 1000  # TODO 実行間隔(ミリ秒)
    do {
      # TODO カスタムする処理をここに記載する
      $logString = Get-Date -Format "yyyy/MM/dd HH:mm:ss"
      $logString | Out-File "$logPath" -Encoding utf8 -Append 

      $signale = wait-ServiceEvent $exitEventName $timeout
    } while ($signale -eq $false)
  }
  catch {
    $_.Exception.Message
  }
}

```

任意のパスにこのスクリプトをコピペしてファイル保存してください。


### 使い方

以下のオプションが使えます。
管理者権限のPowerShellから呼び出し例のようにコマンドを実行してください。
Setupでサービス登録したあとにStartでサービス開始してください。

- サービスの制御には管理者権限が必要です。
- セキュリティでエラーが出る場合は[実行ポリシー](https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3)を設定してください。
 

| オプション | 説明 | 呼び出し例 | 備考 |
|-----------|------|-----------|------|
| Setup | サービスの登録| ServiceTemplate.ps1 -Setup |exeのビルドとサービス登録を行う|
| Remove | サービスの解除| ServiceTemplate.ps1 -Remove |サービス解除のみ ※exeやログは消しません|
| Start | サービスの開始 | ServiceTemplate.ps1 -Start|コマンドだけでなくサービス画面からの開始もできます|
| Stop | サービスの停止 | ServiceTemplate.ps1 -Stop |コマンドだけでなくサービス画面からの停止もできます|
| Status | サービスの状態表示 | ServiceTemplate.ps1 -Status ||



### カスタム方法

- サービス名

以下の記述を変更して他と重複しないサービス名を設定してください。

```
$serviceName = $ps1Info.basename  # TODO サービス名
```


- サービス説明

以下の記述を変更してサービスの説明を設定してください。

```
$serviceDisplayName = "PowerShell Service Template" # TODO サービスの説明
```


- 処理の追加

実行間隔が以下の変数で指定してあるので、任意で変更してください。

```
    $timeout = 10 * 1000  # TODO 実行間隔(ミリ秒)
```


以下のコメントの後にサービスで動作させたい処理を記載してください。


```
      # TODO カスタムする処理をここに記載する
```

テンプレートでは10秒毎にスクリプトと同じフォルダへログファイル書き込みしています。


## おわりに

サービスを作るにはVisual Studioでプロジェクトから作る必要があり利用のハードルが高かったですが、このテンプレートを利用すればエディタだけでサービス開発できるのでいろんな用途で使えると思います。

サービス提供やデバッグにはログが必要と思いますが、テンプレートではまともなログ機能を実装していないので、[こちらの記事](https://qiita.com/magiclib/items/4fe5bab3e879dd8de951)を参考にlog4netを使えば楽かと思います。


# 参考：私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/b4e2e919f02af1b04b58)
[私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)

[サンプルソース置き場](https://github.com/part-time-penguin/qiita_powershell_samples)