From a9689f97cde3005f5e57f13ed9de112d7aec2cfa Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:09:37 -0400 Subject: [PATCH 1/4] README: fixes and FAQ answers from an outside read-through - Quick start: say it's for a saved script ($PSScriptRoot is empty at the prompt), and skip aborted commands instead of reporting them as failures. - Abort: the numbered steps are for Ctrl+C; a window close skips the wait. New note that the script carries on after an abort, with the `exit` line to stop it. - State the defaults for the batch and runspace-job time limits and concurrency. - Invoke-PkRun: a variable named like one of its own reads its value inside the block unless the block uses .GetNewClosure(). - FAQ: pipeline input, which functions are public, and whether child programs get Ctrl+C. - A "Bugs and questions" section pointing to GitHub issues. Co-Authored-By: Claude Opus 5.5 --- README.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 59ee3cb..6af936f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Built with Claude Code. Most uses are this: a list of command-line programs, run a few at a time, where Ctrl+C lets the running ones finish instead of cutting them off. ```powershell +# In a saved script. At the prompt, $PSScriptRoot is empty: use . .\ParallelKit.ps1 instead. . "$PSScriptRoot\ParallelKit.ps1" $commands = foreach ($f in Get-ChildItem *.png) { @@ -28,6 +29,7 @@ $commands = foreach ($f in Get-ChildItem *.png) { $batch = Invoke-PkRun { Invoke-PkProcessBatch -Commands $commands -MaxConcurrent 6 } foreach ($r in $batch.Results) { + if ($r.Aborted) { continue } # stopped by Ctrl+C before it finished; no exit code if ($r.ExitCode -ne 0) { Write-Warning ('Failed: ' + $r.Command.Arguments) } } ``` @@ -132,9 +134,11 @@ Each command is a hashtable with `FilePath`, `Arguments` and optionally `Working `-OnComplete` runs as each command finishes, but only for commands that actually ran. Check `Results` for the ones that couldn't start or were aborted. +Defaults: `-MaxConcurrent` 4, `-AbortTimeoutSeconds` 60 (how long Ctrl+C waits for running programs), `-TimeoutSeconds` 0 (no limit). + `-TimeoutSeconds 600` kills any program still running after 10 minutes. Its result has `TimedOut = $true`, still goes through `-OnComplete`, and the rest of the batch carries on. The default, 0, means no limit. Any programs it started are killed with it. -`Invoke-PkRun { ... }` is what makes Ctrl+C graceful. Whatever the block outputs is passed through, so `$batch` above gets the batch result. It also stops the PC idle-sleeping until the run ends (`-KeepAwake $false` to opt out); the display can still turn off. Without `Invoke-PkRun` the engines still work, but Ctrl+C kills the script the normal way. See "Abort" below for what it means for your own code inside the braces. +`Invoke-PkRun { ... }` is what makes Ctrl+C graceful. Whatever the block outputs is passed through, so `$batch` above gets the batch result. It also stops the PC idle-sleeping until the run ends (`-KeepAwake $false` to opt out); the display can still turn off. Without `Invoke-PkRun` the engines still work, but Ctrl+C kills the script the normal way. See "Abort" below for what it means for your own code inside the braces. The block runs inside `Invoke-PkRun`, so a variable of yours named like one of its own (`$ScriptBlock`, `$KeepAwake`, `$DisableQuickEdit`) reads its value instead; add `.GetNewClosure()` to the block if that could happen. ## Run PowerShell code in parallel @@ -174,6 +178,7 @@ Each worker runs in its own runspace: a separate PowerShell session inside the s - `-NextJobProvider { ... }` can replace `-JobParams` to produce jobs lazily: it's called on the main thread for the next job hashtable, and returns `$null` when there are no more. - `-PollCallback { ... }` runs on the main thread on every loop pass, for progress display and the like. - `-JobCrashedHandler { param($err) ... }` is told when a worker throws. That job's result handler isn't called. +- `-MaxParallel` has no default; you choose it. `-DrainTimeoutSeconds` (how long Ctrl+C waits for running workers) defaults to 60. - If `-ResultHandler`, `-PollCallback` or `-NextJobProvider` throws, the run stops: workers still going are told to stop and the exception comes out of the call. - Returns `@{ ForceStoppedCount; CrashedCount; Aborted }`. @@ -260,19 +265,20 @@ The handler always runs on the main thread, so it can use anything. The kit's ow ## Abort -Inside `Invoke-PkRun { ... }`, Ctrl+C or closing the window: +Inside `Invoke-PkRun { ... }`, Ctrl+C: 1. stops new jobs from starting, 2. cancels the run's token (`Test-PkCancelRequested` turns true in workers), 3. waits for in-flight work (`-DrainTimeoutSeconds` / `-AbortTimeoutSeconds`, default 60), 4. force-stops whatever is left, calling `-JobAbortCleanup` for each killed or never-started process job. -A window close gets no waiting period: Windows allows only a few seconds, so leftovers are stopped straight away. +Closing the window does the same minus step 3: Windows allows only a few seconds, so leftovers are stopped straight away. -Two things to know: +Three things to know: - **Inside `Invoke-PkRun`, Ctrl+C no longer kills anything by itself.** It only raises a flag. The kit's engines watch that flag. A long step of your own in there has to check `Test-PkAbortSignal` now and then and stop when it's true, or Ctrl+C will seem to do nothing until that step ends. - **An abort stays in force until it's reset.** `Invoke-PkRun` resets it at the start of each run. If you call an engine again outside `Invoke-PkRun` after an abort, it returns straight away with `Aborted = $true`. Call `Reset-PkAbortState` first if you mean to carry on. +- **Your script carries on after an abort.** The engines return with `Aborted = $true` and the rest of your script runs. If an abort should end it, say so: `if ($batch.Aborted) { exit 1 }`. `Invoke-PkRun` also turns off the console's QuickEdit mode for the length of the run (`-DisableQuickEdit $false` to opt out). With QuickEdit on in the classic console window, one stray click starts a text selection, and the script freezes at its next console write until the selection is cleared. That looks exactly like a hang. It's turned back on afterwards if it was on before. @@ -345,10 +351,26 @@ Yes. `-OnComplete` (batch) and `-PollCallback` (runspace jobs) run on the main t Start from the CPU core count and measure. [Choosing limits](#choosing-limits) has more. +**Can I pipe into it?** + +No. The engines take a list of hashtables (`-Commands`, `-JobParams`) rather than pipeline input, because each job needs several named values and the kit wants the whole list (or a provider for it) before it starts. Build the list with `foreach`, as the examples do: `$commands = foreach ($f in Get-ChildItem *.png) { @{ FilePath = ...; Arguments = ... } }`. + +**Which functions are meant to be used?** + +The ones in this README: the engines, the [single-process functions](#single-processes), and the abort and message functions named in their sections. Dot-sourcing also brings in internal helpers (`Invoke-PkProcessDispatchCore`, `Get-PkCommandKind` and so on), which show up in tab completion. Those can change between versions; don't call them. + +**Does a program I start get my Ctrl+C too?** + +No. Programs start without a window, in a hidden console of their own, so Ctrl+C in your window never reaches them. That's what lets them finish while the kit drains. When it does need to stop one, the kit kills it. + **Was this written by AI?** It was built with Claude Code, as the top of this page says. The worker rules and abort behaviour come from hangs and bugs reproduced in real runs of the original project, and the test harness runs real processes and runspaces, with nothing mocked, on every change. +## Bugs and questions + +Open an [issue](https://github.com/HalRiveria/PowerShell-ParallelKit/issues) with what you ran, what happened, and your Windows and PowerShell versions (`$PSVersionTable`). + ## License MIT - see `LICENSE`. From cb0c16b4d5f4e0d82575cc4e25589f44ec2b2634 Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:09:46 -0400 Subject: [PATCH 2/4] README: fold the batch defaults into the -OnComplete paragraph Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 6af936f..66eb1b4 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,7 @@ Each command is a hashtable with `FilePath`, `Arguments` and optionally `Working `Results` is in the order commands finished, not the order you gave them. Use `Command` to match them up. -`-OnComplete` runs as each command finishes, but only for commands that actually ran. Check `Results` for the ones that couldn't start or were aborted. - -Defaults: `-MaxConcurrent` 4, `-AbortTimeoutSeconds` 60 (how long Ctrl+C waits for running programs), `-TimeoutSeconds` 0 (no limit). +`-OnComplete` runs as each command finishes, but only for commands that actually ran. Check `Results` for the ones that couldn't start or were aborted. Defaults: `-MaxConcurrent` 4, `-AbortTimeoutSeconds` 60 (how long Ctrl+C waits for running programs), `-TimeoutSeconds` 0 (no limit). `-TimeoutSeconds 600` kills any program still running after 10 minutes. Its result has `TimedOut = $true`, still goes through `-OnComplete`, and the rest of the batch carries on. The default, 0, means no limit. Any programs it started are killed with it. From 585cb8d513a1174c1dfc072e544788c09c059968 Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Wed, 28 Oct 2026 04:10:00 -0400 Subject: [PATCH 3/4] README: move the Invoke-PkRun variable note into the Abort section Co-Authored-By: Claude Opus 5.5 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 66eb1b4..2197389 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Each command is a hashtable with `FilePath`, `Arguments` and optionally `Working `-TimeoutSeconds 600` kills any program still running after 10 minutes. Its result has `TimedOut = $true`, still goes through `-OnComplete`, and the rest of the batch carries on. The default, 0, means no limit. Any programs it started are killed with it. -`Invoke-PkRun { ... }` is what makes Ctrl+C graceful. Whatever the block outputs is passed through, so `$batch` above gets the batch result. It also stops the PC idle-sleeping until the run ends (`-KeepAwake $false` to opt out); the display can still turn off. Without `Invoke-PkRun` the engines still work, but Ctrl+C kills the script the normal way. See "Abort" below for what it means for your own code inside the braces. The block runs inside `Invoke-PkRun`, so a variable of yours named like one of its own (`$ScriptBlock`, `$KeepAwake`, `$DisableQuickEdit`) reads its value instead; add `.GetNewClosure()` to the block if that could happen. +`Invoke-PkRun { ... }` is what makes Ctrl+C graceful. Whatever the block outputs is passed through, so `$batch` above gets the batch result. It also stops the PC idle-sleeping until the run ends (`-KeepAwake $false` to opt out); the display can still turn off. Without `Invoke-PkRun` the engines still work, but Ctrl+C kills the script the normal way. See "Abort" below for what it means for your own code inside the braces. ## Run PowerShell code in parallel @@ -280,6 +280,8 @@ Three things to know: `Invoke-PkRun` also turns off the console's QuickEdit mode for the length of the run (`-DisableQuickEdit $false` to opt out). With QuickEdit on in the classic console window, one stray click starts a text selection, and the script freezes at its next console write until the selection is cleared. That looks exactly like a hang. It's turned back on afterwards if it was on before. +The block you give `Invoke-PkRun` runs inside it, so a variable of yours named like one of its own (`$ScriptBlock`, `$KeepAwake`, `$DisableQuickEdit`) reads its value instead of yours. Add `.GetNewClosure()` to the block if that could happen. + Other pieces: `Request-PkAbort` triggers the same abort from code. `Test-PkAbortSignal` asks whether one is under way. `Register-PkCancellationSource` adds your own `CancellationTokenSource` to the set an abort cancels (a `-Cts` you pass to `Invoke-PkRunspaceJobs` is added automatically; a `-CancelToken` you pass to the dispatcher isn't cancelled by an abort, but the dispatcher stops either way). `Get-PkAbortTriggerLabel` says which kind of abort it was. ## Choosing limits From 83aef9639e9b527ed17b108f1757c5ca486bc811 Mon Sep 17 00:00:00 2001 From: Hal <22532613+HalRiveria@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:07:49 -0400 Subject: [PATCH 4/4] Say a window close stops jobs, not that it waits for them On a window close the drain is skipped (Windows ends the process within seconds), but the announcement still said 'finishing current jobs, please wait'. It now says jobs are being stopped, matching the README. Co-Authored-By: Claude Opus 5.5 --- ParallelKit.Harness.ps1 | 9 +++++++++ ParallelKit.ps1 | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/ParallelKit.Harness.ps1 b/ParallelKit.Harness.ps1 index d2e55e7..dfa1018 100644 --- a/ParallelKit.Harness.ps1 +++ b/ParallelKit.Harness.ps1 @@ -999,6 +999,15 @@ Test-Assert ($threw -and $sw.Elapsed.TotalSeconds -lt 6) ("Invoke-PkProcessWithT Test-Assert (@($script:messages | Where-Object { $_.Message -like '*Ctrl+C detected*' }).Count -eq 1) "and the abort is announced" $null = $abortTimer.EndInvoke($abortTimerHandle); $abortTimer.Dispose() +# A window close gets no drain, so its announcement mustn't say it waits. +Reset-Harness +[PkConsoleCtrl]::IsCloseEvent = $true +[PkConsoleCtrl]::AbortRequested = $true +$null = Test-PkAbortSignal +$closeMessage = @($script:messages | Where-Object { $_.Message -like '*Window close detected*' }) +Test-Assert ($closeMessage.Count -eq 1 -and $closeMessage[0].Message -notlike '*please wait*' -and $closeMessage[0].Message -like '*stopping*') ("a window close is announced as stopping jobs, not waiting for them (" + (@($closeMessage | ForEach-Object { $_.Message }) -join ' | ') + ")") +Reset-PkAbortState + # The same from inside a worker, where the run's cancel is what it sees. Reset-Harness $poll = @{ Count = 0 } diff --git a/ParallelKit.ps1 b/ParallelKit.ps1 index 0401054..5f063a9 100644 --- a/ParallelKit.ps1 +++ b/ParallelKit.ps1 @@ -448,7 +448,8 @@ function Test-PkAbortSignal { $abortMessage = if (-not [PkState]::CloseAbort) { "[ABORT] Ctrl+C detected - finishing current jobs, please wait..." } else { - "[ABORT] Window close detected - finishing current jobs, please wait..." + # No drain on a close: Windows ends the process within seconds. + "[ABORT] Window close detected - stopping current jobs now..." } Invoke-PkMessageHandler -Message $abortMessage -Level Warn }