Windows PowerShell – 在 PowerShell 中编写 Windows 服务

Windows 服务通常就是编译的程序用 C、 c + +、 C# 或其他 Microsoft 基于.NET Framework 的语言,编写并调试此类服务可能会相当困难。在几个月前,通过允许编写服务作为简单的 shell 脚本,其他操作系统启发我开始想知道是否有可能会更简单的方法以及在 Windows 中,创建它们。

这篇文章介绍了此项工作成果的最终结果 ︰ 新颖简便的方法来创建 Windows 服务,通过在 Windows PowerShell 脚本语言中编写它们。没有更多的编译,就可以在任何系统上,而不仅仅是开发人员自己完成一个快速的编辑测试周期。

我提供一个称为 PSService.ps1,以便您可以创建并以分钟为单位,与只是记事本等文本编辑器中测试新的 Windows 服务的通用服务脚本模板。此技术可以保存任何人如想尝试使用 Windows 服务的很大的时间和开发工作量,或甚至提供针对 Windows 的实际服务时不考虑性能的关键因素。可以从下载 PSService.ps1 bit.ly/1Y0XRQB

什么是 Windows 服务?

Windows 服务都是在后台,无需用户交互运行的程序。例如,Web 服务器以无提示方式响应 HTTP 请求的网页从网络,是一种服务,因为是以无提示方式记录性能统计数据或记录硬件传感器事件监视应用程序。

在系统引导时,服务可以自动启动。它们可以按需或启动,如由依赖于它们的应用程序请求。服务在不同于用户界面会话其 Windows 会话中运行。它们运行在大量的系统进程,以仔细选择的权限来限制安全风险。

Windows 服务控制管理器

这些服务进行管理的 Windows 服务控制管理器 (SCM)。SCM 负责配置服务,启动它们,这样做,依此类推。

SCM 控件面板是可通过控制面板访问 |系统和安全 |管理工具 |服务。作为 图 1 所示,它显示所有配置服务,其名称、 说明、 状态、 启动类型和用户名称的列表。

Windows 10 中的 Windows 服务控制管理器 GUI
图 1 Windows 10 中的 Windows 服务控制管理器 GUI

还有 scm 的命令行界面 ︰

  • 旧 net.exe 工具,运用其已知的”net start”和”net stop”命令,从日期可以追溯到 MS-DOS ! 尽管它的名称,它可以用于启动和停止任何服务,而不仅仅是网络服务。键入”net 帮助”,有关详细信息。
  • 名为 sc.exe; 在 Windows NT 中引入的功能更强大工具提供了很好的控制服务管理的各个方面。类型”sc /?”的详细信息。

这些命令行工具,尽管仍会存在于 Windows 10 是现在支持弃用稍后介绍的 Windows PowerShell 服务管理功能。

难题 ︰ Net.exe 和 sc.exe 使用”short”的一个单词服务名称,其中,遗憾的是,并非由 SCM 控件面板显示的更具说明性的名称相同。若要获取这两个名称之间的对应关系,请使用 Windows PowerShell get-service 命令。

服务状态

服务可以在各种状态。某些状态是必需,有些则是可选。停止和启动所有服务都必须都支持两个基本状态。这些显示分别 (空白) 或在状态列下运行 图 1

第三个可选状态已暂停。并卸载的每个服务支持,即使未提及的另一种隐式状态。

服务可以这些状态之间进行转换,如中所示 图 2

服务状态
图 2 服务状态

最后,还有,服务可能会选择性地分别支持多个暂时状态 ︰ Startpending 状态,StopPending,PausePending,ContinuePending。这些是时间的仅在状态转换需要很长很有用。

Windows PowerShell 服务管理功能

Windows PowerShell 自 Windows Vista 以来已建议的系统命令行管理程序。它包括强大的脚本语言和功能,用于管理操作系统的所有方面的大型库。一些 Windows PowerShell 的优势包括 ︰

  • 一致的函数名称
  • 完全面向对象的
  • 方便地进行管理的任何.NET 对象

Windows PowerShell 提供了许多服务管理功能,称为 cmdlet。图 3 显示了一些示例。

