dirsql 0.3.57__tar.gz → 0.3.59__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirsql-0.3.57 → dirsql-0.3.59}/Cargo.lock +1 -1
- {dirsql-0.3.57 → dirsql-0.3.59}/PKG-INFO +1 -1
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/cli/config.md +23 -5
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.57/packages/rust → dirsql-0.3.59/packages/python}/docs/cli/config.md +23 -5
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/e2e-attestation.json +2 -2
- {dirsql-0.3.57/packages/python → dirsql-0.3.59/packages/rust}/docs/cli/config.md +23 -5
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/bin/dirsql.rs +14 -4
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/cli/init.rs +17 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/cli/mod.rs +45 -4
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/cli/router.rs +10 -15
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/cli/server.rs +29 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/command.rs +104 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/config.rs +118 -15
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/lib.rs +585 -9
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/persist.rs +70 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/Cargo.toml +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/README.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/_async.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/py.typed +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/AGENTS.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/api/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/cli/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/cli/init.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/cli/server.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/getting-started.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/guide/async.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/guide/querying.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/guide/tables.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/guide/watching.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/migrations.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/package.json +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/README.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/conftest.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/tests/binding/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/python/tests/smoke/__init__.py +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/README.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.57 → dirsql-0.3.59}/pyproject.toml +0 -0
|
@@ -244,6 +244,13 @@ file is skipped (it contributes no rows) and a one-line warning naming the file
|
|
|
244
244
|
and the error is written to stderr. One bad file never aborts the scan; the
|
|
245
245
|
other files' rows are indexed normally.
|
|
246
246
|
|
|
247
|
+
**Timeout.** Each run is bounded by the default 30-second timeout. When a
|
|
248
|
+
command legitimately needs longer — it downloads a model on first run, or calls
|
|
249
|
+
a rate-limited API — raise the bound with the global
|
|
250
|
+
[`[dirsql].hook-timeout`](#command-execution) key (positive whole seconds). A
|
|
251
|
+
run exceeding the bound is killed and handled with the per-file error isolation
|
|
252
|
+
above (the file is skipped with a stderr warning).
|
|
253
|
+
|
|
247
254
|
See [Command execution](#command-execution) for the full contract (argv
|
|
248
255
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
249
256
|
|
|
@@ -279,7 +286,8 @@ key and the `{"sql": …}` contract returns.
|
|
|
279
286
|
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
280
287
|
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
281
288
|
`error` body. The command runs in the config file's directory and is bounded by
|
|
282
|
-
a
|
|
289
|
+
a **30-second** timeout by default; a slow translator (e.g. LLM-backed) can
|
|
290
|
+
raise it with the global [`[dirsql].hook-timeout`](#command-execution) key.
|
|
283
291
|
|
|
284
292
|
#### The hook owns SQL safety
|
|
285
293
|
|
|
@@ -361,7 +369,8 @@ returns.
|
|
|
361
369
|
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
362
370
|
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
363
371
|
`error` body. The command runs in the config file's directory and is bounded by
|
|
364
|
-
a
|
|
372
|
+
a **30-second** timeout by default; raise it with the global
|
|
373
|
+
[`[dirsql].hook-timeout`](#command-execution) key.
|
|
365
374
|
|
|
366
375
|
See [Command execution](#command-execution) for the full contract (argv
|
|
367
376
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
@@ -409,9 +418,18 @@ Config keys that run an external command — today `on-file`, `pre-query`, and
|
|
|
409
418
|
- **Output framing.** The command's result is the **last non-empty line of
|
|
410
419
|
stdout**; any log/chatter lines above it are ignored. stderr is never data —
|
|
411
420
|
it is captured only to enrich error messages.
|
|
412
|
-
- **Timeout.** Each command run is bounded by a
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
- **Timeout.** Each command run is bounded by a **30-second** timeout by
|
|
422
|
+
default; a command that exceeds it is killed and treated as a failure. Raise
|
|
423
|
+
(or tighten) it for **all** hooks — `on-file`, `pre-query`, and `post-query`
|
|
424
|
+
alike — with a single global `[dirsql].hook-timeout` key, in positive whole
|
|
425
|
+
seconds:
|
|
426
|
+
|
|
427
|
+
```toml
|
|
428
|
+
[dirsql]
|
|
429
|
+
hook-timeout = 300
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Zero and negative values are rejected as a config error.
|
|
415
433
|
- **Errors.** A non-zero exit, a timeout, a spawn failure, or output that does
|
|
416
434
|
not parse as expected is a per-file failure: the file is skipped with a
|
|
417
435
|
stderr warning and the scan continues.
|
|
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
|
|
|
4
4
|
# pypi/maturin handler can rewrite it via `write-version` before
|
|
5
5
|
# `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
|
|
6
6
|
# and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.59"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -244,6 +244,13 @@ file is skipped (it contributes no rows) and a one-line warning naming the file
|
|
|
244
244
|
and the error is written to stderr. One bad file never aborts the scan; the
|
|
245
245
|
other files' rows are indexed normally.
|
|
246
246
|
|
|
247
|
+
**Timeout.** Each run is bounded by the default 30-second timeout. When a
|
|
248
|
+
command legitimately needs longer — it downloads a model on first run, or calls
|
|
249
|
+
a rate-limited API — raise the bound with the global
|
|
250
|
+
[`[dirsql].hook-timeout`](#command-execution) key (positive whole seconds). A
|
|
251
|
+
run exceeding the bound is killed and handled with the per-file error isolation
|
|
252
|
+
above (the file is skipped with a stderr warning).
|
|
253
|
+
|
|
247
254
|
See [Command execution](#command-execution) for the full contract (argv
|
|
248
255
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
249
256
|
|
|
@@ -279,7 +286,8 @@ key and the `{"sql": …}` contract returns.
|
|
|
279
286
|
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
280
287
|
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
281
288
|
`error` body. The command runs in the config file's directory and is bounded by
|
|
282
|
-
a
|
|
289
|
+
a **30-second** timeout by default; a slow translator (e.g. LLM-backed) can
|
|
290
|
+
raise it with the global [`[dirsql].hook-timeout`](#command-execution) key.
|
|
283
291
|
|
|
284
292
|
#### The hook owns SQL safety
|
|
285
293
|
|
|
@@ -361,7 +369,8 @@ returns.
|
|
|
361
369
|
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
362
370
|
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
363
371
|
`error` body. The command runs in the config file's directory and is bounded by
|
|
364
|
-
a
|
|
372
|
+
a **30-second** timeout by default; raise it with the global
|
|
373
|
+
[`[dirsql].hook-timeout`](#command-execution) key.
|
|
365
374
|
|
|
366
375
|
See [Command execution](#command-execution) for the full contract (argv
|
|
367
376
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
@@ -409,9 +418,18 @@ Config keys that run an external command — today `on-file`, `pre-query`, and
|
|
|
409
418
|
- **Output framing.** The command's result is the **last non-empty line of
|
|
410
419
|
stdout**; any log/chatter lines above it are ignored. stderr is never data —
|
|
411
420
|
it is captured only to enrich error messages.
|
|
412
|
-
- **Timeout.** Each command run is bounded by a
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
- **Timeout.** Each command run is bounded by a **30-second** timeout by
|
|
422
|
+
default; a command that exceeds it is killed and treated as a failure. Raise
|
|
423
|
+
(or tighten) it for **all** hooks — `on-file`, `pre-query`, and `post-query`
|
|
424
|
+
alike — with a single global `[dirsql].hook-timeout` key, in positive whole
|
|
425
|
+
seconds:
|
|
426
|
+
|
|
427
|
+
```toml
|
|
428
|
+
[dirsql]
|
|
429
|
+
hook-timeout = 300
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Zero and negative values are rejected as a config error.
|
|
415
433
|
- **Errors.** A non-zero exit, a timeout, a spawn failure, or output that does
|
|
416
434
|
not parse as expected is a per-file failure: the file is skipped with a
|
|
417
435
|
stderr warning and the scan continues.
|
|
@@ -244,6 +244,13 @@ file is skipped (it contributes no rows) and a one-line warning naming the file
|
|
|
244
244
|
and the error is written to stderr. One bad file never aborts the scan; the
|
|
245
245
|
other files' rows are indexed normally.
|
|
246
246
|
|
|
247
|
+
**Timeout.** Each run is bounded by the default 30-second timeout. When a
|
|
248
|
+
command legitimately needs longer — it downloads a model on first run, or calls
|
|
249
|
+
a rate-limited API — raise the bound with the global
|
|
250
|
+
[`[dirsql].hook-timeout`](#command-execution) key (positive whole seconds). A
|
|
251
|
+
run exceeding the bound is killed and handled with the per-file error isolation
|
|
252
|
+
above (the file is skipped with a stderr warning).
|
|
253
|
+
|
|
247
254
|
See [Command execution](#command-execution) for the full contract (argv
|
|
248
255
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
249
256
|
|
|
@@ -279,7 +286,8 @@ key and the `{"sql": …}` contract returns.
|
|
|
279
286
|
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
280
287
|
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
281
288
|
`error` body. The command runs in the config file's directory and is bounded by
|
|
282
|
-
a
|
|
289
|
+
a **30-second** timeout by default; a slow translator (e.g. LLM-backed) can
|
|
290
|
+
raise it with the global [`[dirsql].hook-timeout`](#command-execution) key.
|
|
283
291
|
|
|
284
292
|
#### The hook owns SQL safety
|
|
285
293
|
|
|
@@ -361,7 +369,8 @@ returns.
|
|
|
361
369
|
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
362
370
|
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
363
371
|
`error` body. The command runs in the config file's directory and is bounded by
|
|
364
|
-
a
|
|
372
|
+
a **30-second** timeout by default; raise it with the global
|
|
373
|
+
[`[dirsql].hook-timeout`](#command-execution) key.
|
|
365
374
|
|
|
366
375
|
See [Command execution](#command-execution) for the full contract (argv
|
|
367
376
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
@@ -409,9 +418,18 @@ Config keys that run an external command — today `on-file`, `pre-query`, and
|
|
|
409
418
|
- **Output framing.** The command's result is the **last non-empty line of
|
|
410
419
|
stdout**; any log/chatter lines above it are ignored. stderr is never data —
|
|
411
420
|
it is captured only to enrich error messages.
|
|
412
|
-
- **Timeout.** Each command run is bounded by a
|
|
413
|
-
|
|
414
|
-
|
|
421
|
+
- **Timeout.** Each command run is bounded by a **30-second** timeout by
|
|
422
|
+
default; a command that exceeds it is killed and treated as a failure. Raise
|
|
423
|
+
(or tighten) it for **all** hooks — `on-file`, `pre-query`, and `post-query`
|
|
424
|
+
alike — with a single global `[dirsql].hook-timeout` key, in positive whole
|
|
425
|
+
seconds:
|
|
426
|
+
|
|
427
|
+
```toml
|
|
428
|
+
[dirsql]
|
|
429
|
+
hook-timeout = 300
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Zero and negative values are rejected as a config error.
|
|
415
433
|
- **Errors.** A non-zero exit, a timeout, a spawn failure, or output that does
|
|
416
434
|
not parse as expected is a per-file failure: the file is skipped with a
|
|
417
435
|
stderr warning and the scan continues.
|
|
@@ -228,9 +228,14 @@ fn load_pre_query(cli: &Cli) -> Option<PreQuery> {
|
|
|
228
228
|
return None;
|
|
229
229
|
}
|
|
230
230
|
let resolved = config_path.canonicalize().ok()?;
|
|
231
|
-
let
|
|
231
|
+
let config = dirsql::config::load_config(&resolved).ok()?;
|
|
232
|
+
let command = config.pre_query?;
|
|
232
233
|
let config_dir = resolved.parent()?.to_path_buf();
|
|
233
|
-
|
|
234
|
+
let mut pre_query = PreQuery::new(command, config_dir);
|
|
235
|
+
if let Some(timeout) = config.hook_timeout {
|
|
236
|
+
pre_query = pre_query.with_timeout(timeout);
|
|
237
|
+
}
|
|
238
|
+
Some(pre_query)
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
/// Extract the server-wide `post-query` hook from the config, if any.
|
|
@@ -245,9 +250,14 @@ fn load_post_query(cli: &Cli) -> Option<PostQuery> {
|
|
|
245
250
|
return None;
|
|
246
251
|
}
|
|
247
252
|
let resolved = config_path.canonicalize().ok()?;
|
|
248
|
-
let
|
|
253
|
+
let config = dirsql::config::load_config(&resolved).ok()?;
|
|
254
|
+
let command = config.post_query?;
|
|
249
255
|
let config_dir = resolved.parent()?.to_path_buf();
|
|
250
|
-
|
|
256
|
+
let mut post_query = PostQuery::new(command, config_dir);
|
|
257
|
+
if let Some(timeout) = config.hook_timeout {
|
|
258
|
+
post_query = post_query.with_timeout(timeout);
|
|
259
|
+
}
|
|
260
|
+
Some(post_query)
|
|
251
261
|
}
|
|
252
262
|
|
|
253
263
|
/// Zero-config fallback. When no `.dirsql.toml` is found, dirsql indexes the
|
|
@@ -149,4 +149,21 @@ mod tests {
|
|
|
149
149
|
let msg = format!("{err}").to_lowercase();
|
|
150
150
|
assert!(msg.contains("claude"));
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
/// `run` bails before spawning the agent when the output already exists and
|
|
154
|
+
/// `--force` was not passed (the temp dir itself is an existing path).
|
|
155
|
+
#[test]
|
|
156
|
+
fn run_bails_when_output_exists_without_force() {
|
|
157
|
+
let dir = tempfile::tempdir().unwrap();
|
|
158
|
+
let opts = InitOptions {
|
|
159
|
+
root: dir.path().to_path_buf(),
|
|
160
|
+
output: dir.path().to_path_buf(),
|
|
161
|
+
force: false,
|
|
162
|
+
};
|
|
163
|
+
let err = run(opts).unwrap_err();
|
|
164
|
+
assert!(
|
|
165
|
+
matches!(err, InitError::AlreadyExists { .. }),
|
|
166
|
+
"got: {err:?}"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
152
169
|
}
|
|
@@ -27,6 +27,7 @@ use tokio::task::JoinError;
|
|
|
27
27
|
use tokio::task::JoinHandle;
|
|
28
28
|
|
|
29
29
|
use crate::DirSQL;
|
|
30
|
+
use crate::command::DEFAULT_COMMAND_TIMEOUT;
|
|
30
31
|
|
|
31
32
|
pub mod init;
|
|
32
33
|
pub mod router;
|
|
@@ -51,16 +52,29 @@ pub struct PreQuery {
|
|
|
51
52
|
pub command: String,
|
|
52
53
|
/// The command's working directory — the config file's parent.
|
|
53
54
|
pub config_dir: PathBuf,
|
|
55
|
+
/// Per-run timeout. Defaults to the shared 30-second
|
|
56
|
+
/// [`DEFAULT_COMMAND_TIMEOUT`]; override it via [`Self::with_timeout`]
|
|
57
|
+
/// (the CLI wires the global `[dirsql].hook-timeout` here, #351).
|
|
58
|
+
pub timeout: Duration,
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
impl PreQuery {
|
|
57
|
-
/// Build a [`PreQuery`] from a command template and its working directory
|
|
62
|
+
/// Build a [`PreQuery`] from a command template and its working directory,
|
|
63
|
+
/// with the default 30-second timeout.
|
|
58
64
|
pub fn new(command: impl Into<String>, config_dir: impl Into<PathBuf>) -> Self {
|
|
59
65
|
Self {
|
|
60
66
|
command: command.into(),
|
|
61
67
|
config_dir: config_dir.into(),
|
|
68
|
+
timeout: DEFAULT_COMMAND_TIMEOUT,
|
|
62
69
|
}
|
|
63
70
|
}
|
|
71
|
+
|
|
72
|
+
/// Override the per-run timeout (from the global `[dirsql].hook-timeout`).
|
|
73
|
+
/// A run exceeding it is killed and the request returns 500.
|
|
74
|
+
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
|
75
|
+
self.timeout = timeout;
|
|
76
|
+
self
|
|
77
|
+
}
|
|
64
78
|
}
|
|
65
79
|
|
|
66
80
|
/// A server-wide `post-query` command hook, carrying the command template plus
|
|
@@ -76,16 +90,29 @@ pub struct PostQuery {
|
|
|
76
90
|
pub command: String,
|
|
77
91
|
/// The command's working directory — the config file's parent.
|
|
78
92
|
pub config_dir: PathBuf,
|
|
93
|
+
/// Per-run timeout. Defaults to the shared 30-second
|
|
94
|
+
/// [`DEFAULT_COMMAND_TIMEOUT`]; override it via [`Self::with_timeout`]
|
|
95
|
+
/// (the CLI wires the global `[dirsql].hook-timeout` here, #351).
|
|
96
|
+
pub timeout: Duration,
|
|
79
97
|
}
|
|
80
98
|
|
|
81
99
|
impl PostQuery {
|
|
82
|
-
/// Build a [`PostQuery`] from a command template and its working directory
|
|
100
|
+
/// Build a [`PostQuery`] from a command template and its working directory,
|
|
101
|
+
/// with the default 30-second timeout.
|
|
83
102
|
pub fn new(command: impl Into<String>, config_dir: impl Into<PathBuf>) -> Self {
|
|
84
103
|
Self {
|
|
85
104
|
command: command.into(),
|
|
86
105
|
config_dir: config_dir.into(),
|
|
106
|
+
timeout: DEFAULT_COMMAND_TIMEOUT,
|
|
87
107
|
}
|
|
88
108
|
}
|
|
109
|
+
|
|
110
|
+
/// Override the per-run timeout (from the global `[dirsql].hook-timeout`).
|
|
111
|
+
/// A run exceeding it is killed and the request returns 500.
|
|
112
|
+
pub fn with_timeout(mut self, timeout: Duration) -> Self {
|
|
113
|
+
self.timeout = timeout;
|
|
114
|
+
self
|
|
115
|
+
}
|
|
89
116
|
}
|
|
90
117
|
|
|
91
118
|
/// Configure how the server binds. Defaults to `localhost:7117` with a
|
|
@@ -258,10 +285,17 @@ mod tests {
|
|
|
258
285
|
#[test]
|
|
259
286
|
fn pre_query_constructor_carries_command_and_dir() {
|
|
260
287
|
// `PreQuery::new` is pure data plumbing: the command template and the
|
|
261
|
-
// working directory it will run in.
|
|
288
|
+
// working directory it will run in, with the shared default timeout.
|
|
262
289
|
let pq = PreQuery::new("to_sql.py {args}", "/proj");
|
|
263
290
|
assert_eq!(pq.command, "to_sql.py {args}");
|
|
264
291
|
assert_eq!(pq.config_dir, PathBuf::from("/proj"));
|
|
292
|
+
assert_eq!(pq.timeout, Duration::from_secs(30));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn pre_query_with_timeout_overrides_the_default() {
|
|
297
|
+
let pq = PreQuery::new("cmd {args}", "/proj").with_timeout(Duration::from_secs(60));
|
|
298
|
+
assert_eq!(pq.timeout, Duration::from_secs(60));
|
|
265
299
|
}
|
|
266
300
|
|
|
267
301
|
#[test]
|
|
@@ -275,10 +309,17 @@ mod tests {
|
|
|
275
309
|
#[test]
|
|
276
310
|
fn post_query_constructor_carries_command_and_dir() {
|
|
277
311
|
// `PostQuery::new` is pure data plumbing: the command template and the
|
|
278
|
-
// working directory it will run in.
|
|
312
|
+
// working directory it will run in, with the shared default timeout.
|
|
279
313
|
let pq = PostQuery::new("jq '{results: .}'", "/proj");
|
|
280
314
|
assert_eq!(pq.command, "jq '{results: .}'");
|
|
281
315
|
assert_eq!(pq.config_dir, PathBuf::from("/proj"));
|
|
316
|
+
assert_eq!(pq.timeout, Duration::from_secs(30));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn post_query_with_timeout_overrides_the_default() {
|
|
321
|
+
let pq = PostQuery::new("reshape {args}", "/proj").with_timeout(Duration::from_secs(60));
|
|
322
|
+
assert_eq!(pq.timeout, Duration::from_secs(60));
|
|
282
323
|
}
|
|
283
324
|
|
|
284
325
|
#[test]
|
|
@@ -21,15 +21,6 @@ use super::{AppState, PostQuery, PreQuery};
|
|
|
21
21
|
use crate::command::{Placeholder, run_command};
|
|
22
22
|
use crate::{DirSQL, DirSqlError};
|
|
23
23
|
|
|
24
|
-
/// Fixed timeout for a server-wide `pre-query` command. There is no override
|
|
25
|
-
/// key yet; this module constant is the documented current default (mirrors
|
|
26
|
-
/// `on-file`'s `ON_FILE_TIMEOUT`).
|
|
27
|
-
const PRE_QUERY_TIMEOUT: Duration = Duration::from_secs(30);
|
|
28
|
-
|
|
29
|
-
/// Fixed timeout for a server-wide `post-query` command (mirrors
|
|
30
|
-
/// `PRE_QUERY_TIMEOUT`).
|
|
31
|
-
const POST_QUERY_TIMEOUT: Duration = Duration::from_secs(30);
|
|
32
|
-
|
|
33
24
|
/// Cap on the serialized result payload passed as the `{args}` argv token.
|
|
34
25
|
/// Beyond this, `{args}` is emptied and the operator is directed to stdin
|
|
35
26
|
/// (which always carries the full payload) — comfortably under Linux's 128 KiB
|
|
@@ -155,15 +146,17 @@ fn parse_sql_body(body: &str) -> Result<String, Response> {
|
|
|
155
146
|
async fn run_pre_query(pq: &PreQuery, raw_body: String) -> Result<String, Response> {
|
|
156
147
|
let command = pq.command.clone();
|
|
157
148
|
let config_dir = pq.config_dir.clone();
|
|
149
|
+
let timeout = pq.timeout;
|
|
158
150
|
// `run_command` is blocking — it spawns a child and joins drain threads —
|
|
159
|
-
// so run it off the async runtime. It enforces
|
|
160
|
-
// internally, so no outer
|
|
151
|
+
// so run it off the async runtime. It enforces the hook's timeout
|
|
152
|
+
// (the global `[dirsql].hook-timeout`, default 30s) internally, so no outer
|
|
153
|
+
// `tokio::time::timeout` is needed.
|
|
161
154
|
let outcome = tokio::task::spawn_blocking(move || {
|
|
162
155
|
run_command(
|
|
163
156
|
&command,
|
|
164
157
|
&[Placeholder::new("args", &raw_body)],
|
|
165
158
|
&config_dir,
|
|
166
|
-
|
|
159
|
+
timeout,
|
|
167
160
|
None,
|
|
168
161
|
)
|
|
169
162
|
})
|
|
@@ -198,9 +191,11 @@ async fn run_post_query(
|
|
|
198
191
|
.map_err(|err| error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
|
199
192
|
let command = pq.command.clone();
|
|
200
193
|
let config_dir = pq.config_dir.clone();
|
|
194
|
+
let timeout = pq.timeout;
|
|
201
195
|
// `run_command` is blocking — it spawns a child and joins drain threads —
|
|
202
|
-
// so run it off the async runtime. It enforces
|
|
203
|
-
// internally, so no outer
|
|
196
|
+
// so run it off the async runtime. It enforces the hook's timeout
|
|
197
|
+
// (the global `[dirsql].hook-timeout`, default 30s) internally, so no outer
|
|
198
|
+
// `tokio::time::timeout` is needed.
|
|
204
199
|
let outcome = tokio::task::spawn_blocking(move || {
|
|
205
200
|
let args_value = if payload.len() <= POST_QUERY_ARGS_MAX {
|
|
206
201
|
payload.clone()
|
|
@@ -217,7 +212,7 @@ async fn run_post_query(
|
|
|
217
212
|
&command,
|
|
218
213
|
&[Placeholder::new("args", &args_value)],
|
|
219
214
|
&config_dir,
|
|
220
|
-
|
|
215
|
+
timeout,
|
|
221
216
|
Some(payload.as_bytes()),
|
|
222
217
|
)
|
|
223
218
|
})
|
|
@@ -114,4 +114,33 @@ mod tests {
|
|
|
114
114
|
|
|
115
115
|
handle.shutdown().await.expect("graceful shutdown");
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
// A `Ready` state drives the `serve` convenience wrapper plus
|
|
119
|
+
// `start_watch_task` (which attaches `DirSQL::watch` and pumps its stream
|
|
120
|
+
// into the broadcast channel). Built over an empty temp dir so the scan
|
|
121
|
+
// touches nothing; the port-0 bind and graceful shutdown are real.
|
|
122
|
+
#[tokio::test]
|
|
123
|
+
async fn serve_with_a_ready_db_attaches_the_watcher_then_shuts_down() {
|
|
124
|
+
let dir = tempfile::tempdir().unwrap();
|
|
125
|
+
let db = DirSQL::new(dir.path(), Vec::new()).unwrap();
|
|
126
|
+
let config = ServerConfig::bind("127.0.0.1".to_string(), 0);
|
|
127
|
+
let handle = serve(config, db).await.expect("bind on an ephemeral port");
|
|
128
|
+
assert_ne!(handle.local_addr().port(), 0);
|
|
129
|
+
handle.shutdown().await.expect("graceful shutdown");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Binding to a non-local TEST-NET address (RFC 5737) fails with
|
|
133
|
+
// "cannot assign requested address", surfacing `ServerError::Bind` rather
|
|
134
|
+
// than panicking — deterministic and DNS-free.
|
|
135
|
+
#[tokio::test]
|
|
136
|
+
async fn serve_with_state_surfaces_a_bind_error_for_a_nonlocal_address() {
|
|
137
|
+
let config = ServerConfig::bind("192.0.2.1".to_string(), 9);
|
|
138
|
+
// `serve_with_state` returns `Result<ServerHandle, _>`; ServerHandle is
|
|
139
|
+
// not Debug, so match rather than `unwrap_err`.
|
|
140
|
+
let err = match serve_with_state(config, AppState::Unavailable("x".to_string())).await {
|
|
141
|
+
Ok(_) => panic!("expected a bind error"),
|
|
142
|
+
Err(e) => e,
|
|
143
|
+
};
|
|
144
|
+
assert!(matches!(err, ServerError::Bind { .. }), "got: {err:?}");
|
|
145
|
+
}
|
|
117
146
|
}
|
|
@@ -36,6 +36,11 @@ use std::time::Duration;
|
|
|
36
36
|
|
|
37
37
|
use wait_timeout::ChildExt;
|
|
38
38
|
|
|
39
|
+
/// The default timeout for every command-backed event (`on-file`,
|
|
40
|
+
/// `pre-query`, `post-query`) when the config declares no override
|
|
41
|
+
/// (`[dirsql].hook-timeout`, positive whole seconds).
|
|
42
|
+
pub const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
|
|
43
|
+
|
|
39
44
|
/// A named placeholder substituted into a command's argv.
|
|
40
45
|
///
|
|
41
46
|
/// `name` is the bare identifier (no braces): a `name` of `path` matches the
|
|
@@ -484,4 +489,103 @@ mod tests {
|
|
|
484
489
|
assert!(!Placeholder::new("a", "b").append_if_absent);
|
|
485
490
|
assert!(Placeholder::append("a", "b").append_if_absent);
|
|
486
491
|
}
|
|
492
|
+
|
|
493
|
+
// ----- run_command end-to-end (spawns trivial POSIX commands) ----------
|
|
494
|
+
//
|
|
495
|
+
// `run_command` can only be exercised by actually spawning a child, so
|
|
496
|
+
// these drive `sh`/`echo`/`cat` — universally present on the Linux CI
|
|
497
|
+
// runners. The test code itself statically references only `super::` items,
|
|
498
|
+
// `Duration`, and `std::path::Path`, so the `unit lint` isolation rule is
|
|
499
|
+
// satisfied; the effectful process/thread work lives in production code.
|
|
500
|
+
|
|
501
|
+
fn cwd() -> std::path::PathBuf {
|
|
502
|
+
std::path::PathBuf::from(".")
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#[test]
|
|
506
|
+
fn run_command_returns_last_nonempty_stdout_line() {
|
|
507
|
+
let out = run_command(
|
|
508
|
+
"sh -c 'echo chatter; echo PAYLOAD'",
|
|
509
|
+
&[],
|
|
510
|
+
&cwd(),
|
|
511
|
+
Duration::from_secs(30),
|
|
512
|
+
None,
|
|
513
|
+
)
|
|
514
|
+
.unwrap();
|
|
515
|
+
assert_eq!(out.payload, "PAYLOAD");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#[test]
|
|
519
|
+
fn run_command_writes_stdin_payload_to_the_child() {
|
|
520
|
+
// `cat` echoes its stdin, so the payload round-trips through the
|
|
521
|
+
// stdin-writer thread and the stdout drain.
|
|
522
|
+
let out = run_command(
|
|
523
|
+
"cat",
|
|
524
|
+
&[],
|
|
525
|
+
&cwd(),
|
|
526
|
+
Duration::from_secs(30),
|
|
527
|
+
Some(b"hello-stdin"),
|
|
528
|
+
)
|
|
529
|
+
.unwrap();
|
|
530
|
+
assert_eq!(out.payload, "hello-stdin");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#[test]
|
|
534
|
+
fn run_command_reports_nonzero_exit_with_stderr_tail() {
|
|
535
|
+
let err = run_command(
|
|
536
|
+
"sh -c 'echo oops >&2; exit 3'",
|
|
537
|
+
&[],
|
|
538
|
+
&cwd(),
|
|
539
|
+
Duration::from_secs(30),
|
|
540
|
+
None,
|
|
541
|
+
)
|
|
542
|
+
.unwrap_err();
|
|
543
|
+
match err {
|
|
544
|
+
CommandError::NonZeroExit {
|
|
545
|
+
code, stderr_tail, ..
|
|
546
|
+
} => {
|
|
547
|
+
assert_eq!(code, "3");
|
|
548
|
+
assert!(stderr_tail.contains("oops"), "got: {stderr_tail}");
|
|
549
|
+
}
|
|
550
|
+
other => panic!("expected NonZeroExit, got {other:?}"),
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#[test]
|
|
555
|
+
fn run_command_reports_empty_output_when_stdout_is_blank() {
|
|
556
|
+
let err = run_command("true", &[], &cwd(), Duration::from_secs(30), None).unwrap_err();
|
|
557
|
+
assert!(
|
|
558
|
+
matches!(err, CommandError::EmptyOutput { .. }),
|
|
559
|
+
"got: {err:?}"
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
#[test]
|
|
564
|
+
fn run_command_reports_spawn_failure_for_a_missing_program() {
|
|
565
|
+
let err = run_command(
|
|
566
|
+
"definitely-not-a-real-binary-xyzzy",
|
|
567
|
+
&[],
|
|
568
|
+
&cwd(),
|
|
569
|
+
Duration::from_secs(30),
|
|
570
|
+
None,
|
|
571
|
+
)
|
|
572
|
+
.unwrap_err();
|
|
573
|
+
assert!(matches!(err, CommandError::Spawn { .. }), "got: {err:?}");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#[test]
|
|
577
|
+
fn run_command_kills_and_reports_timeout_even_with_stdin() {
|
|
578
|
+
// A short timeout against a long sleep drives the timeout branch,
|
|
579
|
+
// including joining the stdin-writer thread. `sh` ignoring stdin closes
|
|
580
|
+
// the pipe early, which the writer treats as a clean EOF.
|
|
581
|
+
let err = run_command(
|
|
582
|
+
"sh -c 'sleep 30'",
|
|
583
|
+
&[],
|
|
584
|
+
&cwd(),
|
|
585
|
+
Duration::from_millis(50),
|
|
586
|
+
Some(b"x"),
|
|
587
|
+
)
|
|
588
|
+
.unwrap_err();
|
|
589
|
+
assert!(matches!(err, CommandError::Timeout { .. }), "got: {err:?}");
|
|
590
|
+
}
|
|
487
591
|
}
|