uncontainerizable 0.1.1 → 0.1.2

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.
package/README.md CHANGED
@@ -18,9 +18,10 @@ If the program can run in a real sandbox — namespaces, seccomp, landlock
18
18
  stages.
19
19
  - **Tree-aware teardown.** Helper processes get reaped alongside the root;
20
20
  the container is "empty" only when no member remains.
21
- - **Identity-based singleton.** At most one container per identity is
22
- alive at a time; spawning preempts any predecessor using kernel
23
- primitives on Linux and Windows.
21
+ - **Identity-based preemption.** Spawning with an `identity` preempts
22
+ earlier matching instances. Linux and Windows are identity-scoped;
23
+ macOS `.app` launches use bundle-scoped preemption on the Launch
24
+ Services path.
24
25
  - **Adapter hooks.** Per-app lifecycle callbacks suppress "didn't shut
25
26
  down correctly" dialogs after force-kill.
26
27
  - **Infallible destroy.** `destroy()` aggregates errors into the result
@@ -32,13 +33,24 @@ If the program can run in a real sandbox — namespaces, seccomp, landlock
32
33
  | Platform | Preemption primitive | Quit ladder |
33
34
  | ----------------- | -------------------- | ------------------------------------------ |
34
35
  | Linux (x64/arm64) | cgroup v2 | `SIGTERM` → `SIGKILL` (race-free via freeze) |
35
- | macOS (x64/arm64) | `argv[0]` tag scan | `aevt/quit` → `SIGTERM` → `SIGKILL` |
36
+ | macOS (x64/arm64) | `argv[0]` tag scan / bundle-exec `ps` scan | `aevt/quit` → `SIGTERM` → `SIGKILL` |
36
37
  | Windows (x64/arm64) | named Job Object | `WM_CLOSE` → `TerminateJobObject` |
37
38
 
38
39
  Linux musl is shipped via `cargo-zigbuild`. Identity strings are
39
40
  namespaced by an app-level prefix (conventionally reverse-DNS) so
40
41
  libraries using `uncontainerizable` cannot collide.
41
42
 
43
+ On macOS, direct-exec launches use `argv[0]` tag scanning. Launch
44
+ Services `.app` launches instead match by bundle executable path via
45
+ `ps comm=`, so supplying `identity` there kills any running instance of
46
+ that bundle before relaunch, regardless of which identity started it.
47
+ Launch Services therefore does not support keeping two instances of the
48
+ same `.app` alive concurrently through this route. If you need that for
49
+ an app bundle, first make sure the app itself supports concurrent
50
+ instances, then pass the inner executable path
51
+ (`Foo.app/Contents/MacOS/Foo`) so the launch goes through direct-exec
52
+ instead of Launch Services.
53
+
42
54
  ## Installation
43
55
 
44
56
  ```sh
@@ -80,8 +92,13 @@ console.log(`exited at ${result.quit.exitedAtStage}`);
80
92
  ```
81
93
 
82
94
  A second call to `app.contain(..., { identity: "browser-main" })` will
83
- kill the running instance before launching the new one. Omit `identity`
84
- to skip preemption entirely.
95
+ kill the running instance before launching the new one. On macOS `.app`
96
+ launches, the same option acts as a bundle-scoped clean-slate switch and
97
+ clears any running instance of that bundle. Omit `identity` to skip
98
+ preemption entirely. If you need concurrent instances of a bundled app on
99
+ macOS, pass the executable inside the bundle rather than the `.app`
100
+ directory, and only do that if the app itself supports multiple
101
+ instances.
85
102
 
86
103
  ## API
87
104
 
@@ -103,9 +120,9 @@ Spawns a contained process. Options:
103
120
  | `args` | `string[]` | Command-line arguments. |
104
121
  | `env` | `Record<…>` | Environment overrides. |
105
122
  | `cwd` | `string` | Working directory. |
106
- | `identity` | `string` | Enables singleton enforcement; prior instance is killed first. |
123
+ | `identity` | `string` | Enables preemption; macOS `.app` launches match by bundle, other routes by identity. Use the inner executable path, not the `.app` path, if the app supports concurrent instances and you need more than one at once. |
107
124
  | `adapters` | `Adapter[]` | Per-app lifecycle hooks. |
108
- | `darwinTagArgv0` | `boolean` | macOS only; set `false` if the managed program misreads argv[0]. |
125
+ | `darwinTagArgv0` | `boolean` | macOS direct-exec only; set `false` if the managed program misreads argv[0] (ignored for `.app` bundle launches). |
109
126
 
110
127
  ### `container.quit(options?) → Promise<QuitResult>`
