Quite recently I was facing an issue whereas a new service was introduced which needed deployment through Azure DevOps remotely. Normally spoken I would fallback in Windows Remote Management (WinRM for short) and use it in combination with the Invoke-Command
cmdlet to create in conduction with the New-Service
cmdlet. What if WinRM only cannot be used? Are there any other protocols that I was able to use? In this case, I used the RPC protocol, as in many native tools, it is possible to use it in conduction with a computer name parameter. In this blog, I will explore the native sc.exe
command line tool.
Here came the thing, either I was going to wrap around some functions around the command line tool, or explore other possibilities. So… that’s where I stumbled on Crescendo. As the Github page states:
Crescendo is a development accelerator enabling you to rapidly build PowerShell cmdlets that leverage existing command-line tools.
In my ears, this sounded beautiful, exactly what I was looking for. In this post, I will talk about my story so far with Crescendo.
Crescendo story
While Crescendo is already in development from 2020, recently it was announced that Crescendo went in General Availability. Because I could not exactly found out from some examples on the internet what I was looking for, my story started by going through the awesome post by Sean Wheeler and the general examples that are provided on the Crescendo Github page. My first thought was to create at least 4 functions, which would be responsible for creating, starting, deleting and querying the relevant service. Some good approved verbs that could be used would look something like:
- New-ScService
- Start-ScService
- Remove-ScService
- Get-ScService
My first JSON configuration
Taking the reference template from Sean Wheelers Github, the rough first JSON configuration is shown below.
{
"$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
"Commands": [
{
"Verb": "New",
"Noun": "ScService",
"OriginalName": "C:\\Windows\\System32\\sc.exe",
"OriginalCommandElements": ["create"]
}
]
}
The schema line comes with the Crescendo module, and without it, it was quite difficult to write my development in VSCode. The schema provides IntelliSense for the JSON, which made my life such easier to write the JSON.
Next up in the Commands section, I am defining the Verb and Noun that gets produces when exporting the Crescendo Command. The OriginalName and OriginalCommandElements specifies the native command as executable to run. After calling the Export-CrescendoModule
cmdlet, you can see the following results.
function New-ScService
{
[PowerShellCustomFunctionAttribute(RequiresElevation=$False)]
[CmdletBinding()]
param( )
BEGIN {
$__PARAMETERMAP = @{}
$__outputHandlers = @{ Default = @{ StreamOutput = $true; Handler = { $input } } }
}
PROCESS {
$__boundParameters = $PSBoundParameters
$__defaultValueParameters = $PSCmdlet.MyInvocation.MyCommand.Parameters.Values.Where({$_.Attributes.Where({$_.TypeId.Name -eq "PSDefaultValueAttribute"})}).Name
$__defaultValueParameters.Where({ !$__boundParameters["$_"] }).ForEach({$__boundParameters["$_"] = get-variable -value $_})
$__commandArgs = @()
$MyInvocation.MyCommand.Parameters.Values.Where({$_.SwitchParameter -and $_.Name -notmatch "Debug|Whatif|Confirm|Verbose" -and ! $__boundParameters[$_.Name]}).ForEach({$__boundParameters[$_.Name] = [switch]::new($false)})
if ($__boundParameters["Debug"]){wait-debugger}
$__commandArgs += 'create'
$__commandArgs += '<servername>'
foreach ($paramName in $__boundParameters.Keys|
Where-Object {!$__PARAMETERMAP[$_].ApplyToExecutable}|
Sort-Object {$__PARAMETERMAP[$_].OriginalPosition}) {
$value = $__boundParameters[$paramName]
$param = $__PARAMETERMAP[$paramName]
if ($param) {
if ($value -is [switch]) {
if ($value.IsPresent) {
if ($param.OriginalName) { $__commandArgs += $param.OriginalName }
}
elseif ($param.DefaultMissingValue) { $__commandArgs += $param.DefaultMissingValue }
}
elseif ( $param.NoGap ) {
$pFmt = "{0}{1}"
if($value -match "\s") { $pFmt = "{0}""{1}""" }
$__commandArgs += $pFmt -f $param.OriginalName, $value
}
else {
if($param.OriginalName) { $__commandArgs += $param.OriginalName }
$__commandArgs += $value | Foreach-Object {$_}
}
}
}
$__commandArgs = $__commandArgs | Where-Object {$_ -ne $null}
if ($__boundParameters["Debug"]){wait-debugger}
if ( $__boundParameters["Verbose"]) {
Write-Verbose -Verbose -Message C:\Windows\System32\sc.exe
$__commandArgs | Write-Verbose -Verbose
}
$__handlerInfo = $__outputHandlers[$PSCmdlet.ParameterSetName]
if (! $__handlerInfo ) {
$__handlerInfo = $__outputHandlers["Default"] # Guaranteed to be present
}
$__handler = $__handlerInfo.Handler
if ( $PSCmdlet.ShouldProcess("C:\Windows\System32\sc.exe $__commandArgs")) {
# check for the application and throw if it cannot be found
if ( -not (Get-Command -ErrorAction Ignore "C:\Windows\System32\sc.exe")) {
throw "Cannot find executable 'C:\Windows\System32\sc.exe'"
}
if ( $__handlerInfo.StreamOutput ) {
& "C:\Windows\System32\sc.exe" $__commandArgs | & $__handler
}
else {
$result = & "C:\Windows\System32\sc.exe" $__commandArgs
& $__handler $result
}
}
}
}
Quite advanced neath stuff right there what the Export-CrescendoModule
cmdlet produces.
Creating the parameters
After looking further in the examples, it was time to add some parameters for the sc.exe create
. Looking to the documentation, I added most of the parameters.
"Parameters": [
{
"OriginalName": "",
"Name": "ServiceName",
"ParameterType": "string",
"ParameterSetName": ["Default", "ByUser"],
"NoGap": false,
"Description": "Specifies the service name"
},
{
"OriginalName": "binPath=",
"Name": "Path",
"ParameterType": "string",
"ParameterSetName": ["Default", "ByUser"],
"Mandatory": true,
"NoGap": true,
"Description": "Specifies a path to the service binary file"
},
{
"OriginalName": "type=",
"Name": "ServiceType",
"ParameterType": "string",
"ParameterSetName": ["Default", "ByUser"],
"Mandatory": false,
"NoGap": true,
"DefaultValue": "demand",
"AdditionalParameterAttributes": [
"[ValidateSet('own', 'share', 'kernel', 'filesys', 'rec', 'interact')]"
],
"Description": "Specifies the service type"
},
{
"OriginalName": "start=",
"Name": "StartType",
"ParameterType": "string",
"ParameterSetName": ["Default", "ByUser"],
"Mandatory": false,
"NoGap": true,
"DefaultValue": "demand",
"AdditionalParameterAttributes": [
"[ValidateSet('boot', 'system', 'auto', 'demand', 'disabled')]"
],
"Description": "Specifies the start type for the service"
},
{
"OriginalName": "displayName=",
"Name": "DisplayName",
"ParameterType": "string",
"ParameterSetName": ["Default", "ByUser"],
"Mandatory": false,
"NoGap": true,
"Description": "Specifies a friendly name that can be used by user interface programs to identify the service"
},
{
"OriginalName": "obj=",
"Name": "Username",
"ParameterType": "string",
"ParameterSetName": ["ByUser"],
"Mandatory": true,
"NoGap": true,
"Description": "Specify the name of an account which a service will run"
},
{
"OriginalName": "password=",
"Name": "Password",
"ParameterType": "string",
"ParameterSetName": ["ByUser"],
"Mandatory": true,
"NoGap": true,
"Description": "Specify a password of an account which a service will run"
}
]
When I ran the following commands in the Powershell terminal, I was able to see the syntax that I was looking for.
The only thing that I was still missing, was the -ComputerName
parameter, and that is where the challenge came. The reason is that the native command line executable is expecting it to be on a position. In this case, it begins with it, or it should be left out. An example says thousand words.
Wrapping the methods
On my second approach, I left the OriginalCommandElements empty to build up dynamically the methods it needs to call to the command line. I started out with defining the ComputerName as parameter after going through the documentation and give it a specify Position.
{
"OriginalName": "",
"Name": "ComputerName",
"Mandatory": false,
"ParameterType": "string",
"ParameterSetName": ["Default", "CreateService"],
"OriginalPosition": 0,
"Position": 0,
"Description": "Specify the computer name by UNC path"
}
After that, the dynamic MethodName parameter was defined, allowing for specific sets with the [ValidateSet()]
attribute for now.
{
"OriginalName": "",
"Name": "MethodName",
"Mandatory": true,
"ParameterType": "string",
"ParameterSetName": ["Default", "CreateService"],
"OriginalPosition": 1,
"Position": 1,
"AdditionalParameterAttributes": [
"[ValidateSet('create', 'delete', 'start', 'stop')]"
],
"Description": "Specify the method name to invoke to sc.exe"
}
The full example shown below
{
"$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
"Commands": [
{
"Verb": "Invoke",
"Noun": "ScService",
"OriginalName": "C:\\Windows\\System32\\sc.exe",
"OriginalCommandElements": [],
"SupportsShouldProcess": true,
"ConfirmImpact": "Medium",
"Description": "Invoke-ScService with the specified methods e.g. create, start, delete and their respected parameters",
"Usage": {
"Synopsis": "Invokes the sc.exe with specified parameters and methods"
},
"Examples": [
{
"Command": "Invoke-ScService -MethodName delete -ServiceName MyService",
"Description": "Removes the MyService from the Service Control Manager database",
"OriginalCommand": "C:\\Windows\\System32\\sc.exe delete MyService"
},
{
"Command": "Invoke-ScService -MethodName create -ServiceName MyService -Path '<pathtoexe>' -ServiceType own -StartType auto",
"Description": "Create a new service called MyService that runs in its own process and start automatically after server reboot",
"OriginalCommand": "C:\\Windows\\System32\\sc.exe create MyService binPath=<pathtoexe> type=own start=auto"
},
{
"Command": "Invoke-SCService -ComputerName \\\\MyLocalPc -MethodName create -ServiceName MyService -Path '<pathtoexe>'",
"Description": "Create a new service called MyService on MyLocalPc"
}
],
"DefaultParameterSetName": "Default",
"Parameters": [
{
"OriginalName": "",
"Name": "ComputerName",
"Mandatory": false,
"ParameterType": "string",
"ParameterSetName": ["Default", "CreateService"],
"OriginalPosition": 0,
"Position": 0,
"Description": "Specify the computer name by UNC path"
},
{
"OriginalName": "",
"Name": "MethodName",
"Mandatory": true,
"ParameterType": "string",
"ParameterSetName": ["Default", "CreateService"],
"OriginalPosition": 1,
"Position": 1,
"AdditionalParameterAttributes": [
"[ValidateSet('create', 'delete', 'start', 'stop')]"
],
"Description": "Specify the method name to invoke to sc.exe"
},
{
"OriginalName": "",
"Name": "ServiceName",
"ParameterType": "string",
"ParameterSetName": ["Default", "CreateService"],
"OriginalPosition": 2,
"Position": 2,
"Mandatory": true,
"NoGap": false,
"Description": "Specify the service name"
},
{
"OriginalName": "binPath=",
"Name": "Path",
"ParameterType": "string",
"ParameterSetName": ["CreateService"],
"OriginalPosition": 3,
"Position": 3,
"Mandatory": true,
"NoGap": true,
"Description": "Specifies a path to the service binary file"
},
{
"OriginalName": "type=",
"Name": "ServiceType",
"ParameterType": "string",
"ParameterSetName": ["CreateService"],
"OriginalPosition": 4,
"Position": 4,
"Mandatory": false,
"NoGap": true,
"AdditionalParameterAttributes": [
"[ValidateSet('own', 'share', 'kernel', 'filesys', 'rec', 'interact')]"
],
"Description": "Specifies the service type"
},
{
"OriginalName": "start=",
"Name": "StartType",
"ParameterType": "string",
"ParameterSetName": ["CreateService"],
"OriginalPosition": 5,
"Position": 5,
"Mandatory": false,
"NoGap": true,
"AdditionalParameterAttributes": [
"[ValidateSet('boot', 'system', 'auto', 'demand', 'disabled')]"
],
"Description": "Specifies the start type for the service"
},
{
"OriginalName": "displayName=",
"Name": "DisplayName",
"ParameterType": "string",
"ParameterSetName": ["CreateService"],
"OriginalPosition": 6,
"Position": 6,
"Mandatory": false,
"NoGap": true,
"Description": "Specifies a friendly name that can be used by user interface programs to identify the service"
},
{
"OriginalName": "obj=",
"Name": "Username",
"ParameterType": "string",
"ParameterSetName": ["CreateService"],
"OriginalPosition": 7,
"Position": 7,
"Mandatory": false,
"NoGap": true,
"Description": "Specify the name of an account which a service will run"
},
{
"OriginalName": "password=",
"Name": "Password",
"ParameterType": "string",
"ParameterSetName": ["CreateService"],
"OriginalPosition": 8,
"Position": 8,
"Mandatory": false,
"NoGap": true,
"Description": "Specify a password of an account which a service will run"
}
]
}
]
}
If you are wondering what that
NoGap
value is in the parameters, it means that parameters are constructed as “start=auto”
Let’s see it in action when rebuilding the Crescendo module.
Deleting the service with the delete
option.
Awesome, I am now able to create a local service and a remote service on a computer that can be contacted through RPC protocol.
Conclusion
While this is the bare minimum, at least for me it is, there are still some things to explore. The first main component is now build. But the second component takes some time to write, which is the output handler. The output handler functions parse the output from the native command and return it as Powershell objects to work with. As you’ve probably noticed, the query
or queryex
options are not added yet, as these require output to be transformed. Also, I noticed that it requires of course elevation to create new services in the Service Control Manager database.
Yet so far with the investment and learning the Crescendo module, I would say it is worth spending your time in it, especially if you are working with a lot of native command line tools. If you have command line tools that support outputting objects in JSON, it is much easier to create an output handler.
Anyway, see you in the next blog!