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
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy
- run: uv run mypy --platform win32

test:
strategy:
Expand Down Expand Up @@ -50,13 +51,12 @@ jobs:
if: runner.os == 'Linux'
run: QSV_BIN="$RUNNER_TEMP/qsv/qsvdp" uv run pytest -q tests/test_integration.py

# Integration tests only: the unit tests' fake qsv relies on a shebang and skips on Windows,
# so process-group cleanup is not exercised here.
# Separate from `test` because Windows steps run in PowerShell and use the MSVC build.
test-windows:
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.14"]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]
runs-on: windows-latest
defaults:
run:
Expand All @@ -76,10 +76,10 @@ jobs:
gh release download --repo dathere/qsv --pattern "qsv-*-x86_64-pc-windows-msvc.zip" --dir $dir
Get-ChildItem $dir -Filter *.zip | ForEach-Object { Expand-Archive $_.FullName -DestinationPath $dir -Force }
Get-ChildItem $dir
- name: Integration tests (qsv)
- name: Unit tests and integration tests (qsv)
run: |
$env:QSV_BIN = Join-Path $env:RUNNER_TEMP "qsv\qsv.exe"
uv run pytest -v tests/test_integration.py
uv run pytest -v
- name: Integration tests (qsvlite)
run: |
$env:QSV_BIN = Join-Path $env:RUNNER_TEMP "qsv\qsvlite.exe"
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Added

- Windows support. qsv is started suspended and placed in its own Job Object before it runs,
so a timeout or cancellation stops qsv *and* everything it started: `CTRL_BREAK_EVENT`
first, then `TerminateJobObject` after `kill_grace`. Previously only `qsv.exe` itself was
killed. With `inherit_env=False`, `SYSTEMROOT` is still passed on Windows, since many
programs fail to start without it. CI now runs the full test suite on Windows.

### Changed

- The sdist now contains only the package, tests, README, CHANGELOG and LICENSE. 0.1.0's also
Expand Down
9 changes: 6 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ uv run ruff format . && uv run ruff check . && uv run mypy # must be

CI runs lint, format check, and mypy, plus tests on ubuntu/macos x Python 3.10–3.14 against
qsv, qsvlite, and qsvdp (Linux only).
On Windows (Python 3.10 and 3.14) it runs only the integration tests, against qsv and qsvlite.
On Windows (Python 3.10–3.14) it runs the full suite against qsv, plus the integration tests
against qsvlite. Windows teardown uses Job Objects via ctypes (`_ProcessTree` in `_process.py`);
type-check it with `uv run mypy --platform win32` (the lint job runs both).

## Layout (`src/qsv_client/`)

Expand All @@ -32,8 +34,9 @@ On Windows (Python 3.10 and 3.14) it runs only the integration tests, against qs
- `_Base._build_env` always sets `QSV_ERROR_FORMAT=json`. Older qsv versions ignore it, so
their errors are classified by exit code.
- Exit codes 0 and 255 (qsv's "warning") count as success.
- On timeout, cancellation, or KeyboardInterrupt, the whole process group is killed: SIGTERM,
then SIGKILL after `kill_grace`. A timeout raises `QsvTimeout` (exit code 124) even when
- On timeout, cancellation, or KeyboardInterrupt, the run's whole process tree is killed after
`kill_grace`: SIGTERM then SIGKILL to the process group on POSIX, `CTRL_BREAK_EVENT` then
`TerminateJobObject` on Windows. A timeout raises `QsvTimeout` (exit code 124) even when
`check=False`.
- LLM settings for describegpt are passed as env vars (`QSV_LLM_*`), never on argv.
- `min_version` is enforced inside `_probe`, so it holds however the capabilities cache is
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,15 @@ runs.

## Platform notes

Process-group cleanup uses POSIX sessions (Linux, macOS). On Windows, qsv is started in a new
process group and killed directly; its own children are not reaped.
On a timeout or cancellation, the client stops qsv and every process qsv started:

- **Linux, macOS:** each run gets its own session. The client sends SIGTERM to the process
group, then SIGKILL after `kill_grace`. A process that leaves the group (by calling `setsid`)
survives.
- **Windows:** qsv is started suspended, put in its own Job Object, and only then resumed, so
every process it starts is in the job and can't leave it. The client sends
`CTRL_BREAK_EVENT` to the run's process group, then calls `TerminateJobObject` after
`kill_grace`.

## Development

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ classifiers = [
"Intended Audience :: Developers",
"Operating System :: POSIX",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Scientific/Engineering :: Information Analysis",
Expand Down
Loading
Loading