Metadata-Version: 2.4
Name: fastrun-win
Version: 1.0.0
Summary: Fast Windows process execution library.
Author: Haroon Ahmad
License-Expression: MIT
Keywords: windows,subprocess,process,shell
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: C
Classifier: Operating System :: Microsoft :: Windows
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENCE
Dynamic: license-file
Dynamic: requires-python

# fastrun

A **Windows-only**, quote-safe, string-based, feature-rich alternative to
Python's `subprocess` module, implemented as a native C extension around
`CreateProcessW`.

```python
import fastrun

r = fastrun.run("python --version")
print(r.stdout)        # 'Python 3.12.3\n'
print(r.returncode)    # 0
```

## Feature overview

| Feature | How |
|---|---|
| String-based commands, quote-safe | `run("echo \"hello world\"")` |
| No shell by default (safe) | direct `CreateProcessW`, no `cmd.exe` |
| Optional real shell mode | `run(cmd, shell=True)` -> `cmd.exe /c ...` |
| Timeouts | `run(cmd, timeout=5.0)` -> raises `TimeoutExpired` |
| Kill entire process tree on timeout | `kill_tree=True` (default), via Job Objects |
| Send data to child's stdin | `run(cmd, input="some text")` |
| Full environment replacement | `run(cmd, env={...})` |
| Environment overlay (merge, don't replace) | `run(cmd, extra_env={"FOO": "bar"})` |
| Custom working directory | `run(cmd, cwd="C:\\path")` |
| Redirect output straight to a file (no memory buffering) | `run(cmd, stdout_file="out.log")` |
| Merge stderr into stdout (`2>&1`) | `run(cmd, merge_stderr=True)` |
| Raise on non-zero exit | `run(cmd, check=True)` -> `CalledProcessError` |
| Text or raw bytes output | `run(cmd, text=False)` |
| Custom output encoding | `run(cmd, encoding="cp1252")` |
| Process priority | `run(cmd, priority="below_normal")` |
| Hide/show console window | `run(cmd, hide_window=False)` |
| PID of launched process | `r.pid` |
| Standalone quote-aware tokenizer | `fastrun.split(cmdline)` |
| Standalone Windows argv quoter | `fastrun.quote(arg)` |
| PATH resolution | `fastrun.which("python")` |

## How it works

1. **Tokenizer** (`fastrun.split`) parses the command string into
   argv-like tokens: `'single quotes'`, `"double quotes with \" and \\
   escapes"`, and bare backslashes treated as literal (so
   `C:\Users\me\tool.exe` needs no escaping).
2. **Re-quoter** (`fastrun.quote`) re-serializes tokens using the
   official Microsoft argv-quoting algorithm (the one
   `CommandLineToArgvW` expects -- the same one CPython's own
   `subprocess.list2cmdline` implements).
3. **Launcher** calls `CreateProcessW` directly with redirected
   stdout/stderr pipes (or files), an optional stdin pipe, an optional
   merged/replaced environment block, an optional **Job Object** so
   timeouts kill the whole descendant tree (not just the immediate
   child), and optional priority-class / window-visibility control.

## Full API

### `fastrun.run(cmdline, **options) -> CompletedProcess`

```python
fastrun.run(
    cmdline,                # str: the command line, e.g. "python --version"
    cwd=None,                # str | None: working directory
    env=None,                # dict[str,str] | None: full replacement environment
    extra_env=None,          # dict[str,str] | None: overlay on top of inherited env
    timeout=None,             # float | None: seconds before kill + TimeoutExpired
    text=True,                # bool: decode output to str (vs bytes)
    encoding="utf-8",         # str: decode/encode encoding
    check=False,              # bool: raise CalledProcessError on non-zero exit
    shell=False,               # bool: run via cmd.exe /c instead of our own parser
    input=None,                 # str | bytes | None: data piped to child's stdin
    kill_tree=True,             # bool: timeout kills whole process tree via Job Object
    stdout_file=None,           # str | None: write stdout directly to this file path
    stderr_file=None,           # str | None: write stderr directly to this file path
    merge_stderr=False,          # bool: combine stderr into stdout (like 2>&1)
    hide_window=True,             # bool: suppress the child's console window
    priority=None,                 # str | None: "idle" | "below_normal" | "normal"
                                    #             | "above_normal" | "high" | "realtime"
)
```

Returns a `fastrun.CompletedProcess` with:
- `.args` -- the original command-line string
- `.argv` -- the parsed argument list
- `.returncode` -- process exit code
- `.stdout` / `.stderr` -- captured output (`None` if redirected to a file)
- `.pid` -- the process ID that was assigned
- `.timed_out` -- `True` if the timeout fired
- `.check_returncode()` -- raise `CalledProcessError` if non-zero

### `fastrun.split(cmdline) -> list[str]`
Standalone tokenizer access (like `shlex.split`, but Windows path-aware).

### `fastrun.quote(arg) -> str`
Standalone Windows argv quoting for one argument.

### `fastrun.which(name) -> str | None`
Resolve an executable name to a full path using Windows's own search
order (app dir -> cwd -> System32 -> Windows dir -> `PATH`), trying
`.exe`/`.cmd`/`.bat`/`.com` if no extension was given.

### Exceptions
- `fastrun.CalledProcessError(returncode, cmd, output=None, stderr=None)`
- `fastrun.TimeoutExpired(cmd, output=None)`

---

## How to compile

You need **Windows** (this cannot be built or run on Linux/macOS -- it's a
hard dependency on the Win32 API) plus:

1. **Python** (3.8+), matching architecture (x64 recommended) -- the
   regular python.org installer or the Microsoft Store one both work, as
   long as they include development headers (they do by default).
2. **Microsoft Visual C++ Build Tools** -- the compiler CPython extensions
   require on Windows. Two ways to get it:
   - Install **Visual Studio Community** (free) and select the
     *"Desktop development with C++"* workload during setup, **or**
   - Install just the smaller **"Build Tools for Visual Studio"**
     package from Microsoft and select the *C++ build tools* workload.
3. **setuptools** for your Python: `pip install setuptools wheel`

### Build steps

Open a terminal where the MSVC compiler is on `PATH`. The easiest way is
to use the Start Menu shortcut Visual Studio installs, called something
like **"x64 Native Tools Command Prompt for VS 2022"** (this pre-loads
all the right environment variables -- `cl.exe`, `INCLUDE`, `LIB`, etc.).
Using a plain `cmd.exe`/PowerShell without that shortcut usually fails
with `error: Microsoft Visual C++ 14.0 or greater is required`.

```bat
cd path\to\fastrun
python -m pip install --upgrade setuptools wheel
python setup.py build_ext --inplace
```

If it succeeds, you'll see a new file appear in the project root:

```
fastrun.cp312-win_amd64.pyd      (exact name depends on your Python version)
```

That `.pyd` file **is** the compiled module -- Python can `import` it
directly like any `.py` file, as long as it's on `sys.path` (the project
root, or wherever you copy it, or your site-packages folder).

