@sshkeda/task 0.0.0-stage → 1.0.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/package.json +41 -4
  4. package/src/index.ts +2 -0
  5. package/src/lib/errors/index.ts +15 -0
  6. package/src/lib/errors/internal/errors.ts +147 -0
  7. package/src/lib/kill-leftover-processes/index.ts +17 -0
  8. package/src/lib/kill-leftover-processes/internal/command-words.ts +16 -0
  9. package/src/lib/kill-leftover-processes/internal/find-process-ids.ts +36 -0
  10. package/src/lib/kill-leftover-processes/internal/kill-process-groups.ts +270 -0
  11. package/src/lib/kill-leftover-processes/internal/list-linux-processes-from-proc.ts +191 -0
  12. package/src/lib/kill-leftover-processes/internal/list-macos-processes-with-sysctl.ts +335 -0
  13. package/src/lib/kill-leftover-processes/internal/list-processes.ts +58 -0
  14. package/src/lib/kill-leftover-processes/internal/mark-run-processes.ts +367 -0
  15. package/src/lib/kill-leftover-processes/internal/process-cleanup-errors.ts +17 -0
  16. package/src/lib/kill-leftover-processes/internal/send-signal.ts +32 -0
  17. package/src/lib/kill-leftover-processes/internal/signal-run-processes.ts +48 -0
  18. package/src/lib/kill-leftover-processes/internal/started-process-groups.ts +107 -0
  19. package/src/lib/main-thread/index.ts +3 -0
  20. package/src/lib/main-thread/internal/listen-for-stop-requests.ts +102 -0
  21. package/src/lib/main-thread/internal/run-main-thread.ts +81 -0
  22. package/src/lib/main-thread/internal/run-root-task-fiber.ts +191 -0
  23. package/src/lib/main-thread/internal/stops-at-first-failure.ts +20 -0
  24. package/src/lib/main-thread/internal/trace-the-run.ts +107 -0
  25. package/src/lib/main-thread/lib/find-code-each-task-depends-on/index.ts +7 -0
  26. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/follow-imports-from-entry-files.ts +306 -0
  27. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/load-all-reporting-first-failure.ts +11 -0
  28. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/make-module-graph.ts +48 -0
  29. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/read-configured-loaders.ts +64 -0
  30. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/resolve-module-specifiers.ts +60 -0
  31. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/rewrite-worker-imports.ts +118 -0
  32. package/src/lib/main-thread/lib/find-code-each-task-depends-on/internal/worker-import-errors.ts +19 -0
  33. package/src/lib/main-thread/lib/run-tasks-on-workers/index.ts +7 -0
  34. package/src/lib/main-thread/lib/run-tasks-on-workers/internal/handle-worker-events.ts +348 -0
  35. package/src/lib/main-thread/lib/run-tasks-on-workers/internal/make-task-runs.ts +207 -0
  36. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/index.ts +1 -0
  37. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/await-within-counted-limit.ts +29 -0
  38. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/execute-task.ts +272 -0
  39. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/make-task-setup-clock.ts +129 -0
  40. package/src/lib/main-thread/lib/run-tasks-on-workers/lib/run-requested-task-once/internal/run-requested-tasks.ts +135 -0
  41. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/define-task-run-state.ts +288 -0
  42. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/make-stopwatch.ts +39 -0
  43. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/share-standard-input-with-root-task.ts +36 -0
  44. package/src/lib/main-thread/lib/run-tasks-on-workers/shared/stop-task.ts +166 -0
  45. package/src/lib/main-thread/lib/scan-module-source/index.ts +1 -0
  46. package/src/lib/main-thread/lib/scan-module-source/internal/find-lexical-bindings.ts +154 -0
  47. package/src/lib/main-thread/lib/scan-module-source/internal/find-task-declarations.ts +205 -0
  48. package/src/lib/main-thread/lib/scan-module-source/internal/parse-module-text.ts +34 -0
  49. package/src/lib/main-thread/lib/scan-module-source/internal/read-literal-string.ts +15 -0
  50. package/src/lib/main-thread/lib/scan-module-source/internal/read-module-loads.ts +316 -0
  51. package/src/lib/main-thread/lib/scan-module-source/internal/scan-module-text.ts +70 -0
  52. package/src/lib/main-thread/lib/scan-module-source/internal/unwrap-type-assertions.ts +10 -0
  53. package/src/lib/main-thread/lib/scan-module-source/internal/visit-non-reference-identifiers.ts +21 -0
  54. package/src/lib/main-thread/shared/fail-run.ts +36 -0
  55. package/src/lib/main-thread/shared/find-failure-reason.ts +43 -0
  56. package/src/lib/main-thread/shared/find-package-directories.ts +34 -0
  57. package/src/lib/main-thread/shared/find-tasks-reachable-from-root-task-file.ts +246 -0
  58. package/src/lib/main-thread/shared/list-directory-paths-up-to-root.ts +13 -0
  59. package/src/lib/main-thread/shared/main-thread-errors.ts +373 -0
  60. package/src/lib/main-thread/shared/package-json-errors.ts +93 -0
  61. package/src/lib/main-thread/shared/provide-discovered-tasks.ts +26 -0
  62. package/src/lib/main-thread/shared/provide-process-groups.ts +63 -0
  63. package/src/lib/main-thread/shared/provide-run-root-task-config.ts +66 -0
  64. package/src/lib/main-thread/shared/provide-stop-state.ts +150 -0
  65. package/src/lib/main-thread/shared/provide-worker-pool.ts +607 -0
  66. package/src/lib/main-thread/shared/read-bare-package-name.ts +3 -0
  67. package/src/lib/main-thread/shared/read-package-json.ts +149 -0
  68. package/src/lib/main-thread/shared/report-task-failure.ts +31 -0
  69. package/src/lib/preload-rewritten-imports-into-each-worker/index.ts +5 -0
  70. package/src/lib/preload-rewritten-imports-into-each-worker/internal/load-rewritten-imports.ts +34 -0
  71. package/src/lib/supervise-the-run/index.ts +15 -0
  72. package/src/lib/supervise-the-run/internal/adopt-orphans-on-linux.ts +73 -0
  73. package/src/lib/supervise-the-run/internal/check-root-task-file.ts +8 -0
  74. package/src/lib/supervise-the-run/internal/report-ready.ts +12 -0
  75. package/src/lib/supervise-the-run/internal/share-process-groups.ts +25 -0
  76. package/src/lib/supervise-the-run/internal/start-watching-the-supervisor.ts +6 -0
  77. package/src/lib/supervise-the-run/internal/supervise-run.ts +155 -0
  78. package/src/lib/supervise-the-run/internal/talk-to-the-supervisor.ts +53 -0
  79. package/src/lib/supervise-the-run/internal/watch-the-supervisor.ts +61 -0
  80. package/src/lib/task/index.ts +5 -0
  81. package/src/lib/task/internal/make-tasks.ts +82 -0
  82. package/src/lib/task/internal/run-command.ts +194 -0
  83. package/src/lib/task/internal/run-root-task.ts +263 -0
  84. package/src/lib/task/internal/run-task-lists.ts +51 -0
  85. package/src/lib/task/internal/trust-output-mode-names.ts +18 -0
  86. package/src/lib/task/internal/trust-task-results.ts +35 -0
  87. package/src/lib/task/internal/use-task-services.ts +4 -0
  88. package/src/lib/worker-thread/index.ts +3 -0
  89. package/src/lib/worker-thread/internal/answer-main-thread-messages.ts +141 -0
  90. package/src/lib/worker-thread/internal/describe-task-body-result.ts +91 -0
  91. package/src/lib/worker-thread/internal/name-config-key-not-set.ts +57 -0
  92. package/src/lib/worker-thread/internal/run-task-body.ts +117 -0
  93. package/src/lib/worker-thread/internal/serve-on-the-parent-port.ts +63 -0
  94. package/src/lib/worker-thread/internal/start-worker-thread.ts +41 -0
  95. package/src/lib/worker-thread/lib/provide-task-services/index.ts +1 -0
  96. package/src/lib/worker-thread/lib/provide-task-services/internal/make-task-logger.ts +34 -0
  97. package/src/lib/worker-thread/lib/provide-task-services/internal/provide-task-services.ts +86 -0
  98. package/src/lib/worker-thread/lib/provide-task-services/internal/read-standard-input-from-main-thread.ts +58 -0
  99. package/src/lib/worker-thread/lib/provide-task-services/internal/resolve-paths-from-package-directory.ts +61 -0
  100. package/src/lib/worker-thread/lib/provide-task-services/internal/run-commands-in-package-directory.ts +24 -0
  101. package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-commands-with-bun.ts +545 -0
  102. package/src/lib/worker-thread/lib/provide-task-services/internal/spawn-reported-process.ts +151 -0
  103. package/src/lib/worker-thread/shared/await-main-thread-answers.ts +76 -0
  104. package/src/lib/worker-thread/shared/decode-task-result-json.ts +114 -0
  105. package/src/lib/worker-thread/shared/define-task-body-record.ts +11 -0
  106. package/src/lib/worker-thread/shared/describe-issue-input.ts +52 -0
  107. package/src/lib/worker-thread/shared/plain-object.ts +8 -0
  108. package/src/lib/worker-thread/shared/provide-worker-state.ts +160 -0
  109. package/src/lib/worker-thread/shared/worker-thread-errors.ts +41 -0
  110. package/src/shared/attribute-reported-failures.ts +25 -0
  111. package/src/shared/claim-processes-to-report.ts +22 -0
  112. package/src/shared/command-errors.ts +44 -0
  113. package/src/shared/complete-discarded-calls-once-sent.ts +12 -0
  114. package/src/shared/cut-to-graphemes.ts +12 -0
  115. package/src/shared/environment-config-errors.ts +74 -0
  116. package/src/shared/error-fields.ts +13 -0
  117. package/src/shared/find-bun-loader-for-script.ts +32 -0
  118. package/src/shared/find-node-modules-package-directory.ts +35 -0
  119. package/src/shared/find-user-locations-in-stack-traces.ts +64 -0
  120. package/src/shared/format-error-lines.ts +53 -0
  121. package/src/shared/format-task-lines.ts +21 -0
  122. package/src/shared/hold-task-logs.ts +24 -0
  123. package/src/shared/invariant-errors.ts +28 -0
  124. package/src/shared/mark-task-worker-thread-contexts.ts +29 -0
  125. package/src/shared/module-graph-errors.ts +49 -0
  126. package/src/shared/name-config-key.ts +42 -0
  127. package/src/shared/names-from-package-json.ts +7 -0
  128. package/src/shared/print-to-standard-error.ts +48 -0
  129. package/src/shared/process-errors.ts +191 -0
  130. package/src/shared/read-environment-config.ts +239 -0
  131. package/src/shared/record-tasks-made.ts +10 -0
  132. package/src/shared/report-cause.ts +140 -0
  133. package/src/shared/report-errors.ts +121 -0
  134. package/src/shared/resolve-task-options.ts +20 -0
  135. package/src/shared/rpc.ts +161 -0
  136. package/src/shared/run-in-child-fiber-and-join.ts +16 -0
  137. package/src/shared/run-marker.ts +7 -0
  138. package/src/shared/save-standard-stream-flags.ts +57 -0
  139. package/src/shared/select-cause-reasons.ts +14 -0
  140. package/src/shared/signal-run-called-twice.ts +11 -0
  141. package/src/shared/standard-input-streams.ts +41 -0
  142. package/src/shared/stop-signals.ts +41 -0
  143. package/src/shared/task-errors.ts +224 -0
  144. package/src/shared/task-package.ts +5 -0
  145. package/src/shared/task-results.ts +55 -0
  146. package/src/shared/task-services.ts +30 -0
  147. package/src/shared/write-trace-file.ts +112 -0
