@sshkeda/task 0.0.0-stage → 1.0.1

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,149 @@
1
+ import { Effect, FileSystem, Formatter, Option, Predicate, Schema, Struct } from "effect";
2
+ import type { PlatformError } from "effect";
3
+
4
+ import { dieOnInvariantFailure } from "../../../shared/invariant-errors.ts";
5
+ import { describeCauseMessage } from "../../../shared/report-errors.ts";
6
+ import {
7
+ PackageJsonFieldInvalid,
8
+ PackageJsonNameBlank,
9
+ PackageJsonNameInvalid,
10
+ PackageJsonNameNotString,
11
+ PackageJsonNotJson,
12
+ PackageJsonNotObject,
13
+ PackageJsonUnreadable,
14
+ PackageJsonWithoutName,
15
+ } from "./package-json-errors.ts";
16
+
17
+ const DependencyVersions = Schema.Record(Schema.String, Schema.String).annotate({
18
+ expected: "an object of strings",
19
+ });
20
+
21
+ const CHECKED_PACKAGE_JSON_FIELDS = Schema.Struct({
22
+ version: Schema.String.annotate({ expected: "a string" }),
23
+ dependencies: DependencyVersions,
24
+ devDependencies: DependencyVersions,
25
+ peerDependencies: DependencyVersions,
26
+ optionalDependencies: DependencyVersions,
27
+ });
28
+
29
+ const PackageJsonObject = Schema.Record(Schema.String, Schema.Unknown);
30
+
31
+ const PackageJsonFields = CHECKED_PACKAGE_JSON_FIELDS.mapFields(Struct.map(Schema.optional));
32
+
33
+ const NON_BLANK_TEXT = /\S/u;
34
+
35
+ const isPackageName = Schema.is(
36
+ Schema.String.check(Schema.isPattern(/^(?:@[^./][^/]*\/)?[^./][^/]*$/u)),
37
+ );
38
+
39
+ export const readPackageJsonText = (packageJsonFilePath: string) =>
40
+ FileSystem.FileSystem.use((fileSystem) => fileSystem.readFileString(packageJsonFilePath));
41
+
42
+ export const toPackageJsonUnreadable =
43
+ (packageJsonFilePath: string) => (cause: PlatformError.PlatformError) =>
44
+ new PackageJsonUnreadable({ filePath: packageJsonFilePath, cause });
45
+
46
+ export const parsePackageJsonObject = (packageJsonFilePath: string, packageJsonText: string) =>
47
+ Effect.gen(function* () {
48
+ const parsed = yield* Schema.decodeEffect(Schema.fromJsonString(Schema.Unknown))(
49
+ packageJsonText,
50
+ ).pipe(
51
+ Effect.catchTag("SchemaError", () =>
52
+ Effect.try({
53
+ try: () => Bun.JSONC.parse(packageJsonText),
54
+ catch: (cause) =>
55
+ new PackageJsonNotJson({
56
+ filePath: packageJsonFilePath,
57
+ parserMessage: describeCauseMessage(cause),
58
+ }),
59
+ }).pipe(
60
+ Effect.andThen(
61
+ Effect.fail(
62
+ new PackageJsonNotJson({ filePath: packageJsonFilePath, parserMessage: null }),
63
+ ),
64
+ ),
65
+ ),
66
+ ),
67
+ );
68
+ return yield* Schema.decodeUnknownEffect(PackageJsonObject)(parsed).pipe(
69
+ Effect.mapError(
70
+ () =>
71
+ new PackageJsonNotObject({
72
+ filePath: packageJsonFilePath,
73
+ valueText: Formatter.format(parsed),
74
+ }),
75
+ ),
76
+ );
77
+ });
78
+
79
+ export const decodePackageJsonName = (
80
+ packageJsonFilePath: string,
81
+ packageJsonObject: typeof PackageJsonObject.Type,
82
+ ) => {
83
+ const { name } = packageJsonObject;
84
+ if (name === undefined) {
85
+ return Effect.succeedNone;
86
+ }
87
+ if (!Predicate.isString(name)) {
88
+ return Effect.fail(
89
+ new PackageJsonNameNotString({
90
+ filePath: packageJsonFilePath,
91
+ nameText: Formatter.format(name),
92
+ }),
93
+ );
94
+ }
95
+ return NON_BLANK_TEXT.test(name)
96
+ ? Effect.succeedSome(name)
97
+ : Effect.fail(new PackageJsonNameBlank({ filePath: packageJsonFilePath, packageName: name }));
98
+ };
99
+
100
+ export const decodePackageJsonText = (packageJsonFilePath: string) => (packageJsonText: string) =>
101
+ Effect.gen(function* () {
102
+ const packageJsonObject = yield* parsePackageJsonObject(packageJsonFilePath, packageJsonText);
103
+ const name = yield* decodePackageJsonName(packageJsonFilePath, packageJsonObject);
104
+ for (const [fieldName, fieldSchema] of Object.entries(CHECKED_PACKAGE_JSON_FIELDS.fields)) {
105
+ const value = packageJsonObject[fieldName];
106
+ const isInvalidFieldValue = value !== undefined && !Schema.is(fieldSchema)(value);
107
+ if (isInvalidFieldValue) {
108
+ return yield* new PackageJsonFieldInvalid({
109
+ filePath: packageJsonFilePath,
110
+ fieldName,
111
+ valueText: Formatter.format(value),
112
+ expected: yield* Effect.fromNullishOr(
113
+ Schema.resolveAnnotations(fieldSchema)?.expected,
114
+ ).pipe(
115
+ dieOnInvariantFailure(
116
+ "package.json decoding",
117
+ "each checked field names what it expects",
118
+ ),
119
+ ),
120
+ });
121
+ }
122
+ }
123
+ const fields = yield* Schema.decodeEffect(PackageJsonFields)(packageJsonObject).pipe(
124
+ dieOnInvariantFailure("package.json decoding", "fields that passed their checks decode"),
125
+ );
126
+ return { ...fields, name: Option.getOrUndefined(name) };
127
+ });
128
+
129
+ export const readPackageJson = (packageJsonDirectoryPath: string) => {
130
+ const packageJsonFilePath = `${packageJsonDirectoryPath}/package.json`;
131
+ return readPackageJsonText(packageJsonFilePath).pipe(
132
+ Effect.mapError(toPackageJsonUnreadable(packageJsonFilePath)),
133
+ Effect.flatMap(decodePackageJsonText(packageJsonFilePath)),
134
+ Effect.flatMap(({ name, ...packageJson }) =>
135
+ Effect.gen(function* () {
136
+ if (name === undefined) {
137
+ return yield* new PackageJsonWithoutName({ filePath: packageJsonFilePath });
138
+ }
139
+ if (!isPackageName(name)) {
140
+ return yield* new PackageJsonNameInvalid({
141
+ filePath: packageJsonFilePath,
142
+ packageName: name,
143
+ });
144
+ }
145
+ return { ...packageJson, name };
146
+ }),
147
+ ),
148
+ );
149
+ };
@@ -0,0 +1,31 @@
1
+ import { Cause, Effect, Predicate } from "effect";
2
+
3
+ import { formatCauseLines } from "../../../shared/format-error-lines.ts";
4
+ import { TASK_TRACE_LABEL_PREFIX } from "../../../shared/names-from-package-json.ts";
5
+ import { printTaskLines } from "../../../shared/print-to-standard-error.ts";
6
+ import { reportCause } from "../../../shared/report-cause.ts";
7
+ import { StopState } from "./provide-stop-state.ts";
8
+
9
+ export const reportFailure = (failure: Error) => reportCause(Cause.fail(failure)).failures;
10
+
11
+ export const reportTaskFailure = Effect.fn("reportTaskFailure")(function* (
12
+ taskName: string,
13
+ cause: Parameters<StopState["Service"]["recordTaskFailureCause"]>[1],
14
+ ) {
15
+ const shutdown = yield* StopState;
16
+ yield* shutdown.recordTaskFailureCause(taskName, cause);
17
+ const errorLines = formatCauseLines(cause, taskName);
18
+ if (cause.reasons.length > 0) {
19
+ yield* printTaskLines("Error", taskName, errorLines);
20
+ }
21
+ const failures = cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error);
22
+ for (const failure of failures) {
23
+ yield* shutdown.recordPrintedFailure(failure);
24
+ }
25
+ const details = failures
26
+ .map((failure) => (Predicate.hasProperty(failure, "details") ? failure.details : undefined))
27
+ .filter((detail) => Predicate.isString(detail));
28
+ yield* Effect.logInfo(`failed: ${details.length === 0 ? errorLines : details.join("\n")}`).pipe(
29
+ Effect.annotateLogs(`${TASK_TRACE_LABEL_PREFIX}.task`, taskName),
30
+ );
31
+ }, Effect.uninterruptible);
@@ -0,0 +1,5 @@
1
+ import { Effect } from "effect";
2
+
3
+ import { loadRewrittenImports } from "./internal/load-rewritten-imports.ts";
4
+
5
+ Effect.runSync(loadRewrittenImports);
@@ -0,0 +1,34 @@
1
+ import { workerData } from "node:worker_threads";
2
+
3
+ import { Effect, Schema } from "effect";
4
+
5
+ import { RewrittenModuleUnloadable } from "../../../shared/module-graph-errors.ts";
6
+ import { WorkerData } from "../../../shared/rpc.ts";
7
+
8
+ export const loadRewrittenImports = Effect.gen(function* () {
9
+ const rewrittenFiles = new Map(
10
+ Object.entries((yield* Schema.decodeUnknownEffect(WorkerData)(workerData)).rewrittenFiles),
11
+ );
12
+
13
+ Bun.plugin({
14
+ name: "task rewritten imports",
15
+ target: "bun",
16
+ setup: (build) => {
17
+ build.onLoad(
18
+ {
19
+ filter: new RegExp(
20
+ `^(?:${[...rewrittenFiles.keys()].map((path) => RegExp.escape(path)).join("|")})$`,
21
+ "u",
22
+ ),
23
+ },
24
+ (loaded) => {
25
+ const rewrittenFile = rewrittenFiles.get(loaded.path);
26
+ if (rewrittenFile === undefined) {
27
+ throw new RewrittenModuleUnloadable({ filePath: loaded.path });
28
+ }
29
+ return rewrittenFile;
30
+ },
31
+ );
32
+ },
33
+ });
34
+ });
@@ -0,0 +1,15 @@
1
+ import { Ref } from "effect";
2
+
3
+ import { supervisorProcessId, supervisorRunMarker } from "./internal/watch-the-supervisor.ts";
4
+
5
+ export { isRootTaskFileThere } from "./internal/check-root-task-file.ts";
6
+ export { reportReady } from "./internal/report-ready.ts";
7
+ export {
8
+ processGroupsSharedWithTheSupervisor,
9
+ shareProcessGroups,
10
+ } from "./internal/share-process-groups.ts";
11
+ export { superviseRun } from "./internal/supervise-run.ts";
12
+
13
+ export const readSupervisorProcessId = Ref.get(supervisorProcessId);
14
+
15
+ export const readSupervisorRunMarker = Ref.get(supervisorRunMarker);
@@ -0,0 +1,73 @@
1
+ import { dlopen, FFIType } from "bun:ffi";
2
+
3
+ import { Effect, Stream } from "effect";
4
+
5
+ import { listenToSignals } from "../../../shared/stop-signals.ts";
6
+
7
+ const C_LIBRARY_PATH = "libc.so.6";
8
+ const PR_SET_CHILD_SUBREAPER = 36;
9
+ const PRCTL_ENABLE = 1n;
10
+ const PRCTL_UNUSED_ARGUMENT = 0n;
11
+ const P_ALL = 0;
12
+ const WNOHANG = 1;
13
+ const WEXITED = 4;
14
+ const WNOWAIT = 0x01_00_00_00;
15
+ const SIGINFO_BYTES = 128;
16
+ const SIGINFO_PROCESS_ID_OFFSET = 16;
17
+
18
+ const openCLibrary = Effect.acquireRelease(
19
+ Effect.sync(() =>
20
+ dlopen(C_LIBRARY_PATH, {
21
+ prctl: {
22
+ args: [FFIType.i32, FFIType.u64, FFIType.u64, FFIType.u64, FFIType.u64],
23
+ returns: FFIType.i32,
24
+ },
25
+ waitid: { args: [FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
26
+ waitpid: { args: [FFIType.i32, FFIType.ptr, FFIType.i32], returns: FFIType.i32 },
27
+ }),
28
+ ),
29
+ (library) =>
30
+ Effect.sync(() => {
31
+ library.close();
32
+ }),
33
+ ).pipe(Effect.map((library) => library.symbols));
34
+
35
+ const reapExitedOrphans = (
36
+ symbols: Effect.Success<typeof openCLibrary>,
37
+ runProcessId: number,
38
+ ): void => {
39
+ const information = new Uint8Array(SIGINFO_BYTES);
40
+ const peeked = symbols.waitid(P_ALL, 0, information, WEXITED | WNOHANG | WNOWAIT);
41
+ const processId = new DataView(information.buffer).getInt32(SIGINFO_PROCESS_ID_OFFSET, true);
42
+ const isExitedOrphan = peeked === 0 && processId !== 0 && processId !== runProcessId;
43
+ if (isExitedOrphan) {
44
+ symbols.waitpid(processId, null, 0);
45
+ reapExitedOrphans(symbols, runProcessId);
46
+ }
47
+ };
48
+
49
+ export const adoptOrphans = Effect.gen(function* () {
50
+ if (process.platform !== "linux") {
51
+ return () => Effect.void;
52
+ }
53
+ const symbols = yield* openCLibrary;
54
+ symbols.prctl(
55
+ PR_SET_CHILD_SUBREAPER,
56
+ PRCTL_ENABLE,
57
+ PRCTL_UNUSED_ARGUMENT,
58
+ PRCTL_UNUSED_ARGUMENT,
59
+ PRCTL_UNUSED_ARGUMENT,
60
+ );
61
+ const childSignals = yield* listenToSignals(["SIGCHLD"]);
62
+ return (runProcessId: number) =>
63
+ Stream.fromQueue(childSignals).pipe(
64
+ Stream.runForEach(() =>
65
+ Effect.sync(() => {
66
+ reapExitedOrphans(symbols, runProcessId);
67
+ }),
68
+ ),
69
+ Effect.interruptible,
70
+ Effect.forkScoped,
71
+ Effect.asVoid,
72
+ );
73
+ });
@@ -0,0 +1,8 @@
1
+ import { Effect, FileSystem } from "effect";
2
+
3
+ import { RootTaskFileNotChecked } from "../../../shared/task-errors.ts";
4
+
5
+ export const isRootTaskFileThere = FileSystem.FileSystem.pipe(
6
+ Effect.flatMap((fileSystem) => fileSystem.exists(Bun.main)),
7
+ Effect.mapError((cause) => new RootTaskFileNotChecked({ filePath: Bun.main, cause })),
8
+ );
@@ -0,0 +1,12 @@
1
+ import { Effect } from "effect";
2
+
3
+ import { completeOnceSent } from "../../../shared/complete-discarded-calls-once-sent.ts";
4
+ import { callTheSupervisor } from "./talk-to-the-supervisor.ts";
5
+
6
+ export const reportReady = Effect.scope.pipe(
7
+ Effect.flatMap((scope) =>
8
+ callTheSupervisor((supervisor) =>
9
+ supervisor.ReportReady(undefined, { discard: true }).pipe(completeOnceSent(scope)),
10
+ ),
11
+ ),
12
+ );
@@ -0,0 +1,25 @@
1
+ import { Effect } from "effect";
2
+
3
+ import type { startedProcessGroup } from "../../kill-leftover-processes/index.ts";
4
+ import { callTheSupervisor } from "./talk-to-the-supervisor.ts";
5
+
6
+ export const processGroupsSharedWithTheSupervisor = new Map<
7
+ number,
8
+ ReturnType<typeof startedProcessGroup>
9
+ >();
10
+
11
+ export const shareProcessGroups = (processGroupIds: Iterable<number>) =>
12
+ callTheSupervisor((supervisor) =>
13
+ Effect.forEach(
14
+ processGroupIds,
15
+ (processGroupId) =>
16
+ supervisor.ShareProcessGroup(
17
+ {
18
+ processGroupId,
19
+ startedGroup: processGroupsSharedWithTheSupervisor.get(processGroupId) ?? null,
20
+ },
21
+ { discard: true },
22
+ ),
23
+ { discard: true },
24
+ ),
25
+ );
@@ -0,0 +1,6 @@
1
+ import * as BunServices from "@effect/platform-bun/BunServices";
2
+ import { Effect } from "effect";
3
+
4
+ import { watchSupervisor } from "./watch-the-supervisor.ts";
5
+
6
+ Effect.runFork(watchSupervisor.pipe(Effect.provide(BunServices.layer)));
@@ -0,0 +1,155 @@
1
+ import * as BunSocketServer from "@effect/platform-bun/BunSocketServer";
2
+ import { Deferred, Duration, Effect, Exit, FileSystem, Path, Predicate, Ref, Stream } from "effect";
3
+ import type { Queue } from "effect";
4
+ import { RpcServer } from "effect/rpc";
5
+ import { SocketServer } from "effect/socket";
6
+
7
+ import { TASK_TRACE_LABEL_PREFIX } from "../../../shared/names-from-package-json.ts";
8
+ import { SupervisorSocketNotOpened } from "../../../shared/process-errors.ts";
9
+ import {
10
+ inheritedEnvironment,
11
+ inheritedRunMarkers,
12
+ RUN_MARKERS_VARIABLE_NAME,
13
+ supervisedRunCommand,
14
+ } from "../../../shared/read-environment-config.ts";
15
+ import { saveStandardStreamFlags } from "../../../shared/save-standard-stream-flags.ts";
16
+ import { listenToSignals } from "../../../shared/stop-signals.ts";
17
+ import type { STOP_SIGNALS } from "../../../shared/stop-signals.ts";
18
+ import { createRunMarker, signalRunProcesses } from "../../kill-leftover-processes/index.ts";
19
+ import type { startedProcessGroup } from "../../kill-leftover-processes/index.ts";
20
+ import { adoptOrphans } from "./adopt-orphans-on-linux.ts";
21
+ import { provideSupervisorRpcSerialization, SupervisorRpcs } from "./talk-to-the-supervisor.ts";
22
+ import { WATCH_THE_SUPERVISOR_FLAG } from "./watch-the-supervisor.ts";
23
+
24
+ const FORCED_STOP_GRACE_SECONDS = 5;
25
+
26
+ const FORCED_STOP_GRACE = Duration.seconds(FORCED_STOP_GRACE_SECONDS);
27
+
28
+ const SUPERVISOR_SOCKET_FILE_NAME = "supervisor.sock";
29
+
30
+ const serveTheRun = Effect.fn("serveTheRun")(
31
+ function* (
32
+ ready: Deferred.Deferred<void>,
33
+ startedProcessGroups: Map<number, ReturnType<typeof startedProcessGroup>>,
34
+ ) {
35
+ const fileSystem = yield* FileSystem.FileSystem;
36
+ const path = yield* Path.Path;
37
+ const socketPath = yield* fileSystem
38
+ .makeTempDirectoryScoped({ prefix: `${TASK_TRACE_LABEL_PREFIX}-supervisor-` })
39
+ .pipe(
40
+ Effect.map((socketDirectoryPath) =>
41
+ path.join(socketDirectoryPath, SUPERVISOR_SOCKET_FILE_NAME),
42
+ ),
43
+ );
44
+ const socketServer = yield* BunSocketServer.make({ path: socketPath });
45
+ const handlers = yield* SupervisorRpcs.toHandlers({
46
+ ReportReady: () => Deferred.done(ready, Exit.void).pipe(Effect.asVoid),
47
+ ShareProcessGroup: (shared) =>
48
+ Effect.sync(() => {
49
+ if (shared.startedGroup === null) {
50
+ startedProcessGroups.delete(shared.processGroupId);
51
+ } else {
52
+ startedProcessGroups.set(shared.processGroupId, shared.startedGroup);
53
+ }
54
+ }),
55
+ });
56
+ yield* RpcServer.make(SupervisorRpcs, { disableTracing: true }).pipe(
57
+ Effect.provideContext(handlers),
58
+ Effect.provideServiceEffect(RpcServer.Protocol, RpcServer.makeProtocolSocketServer),
59
+ Effect.provideService(SocketServer.SocketServer, socketServer),
60
+ provideSupervisorRpcSerialization,
61
+ Effect.interruptible,
62
+ Effect.forkScoped,
63
+ );
64
+ return socketPath;
65
+ },
66
+ Effect.mapError((cause) => new SupervisorSocketNotOpened({ cause })),
67
+ );
68
+
69
+ export const superviseRun = Effect.fn("superviseRun")(function* (
70
+ stopSignals: Queue.Dequeue<(typeof STOP_SIGNALS)[number]>,
71
+ ) {
72
+ const reapAdoptedOrphans = yield* adoptOrphans;
73
+ const exited = yield* Deferred.make<NodeJS.Signals | number>();
74
+ const ready = yield* Deferred.make<void>();
75
+ const startedProcessGroups = new Map<number, ReturnType<typeof startedProcessGroup>>();
76
+ const command = yield* supervisedRunCommand(
77
+ WATCH_THE_SUPERVISOR_FLAG,
78
+ yield* serveTheRun(ready, startedProcessGroups),
79
+ );
80
+ const environment = yield* inheritedEnvironment;
81
+ const runMarker = yield* createRunMarker(yield* inheritedRunMarkers);
82
+ const restoreStandardStreamFlags = yield* saveStandardStreamFlags;
83
+ const child = yield* Effect.sync(() =>
84
+ Bun.spawn(command, {
85
+ detached: true,
86
+ stdio: [
87
+ "inherit",
88
+ "inherit",
89
+ "inherit",
90
+ ...(runMarker === null ? [] : [runMarker.fileDescriptor]),
91
+ ],
92
+ env:
93
+ runMarker === null
94
+ ? environment
95
+ : Object.fromEntries(
96
+ Object.entries(environment).filter(([name]) => name !== RUN_MARKERS_VARIABLE_NAME),
97
+ ),
98
+ onExit: (exitedChild, exitCode) => {
99
+ Deferred.doneUnsafe(exited, Exit.succeed(exitedChild.signalCode ?? exitCode ?? 1));
100
+ },
101
+ }),
102
+ );
103
+ yield* restoreStandardStreamFlags;
104
+ yield* reapAdoptedOrphans(child.pid);
105
+ const run = {
106
+ sessionId: child.pid,
107
+ descendantsOfProcessId: process.platform === "linux" ? process.pid : null,
108
+ startedProcessGroups,
109
+ runMarker,
110
+ };
111
+ const forced = yield* Deferred.make<void>();
112
+ const stopCount = yield* Ref.make(0);
113
+ yield* Stream.fromQueue(stopSignals).pipe(
114
+ Stream.runForEach((signal) =>
115
+ Ref.updateAndGet(stopCount, (count) => count + 1).pipe(
116
+ Effect.flatMap((count) => {
117
+ const forcesTheStop = signal === "SIGHUP" || count > 1;
118
+ const forward = Effect.sync(() => {
119
+ child.kill(signal);
120
+ });
121
+ return forcesTheStop
122
+ ? forward.pipe(Effect.andThen(Deferred.done(forced, Exit.void)), Effect.asVoid)
123
+ : Deferred.await(ready).pipe(Effect.andThen(forward), Effect.forkScoped, Effect.asVoid);
124
+ }),
125
+ ),
126
+ ),
127
+ Effect.interruptible,
128
+ Effect.forkScoped,
129
+ );
130
+ const jobControlSignals = yield* listenToSignals(["SIGTSTP", "SIGCONT"]);
131
+ yield* Stream.fromQueue(jobControlSignals).pipe(
132
+ Stream.runForEach((signal) =>
133
+ signal === "SIGTSTP"
134
+ ? signalRunProcesses(run, "SIGSTOP").pipe(
135
+ Effect.andThen(Effect.sync(() => process.kill(process.pid, "SIGSTOP"))),
136
+ )
137
+ : signalRunProcesses(run, "SIGCONT"),
138
+ ),
139
+ Effect.interruptible,
140
+ Effect.forkScoped,
141
+ );
142
+ yield* Deferred.await(exited).pipe(
143
+ Effect.raceFirst(
144
+ Deferred.await(forced).pipe(
145
+ Effect.andThen(Deferred.await(exited).pipe(Effect.timeoutOption(FORCED_STOP_GRACE))),
146
+ ),
147
+ ),
148
+ );
149
+ const runSkippedItsCleanup =
150
+ (yield* Deferred.isDone(forced)) || Predicate.isString(yield* Deferred.await(exited));
151
+ if (runSkippedItsCleanup) {
152
+ yield* signalRunProcesses(run, "SIGKILL");
153
+ }
154
+ return yield* Deferred.await(exited);
155
+ });
@@ -0,0 +1,53 @@
1
+ import * as BunSocket from "@effect/platform-bun/BunSocket";
2
+ import { Deferred, Duration, Effect, Schema } from "effect";
3
+ import { Rpc, RpcClient, RpcGroup, RpcSerialization } from "effect/rpc";
4
+ import type { RpcClientError } from "effect/rpc";
5
+ import { Socket } from "effect/socket";
6
+
7
+ export const ProcessGroupShared = Schema.Struct({
8
+ processGroupId: Schema.Int,
9
+ startedGroup: Schema.NullOr(
10
+ Schema.Struct({
11
+ taskName: Schema.String,
12
+ leaderStartTime: Schema.NullOr(Schema.String),
13
+ latestMemberStartTimeAtLeaderExit: Schema.NullOr(Schema.String),
14
+ }),
15
+ ),
16
+ });
17
+
18
+ export const SupervisorRpcs = RpcGroup.make(
19
+ Rpc.make("ReportReady"),
20
+ Rpc.make("ShareProcessGroup", { payload: ProcessGroupShared }),
21
+ );
22
+
23
+ export const provideSupervisorRpcSerialization = Effect.provideService(
24
+ RpcSerialization.RpcSerialization,
25
+ RpcSerialization.ndjson,
26
+ );
27
+
28
+ export const connectToTheSupervisor = (
29
+ supervisorSocketPath: string,
30
+ onDisconnect: Effect.Effect<void>,
31
+ ) =>
32
+ RpcClient.make(SupervisorRpcs, { disableTracing: true }).pipe(
33
+ Effect.provideServiceEffect(
34
+ RpcClient.Protocol,
35
+ RpcClient.makeProtocolSocket({ pingInterval: Duration.infinity }),
36
+ ),
37
+ Effect.provideService(RpcClient.ConnectionHooks, { onConnect: Effect.void, onDisconnect }),
38
+ Effect.provideServiceEffect(Socket.Socket, BunSocket.makeNet({ path: supervisorSocketPath })),
39
+ provideSupervisorRpcSerialization,
40
+ );
41
+
42
+ export const supervisorConnectionDeferred =
43
+ Deferred.makeUnsafe<Effect.Success<ReturnType<typeof connectToTheSupervisor>>>();
44
+
45
+ export const callTheSupervisor = (
46
+ call: (
47
+ supervisor: Effect.Success<ReturnType<typeof connectToTheSupervisor>>,
48
+ ) => Effect.Effect<void, RpcClientError.RpcClientError>,
49
+ ) =>
50
+ Deferred.await(supervisorConnectionDeferred).pipe(
51
+ Effect.flatMap(call),
52
+ Effect.catchTag("RpcClientError", () => Effect.void),
53
+ );
@@ -0,0 +1,61 @@
1
+ import { Deferred, Effect, Exit, Option, Ref, Stdio, Stream } from "effect";
2
+
3
+ import { formatFailureLines } from "../../../shared/format-error-lines.ts";
4
+ import { takeSupervisorSocketPath } from "../../../shared/read-environment-config.ts";
5
+ import type { RunMarker } from "../../../shared/run-marker.ts";
6
+ import {
7
+ adoptSupervisorRunMarker,
8
+ signalRunProcesses,
9
+ } from "../../kill-leftover-processes/index.ts";
10
+ import { processGroupsSharedWithTheSupervisor } from "./share-process-groups.ts";
11
+ import { connectToTheSupervisor, supervisorConnectionDeferred } from "./talk-to-the-supervisor.ts";
12
+
13
+ export const WATCH_THE_SUPERVISOR_FLAG = `--preload=${Bun.fileURLToPath(import.meta.resolve("./start-watching-the-supervisor.ts"))}`;
14
+
15
+ export const supervisorProcessId = Ref.makeUnsafe<number | null>(null);
16
+
17
+ export const supervisorRunMarker = Ref.makeUnsafe<typeof RunMarker.Type | null>(null);
18
+
19
+ export const watchSupervisor = Effect.gen(function* () {
20
+ yield* Effect.sync(() =>
21
+ process.execArgv.splice(process.execArgv.indexOf(WATCH_THE_SUPERVISOR_FLAG), 1),
22
+ );
23
+ const supervisorSocketPath = yield* takeSupervisorSocketPath;
24
+ if (Option.isNone(supervisorSocketPath)) {
25
+ return;
26
+ }
27
+ yield* Ref.set(supervisorProcessId, process.ppid);
28
+ const stdio = yield* Stdio.Stdio;
29
+ const printFailure = (error: Parameters<typeof formatFailureLines>[0]) =>
30
+ Stream.make(`${formatFailureLines(error, null)}\n`).pipe(Stream.run(stdio.stderr()));
31
+ const runMarker = yield* adoptSupervisorRunMarker.pipe(
32
+ Effect.catchTag(["ProcRunMarkerUnreadable", "SystemLibraryNotLoaded"], (error) =>
33
+ printFailure(error).pipe(Effect.as(null)),
34
+ ),
35
+ );
36
+ yield* Ref.set(supervisorRunMarker, runMarker);
37
+ const disconnected = yield* Deferred.make<void>();
38
+ const supervisor = yield* connectToTheSupervisor(
39
+ supervisorSocketPath.value,
40
+ Deferred.done(disconnected, Exit.void).pipe(Effect.asVoid),
41
+ );
42
+ yield* Deferred.succeed(supervisorConnectionDeferred, supervisor);
43
+ yield* Deferred.await(disconnected).pipe(
44
+ Effect.andThen(
45
+ signalRunProcesses(
46
+ {
47
+ sessionId: process.pid,
48
+ descendantsOfProcessId: process.pid,
49
+ startedProcessGroups: processGroupsSharedWithTheSupervisor,
50
+ runMarker,
51
+ },
52
+ "SIGKILL",
53
+ ),
54
+ ),
55
+ Effect.matchEffect({
56
+ onFailure: (error) => printFailure(error),
57
+ onSuccess: (success) => Effect.succeed(success),
58
+ }),
59
+ Effect.ensuring(Effect.sync(() => process.kill(process.pid, "SIGKILL"))),
60
+ );
61
+ }).pipe(Effect.scoped);
@@ -0,0 +1,5 @@
1
+ export { make, Task } from "./internal/make-tasks.ts";
2
+ export { exec } from "./internal/run-command.ts";
3
+ export { main } from "./internal/run-root-task.ts";
4
+ export { all, allSettled } from "./internal/run-task-lists.ts";
5
+ export { serve } from "./internal/use-task-services.ts";