### Verifying the build

```bat
python test_fastrun.py
```

This runs a small smoke-test suite covering the tokenizer, quoting,
return codes, `check=True`, and timeouts. All lines should print `[PASS]`.

### Building a redistributable wheel (optional)

If you want to `pip install` this on other Windows machines without them
needing MSVC installed:

```bat
python setup.py bdist_wheel
```

This produces a `.whl` file under `dist\` that bundles the compiled
`.pyd`, installable via `pip install dist\fastrun-1.0.0-*.whl` on any
matching Windows/Python-version/architecture combination -- no compiler
needed on the target machine.

### Common build errors

| Error | Fix |
|---|---|
| `Microsoft Visual C++ 14.0 or greater is required` | Install VS Build Tools with the C++ workload, and build from the "x64 Native Tools Command Prompt" |
| `fatal error C1083: Cannot open include file: 'Python.h'` | Your Python install is missing dev headers, or you're mixing a 32-bit Python with a 64-bit compiler prompt (or vice versa) -- match architectures |
| `LINK : fatal error LNK1181: cannot open input file 'python3xx.lib'` | Same architecture mismatch as above; reinstall Python for the matching architecture |
| Import succeeds but crashes / `ImportError: DLL load failed` | You built with a different Python minor version than you're importing into -- rebuild with the interpreter you intend to use |

---

## How to use it

### Basic run

```python
import fastrun

r = fastrun.run("python --version")
print(r.returncode)   # 0
print(r.stdout)        # "Python 3.12.3\n"
```

### Quotes just work

```python
r = fastrun.run('echo "hello world"')
r = fastrun.run("tool.exe --path 'C:\\Program Files\\thing' --flag")
```

### Timeouts that actually kill the whole tree

```python
try:
    fastrun.run("build.bat", timeout=30)
except fastrun.TimeoutExpired:
    print("build took too long, and everything it spawned was killed too")
```

### Piping input to a process

```python
r = fastrun.run('python -c "import sys; print(sys.stdin.read().upper())"',
                 input="hello from python\n")
print(r.stdout)   # "HELLO FROM PYTHON\n"
```

### Environment control

```python
# Full replacement -- child sees ONLY these variables
r = fastrun.run("set", shell=True, env={"PATH": r"C:\Windows\System32"})

# Overlay -- child inherits everything, plus these overrides
r = fastrun.run("python script.py", extra_env={"PYTHONUNBUFFERED": "1"})
```

### Checking for failure

```python
try:
    fastrun.run('python -c "import sys; sys.exit(1)"', check=True)
except fastrun.CalledProcessError as e:
    print(f"failed with code {e.args[0]}")
```

(`CalledProcessError` mirrors `subprocess.CalledProcessError`'s
constructor order: `returncode, cmd, output, stderr`.)

### Large output straight to a file

```python
# Avoids buffering gigabytes of build output in memory
r = fastrun.run("dotnet build MySolution.sln", stdout_file="build.log",
                 stderr_file="build.err.log")
print(r.returncode)
# r.stdout / r.stderr are None here -- check the files instead
```

### Merging stderr into stdout

```python
r = fastrun.run('python -c "import sys; sys.stderr.write(\'oops\\n\')"',
                 merge_stderr=True)
print(r.stdout)   # "oops\n"
```

### Real shell semantics (wildcards, `&&`, env expansion)

```python
# Only do this with trusted input -- shell=True reintroduces the
# injection risk that the default mode avoids.
r = fastrun.run("dir *.txt && echo done", shell=True)
```

### Priority and window visibility

```python
# Run a background batch job at low priority, no visible window
r = fastrun.run("cleanup.bat", priority="below_normal", hide_window=True)

# Launch something the user should actually see
r = fastrun.run("notepad.exe", hide_window=False)
```

### PATH resolution

```python
path = fastrun.which("python")
print(path)   # "C:\\...\\Python312\\python.exe"
```

### Using the tokenizer/quoter standalone

```python
tokens = fastrun.split('tool --flag="a b" \'c d\' plain')
# ['tool', '--flag=a b', 'c d', 'plain']

quoted = fastrun.quote("has space")
# '"has space"'
```

---

## Design notes / limitations

- **No shell semantics by default.** `&&`, `|`, `>`, wildcards, and
  `%VAR%` expansion are not performed unless you pass `shell=True`.
- **argv[0] resolution** (non-shell mode) goes through normal Windows
  executable search behavior (app dir, current dir, System32, Windows
  dir, `PATH`).
- **Output capture** uses a polling read loop (`PeekNamedPipe` +
  `Sleep(5)` when idle) rather than overlapped I/O or dedicated reader
  threads -- small, auditable, at the cost of a few ms latency on very
  chatty long-running processes. Use `stdout_file`/`stderr_file` for
  very large output instead of in-memory capture.
- **`kill_tree`** depends on Job Objects; if `CreateJobObjectW` or
  `AssignProcessToJobObject` fails (e.g. restrictive sandbox, or the
  process is already in a job on some older systems), it silently falls
  back to killing just the direct child.
- Max 512 arguments per command by default (`FASTRUN_MAX_ARGS` in
  `fastrun.c`) -- bump the constant and rebuild if you need more.
- `env` and `extra_env` are mutually exclusive; if both are given, `env`
  wins and `extra_env` is ignored.

## License

Do whatever you want with it. No warranty.
