Advertisement
Guest User

PSService-custom

a guest
Feb 12th, 2019
232
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 61.16 KB | None | 0 0
  1. ###############################################################################
  2. # #
  3. # File name PSService.ps1 #
  4. # #
  5. # Description A sample service in a standalone PowerShell script #
  6. # #
  7. # Notes The latest PSService.ps1 version is available in GitHub #
  8. # repository https://github.com/JFLarvoire/SysToolsLib/ , #
  9. # in the PowerShell subdirectory. #
  10. # Please report any problem in the Issues tab in that #
  11. # GitHub repository in #
  12. # https://github.com/JFLarvoire/SysToolsLib/issues #
  13. # If you do submit a pull request, please add a comment at #
  14. # the end of this header with the date, your initials, and #
  15. # a description of the changes. Also update $scriptVersion. #
  16. # #
  17. # The initial version of this script was described in an #
  18. # article published in the May 2016 issue of MSDN Magazine. #
  19. # https://msdn.microsoft.com/en-us/magazine/mt703436.aspx #
  20. # This updated version has one major change: #
  21. # The -Service handler in the end has been rewritten to be #
  22. # event-driven, with a second thread waiting for control #
  23. # messages coming in via a named pipe. #
  24. # This allows fixing a bug of the original version, that #
  25. # did not stop properly, and left a zombie process behind. #
  26. # The drawback is that the new code is significantly longer,#
  27. # due to the added PowerShell thread management routines. #
  28. # On the other hand, these thread management routines are #
  29. # reusable, and will allow building much more powerful #
  30. # services. #
  31. # #
  32. # Dynamically generates a small PSService.exe wrapper #
  33. # application, that in turn invokes this PowerShell script. #
  34. # #
  35. # Some arguments are inspired by Linux' service management #
  36. # arguments: -Start, -Stop, -Restart, -Status #
  37. # Others are more in the Windows' style: -Setup, -Remove #
  38. # #
  39. # The actual start and stop operations are done when #
  40. # running as SYSTEM, under the control of the SCM (Service #
  41. # Control Manager). #
  42. # #
  43. # To create your own service, make a copy of this file and #
  44. # rename it. The file base name becomes the service name. #
  45. # Then implement your own service code in the if ($Service) #
  46. # {block} at the very end of this file. See the TO DO #
  47. # comment there. #
  48. # There are global settings below the script param() block. #
  49. # They can easily be changed, but the defaults should be #
  50. # suitable for most projects. #
  51. # #
  52. # Service installation and usage: See the dynamic help #
  53. # section below, or run: help .\PSService.ps1 -Detailed #
  54. # #
  55. # Debugging: The Log function writes messages into a file #
  56. # called C:\Windows\Logs\PSService.log (or actually #
  57. # ${env:windir}\Logs\$serviceName.log). #
  58. # It is very convenient to monitor what's written into that #
  59. # file with a WIN32 port of the Unix tail program. Usage: #
  60. # tail -f C:\Windows\Logs\PSService.log #
  61. # #
  62. # History #
  63. # 2015-07-10 JFL jf.larvoire@hpe.com created this script. #
  64. # 2015-10-13 JFL Made this script completely generic, and added comments #
  65. # in the header above. #
  66. # 2016-01-02 JFL Moved the Event Log name into new variable $logName. #
  67. # Improved comments. #
  68. # 2016-01-05 JFL Fixed the StartPending state reporting. #
  69. # 2016-03-17 JFL Removed aliases. Added missing explicit argument names. #
  70. # 2016-04-16 JFL Moved the official repository on GitHub. #
  71. # 2016-04-21 JFL Minor bug fix: New-EventLog did not use variable $logName.#
  72. # 2016-05-25 JFL Bug fix: The service task was not properly stopped; Its #
  73. # finally block was not executed, and a zombie task often #
  74. # remained. Fixed by using a named pipe to send messages #
  75. # to the service task. #
  76. # 2016-06-05 JFL Finalized the event-driven service handler. #
  77. # Fixed the default command setting in PowerShell v2. #
  78. # Added a sample -Control option using the new pipe. #
  79. # 2016-06-08 JFL Rewrote the pipe handler using PSThreads instead of Jobs. #
  80. # 2016-06-09 JFL Finalized the PSThread management routines error handling.#
  81. # This finally fixes issue #1. #
  82. # 2016-08-22 JFL Fixed issue #3 creating the log and install directories. #
  83. # Thanks Nischl. #
  84. # 2016-09-06 JFL Fixed issue #4 detecting the System account. Now done in #
  85. # a language-independent way. Thanks A Gonzalez. #
  86. # 2016-09-19 JFL Fixed issue #5 starting services that begin with a number.#
  87. # Added a $ServiceDescription string global setting, and #
  88. # use it for the service registration. #
  89. # Added comments about Windows event logs limitations. #
  90. # 2016-11-17 RBM Fixed issue #6 Mangled hyphen in final Unregister-Event. #
  91. # 2017-05-10 CJG Added execution policy bypass flag. #
  92. # 2017-10-04 RBL rblindberg Updated C# code OnStop() routine fixing #
  93. # orphaned process left after stoping the service. #
  94. # 2017-12-05 NWK omrsafetyo Added ServiceUser and ServicePassword to the #
  95. # script parameters. #
  96. # 2017-12-10 JFL Removed the unreliable service account detection tests, #
  97. # and instead use dedicated -SCMStart and -SCMStop #
  98. # arguments in the PSService.exe helper app. #
  99. # Renamed variable userName as currentUserName. #
  100. # Renamed arguments ServiceUser and ServicePassword to the #
  101. # more standard UserName and Password. #
  102. # Also added the standard argument -Credential. #
  103. # 2019-02-12 JC Added GrantSeServiceLogonRight function and fixed #
  104. # security issue with password parameter #
  105. # #
  106. ###############################################################################
  107. #Requires -version 2
  108.  
  109. <#
  110. .SYNOPSIS
  111. A sample Windows service, in a standalone PowerShell script.
  112.  
  113. .DESCRIPTION
  114. This script demonstrates how to write a Windows service in pure PowerShell.
  115. It dynamically generates a small PSService.exe wrapper, that in turn
  116. invokes this PowerShell script again for its start and stop events.
  117.  
  118. .PARAMETER Start
  119. Start the service.
  120.  
  121. .PARAMETER Stop
  122. Stop the service.
  123.  
  124. .PARAMETER Restart
  125. Stop then restart the service.
  126.  
  127. .PARAMETER Status
  128. Get the current service status: Not installed / Stopped / Running
  129.  
  130. .PARAMETER Setup
  131. Install the service.
  132. Optionally use the -Credential or -UserName arguments to specify the user
  133. account for running the service. By default, uses the LocalSystem account.
  134. Known limitation with the old PowerShell v2: It is necessary to use -Credential
  135. or -UserName. For example, use -UserName LocalSystem to emulate the v3+ default.
  136.  
  137. .PARAMETER Credential
  138. User and password credential to use for running the service.
  139. For use with the -Setup command.
  140. Generate a PSCredential variable with the Get-Credential command.
  141.  
  142. .PARAMETER UserName
  143. User account to use for running the service.
  144. For use with the -Setup command, in the absence of a Credential variable.
  145. The user must have the "Log on as a service" right. To give him that right,
  146. open the Local Security Policy management console, go to the
  147. "\Security Settings\Local Policies\User Rights Assignments" folder, and edit
  148. the "Log on as a service" policy there.
  149. Services should always run using a user account which has the least amount
  150. of privileges necessary to do its job.
  151. Three accounts are special, and do not require a password:
  152. * LocalSystem - The default if no user is specified. Highly privileged.
  153. * LocalService - Very few privileges, lowest security risk.
  154. Apparently not enough privileges for running PowerShell. Do not use.
  155. * NetworkService - Idem, plus network access. Same problems as LocalService.
  156.  
  157. .PARAMETER Password
  158. Removed for security
  159.  
  160. .PARAMETER Remove
  161. Uninstall the service.
  162.  
  163. .PARAMETER Service
  164. Run the service in the background. Used internally by the script.
  165. Do not use, except for test purposes.
  166.  
  167. .PARAMETER SCMStart
  168. Process Service Control Manager start requests. Used internally by the script.
  169. Do not use, except for test purposes.
  170.  
  171. .PARAMETER SCMStop
  172. Process Service Control Manager stop requests. Used internally by the script.
  173. Do not use, except for test purposes.
  174.  
  175. .PARAMETER Control
  176. Send a control message to the service thread.
  177.  
  178. .PARAMETER Version
  179. Display this script version and exit.
  180.  
  181. .EXAMPLE
  182. # Setup the service and run it for the first time
  183. C:\PS>.\PSService.ps1 -Status
  184. Not installed
  185. C:\PS>.\PSService.ps1 -Setup
  186. C:\PS># At this stage, a copy of PSService.ps1 is present in the path
  187. C:\PS>PSService -Status
  188. Stopped
  189. C:\PS>PSService -Start
  190. C:\PS>PSService -Status
  191. Running
  192. C:\PS># Load the log file in Notepad.exe for review
  193. C:\PS>notepad ${ENV:windir}\Logs\PSService.log
  194.  
  195. .EXAMPLE
  196. # Stop the service and uninstall it.
  197. C:\PS>PSService -Stop
  198. C:\PS>PSService -Status
  199. Stopped
  200. C:\PS>PSService -Remove
  201. C:\PS># At this stage, no copy of PSService.ps1 is present in the path anymore
  202. C:\PS>.\PSService.ps1 -Status
  203. Not installed
  204.  
  205. .EXAMPLE
  206. # Configure the service to run as a different user
  207. C:\PS>$cred = Get-Credential -UserName LAB\Assistant
  208. C:\PS>.\PSService -Setup -Credential $cred
  209.  
  210. .EXAMPLE
  211. # Send a control message to the service, and verify that it received it.
  212. C:\PS>PSService -Control Hello
  213. C:\PS>Notepad C:\Windows\Logs\PSService.log
  214. # The last lines should contain a trace of the reception of this Hello message
  215. #>
  216.  
  217. [CmdletBinding(DefaultParameterSetName='Status')]
  218. Param(
  219. [Parameter(ParameterSetName='Start', Mandatory=$true)]
  220. [Switch]$Start, # Start the service
  221.  
  222. [Parameter(ParameterSetName='Stop', Mandatory=$true)]
  223. [Switch]$Stop, # Stop the service
  224.  
  225. [Parameter(ParameterSetName='Restart', Mandatory=$true)]
  226. [Switch]$Restart, # Restart the service
  227.  
  228. [Parameter(ParameterSetName='Status', Mandatory=$false)]
  229. [Switch]$Status = $($PSCmdlet.ParameterSetName -eq 'Status'), # Get the current service status
  230.  
  231. [Parameter(ParameterSetName='Setup', Mandatory=$true)]
  232. [Parameter(ParameterSetName='Setup2', Mandatory=$true)]
  233. [Switch]$Setup, # Install the service
  234.  
  235. [Parameter(ParameterSetName='Setup', Mandatory=$true)]
  236. [String]$UserName, # Set the service to run as this user
  237.  
  238. #removed for security (J.C), passwords needs to be implemented with securestring
  239. <#
  240. [Parameter(ParameterSetName='Setup', Mandatory=$false)]
  241. [String]$Password, # Use this password for the user
  242. #>
  243. [Parameter(ParameterSetName='Setup2', Mandatory=$false)]
  244. [System.Management.Automation.PSCredential]$Credential, # Service account credential
  245.  
  246. [Parameter(ParameterSetName='Remove', Mandatory=$true)]
  247. [Switch]$Remove, # Uninstall the service
  248.  
  249. [Parameter(ParameterSetName='Service', Mandatory=$true)]
  250. [Switch]$Service, # Run the service (Internal use only)
  251.  
  252. [Parameter(ParameterSetName='SCMStart', Mandatory=$true)]
  253. [Switch]$SCMStart, # Process SCM Start requests (Internal use only)
  254.  
  255. [Parameter(ParameterSetName='SCMStop', Mandatory=$true)]
  256. [Switch]$SCMStop, # Process SCM Stop requests (Internal use only)
  257.  
  258. [Parameter(ParameterSetName='Control', Mandatory=$true)]
  259. [String]$Control = $null, # Control message to send to the service
  260.  
  261. [Parameter(ParameterSetName='Version', Mandatory=$true)]
  262. [Switch]$Version # Get this script version
  263. )
  264.  
  265. $scriptVersion = "2019-02-12"
  266.  
  267. # This script name, with various levels of details
  268. $argv0 = Get-Item $MyInvocation.MyCommand.Definition
  269. $script = $argv0.basename # Ex: PSService
  270. $scriptName = $argv0.name # Ex: PSService.ps1
  271. $scriptFullName = $argv0.fullname # Ex: C:\Temp\PSService.ps1
  272.  
  273. # Global settings
  274. $serviceName = $script # A one-word name used for net start commands
  275. $serviceDisplayName = "A Sample PowerShell Service"
  276. $ServiceDescription = "Shows how a service can be written in PowerShell"
  277. $pipeName = "Service_$serviceName" # Named pipe name. Used for sending messages to the service task
  278. # $installDir = "${ENV:ProgramFiles}\$serviceName" # Where to install the service files
  279. $installDir = "${ENV:windir}\System32" # Where to install the service files
  280. $scriptCopy = "$installDir\$scriptName"
  281. $exeName = "$serviceName.exe"
  282. $exeFullName = "$installDir\$exeName"
  283. $logDir = "${ENV:windir}\Logs" # Where to log the service messages
  284. $logFile = "$logDir\$serviceName.log"
  285. $logName = "Application" # Event Log name (Unrelated to the logFile!)
  286. # Note: The current implementation only supports "classic" (ie. XP-compatble) event logs.
  287. # To support new style (Vista and later) "Applications and Services Logs" folder trees, it would
  288. # be necessary to use the new *WinEvent commands instead of the XP-compatible *EventLog commands.
  289. # Gotcha: If you change $logName to "NEWLOGNAME", make sure that the registry key below does not exist:
  290. # HKLM\System\CurrentControlSet\services\eventlog\Application\NEWLOGNAME
  291. # Else, New-EventLog will fail, saying the log NEWLOGNAME is already registered as a source,
  292. # even though "Get-WinEvent -ListLog NEWLOGNAME" says this log does not exist!
  293.  
  294. # If the -Version switch is specified, display the script version and exit.
  295. if ($Version) {
  296. Write-Output $scriptVersion
  297. return
  298. }
  299.  
  300. #-----------------------------------------------------------------------------#
  301. # #
  302. # Function GrantSeServiceLogonRight #
  303. # #
  304. # Description Grant User SeServiceLogonRight #
  305. # #
  306. # Notes This function was missing from original script, when #
  307. # a pscredential is used to setup the service the #
  308. # SeServiceLogonRight was missing and service will #
  309. # not start #
  310. # #
  311. # History #
  312. # 2019-02-12 Jerome C. #
  313. # #
  314. #-----------------------------------------------------------------------------#
  315. Function GrantSeServiceLogonRight {
  316. Param (
  317. [parameter(Position=0,Mandatory=$true)]
  318. [String] $SeServiceUser #The username of the user to be granted SeServiceLogon Right
  319. )
  320. Begin{
  321. $computerName = $env:COMPUTERNAME
  322. #create a temp folder
  323. $tempDir = -join ((48..57) + (97..122) | Get-Random -Count 10 | % {[char]$_})
  324. New-Item -Path "$env:SystemDrive\$tempDir" -ItemType Directory -Force
  325. $tempPath = "$env:SystemDrive\$tempDir"
  326. #we set paths used by the script
  327. $import = Join-Path -Path $tempPath -ChildPath "import.inf"
  328. $export = Join-Path -Path $tempPath -ChildPath "export.inf"
  329. $secedt = Join-Path -Path $tempPath -ChildPath "secedt.sdb"
  330. $seceditBin = "$env:SystemDrive\windows\system32\secedit.exe"
  331. }
  332. Process{
  333. #information
  334. Write-Host ("Granting SeServiceLogonRight to user account: {0} on host: {1}." -f $SeServiceUser, $computerName)
  335. #get the user SID
  336. $sid = ((New-Object System.Security.Principal.NTAccount($SeServiceUser)).Translate([System.Security.Principal.SecurityIdentifier])).Value
  337. #we export current security settings to file
  338. &$seceditBin /export /cfg $export
  339. #we get a list of SIDS for the SeServiceLogonRight Setting
  340. $sids = (Select-String $export -Pattern "SeServiceLogonRight").Line
  341. #we create a change file to be imported
  342. foreach ($line in @("[Unicode]", "Unicode=yes", "[System Access]", "[Event Audit]", "[Registry Values]", "[Version]", "signature=`"`$CHICAGO$`"", "Revision=1", "[Profile Description]", "Description=GrantLogOnAsAService security template", "[Privilege Rights]", "$sids,*$sid")){
  343. Add-Content $import $line
  344. }
  345. #we apply changes to system
  346. &$seceditBin /import /db $secedt /cfg $import
  347. &$seceditBin /configure /db $secedt
  348. }
  349. End {
  350. #we cleanup temp folder as the files arent needed anymore
  351. $fso = New-Object -ComObject scripting.filesystemobject
  352. $fso.DeleteFolder($tempPath)
  353. }
  354. }
  355.  
  356. #-----------------------------------------------------------------------------#
  357. # #
  358. # Function Now #
  359. # #
  360. # Description Get a string with the current time. #
  361. # #
  362. # Notes The output string is in the ISO 8601 format, except for #
  363. # a space instead of a T between the date and time, to #
  364. # improve the readability. #
  365. # #
  366. # History #
  367. # 2015-06-11 JFL Created this routine. #
  368. # #
  369. #-----------------------------------------------------------------------------#
  370.  
  371. Function Now {
  372. Param (
  373. [Switch]$ms, # Append milliseconds
  374. [Switch]$ns # Append nanoseconds
  375. )
  376. $Date = Get-Date
  377. $now = ""
  378. $now += "{0:0000}-{1:00}-{2:00} " -f $Date.Year, $Date.Month, $Date.Day
  379. $now += "{0:00}:{1:00}:{2:00}" -f $Date.Hour, $Date.Minute, $Date.Second
  380. $nsSuffix = ""
  381. if ($ns) {
  382. if ("$($Date.TimeOfDay)" -match "\.\d\d\d\d\d\d") {
  383. $now += $matches[0]
  384. $ms = $false
  385. } else {
  386. $ms = $true
  387. $nsSuffix = "000"
  388. }
  389. }
  390. if ($ms) {
  391. $now += ".{0:000}$nsSuffix" -f $Date.MilliSecond
  392. }
  393. return $now
  394. }
  395.  
  396. #-----------------------------------------------------------------------------#
  397. # #
  398. # Function Log #
  399. # #
  400. # Description Log a string into the PSService.log file #
  401. # #
  402. # Arguments A string #
  403. # #
  404. # Notes Prefixes the string with a timestamp and the user name. #
  405. # (Except if the string is empty: Then output a blank line.)#
  406. # #
  407. # History #
  408. # 2016-06-05 JFL Also prepend the Process ID. #
  409. # 2016-06-08 JFL Allow outputing blank lines. #
  410. # #
  411. #-----------------------------------------------------------------------------#
  412.  
  413. Function Log () {
  414. Param(
  415. [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
  416. [String]$string
  417. )
  418. if (!(Test-Path $logDir)) {
  419. New-Item -ItemType directory -Path $logDir | Out-Null
  420. }
  421. if ($String.length) {
  422. $string = "$(Now) $pid $currentUserName $string"
  423. }
  424. $string | Out-File -Encoding ASCII -Append "$logFile"
  425. }
  426.  
  427. #-----------------------------------------------------------------------------#
  428. # #
  429. # Function Start-PSThread #
  430. # #
  431. # Description Start a new PowerShell thread #
  432. # #
  433. # Arguments See the Param() block #
  434. # #
  435. # Notes Returns a thread description object. #
  436. # The completion can be tested in $_.Handle.IsCompleted #
  437. # Alternative: Use a thread completion event. #
  438. # #
  439. # References #
  440. # https://learn-powershell.net/tag/runspace/ #
  441. # https://learn-powershell.net/2013/04/19/sharing-variables-and-live-objects-between-powershell-runspaces/
  442. # http://www.codeproject.com/Tips/895840/Multi-Threaded-PowerShell-Cookbook
  443. # #
  444. # History #
  445. # 2016-06-08 JFL Created this function #
  446. # #
  447. #-----------------------------------------------------------------------------#
  448.  
  449. $PSThreadCount = 0 # Counter of PSThread IDs generated so far
  450. $PSThreadList = @{} # Existing PSThreads indexed by Id
  451.  
  452. Function Get-PSThread () {
  453. Param(
  454. [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
  455. [int[]]$Id = $PSThreadList.Keys # List of thread IDs
  456. )
  457. $Id | % { $PSThreadList.$_ }
  458. }
  459.  
  460. Function Start-PSThread () {
  461. Param(
  462. [Parameter(Mandatory=$true, Position=0)]
  463. [ScriptBlock]$ScriptBlock, # The script block to run in a new thread
  464. [Parameter(Mandatory=$false)]
  465. [String]$Name = "", # Optional thread name. Default: "PSThread$Id"
  466. [Parameter(Mandatory=$false)]
  467. [String]$Event = "", # Optional thread completion event name. Default: None
  468. [Parameter(Mandatory=$false)]
  469. [Hashtable]$Variables = @{}, # Optional variables to copy into the script context.
  470. [Parameter(Mandatory=$false)]
  471. [String[]]$Functions = @(), # Optional functions to copy into the script context.
  472. [Parameter(Mandatory=$false)]
  473. [Object[]]$Arguments = @() # Optional arguments to pass to the script.
  474. )
  475.  
  476. $Id = $script:PSThreadCount
  477. $script:PSThreadCount += 1
  478. if (!$Name.Length) {
  479. $Name = "PSThread$Id"
  480. }
  481. $InitialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
  482. foreach ($VarName in $Variables.Keys) { # Copy the specified variables into the script initial context
  483. $value = $Variables.$VarName
  484. Write-Debug "Adding variable $VarName=[$($Value.GetType())]$Value"
  485. $var = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry($VarName, $value, "")
  486. $InitialSessionState.Variables.Add($var)
  487. }
  488. foreach ($FuncName in $Functions) { # Copy the specified functions into the script initial context
  489. $Body = Get-Content function:$FuncName
  490. Write-Debug "Adding function $FuncName () {$Body}"
  491. $func = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry($FuncName, $Body)
  492. $InitialSessionState.Commands.Add($func)
  493. }
  494. $RunSpace = [RunspaceFactory]::CreateRunspace($InitialSessionState)
  495. $RunSpace.Open()
  496. $PSPipeline = [powershell]::Create()
  497. $PSPipeline.Runspace = $RunSpace
  498. $PSPipeline.AddScript($ScriptBlock) | Out-Null
  499. $Arguments | % {
  500. Write-Debug "Adding argument [$($_.GetType())]'$_'"
  501. $PSPipeline.AddArgument($_) | Out-Null
  502. }
  503. $Handle = $PSPipeline.BeginInvoke() # Start executing the script
  504. if ($Event.Length) { # Do this after BeginInvoke(), to avoid getting the start event.
  505. Register-ObjectEvent $PSPipeline -EventName InvocationStateChanged -SourceIdentifier $Name -MessageData $Event
  506. }
  507. $PSThread = New-Object PSObject -Property @{
  508. Id = $Id
  509. Name = $Name
  510. Event = $Event
  511. RunSpace = $RunSpace
  512. PSPipeline = $PSPipeline
  513. Handle = $Handle
  514. } # Return the thread description variables
  515. $script:PSThreadList[$Id] = $PSThread
  516. $PSThread
  517. }
  518.  
  519. #-----------------------------------------------------------------------------#
  520. # #
  521. # Function Receive-PSThread #
  522. # #
  523. # Description Get the result of a thread, and optionally clean it up #
  524. # #
  525. # Arguments See the Param() block #
  526. # #
  527. # Notes #
  528. # #
  529. # History #
  530. # 2016-06-08 JFL Created this function #
  531. # #
  532. #-----------------------------------------------------------------------------#
  533.  
  534. Function Receive-PSThread () {
  535. [CmdletBinding()]
  536. Param(
  537. [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
  538. [PSObject]$PSThread, # Thread descriptor object
  539. [Parameter(Mandatory=$false)]
  540. [Switch]$AutoRemove # If $True, remove the PSThread object
  541. )
  542. Process {
  543. if ($PSThread.Event -and $AutoRemove) {
  544. Unregister-Event -SourceIdentifier $PSThread.Name
  545. Get-Event -SourceIdentifier $PSThread.Name | Remove-Event # Flush remaining events
  546. }
  547. try {
  548. $PSThread.PSPipeline.EndInvoke($PSThread.Handle) # Output the thread pipeline output
  549. } catch {
  550. $_ # Output the thread pipeline error
  551. }
  552. if ($AutoRemove) {
  553. $PSThread.RunSpace.Close()
  554. $PSThread.PSPipeline.Dispose()
  555. $PSThreadList.Remove($PSThread.Id)
  556. }
  557. }
  558. }
  559.  
  560. Function Remove-PSThread () {
  561. [CmdletBinding()]
  562. Param(
  563. [Parameter(Mandatory=$false, ValueFromPipeline=$true, Position=0)]
  564. [PSObject]$PSThread # Thread descriptor object
  565. )
  566. Process {
  567. $_ | Receive-PSThread -AutoRemove | Out-Null
  568. }
  569. }
  570.  
  571. #-----------------------------------------------------------------------------#
  572. # #
  573. # Function Send-PipeMessage #
  574. # #
  575. # Description Send a message to a named pipe #
  576. # #
  577. # Arguments See the Param() block #
  578. # #
  579. # Notes #
  580. # #
  581. # History #
  582. # 2016-05-25 JFL Created this function #
  583. # #
  584. #-----------------------------------------------------------------------------#
  585.  
  586. Function Send-PipeMessage () {
  587. Param(
  588. [Parameter(Mandatory=$true)]
  589. [String]$PipeName, # Named pipe name
  590. [Parameter(Mandatory=$true)]
  591. [String]$Message # Message string
  592. )
  593. $PipeDir = [System.IO.Pipes.PipeDirection]::Out
  594. $PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
  595.  
  596. $pipe = $null # Named pipe stream
  597. $sw = $null # Stream Writer
  598. try {
  599. $pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, $PipeDir, $PipeOpt)
  600. $sw = new-object System.IO.StreamWriter($pipe)
  601. $pipe.Connect(1000)
  602. if (!$pipe.IsConnected) {
  603. throw "Failed to connect client to pipe $pipeName"
  604. }
  605. $sw.AutoFlush = $true
  606. $sw.WriteLine($Message)
  607. } catch {
  608. Log "Error sending pipe $pipeName message: $_"
  609. } finally {
  610. if ($sw) {
  611. $sw.Dispose() # Release resources
  612. $sw = $null # Force the PowerShell garbage collector to delete the .net object
  613. }
  614. if ($pipe) {
  615. $pipe.Dispose() # Release resources
  616. $pipe = $null # Force the PowerShell garbage collector to delete the .net object
  617. }
  618. }
  619. }
  620.  
  621. #-----------------------------------------------------------------------------#
  622. # #
  623. # Function Receive-PipeMessage #
  624. # #
  625. # Description Wait for a message from a named pipe #
  626. # #
  627. # Arguments See the Param() block #
  628. # #
  629. # Notes I tried keeping the pipe open between client connections, #
  630. # but for some reason everytime the client closes his end #
  631. # of the pipe, this closes the server end as well. #
  632. # Any solution on how to fix this would make the code #
  633. # more efficient. #
  634. # #
  635. # History #
  636. # 2016-05-25 JFL Created this function #
  637. # #
  638. #-----------------------------------------------------------------------------#
  639.  
  640. Function Receive-PipeMessage () {
  641. Param(
  642. [Parameter(Mandatory=$true)]
  643. [String]$PipeName # Named pipe name
  644. )
  645. $PipeDir = [System.IO.Pipes.PipeDirection]::In
  646. $PipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
  647. $PipeMode = [System.IO.Pipes.PipeTransmissionMode]::Message
  648.  
  649. try {
  650. $pipe = $null # Named pipe stream
  651. $pipe = New-Object system.IO.Pipes.NamedPipeServerStream($PipeName, $PipeDir, 1, $PipeMode, $PipeOpt)
  652. $sr = $null # Stream Reader
  653. $sr = new-object System.IO.StreamReader($pipe)
  654. $pipe.WaitForConnection()
  655. $Message = $sr.Readline()
  656. $Message
  657. } catch {
  658. Log "Error receiving pipe message: $_"
  659. } finally {
  660. if ($sr) {
  661. $sr.Dispose() # Release resources
  662. $sr = $null # Force the PowerShell garbage collector to delete the .net object
  663. }
  664. if ($pipe) {
  665. $pipe.Dispose() # Release resources
  666. $pipe = $null # Force the PowerShell garbage collector to delete the .net object
  667. }
  668. }
  669. }
  670.  
  671. #-----------------------------------------------------------------------------#
  672. # #
  673. # Function Start-PipeHandlerThread #
  674. # #
  675. # Description Start a new thread waiting for control messages on a pipe #
  676. # #
  677. # Arguments See the Param() block #
  678. # #
  679. # Notes The pipe handler script uses function Receive-PipeMessage.#
  680. # This function must be copied into the thread context. #
  681. # #
  682. # The other functions and variables copied into that thread #
  683. # context are not strictly necessary, but are useful for #
  684. # debugging possible issues. #
  685. # #
  686. # History #
  687. # 2016-06-07 JFL Created this function #
  688. # #
  689. #-----------------------------------------------------------------------------#
  690.  
  691. $pipeThreadName = "Control Pipe Handler"
  692.  
  693. Function Start-PipeHandlerThread () {
  694. Param(
  695. [Parameter(Mandatory=$true)]
  696. [String]$pipeName, # Named pipe name
  697. [Parameter(Mandatory=$false)]
  698. [String]$Event = "ControlMessage" # Event message
  699. )
  700. Start-PSThread -Variables @{ # Copy variables required by function Log() into the thread context
  701. logDir = $logDir
  702. logFile = $logFile
  703. currentUserName = $currentUserName
  704. } -Functions Now, Log, Receive-PipeMessage -ScriptBlock {
  705. Param($pipeName, $pipeThreadName)
  706. try {
  707. Receive-PipeMessage "$pipeName" # Blocks the thread until the next message is received from the pipe
  708. } catch {
  709. Log "$pipeThreadName # Error: $_"
  710. throw $_ # Push the error back to the main thread
  711. }
  712. } -Name $pipeThreadName -Event $Event -Arguments $pipeName, $pipeThreadName
  713. }
  714.  
  715. #-----------------------------------------------------------------------------#
  716. # #
  717. # Function Receive-PipeHandlerThread #
  718. # #
  719. # Description Get what the pipe handler thread received #
  720. # #
  721. # Arguments See the Param() block #
  722. # #
  723. # Notes #
  724. # #
  725. # History #
  726. # 2016-06-07 JFL Created this function #
  727. # #
  728. #-----------------------------------------------------------------------------#
  729.  
  730. Function Receive-PipeHandlerThread () {
  731. Param(
  732. [Parameter(Mandatory=$true)]
  733. [PSObject]$pipeThread # Thread descriptor
  734. )
  735. Receive-PSThread -PSThread $pipeThread -AutoRemove
  736. }
  737.  
  738. #-----------------------------------------------------------------------------#
  739. # #
  740. # Function $source #
  741. # #
  742. # Description C# source of the PSService.exe stub #
  743. # #
  744. # Arguments #
  745. # #
  746. # Notes The lines commented with "SET STATUS" and "EVENT LOG" are #
  747. # optional. (Or blocks between "// SET STATUS [" and #
  748. # "// SET STATUS ]" comments.) #
  749. # SET STATUS lines are useful only for services with a long #
  750. # startup time. #
  751. # EVENT LOG lines are useful for debugging the service. #
  752. # #
  753. # History #
  754. # 2017-10-04 RBL Updated the OnStop() procedure adding the sections #
  755. # try{ #
  756. # }catch{ #
  757. # }finally{ #
  758. # } #
  759. # This resolved the issue where stopping the service would #
  760. # leave the PowerShell process -Service still running. This #
  761. # unclosed process was an orphaned process that would #
  762. # remain until the pid was manually killed or the computer #
  763. # was rebooted #
  764. # #
  765. #-----------------------------------------------------------------------------#
  766.  
  767. $scriptCopyCname = $scriptCopy -replace "\\", "\\" # Double backslashes. (The first \\ is a regexp with \ escaped; The second is a plain string.)
  768. $source = @"
  769. using System;
  770. using System.ServiceProcess;
  771. using System.Diagnostics;
  772. using System.Runtime.InteropServices; // SET STATUS
  773. using System.ComponentModel; // SET STATUS
  774.  
  775. public enum ServiceType : int { // SET STATUS [
  776. SERVICE_WIN32_OWN_PROCESS = 0x00000010,
  777. SERVICE_WIN32_SHARE_PROCESS = 0x00000020,
  778. }; // SET STATUS ]
  779.  
  780. public enum ServiceState : int { // SET STATUS [
  781. SERVICE_STOPPED = 0x00000001,
  782. SERVICE_START_PENDING = 0x00000002,
  783. SERVICE_STOP_PENDING = 0x00000003,
  784. SERVICE_RUNNING = 0x00000004,
  785. SERVICE_CONTINUE_PENDING = 0x00000005,
  786. SERVICE_PAUSE_PENDING = 0x00000006,
  787. SERVICE_PAUSED = 0x00000007,
  788. }; // SET STATUS ]
  789.  
  790. [StructLayout(LayoutKind.Sequential)] // SET STATUS [
  791. public struct ServiceStatus {
  792. public ServiceType dwServiceType;
  793. public ServiceState dwCurrentState;
  794. public int dwControlsAccepted;
  795. public int dwWin32ExitCode;
  796. public int dwServiceSpecificExitCode;
  797. public int dwCheckPoint;
  798. public int dwWaitHint;
  799. }; // SET STATUS ]
  800.  
  801. public enum Win32Error : int { // WIN32 errors that we may need to use
  802. NO_ERROR = 0,
  803. ERROR_APP_INIT_FAILURE = 575,
  804. ERROR_FATAL_APP_EXIT = 713,
  805. ERROR_SERVICE_NOT_ACTIVE = 1062,
  806. ERROR_EXCEPTION_IN_SERVICE = 1064,
  807. ERROR_SERVICE_SPECIFIC_ERROR = 1066,
  808. ERROR_PROCESS_ABORTED = 1067,
  809. };
  810.  
  811. public class Service_$serviceName : ServiceBase { // $serviceName may begin with a digit; The class name must begin with a letter
  812. private System.Diagnostics.EventLog eventLog; // EVENT LOG
  813. private ServiceStatus serviceStatus; // SET STATUS
  814.  
  815. public Service_$serviceName() {
  816. ServiceName = "$serviceName";
  817. CanStop = true;
  818. CanPauseAndContinue = false;
  819. AutoLog = true;
  820.  
  821. eventLog = new System.Diagnostics.EventLog(); // EVENT LOG [
  822. if (!System.Diagnostics.EventLog.SourceExists(ServiceName)) {
  823. System.Diagnostics.EventLog.CreateEventSource(ServiceName, "$logName");
  824. }
  825. eventLog.Source = ServiceName;
  826. eventLog.Log = "$logName"; // EVENT LOG ]
  827. EventLog.WriteEntry(ServiceName, "$exeName $serviceName()"); // EVENT LOG
  828. }
  829.  
  830. [DllImport("advapi32.dll", SetLastError=true)] // SET STATUS
  831. private static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus);
  832.  
  833. protected override void OnStart(string [] args) {
  834. EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Entry. Starting script '$scriptCopyCname' -SCMStart"); // EVENT LOG
  835. // Set the service state to Start Pending. // SET STATUS [
  836. // Only useful if the startup time is long. Not really necessary here for a 2s startup time.
  837. serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS;
  838. serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING;
  839. serviceStatus.dwWin32ExitCode = 0;
  840. serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell
  841. SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ]
  842. // Start a child process with another copy of this script
  843. try {
  844. Process p = new Process();
  845. // Redirect the output stream of the child process.
  846. p.StartInfo.UseShellExecute = false;
  847. p.StartInfo.RedirectStandardOutput = true;
  848. p.StartInfo.FileName = "PowerShell.exe";
  849. p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes.
  850. p.Start();
  851. // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
  852. string output = p.StandardOutput.ReadToEnd();
  853. // Wait for the completion of the script startup code, that launches the -Service instance
  854. p.WaitForExit();
  855. if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
  856. // Success. Set the service state to Running. // SET STATUS
  857. serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS
  858. } catch (Exception e) {
  859. EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Failed to start $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG
  860. // Change the service state back to Stopped. // SET STATUS [
  861. serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED;
  862. Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code
  863. if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is...
  864. w32ex = e.InnerException as Win32Exception;
  865. }
  866. if (w32ex != null) { // Report the actual WIN32 error
  867. serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode;
  868. } else { // Make up a reasonable reason
  869. serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE);
  870. } // SET STATUS ]
  871. } finally {
  872. serviceStatus.dwWaitHint = 0; // SET STATUS
  873. SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
  874. EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Exit"); // EVENT LOG
  875. }
  876. }
  877.  
  878. protected override void OnStop() {
  879. EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Entry"); // EVENT LOG
  880. // Start a child process with another copy of ourselves
  881. try {
  882. Process p = new Process();
  883. // Redirect the output stream of the child process.
  884. p.StartInfo.UseShellExecute = false;
  885. p.StartInfo.RedirectStandardOutput = true;
  886. p.StartInfo.FileName = "PowerShell.exe";
  887. p.StartInfo.Arguments = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStop"; // Works if path has spaces, but not if it contains ' quotes.
  888. p.Start();
  889. // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!)
  890. string output = p.StandardOutput.ReadToEnd();
  891. // Wait for the PowerShell script to be fully stopped.
  892. p.WaitForExit();
  893. if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE));
  894. // Success. Set the service state to Stopped. // SET STATUS
  895. serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; // SET STATUS
  896. } catch (Exception e) {
  897. EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Failed to stop $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG
  898. // Change the service state back to Started. // SET STATUS [
  899. serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING;
  900. Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code
  901. if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is...
  902. w32ex = e.InnerException as Win32Exception;
  903. }
  904. if (w32ex != null) { // Report the actual WIN32 error
  905. serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode;
  906. } else { // Make up a reasonable reason
  907. serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE);
  908. } // SET STATUS ]
  909. } finally {
  910. serviceStatus.dwWaitHint = 0; // SET STATUS
  911. SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS
  912. EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Exit"); // EVENT LOG
  913. }
  914. }
  915.  
  916. public static void Main() {
  917. System.ServiceProcess.ServiceBase.Run(new Service_$serviceName());
  918. }
  919. }
  920. "@
  921.  
  922. #-----------------------------------------------------------------------------#
  923. # #
  924. # Function Main #
  925. # #
  926. # Description Execute the specified actions #
  927. # #
  928. # Arguments See the Param() block at the top of this script #
  929. # #
  930. # Notes #
  931. # #
  932. # History #
  933. # #
  934. #-----------------------------------------------------------------------------#
  935.  
  936. # Identify the user name. We use that for logging.
  937. $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
  938. $currentUserName = $identity.Name # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator"
  939.  
  940. if ($Setup) {Log ""} # Insert one blank line to separate test sessions logs
  941. Log $MyInvocation.Line # The exact command line that was used to start us
  942.  
  943. # The following commands write to the event log, but we need to make sure the PSService source is defined.
  944. New-EventLog -LogName $logName -Source $serviceName -ea SilentlyContinue
  945.  
  946. # Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block
  947. $Status = ($PSCmdlet.ParameterSetName -eq 'Status')
  948.  
  949. if ($SCMStart) { # The SCM tells us to start the service
  950. # Do whatever is necessary to start the service script instance
  951. Log "$scriptName -SCMStart: Starting script '$scriptFullName' -Service"
  952. Write-EventLog -LogName $logName -Source $serviceName -EventId 1001 -EntryType Information -Message "$scriptName -SCMStart: Starting script '$scriptFullName' -Service"
  953. Start-Process PowerShell.exe -ArgumentList ("-c & '$scriptFullName' -Service")
  954. return
  955. }
  956.  
  957. if ($Start) { # The user tells us to start the service
  958. Write-Verbose "Starting service $serviceName"
  959. Write-EventLog -LogName $logName -Source $serviceName -EventId 1002 -EntryType Information -Message "$scriptName -Start: Starting service $serviceName"
  960. Start-Service $serviceName # Ask Service Control Manager to start it
  961. return
  962. }
  963.  
  964. if ($SCMStop) { # The SCM tells us to stop the service
  965. # Do whatever is necessary to stop the service script instance
  966. Write-EventLog -LogName $logName -Source $serviceName -EventId 1003 -EntryType Information -Message "$scriptName -SCMStop: Stopping script $scriptName -Service"
  967. Log "$scriptName -SCMStop: Stopping script $scriptName -Service"
  968. # Send an exit message to the service instance
  969. Send-PipeMessage $pipeName "exit"
  970. return
  971. }
  972.  
  973. if ($Stop) { # The user tells us to stop the service
  974. Write-Verbose "Stopping service $serviceName"
  975. Write-EventLog -LogName $logName -Source $serviceName -EventId 1004 -EntryType Information -Message "$scriptName -Stop: Stopping service $serviceName"
  976. Stop-Service $serviceName # Ask Service Control Manager to stop it
  977. return
  978. }
  979.  
  980. if ($Restart) { # Restart the service
  981. & $scriptFullName -Stop
  982. & $scriptFullName -Start
  983. return
  984. }
  985.  
  986. if ($Status) { # Get the current service status
  987. $spid = $null
  988. $processes = @(Get-WmiObject Win32_Process -filter "Name = 'powershell.exe'" | Where-Object {
  989. $_.CommandLine -match ".*$scriptCopyCname.*-Service"
  990. })
  991. foreach ($process in $processes) { # There should be just one, but be prepared for surprises.
  992. $spid = $process.ProcessId
  993. Write-Verbose "$serviceName Process ID = $spid"
  994. }
  995. # if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\$serviceName") {}
  996. try {
  997. $pss = Get-Service $serviceName -ea stop # Will error-out if not installed
  998. } catch {
  999. "Not Installed"
  1000. return
  1001. }
  1002. $pss.Status
  1003. if (($pss.Status -eq "Running") -and (!$spid)) { # This happened during the debugging phase
  1004. Write-Error "The Service Control Manager thinks $serviceName is started, but $serviceName.ps1 -Service is not running."
  1005. exit 1
  1006. }
  1007. return
  1008. }
  1009.  
  1010. if ($Setup) { # Install the service
  1011. # Check if it's necessary
  1012. try {
  1013. $pss = Get-Service $serviceName -ea stop # Will error-out if not installed
  1014. # Check if this script is newer than the installed copy.
  1015. if ((Get-Item $scriptCopy -ea SilentlyContinue).LastWriteTime -lt (Get-Item $scriptFullName -ea SilentlyContinue).LastWriteTime) {
  1016. Write-Verbose "Service $serviceName is already Installed, but requires upgrade"
  1017. & $scriptFullName -Remove
  1018. throw "continue"
  1019. } else {
  1020. Write-Verbose "Service $serviceName is already Installed, and up-to-date"
  1021. }
  1022. exit 0
  1023. } catch {
  1024. # This is the normal case here. Do not throw or write any error!
  1025. Write-Debug "Installation is necessary" # Also avoids a ScriptAnalyzer warning
  1026. # And continue with the installation.
  1027. }
  1028. if (!(Test-Path $installDir)) {
  1029. New-Item -ItemType directory -Path $installDir | Out-Null
  1030. }
  1031. # Copy the service script into the installation directory
  1032. if ($ScriptFullName -ne $scriptCopy) {
  1033. Write-Verbose "Installing $scriptCopy"
  1034. Copy-Item $ScriptFullName $scriptCopy
  1035. }
  1036. # Generate the service .EXE from the C# source embedded in this script
  1037. try {
  1038. Write-Verbose "Compiling $exeFullName"
  1039. Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false
  1040. } catch {
  1041. $msg = $_.Exception.Message
  1042. Write-error "Failed to create the $exeFullName service stub. $msg"
  1043. exit 1
  1044. }
  1045. # Register the service
  1046. Write-Verbose "Registering service $serviceName"
  1047.  
  1048. if ($UserName -and !$Credential) {
  1049. $emptyPassword = New-Object -Type System.Security.SecureString
  1050. switch ($UserName) {
  1051. {"LocalService", "NetworkService" -contains $_} {
  1052. $Credential = New-Object -Type System.Management.Automation.PSCredential ("NT AUTHORITY\$UserName", $emptyPassword)
  1053. }
  1054. {"LocalSystem", ".\LocalSystem", "${env:COMPUTERNAME}\LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService" -contains $_} {
  1055. $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $emptyPassword)
  1056. }
  1057. default {
  1058. #removed for security (J.C)
  1059. <# if (!$Password) {
  1060. $Credential = Get-Credential -UserName $UserName -Message "Please enter the password for the service user"
  1061. } else {
  1062. $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force
  1063. $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $securePassword)
  1064. } #>
  1065. }
  1066. }
  1067. } #>
  1068. if ($Credential) {
  1069. Log "$scriptName -Setup # Configuring the service to run as $($Credential.UserName)"
  1070. $pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -Credential $Credential
  1071. #we grant logon as service right for user (J.C)
  1072. GrantSeServiceLogonRight -SeServiceUser ($Credential.UserName)
  1073. } else {
  1074. Log "$scriptName -Setup # Configuring the service to run by default as LocalSystem"
  1075. $pss = New-Service $serviceName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic
  1076. }
  1077.  
  1078. return
  1079. }
  1080.  
  1081. if ($Remove) { # Uninstall the service
  1082. # Check if it's necessary
  1083. try {
  1084. $pss = Get-Service $serviceName -ea stop # Will error-out if not installed
  1085. } catch {
  1086. Write-Verbose "Already uninstalled"
  1087. return
  1088. }
  1089. Stop-Service $serviceName # Make sure it's stopped
  1090. # In the absence of a Remove-Service applet, use sc.exe instead.
  1091. Write-Verbose "Removing service $serviceName"
  1092. $msg = sc.exe delete $serviceName
  1093. if ($LastExitCode) {
  1094. Write-Error "Failed to remove the service ${serviceName}: $msg"
  1095. exit 1
  1096. } else {
  1097. Write-Verbose $msg
  1098. }
  1099. # Remove the installed files
  1100. if (Test-Path $installDir) {
  1101. foreach ($ext in ("exe", "pdb", "ps1")) {
  1102. $file = "$installDir\$serviceName.$ext"
  1103. if (Test-Path $file) {
  1104. Write-Verbose "Deleting file $file"
  1105. Remove-Item $file
  1106. }
  1107. }
  1108. if (!(@(Get-ChildItem $installDir -ea SilentlyContinue)).Count) {
  1109. Write-Verbose "Removing directory $installDir"
  1110. Remove-Item $installDir
  1111. }
  1112. }
  1113. Log "" # Insert one blank line to separate test sessions logs
  1114. return
  1115. }
  1116.  
  1117. if ($Control) { # Send a control message to the service
  1118. Send-PipeMessage $pipeName $control
  1119. }
  1120.  
  1121. if ($Service) { # Run the service
  1122. Write-EventLog -LogName $logName -Source $serviceName -EventId 1005 -EntryType Information -Message "$scriptName -Service # Beginning background job"
  1123. # Do the service background job
  1124. try {
  1125. # Start the control pipe handler thread
  1126. $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage"
  1127.  
  1128. ######### TO DO: Implement your own service code here. ##########
  1129. ###### Example that wakes up and logs a line every 10 sec: ######
  1130. # Start a periodic timer
  1131. $timerName = "Sample service timer"
  1132. $period = 10 # seconds
  1133. $timer = new-object System.Timers.Timer
  1134. $timer.Interval = ($period * 1000) # Milliseconds
  1135. $timer.AutoReset = $true # Make it fire repeatedly
  1136. Register-ObjectEvent $timer -EventName Elapsed -SourceIdentifier $timerName -MessageData "TimerTick"
  1137. $timer.start() # Must be stopped in the finally block
  1138. # Now enter the main service event loop
  1139. do { # Keep running until told to exit by the -Stop handler
  1140. $event = Wait-Event # Wait for the next incoming event
  1141. $source = $event.SourceIdentifier
  1142. $message = $event.MessageData
  1143. $eventTime = $event.TimeGenerated.TimeofDay
  1144. Write-Debug "Event at $eventTime from ${source}: $message"
  1145. $event | Remove-Event # Flush the event from the queue
  1146. switch ($message) {
  1147. "ControlMessage" { # Required. Message received by the control pipe thread
  1148. $state = $event.SourceEventArgs.InvocationStateInfo.state
  1149. Write-Debug "$script -Service # Thread $source state changed to $state"
  1150. switch ($state) {
  1151. "Completed" {
  1152. $message = Receive-PipeHandlerThread $pipeThread
  1153. Log "$scriptName -Service # Received control message: $Message"
  1154. if ($message -ne "exit") { # Start another thread waiting for control messages
  1155. $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage"
  1156. }
  1157. }
  1158. "Failed" {
  1159. $error = Receive-PipeHandlerThread $pipeThread
  1160. Log "$scriptName -Service # $source thread failed: $error"
  1161. Start-Sleep 1 # Avoid getting too many errors
  1162. $pipeThread = Start-PipeHandlerThread $pipeName -Event "ControlMessage" # Retry
  1163. }
  1164. }
  1165. }
  1166. "TimerTick" { # Example. Periodic event generated for this example
  1167. Log "$scriptName -Service # Timer ticked"
  1168. }
  1169. default { # Should not happen
  1170. Log "$scriptName -Service # Unexpected event from ${source}: $Message"
  1171. }
  1172. }
  1173. } while ($message -ne "exit")
  1174. } catch { # An exception occurred while runnning the service
  1175. $msg = $_.Exception.Message
  1176. $line = $_.InvocationInfo.ScriptLineNumber
  1177. Log "$scriptName -Service # Error at line ${line}: $msg"
  1178. } finally { # Invoked in all cases: Exception or normally by -Stop
  1179. # Cleanup the periodic timer used in the above example
  1180. Unregister-Event -SourceIdentifier $timerName
  1181. $timer.stop()
  1182. #destrpy autodeploy.run file
  1183. $fso = New-Object -ComObject scripting.filesystemobject
  1184. $fso.DeleteFile("$env:systemdrive\scripts\AutoDeploy.run")
  1185. ############### End of the service code example. ################
  1186. # Terminate the control pipe handler thread
  1187. Get-PSThread | Remove-PSThread # Remove all remaining threads
  1188. # Flush all leftover events (There may be some that arrived after we exited the while event loop, but before we unregistered the events)
  1189. $events = Get-Event | Remove-Event
  1190. # Log a termination event, no matter what the cause is.
  1191. Write-EventLog -LogName $logName -Source $serviceName -EventId 1006 -EntryType Information -Message "$script -Service # Exiting"
  1192. Log "$scriptName -Service # Exiting"
  1193. }
  1194. return
  1195. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement