Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions ParallelKit.Harness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,36 @@ $probeThreads = 0
if ($probe.Stdout -match 'threads=(\d+)') { $probeThreads = [int]$Matches[1] }
Test-Assert ($probeThreads -ge 128) ("and raises the thread pool by itself (minimum " + $probeThreads + ")")

# A script run the way a prompt runs it (& script, or .\script.ps1) holds
# the kit's functions in its own scope, where a .GetNewClosure() block can't
# see them. The kit makes them global too, so the batch (whose callbacks are
# closures) and a caller's closure that calls a kit function both work.
# Needs a fresh process, started with -Command rather than -File.
$promptScript = Join-Path $env:TEMP ("pk-prompt-" + [guid]::NewGuid().ToString("N") + ".ps1")
[System.IO.File]::WriteAllText($promptScript, @"
. '$kitPath'
`$b = Invoke-PkProcessBatch -Commands @(@{ FilePath = '$cmdExe'; Arguments = '/c exit 7' })
Write-Output ('batch=' + `$b.Results.Count + ',' + `$b.Results[0].ExitCode)
`$tag = 'ok'
`$closure = Invoke-PkRun { Test-PkCancelRequested | Out-Null; 'closure=' + `$tag }.GetNewClosure()
Write-Output `$closure
"@)
$prompt = Invoke-PkProcessWithTimeout -FilePath $selfExe -Arguments (ConvertTo-PkArgumentString @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ("& '" + $promptScript + "'"))) -TimeoutMs 60000
[System.IO.File]::Delete($promptScript)
Test-Assert ($prompt.Stdout -match 'batch=1,7') ("a batch works in a script run from a prompt (" + ([string]$prompt.Stdout + [string]$prompt.Stderr).Trim() + ")")
Test-Assert ($prompt.Stdout -match 'closure=ok') "and so does a .GetNewClosure() block that calls a kit function"
# Pasted into a larger script under strict mode, only the kit's functions
# are made global: the script's own (here one defined after the kit, and
# one before) are left alone, and loading raises no errors.
$embedScript = Join-Path $env:TEMP ("pk-embed-" + [guid]::NewGuid().ToString("N") + ".ps1")
[System.IO.File]::WriteAllText($embedScript, "Set-StrictMode -Version Latest`r`n`$ErrorActionPreference = 'Stop'`r`nfunction Get-HarnessBefore { 1 }`r`n`$fnAst = 'mine'`r`n" +
[System.IO.File]::ReadAllText($kitPath) +
"`r`nfunction Get-HarnessAfter { 2 }`r`nWrite-Output ('kitGlobal=' + [bool](Get-ChildItem Function: | Where-Object { `$_.Name -eq 'Start-PkProcess' }))`r`nWrite-Output ('caller var=' + `$fnAst)`r`n")
$embed = Invoke-PkProcessWithTimeout -FilePath $selfExe -Arguments (ConvertTo-PkArgumentString @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ("& '" + $embedScript + "'; 'before global=' + [bool](Get-Command Get-HarnessBefore -ErrorAction SilentlyContinue); 'after global=' + [bool](Get-Command Get-HarnessAfter -ErrorAction SilentlyContinue); 'kit global=' + [bool](Get-Command Start-PkProcess -ErrorAction SilentlyContinue)"))) -TimeoutMs 60000
[System.IO.File]::Delete($embedScript)
$embedOut = ([string]$embed.Stdout + [string]$embed.Stderr).Trim()
Test-Assert ($embedOut -match 'kit global=True' -and $embedOut -match 'before global=False' -and $embedOut -match 'after global=False' -and $embedOut -match 'caller var=mine' -and [string]::IsNullOrWhiteSpace([string]$embed.Stderr)) ("pasted into a larger script, only the kit's functions are made global, without errors or touching the script's variables (" + ($embedOut -replace '\s+', ' ') + ")")

# Loading the kit in Constrained Language Mode fails with a message that
# says so. Needs a fresh process: the mode can't be set back.
$clmScript = Join-Path $env:TEMP ("pk-clm-" + [guid]::NewGuid().ToString("N") + ".ps1")
Expand Down
22 changes: 22 additions & 0 deletions ParallelKit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1960,3 +1960,25 @@ function Test-PkWorkerSafety {

return ,$findings
}

# ==============================================================================
# Make the kit's functions global
# ==============================================================================

# Dot-sourced inside a script run from a prompt (.\script.ps1 or & script),
# the functions above exist only in that script's scope. A .GetNewClosure()
# block - the batch's own callbacks, or yours - can only see global
# functions, so it couldn't find them. Defining each one globally too means
# kit functions work from any closure, however the script was started.
# Only the kit's own functions (Pk-named, plus Initialize-ParallelKit): the
# kit may be pasted into a larger script whose other functions are none of
# its business. Run in a child scope so the loop's variables don't touch
# the caller's.
& {
param($kitAst)
foreach ($fnAst in $kitAst.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false)) {
if ($fnAst.Name -notlike '*-Pk*' -and $fnAst.Name -ne 'Initialize-ParallelKit') { continue }
$fn = Get-Item -LiteralPath ("Function:\" + $fnAst.Name) -ErrorAction SilentlyContinue
if ($null -ne $fn) { $null = Set-Item -Path ("Function:\global:" + $fnAst.Name) -Value $fn.ScriptBlock }
}
} $MyInvocation.MyCommand.ScriptBlock.Ast
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ It works whether or not your script uses `Set-StrictMode`.

Dot-source it; don't `Import-Module` it. As a module it couldn't see the functions you list in `-SharedFunctionNames`.

The kit's functions are also defined globally, so they stay available for the rest of that PowerShell session, and a `.GetNewClosure()` block can call them however your script was started.

`Initialize-ParallelKit` is optional. It registers the Ctrl+C handler, sets up child-process cleanup and sizes the thread pool up front; the engines set up what they need on first use either way. It returns an object with `AbortHandler`, `ChildProcessJob` and `ThreadPool`, each `$true` if that part worked (a failed abort handler or child-process cleanup also prints a warning).

Each public function has a comment block at the top of its definition in `ParallelKit.ps1` listing its parameters and what it returns. `Get-Help` doesn't show these; read the file.
Expand Down Expand Up @@ -236,7 +238,7 @@ All of these callbacks run on whichever thread called the dispatcher: the main t
- An exception from one of your callbacks stops the dispatcher: running processes are killed and the exception is rethrown.
- Returns an object with `GroupResults`, `Aborted`, `ForceKilledCount` and `JobStarterFailedCount`. `GroupResults` holds one object with `GroupInfo`, `Completed` and `Aborted` per group, where `GroupInfo` is the group value.

Closures: build every callback that uses your local variables with `{ ... }.GetNewClosure()`. Without it, a variable name that happens to match one inside the kit silently reads the kit's value instead of yours. And keep any state a callback changes in a hashtable (`$state = @{ Count = 0 }`, then `$state.Count++`). A plain variable assigned inside a closure doesn't stick between calls.
Closures: build every callback that uses your local variables with `{ ... }.GetNewClosure()`. Without it, a variable name that happens to match one inside the kit silently reads the kit's value instead of yours. And keep any state a callback changes in a hashtable (`$state = @{ Count = 0 }`, then `$state.Count++`). A plain variable assigned inside a closure doesn't stick between calls. A closure can't see functions your script defines when the script is run from a prompt (`.\script.ps1`); the kit's own functions always work, but define any of yours that a closure calls as `function global:Get-Thing { ... }`.

## Single processes

Expand Down
Loading