2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

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

Posted at

3行で要約

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

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

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

はじめに

こちらの記事で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でサービス開始してください。

  • サービスの制御には管理者権限が必要です。
  • セキュリティでエラーが出る場合は実行ポリシーを設定してください。
オプション 説明 呼び出し例 備考
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でプロジェクトから作る必要があり利用のハードルが高かったですが、このテンプレートを利用すればエディタだけでサービス開発できるのでいろんな用途で使えると思います。

サービス提供やデバッグにはログが必要と思いますが、テンプレートではまともなログ機能を実装していないので、こちらの記事を参考にlog4netを使えば楽かと思います。

参考:私PowerShellだけど…シリーズ

私PowerShellだけど、君のタスクトレイで暮らしたい
私PowerShellだけど「送る」からファイルを受け取りたい(コンテキストメニュー登録もあるよ)
私powershellだけどタスクトレイの片隅でアイを叫ぶ
私PowerShellだけど子を持つ親になるのはいろいろ大変そう
私PowerShellだけどあなたにトーストを届けたい(プログレスバー付)
私Powershellだけど日付とサイズでログを切り替えたい(log4net)
私PowerShellだけどスクリプトだけでサービス登録したい

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?