Skip to content

PSScript resource #937

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 18, 2025
Merged
11 changes: 10 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ $filesForWindowsPackage = @(
'osinfo.dsc.resource.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
'psscript.dsc.resource.json',
'winpsscript.dsc.resource.json',
'reboot_pending.dsc.resource.json',
'reboot_pending.resource.ps1',
'registry.dsc.resource.json',
Expand Down Expand Up @@ -87,6 +90,8 @@ $filesForLinuxPackage = @(
'osinfo.dsc.resource.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
'psscript.dsc.resource.json',
'RunCommandOnSet.dsc.resource.json',
'runcommandonset',
'sshdconfig',
Expand All @@ -109,6 +114,8 @@ $filesForMacPackage = @(
'osinfo.dsc.resource.json',
'powershell.dsc.resource.json',
'psDscAdapter/',
'psscript.ps1',
'psscript.dsc.resource.json',
'RunCommandOnSet.dsc.resource.json',
'runcommandonset',
'sshdconfig',
Expand Down Expand Up @@ -300,6 +307,7 @@ if (!$SkipBuild) {
"dscecho",
"osinfo",
"powershell-adapter",
'resources/PSScript',
"process",
"runcommandonset",
"sshdconfig",
Expand Down Expand Up @@ -422,7 +430,8 @@ if (!$SkipBuild) {
Copy-Item "*.dsc.resource.json" $target -Force -ErrorAction Ignore
}
else { # don't copy WindowsPowerShell resource manifest
Copy-Item "*.dsc.resource.json" $target -Exclude 'windowspowershell.dsc.resource.json' -Force -ErrorAction Ignore
$exclude = @('windowspowershell.dsc.resource.json', 'winpsscript.dsc.resource.json')
Copy-Item "*.dsc.resource.json" $target -Exclude $exclude -Force -ErrorAction Ignore
}

# be sure that the files that should be executable are executable
Expand Down
25 changes: 25 additions & 0 deletions dsc/examples/psscript.dsc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Example configuration using PowerShell script resource and using parameters and input
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
parameters:
myName:
type: string
defaultValue: Steve
myObject:
type: object
defaultValue:
color: green
number: 10
resources:
- name: Use PS script
type: Microsoft.DSC.Transitional/PowerShellScript
properties:
input:
- name: "[parameters('myName')]"
- object: "[parameters('myObject')]"
getScript: |
param($inputArray)

Write-Warning "This is a warning message"
# any output will be collected and returned
"My name is " + $inputArray[0].name
"My color is " + $inputArray[1].object.color
1 change: 1 addition & 0 deletions resources/PSScript/copy_files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
./psscript.ps1
83 changes: 83 additions & 0 deletions resources/PSScript/psscript.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
"type": "Microsoft.DSC.Transitional/PowerShellScript",
"description": "Enable running PowerShell 7 scripts inline",
"version": "0.1.0",
"get": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$input | ./psscript.ps1",
"get"
],
"input": "stdin"
},
"set": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$input | ./psscript.ps1",
"set"
],
"implementsPretest": true,
"input": "stdin",
"return": "state"
},
"test": {
"executable": "pwsh",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
"$input | ./psscript.ps1",
"test"
],
"input": "stdin",
"return": "state"
},
"exitCodes": {
"0": "Success",
"1": "PowerShell script execution failed",
"2": "PowerShell exception occurred",
"3": "Script had errors"
},
"schema": {
"embedded": {
"type": "object",
"properties": {
"getScript": {
"type": ["string", "null"]
},
"setScript": {
"type": ["string", "null"]
},
"testScript": {
"type": ["string", "null"]
},
"input": {
"type": ["string", "boolean", "integer", "object", "array", "null"]
},
"output": {
"type": ["array", "null"]
},
"_inDesiredState": {
"type": ["boolean", "null"],
"default": null
}
}
}
}
}
177 changes: 177 additions & 0 deletions resources/PSScript/psscript.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet('Get', 'Set', 'Test')]
[string]$Operation,
[Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
[string]$jsonInput
)

function Write-DscTrace {
param(
[Parameter(Mandatory = $true)]
[ValidateSet('Error', 'Warn', 'Info', 'Debug', 'Trace')]
[string]$Level,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$Message,
[switch]$Now
)

$trace = @{$Level.ToLower() = $Message } | ConvertTo-Json -Compress

if ($Now) {
$host.ui.WriteErrorLine($trace)
} else {
$traceQueue.Enqueue($trace)
}
}

$scriptObject = $jsonInput | ConvertFrom-Json

$script = switch ($Operation) {
'Get' {
$scriptObject.GetScript
}
'Set' {
$scriptObject.SetScript
}
'Test' {
$scriptObject.TestScript
}
}

