@effectionx/process 0.5.0

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.
Files changed (58) hide show
  1. package/README.md +12 -0
  2. package/esm/mod.d.ts +3 -0
  3. package/esm/mod.d.ts.map +1 -0
  4. package/esm/mod.js +2 -0
  5. package/esm/package.json +3 -0
  6. package/esm/src/daemon.d.ts +11 -0
  7. package/esm/src/daemon.d.ts.map +1 -0
  8. package/esm/src/daemon.js +20 -0
  9. package/esm/src/eventemitter.d.ts +22 -0
  10. package/esm/src/eventemitter.d.ts.map +1 -0
  11. package/esm/src/eventemitter.js +40 -0
  12. package/esm/src/exec/api.d.ts +70 -0
  13. package/esm/src/exec/api.d.ts.map +1 -0
  14. package/esm/src/exec/api.js +1 -0
  15. package/esm/src/exec/error.d.ts +14 -0
  16. package/esm/src/exec/error.d.ts.map +1 -0
  17. package/esm/src/exec/error.js +54 -0
  18. package/esm/src/exec/posix.d.ts +3 -0
  19. package/esm/src/exec/posix.d.ts.map +1 -0
  20. package/esm/src/exec/posix.js +110 -0
  21. package/esm/src/exec/win32.d.ts +4 -0
  22. package/esm/src/exec/win32.d.ts.map +1 -0
  23. package/esm/src/exec/win32.js +173 -0
  24. package/esm/src/exec.d.ts +16 -0
  25. package/esm/src/exec.d.ts.map +1 -0
  26. package/esm/src/exec.js +75 -0
  27. package/esm/src/helpers.d.ts +12 -0
  28. package/esm/src/helpers.d.ts.map +1 -0
  29. package/esm/src/helpers.js +71 -0
  30. package/package.json +34 -0
  31. package/script/mod.d.ts +3 -0
  32. package/script/mod.d.ts.map +1 -0
  33. package/script/mod.js +20 -0
  34. package/script/package.json +3 -0
  35. package/script/src/daemon.d.ts +11 -0
  36. package/script/src/daemon.d.ts.map +1 -0
  37. package/script/src/daemon.js +23 -0
  38. package/script/src/eventemitter.d.ts +22 -0
  39. package/script/src/eventemitter.d.ts.map +1 -0
  40. package/script/src/eventemitter.js +44 -0
  41. package/script/src/exec/api.d.ts +70 -0
  42. package/script/src/exec/api.d.ts.map +1 -0
  43. package/script/src/exec/api.js +2 -0
  44. package/script/src/exec/error.d.ts +14 -0
  45. package/script/src/exec/error.d.ts.map +1 -0
  46. package/script/src/exec/error.js +59 -0
  47. package/script/src/exec/posix.d.ts +3 -0
  48. package/script/src/exec/posix.d.ts.map +1 -0
  49. package/script/src/exec/posix.js +117 -0
  50. package/script/src/exec/win32.d.ts +4 -0
  51. package/script/src/exec/win32.d.ts.map +1 -0
  52. package/script/src/exec/win32.js +178 -0
  53. package/script/src/exec.d.ts +16 -0
  54. package/script/src/exec.d.ts.map +1 -0
  55. package/script/src/exec.js +92 -0
  56. package/script/src/helpers.d.ts +12 -0
  57. package/script/src/helpers.d.ts.map +1 -0
  58. package/script/src/helpers.js +76 -0
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @effection/process
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Created by Frontside](https://img.shields.io/badge/created%20by-frontside-26abe8.svg)](https://frontside.com)
5
+ [![Chat on Discord](https://img.shields.io/discord/700803887132704931?Label=Discord)](https://discord.gg/Ug5nWH8)
6
+
7
+ [Effection][Effection] is the structured concurrency toolkit for JavaScript. You
8
+ can find detailed information about using effection to manage system process in
9
+ node in the
10
+ [spawning processes guide](https://frontside.com/effection/docs/guides/processes)
11
+
12
+ [Effection]: https://frontside.com/effection
package/esm/mod.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./src/exec.js";
2
+ export { type Daemon, daemon } from "./src/daemon.js";
3
+ //# sourceMappingURL=mod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC"}
package/esm/mod.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./src/exec.js";
2
+ export { daemon } from "./src/daemon.js";
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,11 @@
1
+ import { type Operation } from "effection";
2
+ import { type ExecOptions, type Process } from "./exec.js";
3
+ export interface Daemon extends Operation<void>, Process {
4
+ }
5
+ /**
6
+ * Start a long-running process, like a web server that run perpetually.
7
+ * Daemon operations are expected to run forever, and if they exit pre-maturely
8
+ * before the operation containing them passes out of scope it raises an error.
9
+ */
10
+ export declare function daemon(command: string, options?: ExecOptions): Operation<Daemon>;
11
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../src/src/daemon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,WAAW,CAAC;AAErD,OAAO,EAGL,KAAK,WAAW,EAEhB,KAAK,OAAO,EACb,MAAM,WAAW,CAAC;AAEnB,MAAM,WAAW,MAAO,SAAQ,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO;CACvD;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,WAAgB,GACxB,SAAS,CAAC,MAAM,CAAC,CAanB"}
@@ -0,0 +1,20 @@
1
+ import { resource } from "effection";
2
+ import { DaemonExitError, exec, } from "./exec.js";
3
+ /**
4
+ * Start a long-running process, like a web server that run perpetually.
5
+ * Daemon operations are expected to run forever, and if they exit pre-maturely
6
+ * before the operation containing them passes out of scope it raises an error.
7
+ */
8
+ export function daemon(command, options = {}) {
9
+ return resource(function* (provide) {
10
+ // TODO: should we be able to terminate the process from here?
11
+ let process = yield* exec(command, options);
12
+ yield* provide({
13
+ *[Symbol.iterator]() {
14
+ let status = yield* process.join();
15
+ throw new DaemonExitError(status, command, options);
16
+ },
17
+ ...process,
18
+ });
19
+ });
20
+ }
@@ -0,0 +1,22 @@
1
+ import type { Operation, Stream } from "effection";
2
+ import type { EventEmitter } from "node:stream";
3
+ /**
4
+ * Create a {@link Stream} of events from any EventEmitter.
5
+ *
6
+ * See the guide on [Streams and Subscriptions](https://frontside.com/effection/docs/collections)
7
+ * for details on how to use streams.
8
+ *
9
+ * @param target - the event target whose events will be streamed
10
+ * @param name - the name of the event to stream. E.g. "click"
11
+ * @returns a stream that will see one item for each event
12
+ */
13
+ export declare function on<T extends unknown[]>(target: EventEmitter | null, eventName: string): Stream<T, never>;
14
+ /**
15
+ * Create an {@link Operation} that yields the next event to be emitted by an EventEmitter.
16
+ *
17
+ * @param target - the event target to be watched
18
+ * @param name - the name of the event to watch. E.g. "click"
19
+ * @returns an Operation that yields the next emitted event
20
+ */
21
+ export declare function once<TArgs extends unknown[] = unknown[]>(source: EventEmitter | null, eventName: string): Operation<TArgs>;
22
+ //# sourceMappingURL=eventemitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventemitter.d.ts","sourceRoot":"","sources":["../../src/src/eventemitter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAgB,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,EAAE,CAChB,CAAC,SAAS,OAAO,EAAE,EACnB,MAAM,EAAE,YAAY,GAAG,IAAI,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAC,CAkBlE;AAED;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,KAAK,SAAS,OAAO,EAAE,GAAG,OAAO,EAAE,EACtD,MAAM,EAAE,YAAY,GAAG,IAAI,EAC3B,SAAS,EAAE,MAAM,GAChB,SAAS,CAAC,KAAK,CAAC,CAWlB"}
@@ -0,0 +1,40 @@
1
+ import { createSignal, resource, withResolvers } from "effection";
2
+ /**
3
+ * Create a {@link Stream} of events from any EventEmitter.
4
+ *
5
+ * See the guide on [Streams and Subscriptions](https://frontside.com/effection/docs/collections)
6
+ * for details on how to use streams.
7
+ *
8
+ * @param target - the event target whose events will be streamed
9
+ * @param name - the name of the event to stream. E.g. "click"
10
+ * @returns a stream that will see one item for each event
11
+ */
12
+ export function on(target, eventName) {
13
+ return resource(function* (provide) {
14
+ let signal = createSignal();
15
+ let listener = (...args) => signal.send(args);
16
+ target?.on(eventName, listener);
17
+ try {
18
+ yield* provide(yield* signal);
19
+ }
20
+ finally {
21
+ target?.off(eventName, listener);
22
+ }
23
+ });
24
+ }
25
+ /**
26
+ * Create an {@link Operation} that yields the next event to be emitted by an EventEmitter.
27
+ *
28
+ * @param target - the event target to be watched
29
+ * @param name - the name of the event to watch. E.g. "click"
30
+ * @returns an Operation that yields the next emitted event
31
+ */
32
+ export function once(source, eventName) {
33
+ const result = withResolvers();
34
+ let listener = (...args) => {
35
+ result.resolve(args);
36
+ source?.off(eventName, listener);
37
+ };
38
+ source?.on(eventName, listener);
39
+ return result.operation;
40
+ }
@@ -0,0 +1,70 @@
1
+ import type { Operation } from "effection";
2
+ import type { OutputStream } from "../helpers.js";
3
+ export interface Writable<T> {
4
+ send(message: T): void;
5
+ }
6
+ /**
7
+ * The process type is what is returned by the `exec` operation. It has all of
8
+ * standard io handles, and methods for synchronizing on return.
9
+ */
10
+ export interface Process extends StdIO {
11
+ readonly pid: number;
12
+ /**
13
+ * Completes once the process has finished regardless of whether it was
14
+ * successful or not.
15
+ */
16
+ join(): Operation<ExitStatus>;
17
+ /**
18
+ * Completes once the process has finished successfully. If the process does
19
+ * not complete successfully, it will raise an ExecError.
20
+ */
21
+ expect(): Operation<ExitStatus>;
22
+ }
23
+ export interface ExecOptions {
24
+ /**
25
+ * When not using passing the `shell` option all arguments must be passed
26
+ * as an array.
27
+ */
28
+ arguments?: string[];
29
+ /**
30
+ * Map of environment variables to use for the process.
31
+ */
32
+ env?: Record<string, string>;
33
+ /**
34
+ * Create an intermediate shell process; defaults to `false`. Useful if you
35
+ * need to handle glob expansion or passing environment variables. A truthy value
36
+ * will use an intermediate shell to interpret the command using the default system shell.
37
+ * However, if the value is a string, that will be used as the executable path
38
+ * for the intermediate shell.
39
+ */
40
+ shell?: boolean | string;
41
+ /**
42
+ * Sets the working directory of the process
43
+ */
44
+ cwd?: string;
45
+ }
46
+ export interface StdIO {
47
+ stdout: OutputStream;
48
+ stderr: OutputStream;
49
+ stdin: Writable<string>;
50
+ }
51
+ export interface ExitStatus {
52
+ /**
53
+ * exit code
54
+ * //TODO: is this pertinent on Windows? Do we need an 'OK' flag
55
+ */
56
+ code?: number;
57
+ /**
58
+ * If the process exited with a signal instead of an exit code, it
59
+ * is recorded here.
60
+ */
61
+ signal?: string;
62
+ }
63
+ export interface ProcessResult extends ExitStatus {
64
+ stdout: string;
65
+ stderr: string;
66
+ }
67
+ export interface CreateOSProcess {
68
+ (command: string, options: ExecOptions): Operation<Process>;
69
+ }
70
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/src/exec/api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,OAAQ,SAAQ,KAAK;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,IAAI,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC;IAE9B;;;OAGG;IACH,MAAM,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE7B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEzB;;OAEG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,eAAe;IAC9B,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;CAC7D"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { ExecOptions, ExitStatus } from "./api.js";
2
+ export declare class ExecError extends Error {
3
+ status: ExitStatus;
4
+ command: string;
5
+ options: ExecOptions;
6
+ constructor(status: ExitStatus, command: string, options: ExecOptions);
7
+ name: string;
8
+ get message(): string;
9
+ }
10
+ export declare class DaemonExitError extends ExecError {
11
+ name: string;
12
+ get message(): string;
13
+ }
14
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../../../src/src/exec/error.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAExD,qBAAa,SAAU,SAAQ,KAAK;IAEzB,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,MAAM;IACf,OAAO,EAAE,WAAW;gBAFpB,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW;IAKpB,IAAI,SAAe;IAE5B,IAAa,OAAO,IAAI,MAAM,CAgB7B;CACF;AAED,qBAAa,eAAgB,SAAQ,SAAS;IACnC,IAAI,SAAqB;IAElC,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF"}
@@ -0,0 +1,54 @@
1
+ export class ExecError extends Error {
2
+ constructor(status, command, options) {
3
+ super();
4
+ Object.defineProperty(this, "status", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: status
9
+ });
10
+ Object.defineProperty(this, "command", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: command
15
+ });
16
+ Object.defineProperty(this, "options", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: options
21
+ });
22
+ Object.defineProperty(this, "name", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: "ExecError"
27
+ });
28
+ }
29
+ get message() {
30
+ let code = this.status.code ? `code: ${this.status.code}` : null;
31
+ let signal = this.status.signal ? `signal: ${this.status.signal}` : null;
32
+ let env = `env: ${JSON.stringify(this.options.env || {})}`;
33
+ let shell = this.options.shell ? `shell: ${this.options.shell}` : null;
34
+ let cwd = this.options.cwd ? `cwd: ${this.options.cwd}` : null;
35
+ let command = `$ ${this.command} ${this.options.arguments?.join(" ")}`
36
+ .trim();
37
+ return [code, signal, env, shell, cwd, command].filter((item) => !!item)
38
+ .join("\n");
39
+ }
40
+ }
41
+ export class DaemonExitError extends ExecError {
42
+ constructor() {
43
+ super(...arguments);
44
+ Object.defineProperty(this, "name", {
45
+ enumerable: true,
46
+ configurable: true,
47
+ writable: true,
48
+ value: "DaemonExitError"
49
+ });
50
+ }
51
+ get message() {
52
+ return `daemon process quit unexpectedly\n${super.message}`;
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ import type { CreateOSProcess } from "./api.js";
2
+ export declare const createPosixProcess: CreateOSProcess;
3
+ //# sourceMappingURL=posix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"posix.d.ts","sourceRoot":"","sources":["../../../src/src/exec/posix.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAwB,MAAM,UAAU,CAAC;AAKtE,eAAO,MAAM,kBAAkB,EAAE,eAkHhC,CAAC"}
@@ -0,0 +1,110 @@
1
+ import { spawn as spawnProcess } from "node:child_process";
2
+ import { all, createSignal, Err, Ok, spawn, withResolvers, } from "effection";
3
+ import process from "node:process";
4
+ import { once } from "../eventemitter.js";
5
+ import { useReadable } from "../helpers.js";
6
+ import { ExecError } from "./error.js";
7
+ export const createPosixProcess = function* createPosixProcess(command, options) {
8
+ let processResult = withResolvers();
9
+ // Killing all child processes started by this command is surprisingly
10
+ // tricky. If a process spawns another processes and we kill the parent,
11
+ // then the child process is NOT automatically killed. Instead we're using
12
+ // the `detached` option to force the child into its own process group,
13
+ // which all of its children in turn will inherit. By sending the signal to
14
+ // `-pid` rather than `pid`, we are sending it to the entire process group
15
+ // instead. This will send the signal to all processes started by the child
16
+ // process.
17
+ //
18
+ // More information here: https://unix.stackexchange.com/questions/14815/process-descendants
19
+ let childProcess = spawnProcess(command, options.arguments || [], {
20
+ detached: true,
21
+ shell: options.shell,
22
+ env: options.env,
23
+ cwd: options.cwd,
24
+ stdio: "pipe",
25
+ });
26
+ let { pid } = childProcess;
27
+ let io = {
28
+ stdout: yield* useReadable(childProcess.stdout),
29
+ stderr: yield* useReadable(childProcess.stderr),
30
+ stdoutDone: withResolvers(),
31
+ stderrDone: withResolvers(),
32
+ };
33
+ let stdout = createSignal();
34
+ let stderr = createSignal();
35
+ yield* spawn(function* () {
36
+ let next = yield* io.stdout.next();
37
+ while (!next.done) {
38
+ stdout.send(next.value);
39
+ next = yield* io.stdout.next();
40
+ }
41
+ stdout.close();
42
+ io.stdoutDone.resolve();
43
+ });
44
+ yield* spawn(function* () {
45
+ let next = yield* io.stderr.next();
46
+ while (!next.done) {
47
+ stderr.send(next.value);
48
+ next = yield* io.stderr.next();
49
+ }
50
+ stderr.close();
51
+ io.stderrDone.resolve();
52
+ });
53
+ yield* spawn(function* trapError() {
54
+ let [error] = yield* once(childProcess, "error");
55
+ processResult.resolve(Err(error));
56
+ });
57
+ let stdin = {
58
+ send(data) {
59
+ childProcess.stdin.write(data);
60
+ },
61
+ };
62
+ yield* spawn(function* () {
63
+ try {
64
+ let value = yield* once(childProcess, "close");
65
+ yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
66
+ processResult.resolve(Ok(value));
67
+ }
68
+ finally {
69
+ try {
70
+ if (typeof childProcess.pid === "undefined") {
71
+ // deno-lint-ignore no-unsafe-finally
72
+ throw new Error("no pid for childProcess");
73
+ }
74
+ process.kill(-childProcess.pid, "SIGTERM");
75
+ yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
76
+ }
77
+ catch (_e) {
78
+ // do nothing, process is probably already dead
79
+ }
80
+ }
81
+ });
82
+ function* join() {
83
+ let result = yield* processResult.operation;
84
+ if (result.ok) {
85
+ let [code, signal] = result.value;
86
+ return { command, options, code, signal };
87
+ }
88
+ else {
89
+ throw result.error;
90
+ }
91
+ }
92
+ function* expect() {
93
+ let status = yield* join();
94
+ if (status.code != 0) {
95
+ throw new ExecError(status, command, options);
96
+ }
97
+ else {
98
+ return status;
99
+ }
100
+ }
101
+ // FYI: this function starts a process and returns without blocking
102
+ return {
103
+ pid: pid,
104
+ stdin,
105
+ stdout,
106
+ stderr,
107
+ join,
108
+ expect,
109
+ };
110
+ };
@@ -0,0 +1,4 @@
1
+ import type { CreateOSProcess } from "./api.js";
2
+ export declare const createWin32Process: CreateOSProcess;
3
+ export declare const isWin32: () => boolean;
4
+ //# sourceMappingURL=win32.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"win32.d.ts","sourceRoot":"","sources":["../../../src/src/exec/win32.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAwB,MAAM,UAAU,CAAC;AAkBtE,eAAO,MAAM,kBAAkB,EAAE,eA8KhC,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,OAAiC,CAAC"}
@@ -0,0 +1,173 @@
1
+ import { platform } from "node:os";
2
+ import { all, createSignal, Err, Ok, race, sleep, spawn, withResolvers, } from "effection";
3
+ // @ts-types="npm:@types/cross-spawn@6.0.6"
4
+ import { spawn as spawnProcess } from "cross-spawn";
5
+ import { ctrlc } from "ctrlc-windows";
6
+ import { once } from "../eventemitter.js";
7
+ import { useReadable } from "../helpers.js";
8
+ import { ExecError } from "./error.js";
9
+ function* killTree(pid) {
10
+ try {
11
+ const killer = spawnProcess("cmd.exe", ["/c", "taskkill", "/PID", String(pid), "/T", "/F"], { windowsHide: true, stdio: "ignore" });
12
+ yield* once(killer, "close");
13
+ }
14
+ catch (_) {
15
+ // best-effort; ignore errors
16
+ }
17
+ }
18
+ export const createWin32Process = function* createWin32Process(command, options) {
19
+ let processResult = withResolvers();
20
+ // Windows-specific process spawning with different options than POSIX
21
+ let childProcess = spawnProcess(command, options.arguments || [], {
22
+ // We lose exit information and events if this is detached in windows
23
+ // and it opens a window in windows+powershell.
24
+ detached: false,
25
+ // The `shell` option is passed to `cross-spawn` to control whether a shell is used.
26
+ // On Windows, `shell: true` is necessary to run command strings, as it uses
27
+ // `cmd.exe` to parse the command and find executables in the PATH.
28
+ // Using a boolean `true` was previously disabled, causing ENOENT errors for
29
+ // commands that were not a direct path to an executable.
30
+ shell: options.shell || false,
31
+ // With stdio as pipe, windows gets stuck where neither the child nor the
32
+ // parent wants to close the stream, so we call it ourselves in the exit event.
33
+ stdio: "pipe",
34
+ // Hide the child window so that killing it will not block the parent
35
+ // with a Terminate Batch Process (Y/n)
36
+ windowsHide: true,
37
+ env: options.env,
38
+ cwd: options.cwd,
39
+ });
40
+ let { pid } = childProcess;
41
+ let io = {
42
+ stdout: yield* useReadable(childProcess.stdout),
43
+ stderr: yield* useReadable(childProcess.stderr),
44
+ stdoutDone: withResolvers(),
45
+ stderrDone: withResolvers(),
46
+ };
47
+ const stdout = createSignal();
48
+ const stderr = createSignal();
49
+ yield* spawn(function* () {
50
+ let next = yield* io.stdout.next();
51
+ while (!next.done) {
52
+ stdout.send(next.value);
53
+ next = yield* io.stdout.next();
54
+ }
55
+ stdout.close();
56
+ io.stdoutDone.resolve();
57
+ });
58
+ yield* spawn(function* () {
59
+ let next = yield* io.stderr.next();
60
+ while (!next.done) {
61
+ stderr.send(next.value);
62
+ next = yield* io.stderr.next();
63
+ }
64
+ stderr.close();
65
+ io.stderrDone.resolve();
66
+ });
67
+ yield* spawn(function* trapError() {
68
+ const [error] = yield* once(childProcess, "error");
69
+ processResult.resolve(Err(error));
70
+ });
71
+ let stdin = {
72
+ send(data) {
73
+ childProcess.stdin.write(data);
74
+ },
75
+ };
76
+ yield* spawn(function* () {
77
+ try {
78
+ let value = yield* once(childProcess, "close");
79
+ yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
80
+ processResult.resolve(Ok(value));
81
+ }
82
+ finally {
83
+ try {
84
+ // Only try to kill the process if it hasn't exited yet
85
+ if (childProcess.exitCode === null &&
86
+ childProcess.signalCode === null) {
87
+ if (typeof childProcess.pid === "undefined") {
88
+ // deno-lint-ignore no-unsafe-finally
89
+ throw new Error("no pid for childProcess");
90
+ }
91
+ let stdinStream = childProcess.stdin;
92
+ // Try graceful shutdown with ctrlc
93
+ try {
94
+ ctrlc(childProcess.pid);
95
+ if (stdinStream.writable) {
96
+ try {
97
+ // Terminate batch process (Y/N)
98
+ stdinStream.write("Y\n");
99
+ }
100
+ catch (_err) {
101
+ // not much we can do here
102
+ }
103
+ }
104
+ }
105
+ catch (_err) {
106
+ // ctrlc might fail
107
+ }
108
+ // Close stdin to allow process to exit cleanly
109
+ try {
110
+ stdinStream.end();
111
+ }
112
+ catch (_err) {
113
+ // stdin might already be closed
114
+ }
115
+ // Wait for graceful exit with a timeout
116
+ yield* race([processResult.operation, sleep(300)]);
117
+ // If process still hasn't exited, escalate
118
+ if (childProcess.exitCode === null &&
119
+ childProcess.signalCode === null) {
120
+ // Try regular kill first
121
+ try {
122
+ childProcess.kill();
123
+ }
124
+ catch (_err) {
125
+ // process might already be dead
126
+ }
127
+ // If still alive after kill, force-kill entire process tree
128
+ // This is necessary for bash on Windows where ctrlc doesn't work
129
+ // and child.kill() only kills the shell, leaving grandchildren alive
130
+ if (childProcess.exitCode === null &&
131
+ childProcess.signalCode === null) {
132
+ yield* killTree(childProcess.pid);
133
+ }
134
+ }
135
+ // Wait for streams to finish
136
+ yield* all([io.stdoutDone.operation, io.stderrDone.operation]);
137
+ }
138
+ }
139
+ catch (_e) {
140
+ // do nothing, process is probably already dead
141
+ }
142
+ }
143
+ });
144
+ function* join() {
145
+ let result = yield* processResult.operation;
146
+ if (result.ok) {
147
+ let [code, signal] = result.value;
148
+ return { command, options, code, signal };
149
+ }
150
+ else {
151
+ throw result.error;
152
+ }
153
+ }
154
+ function* expect() {
155
+ let status = yield* join();
156
+ if (status.code != 0) {
157
+ throw new ExecError(status, command, options);
158
+ }
159
+ else {
160
+ return status;
161
+ }
162
+ }
163
+ // FYI: this function starts a process and returns without blocking
164
+ return {
165
+ pid: pid,
166
+ stdin,
167
+ stdout,
168
+ stderr,
169
+ join,
170
+ expect,
171
+ };
172
+ };
173
+ export const isWin32 = () => platform() === "win32";
@@ -0,0 +1,16 @@
1
+ import { type Operation } from "effection";
2
+ import type { ExecOptions, Process, ProcessResult } from "./exec/api.js";
3
+ export * from "./exec/api.js";
4
+ export * from "./exec/error.js";
5
+ export interface Exec extends Operation<Process> {
6
+ join(): Operation<ProcessResult>;
7
+ expect(): Operation<ProcessResult>;
8
+ }
9
+ /**
10
+ * Execute `command` with `options`. You should use this operation for processes
11
+ * that have a finite lifetime and on which you may wish to synchronize on the
12
+ * exit status. If you want to start a process like a server that spins up and runs
13
+ * forever, consider using `daemon()`
14
+ */
15
+ export declare function exec(command: string, options?: ExecOptions): Exec;
16
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/src/exec.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAS,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAEV,WAAW,EAEX,OAAO,EACP,aAAa,EACd,MAAM,eAAe,CAAC;AAIvB,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAEhC,MAAM,WAAW,IAAK,SAAQ,SAAS,CAAC,OAAO,CAAC;IAC9C,IAAI,IAAI,SAAS,CAAC,aAAa,CAAC,CAAC;IACjC,MAAM,IAAI,SAAS,CAAC,aAAa,CAAC,CAAC;CACpC;AAUD;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,IAAI,CAiErE"}