Metadata-Version: 2.4
Name: win-safesubprocess
Version: 0.1.0
Summary: Race-free Windows subprocess trees that terminate when their Python parent exits
License-Expression: MIT
Keywords: windows,subprocess,job-object,process-tree,parent-death
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Operating System
Requires-Python: <3.15,>=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest>=8.3; extra == "test"
Requires-Dist: pytest-cov>=5; extra == "test"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: mypy>=1.12; extra == "dev"
Requires-Dist: pytest>=8.3; extra == "dev"
Requires-Dist: pytest-cov>=5; extra == "dev"
Requires-Dist: ruff>=0.9; extra == "dev"
Requires-Dist: twine>=5.1; extra == "dev"
Dynamic: license-file

# win-safesubprocess

`win-safesubprocess` is a dependency-free `subprocess` wrapper for CPython on
Windows. It creates every child with `CREATE_SUSPENDED`, assigns the process to
an unnamed kill-on-close Job Object, and only then resumes the primary thread.
When the Python parent process terminates, Windows terminates the associated
child and descendant processes.

日本語要約：Windowsで親Pythonプロセスが終了した際に、起動した子・孫プロセスを
Job Objectで終了させるためのパッケージです。子プロセスは`CREATE_SUSPENDED`で
作成し、Job Objectへの割当完了後にだけ再開します。そのため、割当前に子が孫を
起動する競合を排除します。

## Installation

```console
python -m pip install win-safesubprocess
```

Runtime dependencies are not required. CPython 3.10 through 3.14 is covered by
the supplied CI matrix.

## Usage

The common synchronous `subprocess` APIs are exposed under familiar names:

```python
import sys
import win_safesubprocess as subprocess

result = subprocess.run(
    [sys.executable, "-c", "print('protected')"],
    capture_output=True,
    text=True,
    check=True,
)
print(result.stdout)
```

For a long-running process:

```python
from win_safesubprocess import PIPE, Popen

process = Popen(
    ["worker.exe", "--serve"],
    stdout=PIPE,
    stderr=PIPE,
    text=True,
)
```

Supported helpers are `run`, `call`, `check_call`, `check_output`, `getoutput`,
and `getstatusoutput`. Common constants, exceptions, `CompletedProcess`, and
`list2cmdline` are also re-exported.

## Guaranteed Windows start order

A successful `Popen(...)` follows this sequence:

1. Create or retrieve the parent-owned Job Object.
2. Call `CreateProcess(..., creationflags | CREATE_SUSPENDED, ...)`.
3. Call `AssignProcessToJobObject(job, process_handle)`.
4. Call `ResumeThread(primary_thread_handle)` exactly once.
5. Publish the native handles to CPython's normal `subprocess.Popen` logic.

The child cannot execute application code or create descendants between steps 2
and 3. If assignment or resumption fails, the unpublished child is terminated,
its termination is confirmed, both native handles are closed, and the original
error is raised. The package never silently falls back to a runnable unmanaged
child.

## How CPython compatibility is preserved

CPython's Windows `Popen._execute_child` contains version-specific handling for
pipes, inherited handles, `STARTUPINFO`, shell behavior, audit hooks, and command
line conversion. This package does not vendor that private implementation.
Instead, it clones the function supplied by the active interpreter with a
private globals dictionary and replaces only its `_winapi` object with a
forwarding proxy that intercepts `CreateProcess`.

Import fails closed if a future CPython version no longer has the expected
shape. This is safer than silently bypassing the suspended-start invariant.

## Job lifetime and tree behavior

One unnamed Job Object is shared by all safe subprocesses created by a Python
parent. Its handle is explicitly non-inheritable and remains owned by the
parent process. Windows closes the handle during process teardown on normal
exit, `os._exit()`, crashes, and external `TerminateProcess` termination. The
job's `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` policy then terminates all remaining
members, including inherited descendants.

The shared design intentionally means:

- every process started through this package has the same parent lifetime;
- a root child may exit while its descendants continue, but those descendants
  remain in the job and are killed when the Python parent terminates;
- `Popen.kill()` and `Popen.terminate()` retain standard behavior and target the
  individual root process, not every package-managed process;
- no per-launch Job Object handle is leaked in long-running parent processes.

## Important limitations

- Job termination is forceful. Child `finally` blocks, Python `atexit` handlers,
  service stop handlers, and buffered writes are not guaranteed to complete.
- This is lifecycle containment, not a security sandbox. It does not restrict
  file, network, token, memory, or process-handle access.
- A host may already place the Python parent in a Job Object. Modern Windows can
  support nested jobs, but host policies may still reject assignment. The
  package terminates the suspended child and raises instead of weakening the
  guarantee.
- Breakaway limits are not enabled on the package-owned job, so ordinary
  descendants cannot opt out with `CREATE_BREAKAWAY_FROM_JOB`.
- The internal `CREATE_SUSPENDED` state is consumed by the package. A successful
  `Popen` returns a running, job-managed process rather than a suspended one.
- `asyncio.create_subprocess_exec` and `asyncio.create_subprocess_shell` are not
  patched. Use these synchronous APIs directly or call them from a worker
  thread in an async application.
- Outside Windows, the API is a transparent `subprocess` subclass/wrapper. It
  does not add parent-death behavior on POSIX systems.

## Development

```console
python -m pip install -e ".[dev]"
python -m pytest
ruff check .
ruff format --check .
mypy src
mypy --platform win32 src
python -m build
python -m twine check --strict dist/*
```

The Windows integration suite verifies actual Job Object membership,
non-inheritance of the job handle, suspended-start ordering, fail-closed setup
cleanup, concurrent launches, and child-plus-grandchild termination after
normal exit, `os._exit()`, and external `TerminateProcess`.

See [design notes](docs/design.md), [release instructions](docs/releasing.md),
[contribution guidelines](CONTRIBUTING.md), and [security policy](SECURITY.md).