图 3 Windows PowerShell 服务管理功能

函数名称 说明
启动服务 启动一个或多个已停止服务
停止服务 停止一个或多个正在运行的服务
新服务 安装新的服务
获取服务 使用它们的属性的本地或远程计算机上获取的服务
设置服务 启动、 停止和暂停服务,并更改其属性

 

对于具有字符串在其名称中的”服务”的所有命令的完整列表,运行 ︰

复制
Get-Command *service*

对于只是服务管理功能的列表,运行 ︰

复制
Get-Command -module Microsoft.PowerShell.Management *service*

自然地,没有用于删除 Windows PowerShell 函数 (即,卸载) 服务。这是一个极少数情况下仍然需要使用旧的 sc.exe 工具时 ︰

复制
sc.exe delete $serviceName

.NET ServiceBase 类

所有服务必须都创建一个.NET 对象,该对象从 ServiceBase 类派生的。Microsoft 文档介绍所有属性和类的方法。图 4 列出的几个特别值得关注此项目的子集。

图 4,某些属性和方法 ServiceBase 类

成员 说明
ServiceName 用于标识服务对系统的短名称
CanStop 是否可以停止服务一次它已启动
Onstart () 当服务启动时要执行的操作
Onstop () 该服务停止时要执行的操作
Run () SCM 向注册服务可执行文件

 

通过实现这些方法,将能由 SCM 为自动启动,在启动时或按需; 管理服务应用程序它将是可管理由 SCM 控件面板、 旧 net.exe 和 sc.exe 命令,或者通过的新 Windows PowerShell 服务管理功能,若要启动或手动停止。

从 Windows PowerShell 脚本中嵌入的 C# 源代码创建一个可执行文件

PowerShell 就能够轻松使用.NET 对象在脚本中。默认情况下,它提供了对于许多.NET 对象类型,足够实现大多数目的的内置支持。较好的做法,它是可扩展的并允许在 Windows PowerShell 脚本以添加对任何其他.NET 功能的支持嵌入短 C# 代码段。通过添加类型命令,其中,尽管其名称,可以完成更多比只将对新的.NET 对象类型的支持添加到 Windows PowerShell 提供此功能。它甚至可以编译和链接的完整 C# 应用到新的可执行文件。例如,此 hello.ps1 Windows PowerShell 脚本 ︰

复制
$source = @"
  using System;
  class Hello {
    static void Main() {
      Console.WriteLine("Hello World!");
    }
  }
"@
Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly "hello.exe"
  -OutputType ConsoleApplication

将创建一个 hello.exe 应用程序,用于打印”Hello world !”:

复制
PS C:\Temp> .\hello.ps1
PS C:\Temp> .\hello.exe
Hello World!
PS C:\Temp>

综合来讲

PSService.ps1 功能基于所有我到目前为止讨论,现在我可以创建我一直梦想可以 PSService.ps1 脚本有关该 Windows PowerShell 服务 ︰

  • 安装和卸载本身 (使用 Windows PowerShell 服务管理功能)。
  • 启动和停止本身 (使用相同的功能集)。
  • 包含一个短 C# 代码段,这将创建 PSService.exe SCM 预期 (使用添加类型命令)。
  • 请 PSService.exe 存根调回至 PSService.ps1 脚本为 (以向 OnStart、 OnStop 及其他事件的响应) 的实际服务操作。
  • 是可由 SCM 控制面板和 (归功于 PSService.exe 存根中) 的所有命令行工具管理。
  • 将具有复原性,并处理成功时中的任何状态的任何命令。(例如,它可以自动卸载它前, 停止该服务或不执行任何操作时要求开始已启动的服务。)
  • 支持 Windows 7 和更高版本的所有 Windows 版本 (使用 Windows PowerShell v2 的功能仅)。

请注意,我将介绍仅 PSService.ps1 设计和实现在这篇文章中的关键部分。示例脚本还包含调试代码和一些支持可选的服务功能,但此处的说明,将不必要地会增加复杂性及其说明。