if ($null -eq $script) {
Write-DscTrace -Now -Level Info -Message "No script found for operation '$Operation'."
if ($Operation -eq 'Test') {
# if not implemented, we return it's in desired state
@{ _inDesiredState = $true } | ConvertTo-Json -Compress
exit 0
}

# write an empty json object to stdout
'{}'
exit 0
}

# use AST to see if script has param block, if any errors exit with error message
$errors = $null
$tokens = $null
$ast = [System.Management.Automation.Language.Parser]::ParseInput($script, [ref]$tokens, [ref]$errors)
if ($errors.Count -gt 0) {
$errorMessage = $errors | ForEach-Object { $_.ToString() }
Write-DscTrace -Now -Level Error -Message "Script has syntax errors: $errorMessage"
exit 3
}

$paramName = if ($null -ne $ast.ParamBlock) {
# make sure it only specifies one parameter and get the name of that parameter
if ($ast.ParamBlock.Parameters.Count -ne 1) {
Write-DscTrace -Now -Level Error -Message 'Script must have exactly one parameter.'
exit 3
}
$ast.ParamBlock.Parameters[0].Name.VariablePath.UserPath
} else {
$null
}

$ps = [PowerShell]::Create().AddScript({
$DebugPreference = 'Continue'
$VerbosePreference = 'Continue'
$ErrorActionPreference = 'Stop'
}).AddStatement().AddScript($script)

if ($null -ne $scriptObject.input) {
if ($null -eq $paramName) {
Write-DscTrace -Now -Level Error -Message 'Input was provided but script does not have a parameter to accept input.'
exit 3
}
$null = $ps.AddParameter($paramName, $scriptObject.input)
} elseif ($null -ne $paramName) {
Write-DscTrace -Now -Level Error -Message "Script has a parameter '$paramName' but no input was provided."
exit 3
}

$traceQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()

$null = Register-ObjectEvent -InputObject $ps.Streams.Error -EventName DataAdding -MessageData $traceQueue -Action {
$traceQueue = $Event.MessageData
# convert error to string since it's an ErrorRecord
$traceQueue.Enqueue((@{ error = [string]$EventArgs.ItemAdded } | ConvertTo-Json -Compress))
}
$null = Register-ObjectEvent -InputObject $ps.Streams.Warning -EventName DataAdding -MessageData $traceQueue -Action {
$traceQueue = $Event.MessageData
$traceQueue.Enqueue((@{ warn = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress))
}
$null = Register-ObjectEvent -InputObject $ps.Streams.Information -EventName DataAdding -MessageData $traceQueue -Action {
$traceQueue = $Event.MessageData
if ($null -ne $EventArgs.ItemAdded.MessageData) {
if ($EventArgs.ItemAdded.Tags -contains 'PSHOST') {
$traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress))
} else {
$traceQueue.Enqueue((@{ trace = $EventArgs.ItemAdded.MessageData.ToString() } | ConvertTo-Json -Compress))
}
return
}
}
$null = Register-ObjectEvent -InputObject $ps.Streams.Verbose -EventName DataAdding -MessageData $traceQueue -Action {
$traceQueue = $Event.MessageData
$traceQueue.Enqueue((@{ info = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress))
}
$null = Register-ObjectEvent -InputObject $ps.Streams.Debug -EventName DataAdding -MessageData $traceQueue -Action {
$traceQueue = $Event.MessageData
$traceQueue.Enqueue((@{ debug = $EventArgs.ItemAdded.Message } | ConvertTo-Json -Compress))
}
$outputObjects = [System.Collections.Generic.List[Object]]::new()

function Write-TraceQueue() {
$trace = $null
while (!$traceQueue.IsEmpty) {
if ($traceQueue.TryDequeue([ref] $trace)) {
$host.ui.WriteErrorLine($trace)
}
}
}

try {
$asyncResult = $ps.BeginInvoke()
while (-not $asyncResult.IsCompleted) {
Write-TraceQueue

Start-Sleep -Milliseconds 100
}
$outputCollection = $ps.EndInvoke($asyncResult)
Write-TraceQueue


if ($ps.HadErrors) {
# If there are any errors, we will exit with an error code
Write-DscTrace -Now -Level Error -Message 'Errors occurred during script execution.'
exit 1
}

foreach ($output in $outputCollection) {
$outputObjects.Add($output)
}
}
catch {
Write-DscTrace -Now -Level Error -Message $_
exit 1
}
finally {
$ps.Dispose()
Get-EventSubscriber | Unregister-Event
}

# Test should return a single boolean value indicating if in the desired state
if ($Operation -eq 'Test') {
if ($outputObjects.Count -eq 1 -and $outputObjects[0] -is [bool]) {
@{ _inDesiredState = $outputObjects[0] } | ConvertTo-Json -Compress
} else {
Write-DscTrace -Now -Level Error -Message 'Test operation did not return a single boolean value.'
exit 1
}
} else {
@{ output = $outputObjects } | ConvertTo-Json -Compress -Depth 10
}
Loading
Loading