@@ -0,0 +1,58 @@
1
+ import { Cause, Effect, Stdio, Stream } from "effect";
2
+ import type { Array as Arrays, Pull } from "effect";
3
+ import { systemError } from "effect/PlatformError";
4
+ import type { PlatformError } from "effect/PlatformError";
5
+
6
+ import { StandardInputRead } from "../../../../../shared/rpc.ts";
7
+ import type { StartTaskFiber } from "../../../../../shared/rpc.ts";
8
+ import { StandardInputReadOutsideRootTask } from "../../../../../shared/task-errors.ts";
9
+ import type { WorkerState } from "../../../shared/provide-worker-state.ts";
10
+
11
+ const pullOfStandardInputAnswer = (
12
+ read: typeof StandardInputRead.Type,
13
+ ): Pull.Pull<Arrays.NonEmptyReadonlyArray<Uint8Array>, PlatformError> => {
14
+ if (StandardInputRead.guards.Chunks(read)) {
15
+ return Effect.succeed(read.chunks);
16
+ }
17
+ if (StandardInputRead.guards.Ended(read)) {
18
+ return Cause.done();
19
+ }
20
+ return Effect.fail(
21
+ systemError({ _tag: "Unknown", module: "Stdio", method: "stdin", description: read.reason }),
22
+ );
23
+ };
24
+
25
+ const readNextChunksFromMainThread = (taskName: string, workerState: WorkerState["Service"]) =>
26
+ workerState.requestStandardInputRead(taskName).pipe(Effect.flatMap(pullOfStandardInputAnswer));
27
+
28
+ export const refuseStandardInputOutsideRootTask = (startMessage: typeof StartTaskFiber.Type) =>
29
+ startMessage.taskName === startMessage.standardInput.rootTaskName
30
+ ? null
31
+ : new StandardInputReadOutsideRootTask({
32
+ taskName: startMessage.taskName,
33
+ rootTaskName: startMessage.standardInput.rootTaskName,
34
+ });
35
+
36
+ const makeTaskStdio = (
37
+ startMessage: typeof StartTaskFiber.Type,
38
+ workerState: WorkerState["Service"],
39
+ ) =>
40
+ Effect.gen(function* () {
41
+ const runtimeStdio = yield* Stdio.Stdio;
42
+ const refusal = refuseStandardInputOutsideRootTask(startMessage);
43
+ return Stdio.make({
44
+ ...runtimeStdio,
45
+ stdinIsTerminal: Effect.succeed(startMessage.standardInput.isTerminal),
46
+ stdin:
47
+ refusal === null
48
+ ? Stream.fromPull(
49
+ Effect.succeed(readNextChunksFromMainThread(startMessage.taskName, workerState)),
50
+ )
51
+ : Stream.die(refusal),
52
+ });
53
+ });
54
+
55
+ export const provideTaskStdio =
56
+ (startMessage: typeof StartTaskFiber.Type, workerState: WorkerState["Service"]) =>
57
+ <Success, Failure, Services>(effect: Effect.Effect<Success, Failure, Services>) =>
58
+ Effect.provideServiceEffect(effect, Stdio.Stdio, makeTaskStdio(startMessage, workerState));
@@ -0,0 +1,61 @@
1
+ import { FileSystem, Path } from "effect";
2
+
3
+ export const resolvePathsFromPackageDirectory = (
4
+ path: Path.Path,
5
+ fileSystem: FileSystem.FileSystem,
6
+ packageDirectoryPath: string,
7
+ ) => {
8
+ const resolve = (...pathSegments: readonly string[]) =>
9
+ path.resolve(packageDirectoryPath, ...pathSegments);
10
+ const resolveTemporaryDirectory = <Options extends { readonly directory?: string | undefined }>(
11
+ temporaryOptions: Options | undefined,
12
+ ) =>
13
+ temporaryOptions?.directory === undefined
14
+ ? temporaryOptions
15
+ : { ...temporaryOptions, directory: resolve(temporaryOptions.directory) };
16
+ const packagePath = Path.Path.of({
17
+ ...path,
18
+ resolve,
19
+ relative: (fromPath, toPath) => path.relative(resolve(fromPath), resolve(toPath)),
20
+ toFileUrl: (filePath) => path.toFileUrl(resolve(filePath)),
21
+ });
22
+ const packageFileSystem = FileSystem.make({
23
+ ...fileSystem,
24
+ access: (filePath, accessOptions) => fileSystem.access(resolve(filePath), accessOptions),
25
+ copy: (fromPath, toPath, copyOptions) =>
26
+ fileSystem.copy(resolve(fromPath), resolve(toPath), copyOptions),
27
+ copyFile: (fromPath, toPath) => fileSystem.copyFile(resolve(fromPath), resolve(toPath)),
28
+ chmod: (filePath, mode) => fileSystem.chmod(resolve(filePath), mode),
29
+ chown: (filePath, userId, groupId) => fileSystem.chown(resolve(filePath), userId, groupId),
30
+ link: (fromPath, toPath) => fileSystem.link(resolve(fromPath), resolve(toPath)),
31
+ makeDirectory: (directoryPath, directoryOptions) =>
32
+ fileSystem.makeDirectory(resolve(directoryPath), directoryOptions),
33
+ makeTempDirectory: (temporaryOptions) =>
34
+ fileSystem.makeTempDirectory(resolveTemporaryDirectory(temporaryOptions)),
35
+ makeTempDirectoryScoped: (temporaryOptions) =>
36
+ fileSystem.makeTempDirectoryScoped(resolveTemporaryDirectory(temporaryOptions)),
37
+ makeTempFile: (temporaryOptions) =>
38
+ fileSystem.makeTempFile(resolveTemporaryDirectory(temporaryOptions)),
39
+ makeTempFileScoped: (temporaryOptions) =>
40
+ fileSystem.makeTempFileScoped(resolveTemporaryDirectory(temporaryOptions)),
41
+ open: (filePath, openOptions) => fileSystem.open(resolve(filePath), openOptions),
42
+ readDirectory: (directoryPath, readDirectoryOptions) =>
43
+ fileSystem.readDirectory(resolve(directoryPath), readDirectoryOptions),
44
+ readDirectoryEntries: (directoryPath) =>
45
+ fileSystem.readDirectoryEntries(resolve(directoryPath)),
46
+ readFile: (filePath) => fileSystem.readFile(resolve(filePath)),
47
+ readLink: (filePath) => fileSystem.readLink(resolve(filePath)),
48
+ realPath: (filePath) => fileSystem.realPath(resolve(filePath)),
49
+ remove: (filePath, removeOptions) => fileSystem.remove(resolve(filePath), removeOptions),
50
+ rename: (fromPath, toPath) => fileSystem.rename(resolve(fromPath), resolve(toPath)),
51
+ stat: (filePath) => fileSystem.stat(resolve(filePath)),
52
+ symlink: (targetPath, linkPath) => fileSystem.symlink(targetPath, resolve(linkPath)),
53
+ truncate: (filePath, length) => fileSystem.truncate(resolve(filePath), length),
54
+ utimes: (filePath, accessed, modified) =>
55
+ fileSystem.utimes(resolve(filePath), accessed, modified),
56
+ watch: (watchedPath, watchOptions) => fileSystem.watch(resolve(watchedPath), watchOptions),
57
+ writeFile: (filePath, contents, writeOptions) =>
58
+ fileSystem.writeFile(resolve(filePath), contents, writeOptions),
59
+ });
60
+ return { path: packagePath, fileSystem: packageFileSystem };
61
+ };
@@ -0,0 +1,24 @@
1
+ import type { Path } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
3
+
4
+ const resolveWorkingDirectories = (
5
+ path: Path.Path,
6
+ packageDirectoryPath: string,
7
+ command: ChildProcess.Command,
8
+ ): ChildProcess.Command =>
9
+ ChildProcess.isStandardCommand(command)
10
+ ? ChildProcess.setCwd(command, path.resolve(packageDirectoryPath, command.options.cwd ?? "."))
11
+ : ChildProcess.pipeTo(
12
+ resolveWorkingDirectories(path, packageDirectoryPath, command.left),
13
+ resolveWorkingDirectories(path, packageDirectoryPath, command.right),
14
+ command.options,
15
+ );
16
+
17
+ export const runCommandsInPackageDirectory = (
18
+ path: Path.Path,
19
+ packageDirectoryPath: string,
20
+ spawn: ChildProcessSpawner.ChildProcessSpawner["Service"]["spawn"],
21
+ ) =>
22
+ ChildProcessSpawner.make((command) =>
23
+ spawn(resolveWorkingDirectories(path, packageDirectoryPath, command)),
24
+ );
@@ -0,0 +1,545 @@
1
+ import {
2
+ Array as Arrays,
3
+ Cause,
4
+ Deferred,
5
+ Duration,
6
+ Effect,
7
+ Exit,
8
+ Fiber,
9
+ FileSystem,
10
+ Option,
11
+ PlatformError,
12
+ Predicate,
13
+ Record,
14
+ Redacted,
15
+ Sink,
16
+ Stream,
17
+ } from "effect";
18
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
19
+ import type { CommandLine } from "effect/process";
20
+
21
+ import {
22
+ CommandOptionsRefused,
23
+ CommandPipelineRefused,
24
+ RedactedValueWithNulRefused,
25
+ } from "../../../../../shared/command-errors.ts";
26
+ import type { CommandRefusal } from "../../../../../shared/command-errors.ts";
27
+ import { formatFailureLines } from "../../../../../shared/format-error-lines.ts";
28
+ import { TASK_TRACE_LABEL_PREFIX } from "../../../../../shared/names-from-package-json.ts";
29
+ import {
30
+ environmentEveryCommandSees,
31
+ RUN_MARKERS_VARIABLE_NAME,
32
+ } from "../../../../../shared/read-environment-config.ts";
33
+ import { CauseWithoutFailure } from "../../../../../shared/report-errors.ts";
34
+ import { failuresOfCause } from "../../../../../shared/select-cause-reasons.ts";
35
+ import {
36
+ makeStandardInputRunner,
37
+ markStandardInputClosedByCommand,
38
+ } from "../../../../../shared/standard-input-streams.ts";
39
+ import type { StandardInputReadOutsideRootTask } from "../../../../../shared/task-errors.ts";
40
+ import { TASK_PACKAGE } from "../../../../../shared/task-package.ts";
41
+ import {
42
+ findStartedGroupWitnessNow,
43
+ processGroupHasOnlyEndedMembers,
44
+ readLatestMemberStartTime,
45
+ } from "../../../../kill-leftover-processes/index.ts";
46
+ import { WorkerState } from "../../../shared/provide-worker-state.ts";
47
+ import { ProcessGroupNotSignaledByWorker } from "../../../shared/worker-thread-errors.ts";
48
+
49
+ const MODULE = "ChildProcess";
50
+
51
+ const OUTPUT_NAMES = ["stdout", "stderr"] as const;
52
+
53
+ export const formatProgramNames = (command: ChildProcess.Command): string =>
54
+ ChildProcess.isStandardCommand(command)
55
+ ? command.command.slice(command.command.lastIndexOf("/") + 1)
56
+ : `${formatProgramNames(command.left)} | ${formatProgramNames(command.right)}`;
57
+
58
+ const refuseUnsupportedCommand = (unsupportedCommand: typeof CommandRefusal.Type) =>
59
+ new PlatformError.PlatformError(
60
+ new PlatformError.BadArgument({
61
+ module: MODULE,
62
+ method: "spawn",
63
+ description: unsupportedCommand.message,
64
+ cause: unsupportedCommand,
65
+ }),
66
+ );
67
+
68
+ const toPlatformError =
69
+ (method: string, command: string, step?: string) => (thrown: Cause.UnknownError) => {
70
+ const isNotFound =
71
+ Predicate.hasProperty(thrown.cause, "code") &&
72
+ (thrown.cause.code === "ENOENT" || thrown.cause.code === "ESRCH");
73
+ return PlatformError.systemError({
74
+ _tag: isNotFound ? "NotFound" : "Unknown",
75
+ module: MODULE,
76
+ method,
77
+ pathOrDescriptor: command,
78
+ description:
79
+ step === undefined ? String(thrown.cause) : `${step} failed with ${String(thrown.cause)}`,
80
+ cause: thrown.cause,
81
+ });
82
+ };
83
+
84
+ const reveal = <Plain extends string | undefined>(word: Plain | Redacted.Redacted) =>
85
+ Redacted.isRedacted(word) ? Redacted.value(word) : word;
86
+
87
+ const holdsNul = (word: CommandLine.Word | undefined) =>
88
+ Redacted.isRedacted(word) && Redacted.value(word).includes("\0");
89
+
90
+ const isBrokenPipe = (failure: PlatformError.PlatformError) =>
91
+ Predicate.hasProperty(failure.cause, "code") && failure.cause.code === "EPIPE";
92
+
93
+ const describeExit = (subprocess: Bun.Subprocess, latestMemberStartTime: string | null) => ({
94
+ code: subprocess.exitCode,
95
+ signal: subprocess.signalCode,
96
+ usage: subprocess.resourceUsage(),
97
+ latestMemberStartTime,
98
+ });
99
+
100
+ export const spawnWithBun = Effect.fn("spawnWithBun")(function* (
101
+ command: ChildProcess.Command,
102
+ standardInputRefusal: StandardInputReadOutsideRootTask | null,
103
+ packageDirectoryPath: string,
104
+ cleanupDuration: Duration.Duration,
105
+ leaderExitHandlers: {
106
+ readonly onLeaderExited: (
107
+ processId: number,
108
+ latestMemberStartTime: string | null,
109
+ ) => Effect.Effect<void>;
110
+ readonly killMembersLeftByLeader: (
111
+ processId: number,
112
+ latestMemberStartTime: string,
113
+ ) => Effect.Effect<void, { readonly message: string }>;
114
+ },
115
+ ) {
116
+ if (!ChildProcess.isStandardCommand(command)) {
117
+ return yield* refuseUnsupportedCommand(
118
+ new CommandPipelineRefused({ programNames: formatProgramNames(command) }),
119
+ );
120
+ }
121
+ const workerState = yield* WorkerState;
122
+ const programName = formatProgramNames(command);
123
+ const spawnOptions = command.options;
124
+ const readForceKillAfter = (killOptions: ChildProcess.KillOptions | undefined) =>
125
+ Duration.fromInputUnsafe(killOptions?.forceKillAfter ?? cleanupDuration);
126
+ const unsupported = [
127
+ spawnOptions.shell !== undefined && "shell",
128
+ spawnOptions.detached === false && "detached: false",
129
+ spawnOptions.additionalFds !== undefined && "additionalFds",
130
+ ...(["stdin", "stdout", "stderr"] as const).map((name) => {
131
+ const option = spawnOptions[name];
132
+ const stream = Predicate.hasProperty(option, "stream") ? option.stream : option;
133
+ return stream === "overlapped"
134
+ ? `${name}: "overlapped"`
135
+ : Sink.isSink(stream) && `${name} as a Sink`;
136
+ }),
137
+ ].filter((unsupportedOption) => Predicate.isString(unsupportedOption));
138
+ if (Arrays.isArrayNonEmpty(unsupported)) {
139
+ return yield* refuseUnsupportedCommand(
140
+ new CommandOptionsRefused({ programName, optionNames: unsupported }),
141
+ );
142
+ }
143
+
144
+ const redactedValuesWithNul = [
145
+ ...command.args.flatMap((word, index) =>
146
+ holdsNul(word) ? [`argument ${String(index + 1)}`] : [],
147
+ ),
148
+ ...Object.entries(spawnOptions.env ?? {}).flatMap(([name, value]) =>
149
+ holdsNul(value) ? [`the environment variable ${name}`] : [],
150
+ ),
151
+ ];
152
+ if (Arrays.isArrayNonEmpty(redactedValuesWithNul)) {
153
+ return yield* refuseUnsupportedCommand(
154
+ new RedactedValueWithNulRefused({ programName, valueNames: redactedValuesWithNul }),
155
+ );
156
+ }
157
+
158
+ const standardInputOption = Predicate.hasProperty(spawnOptions.stdin, "stream")
159
+ ? spawnOptions.stdin.stream
160
+ : (spawnOptions.stdin ?? "pipe");
161
+ const endStandardInputOnDone = Predicate.hasProperty(spawnOptions.stdin, "stream")
162
+ ? (spawnOptions.stdin.endOnDone ?? true)
163
+ : true;
164
+ const standardStreamOption = (name: "stderr" | "stdin" | "stdout") => {
165
+ const option = spawnOptions[name];
166
+ const stream = Predicate.hasProperty(option, "stream") ? option.stream : option;
167
+ const isPassedThrough = stream === "inherit" || stream === "ignore";
168
+ return isPassedThrough ? stream : "pipe";
169
+ };
170
+ const inheritsRefusedStandardInput =
171
+ standardStreamOption("stdin") === "inherit" && standardInputRefusal !== null;
172
+ if (inheritsRefusedStandardInput) {
173
+ return yield* Effect.die(standardInputRefusal);
174
+ }
175
+ const outputReleased = {
176
+ stdout: Deferred.makeUnsafe<void>(),
177
+ stderr: Deferred.makeUnsafe<void>(),
178
+ };
179
+ const exitedDeferred = Deferred.makeUnsafe<
180
+ ReturnType<typeof describeExit>,
181
+ PlatformError.PlatformError
182
+ >();
183
+ const listingFailureToPlatformError =
184
+ (method: string) => (listingFailure: { readonly message: string }) =>
185
+ PlatformError.systemError({
186
+ _tag: "Unknown",
187
+ module: MODULE,
188
+ method,
189
+ pathOrDescriptor: programName,
190
+ description: listingFailure.message,
191
+ cause: listingFailure,
192
+ });
193
+ const signalProcessGroup = (subprocess: Bun.Subprocess, signal: ChildProcess.Signal) =>
194
+ Effect.try(() => process.kill(-subprocess.pid, signal)).pipe(
195
+ Effect.as(true),
196
+ Effect.catchTag("UnknownError", (error) => {
197
+ const permissionDenied =
198
+ Predicate.hasProperty(error.cause, "code") && error.cause.code === "EPERM";
199
+ return permissionDenied
200
+ ? processGroupHasOnlyEndedMembers(null, subprocess.pid).pipe(
201
+ workerState.provideRuntimeServices,
202
+ Effect.mapError(listingFailureToPlatformError("kill")),
203
+ Effect.flatMap((ended) => (ended ? Effect.succeed(false) : Effect.fail(error))),
204
+ )
205
+ : Effect.fail(error);
206
+ }),
207
+ Effect.mapError((error) =>
208
+ PlatformError.isPlatformError(error) ? error : toPlatformError("kill", programName)(error),
209
+ ),
210
+ Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(false)),
211
+ );
212
+ const signalProcessGroupUnlessReused = (
213
+ subprocess: Bun.Subprocess,
214
+ signal: ChildProcess.Signal,
215
+ ) =>
216
+ Effect.gen(function* () {
217
+ if (!Deferred.isDoneUnsafe(exitedDeferred)) {
218
+ return yield* signalProcessGroup(subprocess, signal);
219
+ }
220
+ const processExit = yield* Effect.exit(Deferred.await(exitedDeferred));
221
+ const leftNoMembers =
222
+ Exit.isFailure(processExit) || processExit.value.latestMemberStartTime === null;
223
+ if (leftNoMembers) {
224
+ return false;
225
+ }
226
+ const witness = yield* findStartedGroupWitnessNow(null, subprocess.pid, {
227
+ leaderStartTime: null,
228
+ latestMemberStartTimeAtLeaderExit: processExit.value.latestMemberStartTime,
229
+ }).pipe(
230
+ workerState.provideRuntimeServices,
231
+ Effect.mapError(listingFailureToPlatformError("kill")),
232
+ );
233
+ return witness === null ? false : yield* signalProcessGroup(subprocess, signal);
234
+ });
235
+
236
+ const reportProcessCleanupFailure =
237
+ (processId: number) =>
238
+ <Success, Failure, Services>(cleanup: Effect.Effect<Success, Failure, Services>) =>
239
+ cleanup.pipe(
240
+ Effect.matchEffect({
241
+ onFailure: (error) =>
242
+ Effect.logWarning(
243
+ formatFailureLines(
244
+ new ProcessGroupNotSignaledByWorker({
245
+ processGroupId: processId,
246
+ programName,
247
+ cause: error,
248
+ }),
249
+ null,
250
+ ),
251
+ ).pipe(
252
+ Effect.annotateLogs(
253
+ `${TASK_TRACE_LABEL_PREFIX}.cause`,
254
+ Cause.pretty(Cause.die(error)),
255
+ ),
256
+ ),
257
+ onSuccess: (success) => Effect.succeed(success),
258
+ }),
259
+ );
260
+
261
+ const terminate = (
262
+ subprocess: Bun.Subprocess,
263
+ killOptions: ChildProcess.KillOptions | undefined,
264
+ ) =>
265
+ Effect.gen(function* () {
266
+ const signal = killOptions?.killSignal ?? "SIGTERM";
267
+ const forceKillAfter = readForceKillAfter(killOptions);
268
+ yield* signalProcessGroupUnlessReused(subprocess, signal);
269
+ const ended = yield* Deferred.await(exitedDeferred).pipe(
270
+ Effect.timeoutOption(forceKillAfter),
271
+ );
272
+ if (Option.isNone(ended)) {
273
+ yield* Effect.logWarning(
274
+ `${programName} did not exit within ${Duration.format(forceKillAfter)} of ${signal}, so ${TASK_PACKAGE.name} sent SIGKILL to its process group`,
275
+ );
276
+ yield* signalProcessGroupUnlessReused(subprocess, "SIGKILL");
277
+ }
278
+ yield* Deferred.await(exitedDeferred);
279
+ });
280
+
281
+ const endMembersLeftByLeader = (
282
+ subprocess: Bun.Subprocess,
283
+ latestMemberStartTimeAtLeaderExit: string,
284
+ ) =>
285
+ signalProcessGroup(subprocess, spawnOptions.killSignal ?? "SIGTERM").pipe(
286
+ Effect.andThen(
287
+ Effect.forEach(
288
+ OUTPUT_NAMES.filter((name) => subprocess[name] instanceof ReadableStream),
289
+ (name) => Deferred.await(outputReleased[name]),
290
+ { discard: true },
291
+ ).pipe(
292
+ Effect.timeoutOption(readForceKillAfter(spawnOptions)),
293
+ Effect.flatMap((released) =>
294
+ Option.isSome(released)
295
+ ? Effect.void
296
+ : leaderExitHandlers.killMembersLeftByLeader(
297
+ subprocess.pid,
298
+ latestMemberStartTimeAtLeaderExit,
299
+ ),
300
+ ),
301
+ reportProcessCleanupFailure(subprocess.pid),
302
+ workerState.forkInWorkerScope,
303
+ ),
304
+ ),
305
+ );
306
+
307
+ const describeWorkingDirectory = (workingDirectory: string) =>
308
+ workingDirectory.startsWith(`${packageDirectoryPath}/`)
309
+ ? workingDirectory.slice(packageDirectoryPath.length + 1)
310
+ : workingDirectory;
311
+ const blameMissingWorkingDirectory = (
312
+ workingDirectory: string,
313
+ spawnFailure: PlatformError.PlatformError,
314
+ ) => {
315
+ const workingDirectoryFailure = (_tag: "BadResource" | "NotFound", whatIsWrong: string) =>
316
+ PlatformError.systemError({
317
+ _tag,
318
+ module: MODULE,
319
+ method: "spawn",
320
+ pathOrDescriptor: programName,
321
+ description: `the working directory ${describeWorkingDirectory(workingDirectory)} ${whatIsWrong}`,
322
+ cause: spawnFailure.reason.cause,
323
+ });
324
+ return FileSystem.FileSystem.use((fileSystem) => fileSystem.stat(workingDirectory)).pipe(
325
+ workerState.provideRuntimeServices,
326
+ Effect.map((workingDirectoryInformation) =>
327
+ workingDirectoryInformation.type === "Directory"
328
+ ? spawnFailure
329
+ : workingDirectoryFailure("BadResource", "is not a directory"),
330
+ ),
331
+ Effect.catchReason("PlatformError", "NotFound", () =>
332
+ Effect.succeed(workingDirectoryFailure("NotFound", "doesn't exist")),
333
+ ),
334
+ Effect.flatMap(Effect.fail),
335
+ );
336
+ };
337
+
338
+ const extendsEnvironmentEveryCommandSees =
339
+ spawnOptions.extendEnv === true || spawnOptions.env === undefined;
340
+ const declaredEnvironment = extendsEnvironmentEveryCommandSees
341
+ ? { ...(yield* environmentEveryCommandSees), ...spawnOptions.env }
342
+ : spawnOptions.env;
343
+ const { runMarker } = workerState;
344
+ const environment =
345
+ runMarker === null
346
+ ? declaredEnvironment
347
+ : { ...declaredEnvironment, [RUN_MARKERS_VARIABLE_NAME]: runMarker.environmentValue };
348
+ const child = yield* Effect.acquireRelease(
349
+ Effect.try(() =>
350
+ Bun.spawn([command.command, ...command.args.map(reveal)], {
351
+ ...(spawnOptions.cwd === undefined ? {} : { cwd: spawnOptions.cwd }),
352
+ env: Record.map(environment, reveal),
353
+ stdio: [
354
+ standardStreamOption("stdin"),
355
+ standardStreamOption("stdout"),
356
+ standardStreamOption("stderr"),
357
+ ...(runMarker === null ? [] : [runMarker.fileDescriptor]),
358
+ ],
359
+ detached: true,
360
+ }),
361
+ ).pipe(
362
+ Effect.mapError(toPlatformError("spawn", programName)),
363
+ Effect.catchTag("PlatformError", (error) =>
364
+ spawnOptions.cwd === undefined
365
+ ? Effect.fail(error)
366
+ : blameMissingWorkingDirectory(spawnOptions.cwd, error),
367
+ ),
368
+ Effect.tap((subprocess) =>
369
+ Deferred.complete(
370
+ exitedDeferred,
371
+ Effect.tryPromise(() => subprocess.exited).pipe(
372
+ Effect.mapError(toPlatformError("exited", programName)),
373
+ Effect.andThen(() =>
374
+ readLatestMemberStartTime(null, subprocess.pid).pipe(
375
+ workerState.provideRuntimeServices,
376
+ Effect.mapError(listingFailureToPlatformError("exited")),
377
+ ),
378
+ ),
379
+ Effect.tap((latestMemberStartTime) =>
380
+ leaderExitHandlers.onLeaderExited(subprocess.pid, latestMemberStartTime),
381
+ ),
382
+ Effect.tap((latestMemberStartTime) =>
383
+ latestMemberStartTime === null
384
+ ? Effect.void
385
+ : endMembersLeftByLeader(subprocess, latestMemberStartTime).pipe(
386
+ reportProcessCleanupFailure(subprocess.pid),
387
+ ),
388
+ ),
389
+ Effect.flatMap((latestMemberStartTime) =>
390
+ Effect.try(() => describeExit(subprocess, latestMemberStartTime)).pipe(
391
+ Effect.mapError(toPlatformError("resourceUsage", programName)),
392
+ ),
393
+ ),
394
+ ),
395
+ ).pipe(workerState.forkInWorkerScope),
396
+ ),
397
+ ),
398
+ (subprocess) =>
399
+ Deferred.isDone(exitedDeferred).pipe(
400
+ Effect.flatMap((done) =>
401
+ done
402
+ ? signalProcessGroupUnlessReused(subprocess, spawnOptions.killSignal ?? "SIGTERM")
403
+ : terminate(subprocess, spawnOptions),
404
+ ),
405
+ reportProcessCleanupFailure(subprocess.pid),
406
+ Effect.andThen(
407
+ Effect.forEach(
408
+ [subprocess.stdout, subprocess.stderr],
409
+ (readable) => {
410
+ const isPipeWithoutReader = readable instanceof ReadableStream && !readable.locked;
411
+ return isPipeWithoutReader ? Effect.promise(() => readable.cancel()) : Effect.void;
412
+ },
413
+ { discard: true },
414
+ ),
415
+ ),
416
+ ),
417
+ );
418
+
419
+ const readOutput = (
420
+ readable: ReadableStream<Uint8Array> | number | undefined,
421
+ name: (typeof OUTPUT_NAMES)[number],
422
+ ) =>
423
+ readable instanceof ReadableStream
424
+ ? Stream.fromReadableStream({
425
+ evaluate: () => readable,
426
+ releaseLockOnEnd: true,
427
+ onError: (cause) =>
428
+ toPlatformError(`fromReadable(${name})`, programName)(new Cause.UnknownError(cause)),
429
+ }).pipe(Stream.ensuring(Deferred.done(outputReleased[name], Exit.void)))
430
+ : Stream.empty;
431
+ const standardOutput = readOutput(child.stdout, "stdout");
432
+ const standardError = readOutput(child.stderr, "stderr");
433
+
434
+ const standardInputWriter = child.stdin;
435
+ const writeStandardInput = (
436
+ method: string,
437
+ step: string,
438
+ write: () => unknown,
439
+ brokenPipeEndsTheInput: boolean,
440
+ ) =>
441
+ Effect.tryPromise(() => Promise.resolve(write())).pipe(
442
+ Effect.mapError(toPlatformError(method, programName, step)),
443
+ Effect.as(true),
444
+ Effect.catchTag("PlatformError", (failure) => {
445
+ const brokenPipeEndedTheInput = brokenPipeEndsTheInput && isBrokenPipe(failure);
446
+ return brokenPipeEndedTheInput ? Effect.succeed(false) : Effect.fail(failure);
447
+ }),
448
+ Effect.mapError((failure) =>
449
+ isBrokenPipe(failure) ? markStandardInputClosedByCommand(failure) : failure,
450
+ ),
451
+ );
452
+ const hasNoStandardInputPipe =
453
+ standardInputWriter === undefined || typeof standardInputWriter === "number";
454
+ const makeStandardInputSink = (closedByCommand: "ends the input" | "fails") =>
455
+ hasNoStandardInputPipe
456
+ ? Sink.drain
457
+ : Sink.forEachWhile((chunk: Uint8Array) =>
458
+ writeStandardInput(
459
+ "fromWritable(stdin)",
460
+ "writing standard input",
461
+ () => standardInputWriter.write(chunk),
462
+ closedByCommand === "ends the input",
463
+ ),
464
+ ).pipe(
465
+ Sink.onExit((sinkExit) =>
466
+ endStandardInputOnDone
467
+ ? writeStandardInput(
468
+ "end(stdin)",
469
+ "ending standard input",
470
+ () => standardInputWriter.end(),
471
+ closedByCommand === "ends the input" || Exit.isFailure(sinkExit),
472
+ )
473
+ : Effect.void,
474
+ ),
475
+ );
476
+ const standardInput = makeStandardInputRunner();
477
+ const standardInputFiber = Stream.isStream(standardInputOption)
478
+ ? yield* standardInput
479
+ .run(Stream.run(standardInputOption, makeStandardInputSink("ends the input")))
480
+ .pipe(
481
+ Effect.onError((cause) => {
482
+ const stoppedOnRequest =
483
+ Cause.hasInterruptsOnly(cause) && !standardInput.interruptedItself();
484
+ return stoppedOnRequest
485
+ ? Effect.void
486
+ : terminate(child, spawnOptions).pipe(reportProcessCleanupFailure(child.pid));
487
+ }),
488
+ Effect.forkScoped,
489
+ )
490
+ : undefined;
491
+ const failIfStandardInputStreamFailed =
492
+ standardInputFiber === undefined
493
+ ? Effect.void
494
+ : Fiber.interrupt(standardInputFiber).pipe(
495
+ Effect.andThen(Fiber.await(standardInputFiber)),
496
+ Effect.flatMap((standardInputExit) => {
497
+ if (Exit.isSuccess(standardInputExit)) {
498
+ return Effect.void;
499
+ }
500
+ if (!Cause.hasInterruptsOnly(standardInputExit.cause)) {
501
+ return Effect.failCause(failuresOfCause(standardInputExit.cause));
502
+ }
503
+ return standardInput.interruptedItself()
504
+ ? Effect.die(
505
+ new CauseWithoutFailure({ details: Cause.pretty(standardInputExit.cause) }),
506
+ )
507
+ : Effect.void;
508
+ }),
509
+ );
510
+
511
+ const handle = ChildProcessSpawner.makeHandle({
512
+ pid: ChildProcessSpawner.ProcessId(child.pid),
513
+ exitCode: Deferred.await(exitedDeferred).pipe(
514
+ Effect.tap(() => failIfStandardInputStreamFailed),
515
+ Effect.flatMap((processExit) =>
516
+ processExit.code === null
517
+ ? Effect.fail(
518
+ PlatformError.systemError({
519
+ _tag: "Unknown",
520
+ module: MODULE,
521
+ method: "exitCode",
522
+ pathOrDescriptor: programName,
523
+ description: `process interrupted by signal ${processExit.signal}`,
524
+ }),
525
+ )
526
+ : Effect.succeed(ChildProcessSpawner.ExitCode(processExit.code)),
527
+ ),
528
+ ),
529
+ isRunning: Effect.map(Deferred.isDone(exitedDeferred), (done) => !done),
530
+ kill: (killOptions) => terminate(child, killOptions),
531
+ stdin: makeStandardInputSink("fails"),
532
+ stdout: standardOutput,
533
+ stderr: standardError,
534
+ all: Stream.merge(standardOutput, standardError),
535
+ getInputFd: () => Sink.drain,
536
+ getOutputFd: () => Stream.empty,
537
+ unref: Effect.sync(() => {
538
+ child.unref();
539
+ return Effect.sync(() => {
540
+ child.ref();
541
+ });
542
+ }),
543
+ });
544
+ return { handle, exited: Deferred.await(exitedDeferred) };
545
+ });