############################################################################### # # # File name PSService.ps1 # # # # Description A sample service in a standalone PowerShell script # # # # Notes The latest PSService.ps1 version is available in GitHub # # repository https://github.com/JFLarvoire/SysToolsLib/ , # # in the PowerShell subdirectory. # # Please report any problem in the Issues tab in that # # GitHub repository in # # https://github.com/JFLarvoire/SysToolsLib/issues # # If you do submit a pull request, please add a comment at # # the end of this header with the date, your initials, and # # a description of the changes. Also update $scriptVersion. # # # # The initial version of this script was described in an # # article published in the May 2016 issue of MSDN Magazine. # # https://msdn.microsoft.com/en-us/magazine/mt703436.aspx # # This updated version has one major change: # # The -Service handler in the end has been rewritten to be # # event-driven, with a second thread waiting for control # # messages coming in via a named pipe. # # This allows fixing a bug of the original version, that # # did not stop properly, and left a zombie process behind. # # The drawback is that the new code is significantly longer,# # due to the added PowerShell thread management routines. # # On the other hand, these thread management routines are # # reusable, and will allow building much more powerful # # services. # # # # Dynamically generates a small PSService.exe wrapper # # application, that in turn invokes this PowerShell script. # # # # Some arguments are inspired by Linux' service management # # arguments: -Start, -Stop, -Restart, -Status # # Others are more in the Windows' style: -Setup, -Remove # # # # The actual start and stop operations are done when # # running as SYSTEM, under the control of the SCM (Service # # Control Manager). # # # # To create your own service, make a copy of this file and # # rename it. The file base name becomes the service name. # # Then implement your own service code in the if ($Service) # # {block} at the very end of this file. See the TO DO # # comment there. # # There are global settings below the script param() block. # # They can easily be changed, but the defaults should be # # suitable for most projects. # # # # Service installation and usage: See the dynamic help # # section below, or run: help .\PSService.ps1 -Detailed # # # # Debugging: The Log function writes messages into a file # # called C:\Windows\Logs\PSService.log (or actually # # ${env:windir}\Logs\$serviceName.log). # # It is very convenient to monitor what's written into that # # file with a WIN32 port of the Unix tail program. Usage: # # tail -f C:\Windows\Logs\PSService.log # # # # History # # 2015-07-10 JFL jf.larvoire@hpe.com created this script. # # 2015-10-13 JFL Made this script completely generic, and added comments # # in the header above. # # 2016-01-02 JFL Moved the Event Log name into new variable $logName. # # Improved comments. # # 2016-01-05 JFL Fixed the StartPending state reporting. # # 2016-03-17 JFL Removed aliases. Added missing explicit argument names. # # 2016-04-16 JFL Moved the official repository on GitHub. # # 2016-04-21 JFL Minor bug fix: New-EventLog did not use variable $logName.# # 2016-05-25 JFL Bug fix: The service task was not properly stopped; Its # # finally block was not executed, and a zombie task often # # remained. Fixed by using a named pipe to send messages # # to the service task. # # 2016-06-05 JFL Finalized the event-driven service handler. # # Fixed the default command setting in PowerShell v2. # # Added a sample -Control option using the new pipe. # # 2016-06-08 JFL Rewrote the pipe handler using PSThreads instead of Jobs. # # 2016-06-09 JFL Finalized the PSThread management routines error handling.# # This finally fixes issue #1. # # 2016-08-22 JFL Fixed issue #3 creating the log and install directories. # # Thanks Nischl. # # 2016-09-06 JFL Fixed issue #4 detecting the System account. Now done in # # a language-independent way. Thanks A Gonzalez. # # 2016-09-19 JFL Fixed issue #5 starting services that begin with a number.# # Added a $ServiceDescription string global setting, and # # use it for the service registration. # # Added comments about Windows event logs limitations. # # 2016-11-17 RBM Fixed issue #6 Mangled hyphen in final Unregister-Event. # # 2017-05-10 CJG Added execution policy bypass flag. # # 2017-10-04 RBL rblindberg Updated C# code OnStop() routine fixing # # orphaned process left after stoping the service. # # 2017-12-05 NWK omrsafetyo Added ServiceUser and ServicePassword to the # # script parameters. # # 2017-12-10 JFL Removed the unreliable service account detection tests, # # and instead use dedicated -SCMStart and -SCMStop # # arguments in the PSService.exe helper app. # # Renamed variable userName as currentUserName. # # Renamed arguments ServiceUser and ServicePassword to the # # more standard UserName and Password. # # Also added the standard argument -Credential. # # # ############################################################################### #Requires -version 5.1 <# .SYNOPSIS A sample Windows service, in a standalone PowerShell script. .DESCRIPTION This script demonstrates how to write a Windows service in pure PowerShell. It dynamically generates a small PSService.exe wrapper, that in turn invokes this PowerShell script again for its start and stop events. .PARAMETER Start Start the service. .PARAMETER Stop Stop the service. .PARAMETER Restart Stop then restart the service. .PARAMETER Status Get the current service status: Not installed / Stopped / Running .PARAMETER Setup Install the service. Optionally use the -Credential or -UserName arguments to specify the user account for running the service. By default, uses the LocalSystem account. Known limitation with the old PowerShell v2: It is necessary to use -Credential or -UserName. For example, use -UserName LocalSystem to emulate the v3+ default. .PARAMETER Credential User and password credential to use for running the service. For use with the -Setup command. Generate a PSCredential variable with the Get-Credential command. .PARAMETER UserName User account to use for running the service. For use with the -Setup command, in the absence of a Credential variable. The user must have the "Log on as a service" right. To give him that right, open the Local Security Policy management console, go to the "\Security Settings\Local Policies\User Rights Assignments" folder, and edit the "Log on as a service" policy there. Services should always run using a user account which has the least amount of privileges necessary to do its job. Three accounts are special, and do not require a password: * LocalSystem - The default if no user is specified. Highly privileged. * LocalService - Very few privileges, lowest security risk. Apparently not enough privileges for running PowerShell. Do not use. * NetworkService - Idem, plus network access. Same problems as LocalService. .PARAMETER Password Password for UserName. If not specified, you will be prompted for it. It is strongly recommended NOT to use that argument, as that password is visible on the console, and in the task manager list. Instead, use the -UserName argument alone, and wait for the prompt; or, even better, use the -Credential argument. .PARAMETER Remove Uninstall the service. .PARAMETER Service Run the service in the background. Used internally by the script. Do not use, except for test purposes. .PARAMETER SCMStart Process Service Control Manager start requests. Used internally by the script. Do not use, except for test purposes. .PARAMETER SCMResume Process Service Control Manager resume requests. Used internally by the script. Do not use, except for test purposes. .PARAMETER SCMStop Process Service Control Manager stop requests. Used internally by the script. Do not use, except for test purposes. .PARAMETER SCMSuspend Process Service Control Manager suspend requests. Used internally by the script. Do not use, except for test purposes. .PARAMETER Control Send a control message to the service thread. .PARAMETER Version Display this script version and exit. .EXAMPLE # Setup the service and run it for the first time C:\PS>.\PSService.ps1 -Status Not installed C:\PS>.\PSService.ps1 -Setup C:\PS># At this stage, a copy of PSService.ps1 is present in the path C:\PS>PSService -Status Stopped C:\PS>PSService -Start C:\PS>PSService -Status Running C:\PS># Load the log file in Notepad.exe for review C:\PS>notepad ${ENV:windir}\Logs\PSService.log .EXAMPLE # Stop the service and uninstall it. C:\PS>PSService -Stop C:\PS>PSService -Status Stopped C:\PS>PSService -Remove C:\PS># At this stage, no copy of PSService.ps1 is present in the path anymore C:\PS>.\PSService.ps1 -Status Not installed .EXAMPLE # Configure the service to run as a different user C:\PS>$cred = Get-Credential -UserName LAB\Assistant C:\PS>.\PSService -Setup -Credential $cred .EXAMPLE # Send a control message to the service, and verify that it received it. C:\PS>PSService -Control Hello C:\PS>Notepad C:\Windows\Logs\PSService.log # The last lines should contain a trace of the reception of this Hello message #> [CmdletBinding(DefaultParameterSetName='Version')] Param( [Parameter(ParameterSetName='Start', Mandatory=$true)] [Switch]$Start, # Start the service [Parameter(ParameterSetName='Stop', Mandatory=$true)] [Switch]$Stop, # Stop the service [Parameter(ParameterSetName='Restart', Mandatory=$true)] [Switch]$Restart, # Restart the service [Parameter(ParameterSetName='Status', Mandatory=$false)] [Switch]$Status = $($PSCmdlet.ParameterSetName -eq 'Status'), # Get the current service status [Parameter(ParameterSetName='Setup', Mandatory=$true)] [Parameter(ParameterSetName='Setup2', Mandatory=$true)] [Switch]$Setup, # Install the service [Parameter(ParameterSetName='Setup', Mandatory=$true)] [String]$UserName, # Set the service to run as this user [Parameter(ParameterSetName='Setup', Mandatory=$false)] [String]$Password, # Use this password for the user [Parameter(ParameterSetName='Setup2', Mandatory=$false)] [System.Management.Automation.PSCredential]$Credential, # Service account credential [Parameter(ParameterSetName='Remove', Mandatory=$true)] [Switch]$Remove, # Uninstall the service [Parameter(ParameterSetName='Build', Mandatory=$true)] [Switch]$Build, # Run the service (Internal use only) [Parameter(ParameterSetName='Version', Mandatory=$true)] [Switch]$Version # Get this script version ) # Don't forget to increment version when updating service $serviceVersion = '1.1.1.1' # This script name, with various levels of details $argv0 = Get-Item $MyInvocation.MyCommand.Definition $script = $argv0.basename # Ex: PSService $scriptName = $argv0.name # Ex: PSService.ps1 $scriptFullName = $argv0.fullname # Ex: C:\Temp\PSService.ps1 # Global settings $serviceName = "WinBGP" # A one-word name used for net start commands $serviceDisplayName = "WinBGP Engine" $ServiceDescription = "The BGP swiss army knife of networking on Windows" $installDir = "$($ENV:ProgramW6432)\WinBGP" # Where to install the service files $scriptCopy = "$installDir\$scriptName" $exeName = "$serviceName-Service.exe" $exeFullName = "$installDir\$exeName" # Remove file log #$logDir = "${ENV:programfiles}\WinBGP\Logs" # Where to log the service messages #$logFile = "$logDir\$serviceName.log" $logName = "Application" # Event Log name (Unrelated to the logFile!) # Note: The current implementation only supports "classic" (ie. XP-compatble) event logs. # To support new style (Vista and later) "Applications and Services Logs" folder trees, it would # be necessary to use the new *WinEvent commands instead of the XP-compatible *EventLog commands. # Gotcha: If you change $logName to "NEWLOGNAME", make sure that the registry key below does not exist: # HKLM\System\CurrentControlSet\services\eventlog\Application\NEWLOGNAME # Else, New-EventLog will fail, saying the log NEWLOGNAME is already registered as a source, # even though "Get-WinEvent -ListLog NEWLOGNAME" says this log does not exist! # If the -Version switch is specified, display the script version and exit. if ($Version) { return $serviceVersion } #-----------------------------------------------------------------------------# # # # Function Now # # # # Description Get a string with the current time. # # # # Notes The output string is in the ISO 8601 format, except for # # a space instead of a T between the date and time, to # # improve the readability. # # # # History # # 2015-06-11 JFL Created this routine. # # # #-----------------------------------------------------------------------------# Function Now { Param ( [Switch]$ms, # Append milliseconds [Switch]$ns # Append nanoseconds ) $Date = Get-Date $now = "" $now += "{0:0000}-{1:00}-{2:00} " -f $Date.Year, $Date.Month, $Date.Day $now += "{0:00}:{1:00}:{2:00}" -f $Date.Hour, $Date.Minute, $Date.Second $nsSuffix = "" if ($ns) { if ("$($Date.TimeOfDay)" -match "\.\d\d\d\d\d\d") { $now += $matches[0] $ms = $false } else { $ms = $true $nsSuffix = "000" } } if ($ms) { $now += ".{0:000}$nsSuffix" -f $Date.MilliSecond } return $now } #-----------------------------------------------------------------------------# # # # Function Log # # # # Description Log a string into the PSService.log file # # # # Arguments A string # # # # Notes Prefixes the string with a timestamp and the user name. # # (Except if the string is empty: Then output a blank line.)# # # # History # # 2016-06-05 JFL Also prepend the Process ID. # # 2016-06-08 JFL Allow outputing blank lines. # # # #-----------------------------------------------------------------------------# #Logging function function Write-Log { <# .Synopsis Write-Log writes a message to a specified log file with the current time stamp. .DESCRIPTION The Write-Log function is designed to add logging capability to other scripts. In addition to writing output and/or verbose you can write to a log file for later debugging. .NOTES Created by: Jason Wasser @wasserja Modified: 11/24/2015 09:30:19 AM Changelog: * Code simplification and clarification - thanks to @juneb_get_help * Added documentation. * Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks * Revised the Force switch to work as it should - thanks to @JeffHicks To Do: * Add error handling if trying to create a log file in a inaccessible location. * Add ability to write $Message to $Verbose or $Error pipelines to eliminate duplicates. .PARAMETER Message Message is the content that you wish to add to the log file. .PARAMETER Level Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational) .PARAMETER NoClobber Use NoClobber if you do not wish to overwrite an existing file. .EXAMPLE Write-Log -Message 'Log message' Writes the message to c:\Logs\PowerShellLog.log. .EXAMPLE Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log Writes the content to the specified log file and creates the path and file specified. .EXAMPLE Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error Writes the message to the specified log file as an error message, and writes the message to the error pipeline. .LINK https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0 #> [CmdletBinding()] Param ( [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNullOrEmpty()] [Alias("LogContent")] [string]$Message, [Parameter(Mandatory=$false)] [ValidateSet("Error","Warning","Information")] [string]$Level="Information", [Parameter(Mandatory=$false)] [string]$EventLogName=$logName, [Parameter(Mandatory=$false)] [string]$EventLogSource=$serviceName, [Parameter(Mandatory=$false)] [string]$EventLogId=1006, [Parameter(Mandatory=$false)] [string]$EventLogCategory=0, [Parameter(Mandatory=$false)] [Array]$AdditionalFields=$null, [Parameter(Mandatory=$false)] [switch]$NoClobber ) Begin { } Process { # Manage AdditionalFields (Not by default with PowerShell function) if ($AdditionalFields) { $EventInstance = [System.Diagnostics.EventInstance]::new($EventLogId, $EventLogCategory, $Level) $NewEvent = [System.Diagnostics.EventLog]::new() $NewEvent.Log = $EventLogName $NewEvent.Source = $EventLogSource [Array] $JoinedMessage = @( $Message $AdditionalFields | ForEach-Object { $_ } ) $NewEvent.WriteEvent($EventInstance, $JoinedMessage) } else { #Write log to event viewer (Enabled by default) Write-EventLog -LogName $EventLogName -Source $EventLogSource -EventId $EventLogId -EntryType $Level -Category $EventLogCategory -Message "$Message" } } End { } } #-----------------------------------------------------------------------------# # # # Function $source # # # # Description C# source of the PSService.exe stub # # # # Arguments # # # # Notes The lines commented with "SET STATUS" and "EVENT LOG" are # # optional. (Or blocks between "// SET STATUS [" and # # "// SET STATUS ]" comments.) # # SET STATUS lines are useful only for services with a long # # startup time. # # EVENT LOG lines are useful for debugging the service. # # # # History # # 2017-10-04 RBL Updated the OnStop() procedure adding the sections # # try{ # # }catch{ # # }finally{ # # } # # This resolved the issue where stopping the service would # # leave the PowerShell process -Service still running. This # # unclosed process was an orphaned process that would # # remain until the pid was manually killed or the computer # # was rebooted # # # #-----------------------------------------------------------------------------# # Overwrite for builder $scriptCopy= "$installDir\WinBGP-Engine.ps1" $scriptCopyCname = $scriptCopy -replace "\\", "\\" # Double backslashes. (The first \\ is a regexp with \ escaped; The second is a plain string.) $source = @" using System; using System.ServiceProcess; using System.Diagnostics; using System.Runtime.InteropServices; // SET STATUS using System.ComponentModel; // SET STATUS using System.Reflection; // SET STATUS [assembly: AssemblyVersion("$serviceVersion")] // SET VERSION public enum ServiceType : int { // SET STATUS [ SERVICE_WIN32_OWN_PROCESS = 0x00000010, SERVICE_WIN32_SHARE_PROCESS = 0x00000020, }; // SET STATUS ] public enum ServiceState : int { // SET STATUS [ SERVICE_STOPPED = 0x00000001, SERVICE_START_PENDING = 0x00000002, SERVICE_STOP_PENDING = 0x00000003, SERVICE_RUNNING = 0x00000004, SERVICE_CONTINUE_PENDING = 0x00000005, SERVICE_PAUSE_PENDING = 0x00000006, SERVICE_PAUSED = 0x00000007, }; // SET STATUS ] [StructLayout(LayoutKind.Sequential)] // SET STATUS [ public struct ServiceStatus { public ServiceType dwServiceType; public ServiceState dwCurrentState; public int dwControlsAccepted; public int dwWin32ExitCode; public int dwServiceSpecificExitCode; public int dwCheckPoint; public int dwWaitHint; }; // SET STATUS ] public enum Win32Error : int { // WIN32 errors that we may need to use NO_ERROR = 0, ERROR_APP_INIT_FAILURE = 575, ERROR_FATAL_APP_EXIT = 713, ERROR_SERVICE_NOT_ACTIVE = 1062, ERROR_EXCEPTION_IN_SERVICE = 1064, ERROR_SERVICE_SPECIFIC_ERROR = 1066, ERROR_PROCESS_ABORTED = 1067, }; public class Service_$serviceName : ServiceBase { // $serviceName may begin with a digit; The class name must begin with a letter private System.Diagnostics.EventLog eventLog; // EVENT LOG private ServiceStatus serviceStatus; // SET STATUS public const int SERVICE_ACCEPT_PRESHUTDOWN = 0x100; // Preshutdown public const int SERVICE_CONTROL_PRESHUTDOWN = 0xf; // Preshutdown public Service_$serviceName() { ServiceName = "$serviceName"; CanStop = true; CanShutdown = true; CanPauseAndContinue = true; AutoLog = true; // PreShutdown Section FieldInfo acceptedCommandsFieldInfo = typeof(ServiceBase).GetField("acceptedCommands", BindingFlags.Instance | BindingFlags.NonPublic); if (acceptedCommandsFieldInfo == null) { throw new ApplicationException("acceptedCommands field not found"); } int value = (int)acceptedCommandsFieldInfo.GetValue(this); acceptedCommandsFieldInfo.SetValue(this, value | SERVICE_ACCEPT_PRESHUTDOWN); // End PreShutdown Section eventLog = new System.Diagnostics.EventLog(); // EVENT LOG [ if (!System.Diagnostics.EventLog.SourceExists(ServiceName)) { System.Diagnostics.EventLog.CreateEventSource(ServiceName, "$logName"); } eventLog.Source = ServiceName; eventLog.Log = "$logName"; // EVENT LOG ] EventLog.WriteEntry(ServiceName, "$exeName $serviceName()"); // EVENT LOG } [DllImport("advapi32.dll", SetLastError=true)] // SET STATUS private static extern bool SetServiceStatus(IntPtr handle, ref ServiceStatus serviceStatus); protected override void OnStart(string [] args) { EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Entry. Starting script '$scriptCopyCname' -SCMStart"); // EVENT LOG // Set the service state to Start Pending. // SET STATUS [ // Only useful if the startup time is long. Not really necessary here for a 2s startup time. serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; serviceStatus.dwCurrentState = ServiceState.SERVICE_START_PENDING; serviceStatus.dwWin32ExitCode = 0; serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ] // 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 = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStart"; // Works if path has spaces, but not if it contains ' quotes. p.Start(); // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) string output = p.StandardOutput.ReadToEnd(); // Wait for the completion of the script startup code, that launches the -Service instance p.WaitForExit(); if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); // Success. Set the service state to Running. // SET STATUS serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS } catch (Exception e) { EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Failed to start $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG // Change the service state back to Stopped. // SET STATUS [ serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is... w32ex = e.InnerException as Win32Exception; } if (w32ex != null) { // Report the actual WIN32 error serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode; } else { // Make up a reasonable reason serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE); } // SET STATUS ] } finally { serviceStatus.dwWaitHint = 0; // SET STATUS SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS EventLog.WriteEntry(ServiceName, "$exeName OnStart() // Exit"); // EVENT LOG } } protected override void OnContinue() { EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Entry. Starting script '$scriptCopyCname' -SCMResume"); // EVENT LOG // Set the service state to Continue Pending. // SET STATUS [ // Only useful if the startup time is long. Not really necessary here for a 2s startup time. serviceStatus.dwServiceType = ServiceType.SERVICE_WIN32_OWN_PROCESS; serviceStatus.dwCurrentState = ServiceState.SERVICE_CONTINUE_PENDING; serviceStatus.dwWin32ExitCode = 0; serviceStatus.dwWaitHint = 2000; // It takes about 2 seconds to start PowerShell SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS ] // 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 = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMResume"; // Works if path has spaces, but not if it contains ' quotes. p.Start(); // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) string output = p.StandardOutput.ReadToEnd(); // Wait for the completion of the script startup code, that launches the -Service instance p.WaitForExit(); if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); // Success. Set the service state to Running. // SET STATUS serviceStatus.dwCurrentState = ServiceState.SERVICE_RUNNING; // SET STATUS } catch (Exception e) { EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Failed to resume $scriptCopyCname. " + e.Message, EventLogEntryType.Error); // EVENT LOG // Change the service state back to Paused. // SET STATUS [ serviceStatus.dwCurrentState = ServiceState.SERVICE_PAUSED; Win32Exception w32ex = e as Win32Exception; // Try getting the WIN32 error code if (w32ex == null) { // Not a Win32 exception, but maybe the inner one is... w32ex = e.InnerException as Win32Exception; } if (w32ex != null) { // Report the actual WIN32 error serviceStatus.dwWin32ExitCode = w32ex.NativeErrorCode; } else { // Make up a reasonable reason serviceStatus.dwWin32ExitCode = (int)(Win32Error.ERROR_APP_INIT_FAILURE); } // SET STATUS ] } finally { serviceStatus.dwWaitHint = 0; // SET STATUS SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS EventLog.WriteEntry(ServiceName, "$exeName OnContinue() // Exit"); // EVENT LOG } } private void StopSCM() { // Start a child process with another copy of ourselves 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 = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMStop"; // Works if path has spaces, but not if it contains ' quotes. p.Start(); // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) string output = p.StandardOutput.ReadToEnd(); // Wait for the PowerShell script to be fully stopped. p.WaitForExit(); if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); // Success. Set the service state to Stopped. // SET STATUS serviceStatus.dwCurrentState = ServiceState.SERVICE_STOPPED; // SET STATUS } catch (Exception e) { EventLog.WriteEntry(ServiceName, "$exeName StopSCM() // Failed to stop $scriptCopyCname.", EventLogEntryType.Error); // EVENT LOG throw e; // SET STATUS ] } finally { serviceStatus.dwWaitHint = 0; // SET STATUS SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS } } private void SuspendSCM() { // Start a child process with another copy of ourselves 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 = "-ExecutionPolicy Bypass -c & '$scriptCopyCname' -SCMSuspend"; // Works if path has spaces, but not if it contains ' quotes. p.Start(); // Read the output stream first and then wait. (To avoid deadlocks says Microsoft!) string output = p.StandardOutput.ReadToEnd(); // Wait for the PowerShell script to be fully stopped. p.WaitForExit(); if (p.ExitCode != 0) throw new Win32Exception((int)(Win32Error.ERROR_APP_INIT_FAILURE)); // Success. Set the service state to Suspended. // SET STATUS serviceStatus.dwCurrentState = ServiceState.SERVICE_PAUSED; // SET STATUS } catch (Exception e) { EventLog.WriteEntry(ServiceName, "$exeName SuspendSCM() // Failed to suspend $scriptCopyCname.", EventLogEntryType.Error); // EVENT LOG throw e; // SET STATUS ] } finally { serviceStatus.dwWaitHint = 0; // SET STATUS SetServiceStatus(ServiceHandle, ref serviceStatus); // SET STATUS } } protected override void OnStop() { EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Entry"); // EVENT LOG try { this.StopSCM(); base.OnStop(); } catch(Exception e) { EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG throw e; } EventLog.WriteEntry(ServiceName, "$exeName OnStop() // Exit"); // EVENT LOG } protected override void OnPause() { EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Entry"); // EVENT LOG try { this.SuspendSCM(); base.OnPause(); // This will set the service status to "Paused" } catch(Exception e) { EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG throw e; } EventLog.WriteEntry(ServiceName, "$exeName OnPause() // Exit"); // EVENT LOG } protected override void OnCustomCommand(int command) { if (command == SERVICE_CONTROL_PRESHUTDOWN) { EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Entry"); // EVENT LOG try{ this.StopSCM(); } catch(Exception e) { EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Fail. " + e.Message, EventLogEntryType.Error); // EVENT LOG throw e; } EventLog.WriteEntry(ServiceName, "$exeName OnPreshutdown() // Exit"); // EVENT LOG } base.OnCustomCommand(command); } protected override void OnShutdown() { // * NOP * } public static void Main() { System.ServiceProcess.ServiceBase.Run(new Service_$serviceName()); } } "@ #-----------------------------------------------------------------------------# # # # Function Main # # # # Description Execute the specified actions # # # # Arguments See the Param() block at the top of this script # # # # Notes # # # # History # # # #-----------------------------------------------------------------------------# # Identify the user name. We use that for logging. $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $currentUserName = $identity.Name # Ex: "NT AUTHORITY\SYSTEM" or "Domain\Administrator" # Remove file log # if ($Setup) {Log ""} # Insert one blank line to separate test sessions logs # Log $MyInvocation.Line # The exact command line that was used to start us # The following commands write to the event log, but we need to make sure the PSService source is defined. New-EventLog -LogName $logName -Source $serviceName -ea SilentlyContinue # Workaround for PowerShell v2 bug: $PSCmdlet Not yet defined in Param() block $Status = ($PSCmdlet.ParameterSetName -eq 'Status') if ($Status) { # Get the current service status $spid = $null $processes = @(Get-WmiObject Win32_Process -filter "Name = 'powershell.exe'" | Where-Object { $_.CommandLine -match ".*$scriptCopyCname.*-Service" }) foreach ($process in $processes) { # There should be just one, but be prepared for surprises. $spid = $process.ProcessId Write-Verbose "$serviceName Process ID = $spid" } # if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\services\$serviceName") {} try { $pss = Get-Service $serviceName -ea stop # Will error-out if not installed } catch { "Not Installed" return } $pss.Status 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 } return } if ($Setup) { # Install the service # Check if it's necessary try { $pss = Get-Service $serviceName -ea stop # Will error-out if not installed # Check if this script is newer than the installed copy. if ((Get-Item $scriptCopy -ea SilentlyContinue).LastWriteTime -lt (Get-Item $scriptFullName -ea SilentlyContinue).LastWriteTime) { Write-Verbose "Service $serviceName is already Installed, but requires upgrade" & $scriptFullName -Remove throw "continue" } else { Write-Verbose "Service $serviceName is already Installed, and up-to-date" } exit 0 } catch { # This is the normal case here. Do not throw or write any error! Write-Debug "Installation is necessary" # Also avoids a ScriptAnalyzer warning # And continue with the installation. } if (!(Test-Path $installDir)) { New-Item -ItemType directory -Path $installDir | Out-Null } # Copy the service script into the installation directory if ($ScriptFullName -ne $scriptCopy) { Write-Verbose "Installing $scriptCopy" Copy-Item $ScriptFullName $scriptCopy } # Generate the service .EXE from the C# source embedded in this script try { Write-Verbose "Compiling $exeFullName" Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false } catch { $msg = $_.Exception.Message Write-error "Failed to create the $exeFullName service stub. $msg" exit 1 } # Register the service Write-Verbose "Registering service $serviceName" if ($UserName -and !$Credential.UserName) { $emptyPassword = New-Object -Type System.Security.SecureString switch ($UserName) { {"LocalService", "NetworkService" -contains $_} { $Credential = New-Object -Type System.Management.Automation.PSCredential ("NT AUTHORITY\$UserName", $emptyPassword) } {"LocalSystem", ".\LocalSystem", "${env:COMPUTERNAME}\LocalSystem", "NT AUTHORITY\LocalService", "NT AUTHORITY\NetworkService" -contains $_} { $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $emptyPassword) } default { if (!$Password) { $Credential = Get-Credential -UserName $UserName -Message "Please enter the password for the service user" } else { $securePassword = ConvertTo-SecureString $Password -AsPlainText -Force $Credential = New-Object -Type System.Management.Automation.PSCredential ($UserName, $securePassword) } } } } #Find if exeFullName contain withespace (Security issue with Unquoted Service Path) # TO IMPROVE - Create PR on GitHub if ($exeFullName -match "\s") { $exeFullName = "`"$exeFullName`""} if ($Credential.UserName) { Write-Log -Message "Configuring the service to run as $($Credential.UserName)" # TO IMPROVE - Add variable to manage DependsOn and Create PR on GitHub $pss = New-Service -Name $serviceName -BinaryPathName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -Credential $Credential -DependsOn 'RemoteAccess' } else { Write-Log -Message "Configuring the service to run by default as LocalSystem" # TO IMPROVE - Add variable to manage DependsOn and Create PR on GitHub $pss = New-Service -Name $serviceName -BinaryPathName $exeFullName -DisplayName $serviceDisplayName -Description $ServiceDescription -StartupType Automatic -DependsOn 'RemoteAccess' } return } if ($Build) { # Install the service # Generate the service .EXE from the C# source embedded in this script # Overwrite for builder $exeFullName="$exeName" try { Write-Verbose "Compiling $exeFullName" Add-Type -TypeDefinition $source -Language CSharp -OutputAssembly $exeFullName -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess" -Debug:$false } catch { $msg = $_.Exception.Message Write-error "Failed to create the $exeFullName service stub. $msg" exit 1 } return } if ($Remove) { # Uninstall the service # Check if it's necessary try { $pss = Get-Service $serviceName -ea stop # Will error-out if not installed } catch { Write-Verbose "Already uninstalled" return } Stop-Service $serviceName # Make sure it's stopped # In the absence of a Remove-Service applet, use sc.exe instead. Write-Verbose "Removing service $serviceName" $msg = sc.exe delete $serviceName if ($LastExitCode) { Write-Error "Failed to remove the service ${serviceName}: $msg" exit 1 } else { Write-Verbose $msg } # Remove the installed files if (Test-Path $installDir) { foreach ($ext in ("exe", "pdb", "ps1")) { $file = "$installDir\$serviceName.$ext" if (Test-Path $file) { Write-Verbose "Deleting file $file" Remove-Item $file } } if (!(@(Get-ChildItem $installDir -ea SilentlyContinue)).Count) { Write-Verbose "Removing directory $installDir" Remove-Item $installDir } } # Remove file log # Log "" # Insert one blank line to separate test sessions logs return }