Search
Recent Tweets
« Compression Experiments In the Build and Deployment Process | Main | Get Latest Version of Specific Files With TFS Power Tools »
Thursday
Mar222012

Install a Windows Service Remotely with PowerShell

The Objective

Our previous application deployment process worked in a pull manner, meaning the deployment package was pulled from another computer (such as a build server) to the target server, and the install was performed locally on the target machine. It is usually easier to make changes locally on a box than remotely but this had some drawbacks I did not like.

First, for continuous deployment scenarios, it made more sense to me to deploy in a push manner. Previously we had a scheduled task on the target app servers that would periodically install our latest application bits, but it had no clue whether it was reinstalling the same build or not, or if there might be a problem with the build it is trying to deploy. Additionally it is more convenient to be able to deploy an app from any given location to any other location. With that in mind I set out to change our deployment process to a push model.

The Problem

Going into this I knew that we would no longer be able to directly use installutil to install our Windows Service on our app server as that only works locally. What I was not sure of was what to replace that with. My deployment script is in PowerShell and I was not finding much Googling a solution for remotely installing services with PowerShell or otherwise.

Some Solutions

WMI

I did come across this Fervent Coder post on tackling this problem using WMI and C# and I started to base my solution on this. However WMI felt a bit low-level, raw, and dirty to me and initially I did not want to create a C# DLL for this or translate that code into equivalent PowerShell script.

PS Remoting

The PowerShell ninja @beefarino suggested PS Remoting. That might be a valid option to explore but I was a little leery of it; maybe if I had Jim's skills and was feeling more daring. PSExec is another similar option but probably less desirable.


SC.exe

SC.exe in the Windows Resource Kit is an option. I did not really evaluate this much as I did not run across it until late in the process.

Other

There are likely external tools that could be used or hackery like remote registry changes.

Pick Something Quick

Somehow things fell such that I did not start this until the day before a new release was due for testing. With little time to evaluate options I went the PowerShell WMI approach. Despite the cons mentioned it is fairly lightweight, allowed complete control, and worked reliably in my testing.

With a basis on the Fervent Coder WMI post I began to come up with a PowerShell version of that C# implementation. Loading a .net 4 dll in PowerShell was a bit of a pain though @beefarino had a solution for that too.

Enough, On With the Code Already

So I'm no PowerShell expert and this was done under the gun but...