111
128
 
package/dist/index.d.mts CHANGED
@@ -17,10 +17,15 @@ declare class App {
17
17
  constructor(prefix: string);
18
18
  get prefix(): string;
19
19
  /**
20
- * Spawn a contained process. If `options.identity` is set, any previous
21
- * instance with the same (prefix, identity) pair is killed before this
22
- * one launches. If `options.adapters` is non-empty the Rust
23
- * orchestrator drives their lifecycle hooks around the quit ladder.
20
+ * Spawn a contained process. If `options.identity` is set, a prior
21
+ * matching instance is killed before launch. On macOS Launch Services
22
+ * `.app` launches, matching is bundle-scoped rather than
23
+ * `(prefix, identity)`-scoped, so this route cannot keep two instances
24
+ * of the same app alive concurrently. If the app itself supports
25
+ * multiple concurrent instances, pass the inner executable path
26
+ * (`Foo.app/Contents/MacOS/Foo`) to use the direct-exec route instead.
27
+ * If `options.adapters` is non-empty the Rust orchestrator drives their
28
+ * lifecycle hooks around the quit ladder.
24
29
  */
25
30
  contain(command: string, options?: ContainOptions): Promise<Container>;
26
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;AAwBA;;;;;;;;;cAAa,GAAA;EAAA;cAGC,MAAA;EAAA,IAIR,MAAA,CAAA;EAUI;;;;;;EAAR,OAAA,CAAQ,OAAA,UAAiB,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,SAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;AAwBA;;;;;;;;;cAAa,GAAA;EAAA;cAGC,MAAA;EAAA,IAIR,MAAA,CAAA;EAeI;;;;;;;;;;;EAAR,OAAA,CAAQ,OAAA,UAAiB,OAAA,GAAS,cAAA,GAAsB,OAAA,CAAQ,SAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -20,10 +20,15 @@ var App = class {
20
20
  return this.#inner.prefix;
21
21
  }
22
22
  /**
23
- * Spawn a contained process. If `options.identity` is set, any previous
24
- * instance with the same (prefix, identity) pair is killed before this
25
- * one launches. If `options.adapters` is non-empty the Rust
26
- * orchestrator drives their lifecycle hooks around the quit ladder.
23
+ * Spawn a contained process. If `options.identity` is set, a prior
24
+ * matching instance is killed before launch. On macOS Launch Services
25
+ * `.app` launches, matching is bundle-scoped rather than
26
+ * `(prefix, identity)`-scoped, so this route cannot keep two instances
27
+ * of the same app alive concurrently. If the app itself supports
28
+ * multiple concurrent instances, pass the inner executable path
29
+ * (`Foo.app/Contents/MacOS/Foo`) to use the direct-exec route instead.
30
+ * If `options.adapters` is non-empty the Rust orchestrator drives their
31
+ * lifecycle hooks around the quit ladder.
27
32
  */
28
33
  contain(command, options = {}) {
29
34
  const { adapters, ...nativeOpts } = options;
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["#inner"],"sources":["../src/index.ts"],"sourcesContent":["import { type JsAdapter, NodeApp } from \"@uncontainerizable/native\";\n\nimport type { Adapter, ContainOptions, Container } from \"#/types.js\";\n\nexport { coreVersion } from \"@uncontainerizable/native\";\n\nexport {\n appkit,\n chromium,\n crashReporter,\n defaultAdapters,\n firefox,\n} from \"#/adapters/index.js\";\n\n/**\n * Namespaced handle for spawning contained processes.\n *\n * Construct once per application, typically with a reverse-DNS string like\n * `\"com.example.my-supervisor\"`. The prefix namespaces identity strings so\n * two unrelated libraries using `uncontainerizable` cannot collide.\n *\n * Throws `INVALID_IDENTITY` if the prefix contains characters outside\n * `[A-Za-z0-9._:-]`.\n */\nexport class App {\n readonly #inner: NodeApp;\n\n constructor(prefix: string) {\n this.#inner = new NodeApp(prefix);\n }\n\n get prefix(): string {\n return this.#inner.prefix;\n }\n\n /**\n * Spawn a contained process. If `options.identity` is set, any previous\n * instance with the same (prefix, identity) pair is killed before this\n * one launches. If `options.adapters` is non-empty the Rust\n * orchestrator drives their lifecycle hooks around the quit ladder.\n */\n contain(command: string, options: ContainOptions = {}): Promise<Container> {\n const { adapters, ...nativeOpts } = options;\n return this.#inner.contain(command, {\n ...nativeOpts,\n adapters: adapters?.map(normalizeAdapter),\n });\n }\n}\n\n/**\n * Normalize a user-provided `Adapter` (which may declare sync methods)\n * into the always-async shape the napi bridge expects. Every optional\n * hook is only forwarded if defined, so the Rust side keeps its\n * \"undefined means skip\" semantics.\n */\nfunction normalizeAdapter(adapter: Adapter): JsAdapter {\n return {\n name: adapter.name,\n matches: async (probe) => Boolean(await adapter.matches(probe)),\n beforeQuit: adapter.beforeQuit\n ? async (probe) => {\n await adapter.beforeQuit?.(probe);\n }\n : undefined,\n beforeStage: adapter.beforeStage\n ? async (probe, stageName) => {\n await adapter.beforeStage?.(probe, stageName);\n }\n : undefined,\n afterStage: adapter.afterStage\n ? async (probe, result) => {\n await adapter.afterStage?.(probe, result);\n }\n : undefined,\n afterQuit: adapter.afterQuit\n ? async (probe, result) => {\n await adapter.afterQuit?.(probe, result);\n }\n : undefined,\n clearCrashState: adapter.clearCrashState\n ? async (probe) => {\n await adapter.clearCrashState?.(probe);\n }\n : undefined,\n };\n}\n\nexport type {\n Adapter,\n ContainOptions,\n Container,\n DestroyOptions,\n DestroyResult,\n Probe,\n QuitOptions,\n QuitResult,\n StageResult,\n SupportedPlatform,\n} from \"#/types.js\";\n"],"mappings":";;;;;;;;;;;;;AAwBA,IAAa,MAAb,MAAiB;CACf;CAEA,YAAY,QAAgB;AAC1B,QAAA,QAAc,IAAI,QAAQ,OAAO;;CAGnC,IAAI,SAAiB;AACnB,SAAO,MAAA,MAAY;;;;;;;;CASrB,QAAQ,SAAiB,UAA0B,EAAE,EAAsB;EACzE,MAAM,EAAE,UAAU,GAAG,eAAe;AACpC,SAAO,MAAA,MAAY,QAAQ,SAAS;GAClC,GAAG;GACH,UAAU,UAAU,IAAI,iBAAiB;GAC1C,CAAC;;;;;;;;;AAUN,SAAS,iBAAiB,SAA6B;AACrD,QAAO;EACL,MAAM,QAAQ;EACd,SAAS,OAAO,UAAU,QAAQ,MAAM,QAAQ,QAAQ,MAAM,CAAC;EAC/D,YAAY,QAAQ,aAChB,OAAO,UAAU;AACf,SAAM,QAAQ,aAAa,MAAM;MAEnC,KAAA;EACJ,aAAa,QAAQ,cACjB,OAAO,OAAO,cAAc;AAC1B,SAAM,QAAQ,cAAc,OAAO,UAAU;MAE/C,KAAA;EACJ,YAAY,QAAQ,aAChB,OAAO,OAAO,WAAW;AACvB,SAAM,QAAQ,aAAa,OAAO,OAAO;MAE3C,KAAA;EACJ,WAAW,QAAQ,YACf,OAAO,OAAO,WAAW;AACvB,SAAM,QAAQ,YAAY,OAAO,OAAO;MAE1C,KAAA;EACJ,iBAAiB,QAAQ,kBACrB,OAAO,UAAU;AACf,SAAM,QAAQ,kBAAkB,MAAM;MAExC,KAAA;EACL"}
1
+ {"version":3,"file":"index.mjs","names":["#inner"],"sources":["../src/index.ts"],"sourcesContent":["import { type JsAdapter, NodeApp } from \"@uncontainerizable/native\";\n\nimport type { Adapter, ContainOptions, Container } from \"#/types.js\";\n\nexport { coreVersion } from \"@uncontainerizable/native\";\n\nexport {\n appkit,\n chromium,\n crashReporter,\n defaultAdapters,\n firefox,\n} from \"#/adapters/index.js\";\n\n/**\n * Namespaced handle for spawning contained processes.\n *\n * Construct once per application, typically with a reverse-DNS string like\n * `\"com.example.my-supervisor\"`. The prefix namespaces identity strings so\n * two unrelated libraries using `uncontainerizable` cannot collide.\n *\n * Throws `INVALID_IDENTITY` if the prefix contains characters outside\n * `[A-Za-z0-9._:-]`.\n */\nexport class App {\n readonly #inner: NodeApp;\n\n constructor(prefix: string) {\n this.#inner = new NodeApp(prefix);\n }\n\n get prefix(): string {\n return this.#inner.prefix;\n }\n\n /**\n * Spawn a contained process. If `options.identity` is set, a prior\n * matching instance is killed before launch. On macOS Launch Services\n * `.app` launches, matching is bundle-scoped rather than\n * `(prefix, identity)`-scoped, so this route cannot keep two instances\n * of the same app alive concurrently. If the app itself supports\n * multiple concurrent instances, pass the inner executable path\n * (`Foo.app/Contents/MacOS/Foo`) to use the direct-exec route instead.\n * If `options.adapters` is non-empty the Rust orchestrator drives their\n * lifecycle hooks around the quit ladder.\n */\n contain(command: string, options: ContainOptions = {}): Promise<Container> {\n const { adapters, ...nativeOpts } = options;\n return this.#inner.contain(command, {\n ...nativeOpts,\n adapters: adapters?.map(normalizeAdapter),\n });\n }\n}\n\n/**\n * Normalize a user-provided `Adapter` (which may declare sync methods)\n * into the always-async shape the napi bridge expects. Every optional\n * hook is only forwarded if defined, so the Rust side keeps its\n * \"undefined means skip\" semantics.\n */\nfunction normalizeAdapter(adapter: Adapter): JsAdapter {\n return {\n name: adapter.name,\n matches: async (probe) => Boolean(await adapter.matches(probe)),\n beforeQuit: adapter.beforeQuit\n ? async (probe) => {\n await adapter.beforeQuit?.(probe);\n }\n : undefined,\n beforeStage: adapter.beforeStage\n ? async (probe, stageName) => {\n await adapter.beforeStage?.(probe, stageName);\n }\n : undefined,\n afterStage: adapter.afterStage\n ? async (probe, result) => {\n await adapter.afterStage?.(probe, result);\n }\n : undefined,\n afterQuit: adapter.afterQuit\n ? async (probe, result) => {\n await adapter.afterQuit?.(probe, result);\n }\n : undefined,\n clearCrashState: adapter.clearCrashState\n ? async (probe) => {\n await adapter.clearCrashState?.(probe);\n }\n : undefined,\n };\n}\n\nexport type {\n Adapter,\n ContainOptions,\n Container,\n DestroyOptions,\n DestroyResult,\n Probe,\n QuitOptions,\n QuitResult,\n StageResult,\n SupportedPlatform,\n} from \"#/types.js\";\n"],"mappings":";;;;;;;;;;;;;AAwBA,IAAa,MAAb,MAAiB;CACf;CAEA,YAAY,QAAgB;AAC1B,QAAA,QAAc,IAAI,QAAQ,OAAO;;CAGnC,IAAI,SAAiB;AACnB,SAAO,MAAA,MAAY;;;;;;;;;;;;;CAcrB,QAAQ,SAAiB,UAA0B,EAAE,EAAsB;EACzE,MAAM,EAAE,UAAU,GAAG,eAAe;AACpC,SAAO,MAAA,MAAY,QAAQ,SAAS;GAClC,GAAG;GACH,UAAU,UAAU,IAAI,iBAAiB;GAC1C,CAAC;;;;;;;;;AAUN,SAAS,iBAAiB,SAA6B;AACrD,QAAO;EACL,MAAM,QAAQ;EACd,SAAS,OAAO,UAAU,QAAQ,MAAM,QAAQ,QAAQ,MAAM,CAAC;EAC/D,YAAY,QAAQ,aAChB,OAAO,UAAU;AACf,SAAM,QAAQ,aAAa,MAAM;MAEnC,KAAA;EACJ,aAAa,QAAQ,cACjB,OAAO,OAAO,cAAc;AAC1B,SAAM,QAAQ,cAAc,OAAO,UAAU;MAE/C,KAAA;EACJ,YAAY,QAAQ,aAChB,OAAO,OAAO,WAAW;AACvB,SAAM,QAAQ,aAAa,OAAO,OAAO;MAE3C,KAAA;EACJ,WAAW,QAAQ,YACf,OAAO,OAAO,WAAW;AACvB,SAAM,QAAQ,YAAY,OAAO,OAAO;MAE1C,KAAA;EACJ,iBAAiB,QAAQ,kBACrB,OAAO,UAAU;AACf,SAAM,QAAQ,kBAAkB,MAAM;MAExC,KAAA;EACL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uncontainerizable",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Graceful process lifecycle for programs that can't be containerized",
5
5
  "keywords": [
6
6
  "lifecycle",
@@ -41,7 +41,7 @@
41
41
  "provenance": true
42
42
  },
43
43
  "dependencies": {
44
- "@uncontainerizable/native": "0.1.1"
44
+ "@uncontainerizable/native": "0.1.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24",