PSService.ps1 体系结构脚本的组织中的一系列节 ︰

  • 描述该文件中的标头注释。
  • 基于注释的帮助块。
  • 定义命令行开关参数块。
  • 全局变量。
  • Helper 例程 ︰ 现在和日志。
  • C# 源块 PSService.exe 存根 (stub)。
  • 主例程中,处理每个命令行开关。

全局设置

立即在参数块中,下面 PSService.ps1 包含定义全局设置,可以根据需要更改的全局变量。默认值均以 图 5

图 5 全局变量默认值

变量 说明 默认
$serviceName Net start 命令和其他用于一个字词名称 脚本的基名称
$serviceDisplayName 服务的更具描述性名称 示例 PowerShell 服务
$installDir 若要安装的服务文件的位置 ${Env: windir} \System32
$logFile 中要记录服务的消息,其文件的名称 ${ENV:windir}\Logs\$serviceName.log
$logName 在其中记录服务事件的事件日志名称 应用程序

 

该文件的基名称用作服务名称 (例如,PSService.ps1 的 PSService) 允许您只需通过复制的脚本,请重命名的副本,然后安装该副本从同一个脚本,创建多个服务。

命令行参数

为了更易于使用,该脚本支持命令行参数相匹配的所有状态转换,如中所示 图 6

图 6 的状态转换的命令行参数

交换机 说明
-开始 启动服务
-停止 停止服务
-安装 作为一项服务自行安装
-删除 卸载服务

 

(支持暂停状态未实现但可以很容易地添加,请使用相应的状态转换选项。)

图 7 显示了该脚本支持的几个更多的管理参数。

图 7 支持管理参数

交换机 说明
重新启动 停止服务,然后重新启动
-状态 显示该服务的当前状态
服务 运行服务实例 (以仅供 service.exe 存根 (stub))
版本 显示的服务版本
公用参数 -?-Verbose、-调试等等

 

每个状态转换交换机提供两种操作模式 ︰

  • 当调用的最终用户 ︰ 使用 Windows PowerShell 服务管理功能来触发状态转换。
  • 当通过 (间接通过 service.exe 存根) SCM 来调用 ︰ 相应地管理 service.ps1 服务实例。

可以在运行时区分两种情况下,通过检查的用户名称 ︰ 在第一种情况很普通用户 (系统管理员);在第二种情况下它是实际的 Windows 系统用户。系统用户可以确定如下 ︰

复制
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$userName = $identity.Name   # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator"
$isSystem = ($userName -eq "NT AUTHORITY\SYSTEM")

安装

服务安装的目标是将存储在本地目录中,然后声明这 SCM,服务文件的副本,以便它知道要运行以启动该服务的程序。

此处由-安装程序开关执行的操作顺序处理 ︰

  1. 如果有的话,请卸载任何以前实例。
  2. 如果需要,创建的安装目录。(这不所需的默认值 ︰ C:\Windows\System32。)
  3. 将服务脚本复制到安装目录中。
  4. 在该相同安装目录中,从脚本中的 C# 代码段创建一个 service.exe 存根。
  5. 注册服务。

请注意,从一个 Windows PowerShell 源脚本 (PSService.ps1) 开始,我最终会使用 C:\Windows\System32 中安装的三个文件 ︰ PSService.ps1、 PSService.pdb 和 PSService.exe。这三个文件将需要卸载过程中删除。安装是通过在脚本中包括两种代码实现的 ︰

  • 定义的脚本的开头的 Param 块中的安装程序开关 ︰
复制
[Parameter(ParameterSetName='Setup', Mandatory=$true)]
[Switch]$Setup,    # Install the service
  • If 块,如中所示 图 8 用于处理在主例程中脚本的结尾处的安装程序开关。
图 8 安装程序代码处理程序
复制
if ($Setup) {
  # Install the service
  # Check if it's necessary (if not installed,
  # or if this script is newer than the installed copy).
  [...] # If necessary and already installed, uninstall the old copy.
  # Copy the service script into the installation directory.
  if ($ScriptFullName -ne $scriptCopy) {
    Copy-Item $ScriptFullName $scriptCopy
  }
  # Generate the service .EXE from the C# source embedded in this script.
  try {
    Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName
      -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess"
  } catch {
    $msg = $_.Exception.Message
    Write-error "Failed to create the $exeFullName service stub. $msg"
    exit 1
  }
  # Register the service
  $pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName
    -StartupType Automatic
  return
}

