Using PowerShell in your extension
Let's go more in-depth into the Windows Admin Center Extensions SDK - let's talk about adding PowerShell commands to your extension.
PowerShell in TypeScript
The gulp build process has a generate step that will take any {!ScriptName}.ps1
that is placed in the \src\resources\scripts
folder and build them into the powershell-scripts
class under the \src\generated
folder.
Note
Don't manually update the powershell-scripts.ts
nor the strings.ts
files. Any change you make will be overwritten on the next generate.
Running a PowerShell Script
Any scripts that you want to run on a node can be placed in \src\resources\scripts\{!ScriptName}.ps1
.
Important
Any changes make in a {!ScriptName}.ps1
file will not be reflected in your project until gulp generate
has been run.
The API works by first creating a PowerShell session on the nodes you are targeting, creating the PowerShell script with any parameters that need to be passed in, and then running the script on the sessions that were created.
For example, we have this script \src\resources\scripts\Get-NodeName.ps1
:
Param
(
[String] $stringFormat
)
$nodeName = [string]::Format($stringFormat,$env:COMPUTERNAME)
Write-Output $nodeName
We will create a PowerShell session for our target node:
const session = this.appContextService.powerShell.createSession('{!TargetNode}');
Then we will create the PowerShell script with an input parameter:
const command = PowerShell.createCommand(PowerShellScripts.Get_NodeName, {stringFormat: 'The name of the node is {0}!'});
Lastly, we need to run that script in the session we created:
public ngOnInit(): void {
this.session = this.appContextService.powerShell.createAutomaticSession('{!TargetNode}');
}
public getNodeName(): Observable<any> {
const command = PowerShell.createCommand(PowerShellScripts.Get_NodeName, { stringFormat: 'The name of the node is {0}!'});
return this.appContextService.powerShell.run(this.session, command)
.pipe(
map(
response => {
if (response && response.results) {
return response.results;
}
return 'no response';
}
)
);
}
public ngOnDestroy(): void {
this.session.dispose()
}
Now we will need to subscribe to the observable function we just created. Place this where you need to call the function to run the PowerShell script:
this.getNodeName().subscribe(
response => {
console.log(response)
}
);
By providing the node name to the createSession method, a new PowerShell session is created, used, and then immediately destroyed upon completion of the PowerShell call.
Key Options
A few options are available when calling the PowerShell API. Each time a session is created it can be created with or without a key.
Key: This creates a keyed session that can be looked up and reused, even across components (meaning that Component 1 can create a session with key "SME-ROCKS," and Component 2 can use that same session). If a key is provided, the session that is created must be disposed of by calling dispose() as was done in the example above. A session should not be kept without being disposed of for more than 5 minutes.
const session = this.appContextService.powerShell.createSession('{!TargetNode}', '{!Key}');
Keyless: A key will automatically be created for the session. This session with be disposed of automatically after 3 minutes. Using keyless allows your extension to recycle the use of any runspace that is already available at the time of creation of a session. If no runspace is available then a new one will be created. This functionality is good for one-off calls, but repeated use can affect performance. A session takes approximately 1 second to create, so continuously recycling sessions can cause slowdowns.
const session = this.appContextService.powerShell.createSession('{!TargetNodeName}');
or
const session = this.appContextService.powerShell.createAutomaticSession('{!TargetNodeName}');
In most situations, create a keyed session in the ngOnInit()
method, and then dispose of it in ngOnDestroy()
. Follow this pattern when there are multiple PowerShell scripts in a component but the underlying session IS NOT shared across components.
For best results, make sure session creation is managed inside of components rather than services - this helps ensure that lifetime and cleanup can be managed properly.
For best results, make sure session creation is managed inside of components rather than services - this helps ensure that lifetime and cleanup can be managed properly.
PowerShell Stream
If you have a long running script and data is outputted progressively, a PowerShell stream will allow you to process the data without having to wait for the script to finish. The observable next() will be called as soon as data is received.
this.appContextService.powerShellStream.run(session, script);
Long Running Scripts
If you have a long running script that you would like to run in the background, a work item can be submitted. The state of the script will be tracked by the Gateway and updates to the status can be sent to a notification.
const workItem: WorkItemSubmitRequest = {
typeId: 'Long Running Script',
objectName: 'My long running service',
powerShellScript: script,
//in progress notifications
inProgressTitle: 'Executing long running request',
startedMessage: 'The long running request has been started',
progressMessage: 'Working on long running script – {{ percent }} %',
//success notification
successTitle: 'Successfully executed a long running script!',
successMessage: '{{objectName}} was successful',
successLinkText: 'Bing',
successLink: 'http://www.bing.com',
successLinkType: NotificationLinkType.Absolute,
//error notification
errorTitle: 'Failed to execute long running script',
errorMessage: 'Error: {{ message }}'
nodeRequestOptions: {
logAudit: true,
logTelemetry: true
}
};
return this.appContextService.workItem.submit('{!TargetNode}', workItem);
Note
For progress to be shown, Write-Progress must be included in the script that you have written. For example:
Write-Progress -Activity ‘The script is almost done!' -percentComplete 95
WorkItem Options
function | Explanation |
---|---|
submit() | Submits the work item |
submitAndWait() | Submit the work item and wait for the completion of its execution |
wait() | Wait for existing work item to complete |
query() | Query for an existing work item by ID |
find() | Find and existing work item by the TargetNodeName, ModuleName, or typeId. |
PowerShell Batch APIs
If you need to run the same script on multiple nodes, then a batch PowerShell session can be used. For example:
const batchSession = this.appContextService.powerShell.createBatchSession(
['{!TargetNode1}', '{!TargetNode2}', sessionKey);
this.appContextService.powerShell.runBatchSingleCommand(batchSession, command).subscribe((responses: PowerShellBatchResponseItem[]) => {
for (const response of responses) {
if (response.error || response.errors) {
//handle error
} else {
const results = response.properties && response.properties.results;
//response.nodeName
//results[0]
}
}
},
Error => { /* handle error */ });
PowerShellBatch options
option | Explanation |
---|---|
runSingleCommand | Run a single command against all the nodes in the array |
run | Run corresponding command on paired node |
cancel | Cancel the command on all nodes in the array |