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
9 changes: 9 additions & 0 deletions ParallelKit.Harness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 2 additions & 1 deletion ParallelKit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) }
}
```
Expand Down Expand Up @@ -130,7 +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.
`-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.

Expand Down Expand Up @@ -174,6 +176,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 }`.

Expand Down Expand Up @@ -260,22 +263,25 @@ 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.

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
Expand Down Expand Up @@ -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`.
Loading