启动

负责管理服务机构是 SCM。每次启动操作必须通过 SCM,因此它可以跟踪的服务的状态。因此即使用户想要手动启动启动时将使用服务脚本,必须向 SCM 请求通过完成该启动。在这种情况下,操作的顺序是 ︰

  1. 用户 (管理员) 运行第一个实例 ︰ PSService.ps1-开始。
  2. 此第一个实例通知 SCM 要启动服务 ︰ 启动服务 $serviceName。
  3. SCM 运行 PSService.exe。它将主例程创建一个服务对象,然后调用其 Run 方法。
  4. SCM 调用 OnStart 方法中的服务对象。
  5. 在 C# OnStart 方法运行第二个脚本实例 ︰ PSService.ps1-开始。
  6. 此第二个实例中,现在在后台作为系统用户运行启动第三个实例,它将保留在内存中作为实际的服务 ︰ PSService.ps1 的服务。这是它上次的服务实例,它完成实际的服务任务中,您为所需的任务自定义。

在结束时,将运行的两个任务 ︰ PSService.exe,并运行 PSService.ps1 的 PowerShell.exe 实例的服务。

所有这一点被通过在脚本中有三个代码段 ︰

  • 定义的脚本的开头的 Param 块中了-Start 参数 ︰
复制
[Parameter(ParameterSetName='Start', Mandatory=$true)]
[Switch]$Start, # Start the service
  • 在主例程中,该脚本的末尾 if 块处理了-Start 参数 ︰
复制
if ($Start) {# Start the service
  if ($isSystem) { # If running as SYSTEM, ie. invoked as a service
    Start-Process PowerShell.exe -ArgumentList (
      "-c & '$scriptFullName' -Service")
  } else { # Invoked manually by the administrator
  Start-Service $serviceName # Ask Service Control Manager to start it
  }
  return
}
  • 在 C# 源代码段标识符、 将主例程和运行 PSService.ps1 的 OnStart 方法的处理程序的启动,如中所示 图 9
图 9 开始代码处理程序
复制
public static void Main() {
  System.ServiceProcess.ServiceBase.Run(new $serviceName());
}
protected override void OnStart(string [] args) {
  // Start a child process with another copy of this script.
  try {
    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 = "-c & '$scriptCopyCname' -Start";
    p.Start();
    // Read the output stream first and then wait. (Supposed to avoid deadlocks.)
    string output = p.StandardOutput.ReadToEnd();
    // Wait for the completion of the script startup code,     // which launches the -Service instance.
    p.WaitForExit();
  } catch (Exception e) {
    // Log the failure.
  }
}

获取服务状态

处理程序只需要求 SCM 提供服务状态,并将其发送给输出管道-状态 ︰

复制
try {
  $pss = Get-Service $serviceName -ea stop # Will error-out if not installed.
} catch {
  "Not Installed"
  return
}
$pss.Status

但是,在调试阶段中,您可能会遇到脚本失败,截止日期,例如中脚本以及类似, 的语法错误。在这种情况下,SCM 状态可能会得到不正确。我实际上遇到了好几次准备这篇文章时。为了帮助诊断等等,是比较明智的做法是,请仔细检查和搜索的服务实例 ︰

复制
$spid = $null
$processes = @(gwmi Win32_Process -filter "Name = 'powershell.exe'" | where {
  $_.CommandLine -match ".*$scriptCopyCname.*-Service"
})
foreach ($process in $processes) { # Normally there is only one.
  $spid = $process.ProcessId
  Write-Verbose "$serviceName Process ID = $spid"
}
if (($pss.Status -eq "Running") -and (!$spid)) {
# This happened during the debugging phase.
  Write-Error "The Service Control Manager thinks $serviceName is started,
    but $serviceName.ps1 -Service is not running."
  exit 1
}

停止和卸载

停止和删除操作基本上撤消安装程序并开始做 ︰

  • 阻 (如果用户调用了) 通知 SCM 要停止服务。
  • 如果由系统调用,它只需将取消 PSService.ps1 的服务实例。
  • -删除停止服务,注销它使用 sc.exe delete $serviceName,然后删除安装目录中的文件。