First a function to get a handle to the desired remote service.
	function Get-Service(
	    [string]$serviceName = $(throw "serviceName is required"), 
	    [string]$targetServer = $(throw "targetServer is required"))
	{
	    $service = Get-WmiObject -Namespace "root\cimv2" -Class "Win32_Service" `
			-ComputerName $targetServer -Filter "Name='$serviceName'" -Impersonation 3    
	    return $service
	}
An Impersonation value of 3 indicates "Impersonate (Allows objects to use the credentials of the caller)." I believe if not specified the default value is read from the registry. In my case the default did not appear to be Impersonate.

Next a function to stop and uninstall the service if it exists already.
function Uninstall-Service(
    [string]$serviceName = $(throw "serviceName is required"), 
    [string]$targetServer = $(throw "targetServer is required"))
{
    $service = Get-Service $serviceName $targetServer
    
    if (!($service))
    { 
        Write-Warning "Failed to find service $serviceName on $targetServer. Nothing to uninstall."
        return
    }
    
    "Found service $serviceName on $targetServer; checking status"
            
    if ($service.Started)
    {
        "Stopping service $serviceName on $targetServer"
        #could also use Set-Service, net stop, SC, psservice, psexec etc.
        $result = $service.StopService()
        Test-ServiceResult -operation "Stop service $serviceName on $targetServer" -result $result
    }
    
    "Attempting to uninstall service $serviceName on $targetServer"
    $result = $service.Delete()
    Test-ServiceResult -operation "Delete service $serviceName on $targetServer" -result $result    
}
Test-ServiceResult inspects the return code of a service operation; if it represents an error it maps the code to an error message and writes an error or warning.
function Test-ServiceResult(
    [string]$operation = $(throw "operation is required"), 
    [object]$result = $(throw "result is required"), 
    [switch]$continueOnError = $false)
{
    $retVal = -1
    if ($result.GetType().Name -eq "UInt32") { $retVal = $result } else {$retVal = $result.ReturnValue}
        
    if ($retVal -eq 0) {return}
    
    $errorcode = 'Success,Not Supported,Access Denied,Dependent Services Running,Invalid Service Control'
    $errorcode += ',Service Cannot Accept Control, Service Not Active, Service Request Timeout'
    $errorcode += ',Unknown Failure, Path Not Found, Service Already Running, Service Database Locked'
    $errorcode += ',Service Dependency Deleted, Service Dependency Failure, Service Disabled'
    $errorcode += ',Service Logon Failure, Service Marked for Deletion, Service No Thread'
    $errorcode += ',Status Circular Dependency, Status Duplicate Name, Status Invalid Name'
    $errorcode += ',Status Invalid Parameter, Status Invalid Service Account, Status Service Exists'
    $errorcode += ',Service Already Paused'
    $desc = $errorcode.Split(',')[$retVal]
    
    $msg = ("{0} failed with code {1}:{2}" -f $operation, $retVal, $desc)
    
    if (!$continueOnError) { Write-Error $msg } else { Write-Warning $msg }        
}
The code to install a service remotely took more trial and error. The below worked for my needs but there are some hard-coded values that may be desirable to change depending upon the service and need.
function Install-Service(
    [string]$serviceName = $(throw "serviceName is required"), 
    [string]$targetServer = $(throw "targetServer is required"),
    [string]$displayName = $(throw "displayName is required"),
    [string]$physicalPath = $(throw "physicalPath is required"),
    [string]$userName = $(throw "userName is required"),
    [string]$password = "",
    [string]$startMode = "Automatic",
    [string]$description = "",
    [bool]$interactWithDesktop = $false
)
{
    # can't use installutil; only for installing services locally
    #[wmiclass]"Win32_Service" | Get-Member -memberType Method | format-list -property:*    
    #[wmiclass]"Win32_Service"::Create( ... )        
        
    # todo: cleanup this section 
    $serviceType = 16          # OwnProcess
    $serviceErrorControl = 1   # UserNotified
    $loadOrderGroup = $null
    $loadOrderGroupDepend = $null
    $dependencies = $null
   
    # description?
    $params = `
        $serviceName, `
        $displayName, `
        $physicalPath, `
        $serviceType, `
        $serviceErrorControl, `
        $startMode, `
        $interactWithDesktop, `
        $userName, `
        $password, `
        $loadOrderGroup, `
        $loadOrderGroupDepend, `
        $dependencies `
        
    $scope = new-object System.Management.ManagementScope("\\$targetServer\root\cimv2", `
        (new-object System.Management.ConnectionOptions))
    "Connecting to $targetServer"
    $scope.Connect()
    $mgt = new-object System.Management.ManagementClass($scope, `
        (new-object System.Management.ManagementPath("Win32_Service")), `
        (new-object System.Management.ObjectGetOptions))
    
    $op = "service $serviceName ($physicalPath) on $targetServer"    
    "Installing $op"
    $result = $mgt.InvokeMethod("Create", $params)    
    Test-ServiceResult -operation "Install $op" -result $result
    "Installed $op"
    
    "Setting $serviceName description to '$description'"
    Set-Service -ComputerName $targetServer -Name $serviceName -Description $description
    "Service install complete"
}
Starting the service could be done a variety of ways including net start, Set-Service and other means but since I was already in WMI land...
function Start-Service(
    [string]$serviceName = $(throw "serviceName is required"), 
    [string]$targetServer = $(throw "targetServer is required"))
{
    "Getting service $serviceName on server $targetServer"
    $service = Get-Service $serviceName $targetServer
    if (!($service.Started))
    {
        "Starting service $serviceName on server $targetServer"
        $result = $service.StartService()
        Test-ServiceResult -operation "Starting service $serviceName on $targetServer" -result $result    
    }
}
An example of calling these functions together might look like the below. The various helper functions this script calls are of little import; Set-Activity and Write-Log deal with writing to both output and progress and Copy-Files captures and writes copy output and optionally makes additional attempts on error.
function Publish-Service
{
    Set-Activity "Deploying Service files"
    $serviceName = "MyAppDataService"
    
    Write-Log "Stopping, uninstalling service $serviceName on $_targetServer"
    Uninstall-Service $serviceName $_targetServer

    "Pausing to avoid potential temporary access denied"
    Start-Sleep -s 5 # Yeah I know, don't beat me up over this
    
    Remove-RootFilesInDir $_targetServicesDir
    
    Copy-Files "$_packagePath\Services\**" $_targetServicesDir -recurse
    
    Install-Service `
    -ServiceName $serviceName `
    -TargetServer $_targetServer `
    -DisplayName "MyApp Data Service" `
    -PhysicalPath "D:\Apps\MyApp\Services\MyApp.DataService.exe" `
    -Username "NT AUTHORITY\NetworkService" `
    -Description "Provides remote TCP/IP communication between the MyApp client application and the database tier."
        
    Start-Service $serviceName $_targetServer
}

PrintView Printer Friendly Version

Reader Comments (1)

Nice post how to create windows service using PoserShell cmdlets
http://www.yaplex.com/powershell/create-a-windows-service-using-powershell/

October 31, 2012 | Unregistered Commenteralex

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>