实现也是非常类似于安装程序并开始 ︰

  1. 该脚本开头的 Param 块中的每个开关的定义。
  2. If 块处理中主例程中,在脚本末尾的开关。
  3. 停止操作,在 C# 源代码段中,处理程序 OnStop 方法运行 PSService.ps1-停止。停止操作执行以不同的方式根据用户是否是真实的用户或系统的操作。

事件日志记录

服务在后台,UI 的情况下运行。这使它们难以调试 ︰ 您如何能诊断出了什么问题,当设计使然不可见时? 常用方法是保留的时间戳的所有错误消息的记录以及记录得好,如状态转换的重要事件。

示例 PSService.ps1 脚本实现两个不同的日志记录方法,并使用这两个 (包括在上一代码提取,此处删除,以阐明的基本操作的部分中) 的关键点 ︰

  • 它将事件对象写入应用程序日志,服务名称与源的名称,如中所示 图 10。这些事件对象显示在事件查看器中,并可以筛选搜索使用该工具的所有功能。您还可以使用 Get-eventlog cmdlet 这些条目 ︰

与 PSService 事件的事件查看器
图 10 使用 PSService 事件的事件查看器

复制
Get-Eventlog -LogName Application -Source PSService | select -First 10
  • 消息行写入文本文件,在 Windows 日志目录中,${ENV:windir}\Logs\$serviceName.log,如中所示 图 11。此日志文件,读取使用记事本,并可以使用 findstr.exe 或 grep、 尾部和等 Win32 端口搜索。
图 11 示例日志文件
复制
PS C:\Temp> type C:\Windows\Logs\PSService.log
2016-01-02 15:29:47 JFLZB\Larvoire C:\SRC\PowerShell\SRC\PSService.ps1 -Status
2016-01-02 15:30:38 JFLZB\Larvoire C:\SRC\PowerShell\SRC\PSService.ps1 -Setup
2016-01-02 15:30:42 JFLZB\Larvoire PSService.ps1 -Status
2016-01-02 15:31:13 JFLZB\Larvoire PSService.ps1 -Start
2016-01-02 15:31:15 NT AUTHORITY\SYSTEM & 'C:\WINDOWS\System32\PSService.ps1' -Start
2016-01-02 15:31:15 NT AUTHORITY\SYSTEM PSService.ps1 -Start: Starting script 'C:\WINDOWS\System32\PSService.ps1' -Service
2016-01-02 15:31:15 NT AUTHORITY\SYSTEM & 'C:\WINDOWS\System32\PSService.ps1' -Service
2016-01-02 15:31:15 NT AUTHORITY\SYSTEM PSService.ps1 -Service # Beginning background job
2016-01-02 15:31:25 NT AUTHORITY\SYSTEM PSService -Service # Awaken after 10s
2016-01-02 15:31:36 NT AUTHORITY\SYSTEM PSService -Service # Awaken after 10s
2016-01-02 15:31:46 NT AUTHORITY\SYSTEM PSService -Service # Awaken after 10s
2016-01-02 15:31:54 JFLZB\Larvoire PSService.ps1 -Stop
2016-01-02 15:31:55 NT AUTHORITY\SYSTEM & 'C:\WINDOWS\System32\PSService.ps1' -Stop
2016-01-02 15:31:55 NT AUTHORITY\SYSTEM PSService.ps1 -Stop: Stopping script PSService.ps1 -Service
2016-01-02 15:31:55 NT AUTHORITY\SYSTEM Stopping PID 34164
2016-01-02 15:32:01 JFLZB\Larvoire PSService.ps1 -Remove
PS C:\Temp>

Log 函数使得可以轻松编写此类消息,会自动作为前缀的 ISO 8601 时间戳和当前用户名称 ︰

复制
Function Log ([String]$string) {
  if (!(Test-Path $logDir)) {
    mkdir $logDir
  }
  "$(Now) $userName $string" |
    out-file -Encoding ASCII -append "$logDir\$serviceName.log"
}

示例在测试会话

下面是如何生成上述日志 ︰

复制
PS C:\Temp> C:\SRC\PowerShell\SRC\PSService.ps1 -Status
Not Installed
PS C:\Temp> PSService.ps1 -Status
PSService.ps1 : The term 'PSService.ps1' is not recognized as the name of a cmdlet, function, script file, or operable program.
[...]
PS C:\Temp> C:\SRC\PowerShell\SRC\PSService.ps1 -Setup
PS C:\Temp> PSService.ps1 -Status
Stopped
PS C:\Temp> PSService.ps1 -Start
PS C:\Temp>

这将显示如何在一般情况下使用该服务。请记住,它必须由用户运行的本地管理员权限,以管理员身份运行的 Windows PowerShell 会话中。请注意 PSService.ps1 脚本上的路径,首先,则它是执行的安装程序操作后的是如此。(第一次-不带路径指定的状态调用将失败; 第二个-状态调用成功。)

复制
Calling PSService.ps1 -Status at this stage would produce this output: Running. And this, after waiting 30 seconds:
PS C:\Temp> PSService.ps1 -Stop
PS C:\Temp> PSService.ps1 -Remove
PS C:\Temp>

自定义服务

若要创建您自己的服务,只需执行以下操作 ︰

  • 将示例服务复制到一个新的基名称,例如 C:\Temp\MyService.ps1 具有的新文件。
  • 更改全局变量部分中的长时间的服务名称。
  • 更改收件人就在脚本末尾的-服务处理程序中阻塞。目前,while ($true) 块中只包含唤醒日志文件中每隔 10 秒,日志一条消息的伪代码 ︰
复制
######### TO DO: Implement your own service code here. ##########
###### Example that wakes up and logs a line every 10 sec: ######
Start-Sleep 10
Log "$script -Service # Awaken after 10s"
  • 安装并启动测试 ︰
复制
C:\Temp\MyService.ps1 -Setup
MyService.ps1 -Start
type C:\Windows\Logs\MyService.log

您不必更改脚本,除了可以添加对新的 SCM 功能,如暂停状态的支持外的其余部分中的任何内容。

限制和问题

服务脚本必须在具有管理员权限运行的 shell 中运行,否则将显示各种拒绝访问错误。

在 Windows 版本 XP 为 10 和相应的服务器版本中工作的示例脚本。在 Windows XP 中,您必须安装 Windows PowerShell v2,这并不按默认提供的。下载并安装 Windows Management Framework v2 的 XP (bit.ly/1MpOdpV),其中包括 Windows PowerShell v2。请注意,我已经完成很少测试在该 OS 中,因为它不再支持。

在许多系统上的 Windows PowerShell 脚本执行默认处于禁用状态。如果您遇到类似错误,”执行脚本时禁用了此系统上,”在尝试运行 PSService.ps1,然后使用 ︰

复制
Set-ExecutionPolicy RemoteSigned

有关详细信息,请参阅”参考”。

显然,这类服务脚本不能为作为已编译的程序的高性能。Windows PowerShell 中编写的服务脚本便可以很好,用于原型制作一个概念,并且对于与类似系统等群集服务监视的性能较低成本的任务。但对于任何高性能的任务,建议在 c + + 或 C# 中的重写。

内存占用量也不是程序的相当于,已编译,因为它需要在系统会话中加载完整的 Windows PowerShell 解释器。在当今世界,与系统拥有许多千兆字节的 RAM,这不是很了不起。

此脚本与 Mark Russinovich PsService.exe 完全无关。我知道有关 homonymy 之前,我选择 PSService.ps1 名称。我将使其保持对于此示例脚本,因为我认为该名称使得清除其用途。当然,如果你计划尝试使用您自己的 Windows PowerShell 服务,您必须将其重命名,若要获得唯一的脚本的基名称唯一的服务名称 !

参考


Jean François Larvoire 适用于 Hewlett-Packard 格勒诺布尔,法国的企业环境。他一直在努力开发软件的 30 年的 PC BIOS,Windows 驱动程序、 Windows 和 Linux 系统管理。他的联系方式 jf.larvoire@hpe.com

发表评论