chati-dev 4.5.28 → 4.5.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/* Minimal synchronous Node-API boundary for descriptor-relative POSIX I/O.
|
|
2
|
+
* No JS execution, subprocesses, path resolution or implicit global cwd changes.
|
|
3
|
+
* Each name is one component and each directory is an explicit open descriptor.
|
|
4
|
+
*/
|
|
5
|
+
#define _GNU_SOURCE
|
|
6
|
+
#include <node_api.h>
|
|
7
|
+
#include <dirent.h>
|
|
8
|
+
#include <errno.h>
|
|
9
|
+
#include <fcntl.h>
|
|
10
|
+
#include <limits.h>
|
|
11
|
+
#include <math.h>
|
|
12
|
+
#include <stdint.h>
|
|
13
|
+
#include <stdio.h>
|
|
14
|
+
#include <string.h>
|
|
15
|
+
#include <sys/stat.h>
|
|
16
|
+
#include <sys/file.h>
|
|
17
|
+
#include <unistd.h>
|
|
18
|
+
#ifdef __APPLE__
|
|
19
|
+
#include <sys/sysctl.h>
|
|
20
|
+
#include <uuid/uuid.h>
|
|
21
|
+
#include <time.h>
|
|
22
|
+
#endif
|
|
23
|
+
#ifdef __linux__
|
|
24
|
+
#include <sys/syscall.h>
|
|
25
|
+
#include <linux/fs.h>
|
|
26
|
+
#endif
|
|
27
|
+
|
|
28
|
+
#define CHECK(call) do { if ((call) != napi_ok) { \
|
|
29
|
+
napi_throw_error(env, "NATIVE_STORAGE_API_ERROR", "Node-API call failed"); return NULL; \
|
|
30
|
+
} } while (0)
|
|
31
|
+
|
|
32
|
+
static napi_value failure(napi_env env, const char *operation, int error) {
|
|
33
|
+
const char *code = "EIO";
|
|
34
|
+
#define ERROR_NAME(value) case value: code = #value; break
|
|
35
|
+
switch (error) {
|
|
36
|
+
ERROR_NAME(EACCES); ERROR_NAME(EBADF); ERROR_NAME(EEXIST); ERROR_NAME(EFAULT);
|
|
37
|
+
ERROR_NAME(EFBIG); ERROR_NAME(EINTR); ERROR_NAME(EINVAL); ERROR_NAME(EIO);
|
|
38
|
+
ERROR_NAME(EISDIR); ERROR_NAME(ELOOP); ERROR_NAME(EMFILE); ERROR_NAME(ENAMETOOLONG);
|
|
39
|
+
ERROR_NAME(ENFILE); ERROR_NAME(ENOENT); ERROR_NAME(ENOMEM); ERROR_NAME(ENOSPC);
|
|
40
|
+
ERROR_NAME(ENOTDIR); ERROR_NAME(ENOTEMPTY); ERROR_NAME(ENXIO); ERROR_NAME(EPERM);
|
|
41
|
+
ERROR_NAME(EROFS); ERROR_NAME(EXDEV); ERROR_NAME(EOVERFLOW);
|
|
42
|
+
ERROR_NAME(ESRCH); ERROR_NAME(ENOSYS); ERROR_NAME(ETIMEDOUT); ERROR_NAME(EADDRINUSE);
|
|
43
|
+
default: break;
|
|
44
|
+
}
|
|
45
|
+
#undef ERROR_NAME
|
|
46
|
+
napi_value message, error_code, exception, number;
|
|
47
|
+
char text[256];
|
|
48
|
+
snprintf(text, sizeof(text), "%s: %s", operation, strerror(error));
|
|
49
|
+
CHECK(napi_create_string_utf8(env, text, NAPI_AUTO_LENGTH, &message));
|
|
50
|
+
CHECK(napi_create_string_utf8(env, code, NAPI_AUTO_LENGTH, &error_code));
|
|
51
|
+
CHECK(napi_create_error(env, error_code, message, &exception));
|
|
52
|
+
CHECK(napi_create_int32(env, error, &number));
|
|
53
|
+
CHECK(napi_set_named_property(env, exception, "errno", number));
|
|
54
|
+
CHECK(napi_throw(env, exception));
|
|
55
|
+
return NULL;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static int integer(napi_env env, napi_value value, int *result) {
|
|
59
|
+
double number;
|
|
60
|
+
if (napi_get_value_double(env, value, &number) != napi_ok || !isfinite(number)
|
|
61
|
+
|| number < 0 || number > INT_MAX || floor(number) != number) {
|
|
62
|
+
napi_throw_type_error(env, "INVALID_STORAGE_ARGUMENT", "expected a nonnegative signed integer");
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
*result = (int)number;
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static int component(napi_env env, napi_value value, char *name) {
|
|
70
|
+
size_t length, written;
|
|
71
|
+
if (napi_get_value_string_utf8(env, value, NULL, 0, &length) != napi_ok
|
|
72
|
+
|| length == 0 || length > NAME_MAX
|
|
73
|
+
|| napi_get_value_string_utf8(env, value, name, NAME_MAX + 1, &written) != napi_ok
|
|
74
|
+
|| written != length || strlen(name) != length
|
|
75
|
+
|| strchr(name, '/') || strchr(name, '\\') || !strcmp(name, ".") || !strcmp(name, "..")) {
|
|
76
|
+
napi_throw_type_error(env, "INVALID_STORAGE_NAME", "expected one bounded path component");
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static int arguments(napi_env env, napi_callback_info info, size_t required, napi_value *args) {
|
|
83
|
+
size_t count = required + 1;
|
|
84
|
+
if (napi_get_cb_info(env, info, &count, args, NULL, NULL) != napi_ok || count != required) {
|
|
85
|
+
napi_throw_type_error(env, "INVALID_STORAGE_ARGUMENT", "incorrect argument count");
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static napi_value open_at(napi_env env, napi_callback_info info) {
|
|
92
|
+
napi_value args[5], result;
|
|
93
|
+
int dir, flags, mode;
|
|
94
|
+
char name[NAME_MAX + 1];
|
|
95
|
+
if (!arguments(env, info, 4, args) || !integer(env, args[0], &dir)
|
|
96
|
+
|| !component(env, args[1], name) || !integer(env, args[2], &flags)
|
|
97
|
+
|| !integer(env, args[3], &mode)) return NULL;
|
|
98
|
+
if (mode > 0777) return failure(env, "openat mode", EINVAL);
|
|
99
|
+
int fd = openat(dir, name, flags | O_CLOEXEC | O_NOFOLLOW, (mode_t)mode);
|
|
100
|
+
if (fd < 0) return failure(env, "openat", errno);
|
|
101
|
+
if (napi_create_int32(env, fd, &result) != napi_ok) {
|
|
102
|
+
close(fd);
|
|
103
|
+
napi_throw_error(env, "NATIVE_STORAGE_API_ERROR", "could not return descriptor");
|
|
104
|
+
return NULL;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Hold until the caller closes its CLOEXEC descriptor. Never wait or unlink. */
|
|
110
|
+
static napi_value try_exclusive_file_lock(napi_env env, napi_callback_info info) {
|
|
111
|
+
napi_value args[2], result;
|
|
112
|
+
int fd;
|
|
113
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &fd)) return NULL;
|
|
114
|
+
int status = flock(fd, LOCK_EX | LOCK_NB);
|
|
115
|
+
if (status < 0 && errno != EWOULDBLOCK && errno != EAGAIN) return failure(env, "flock", errno);
|
|
116
|
+
CHECK(napi_get_boolean(env, status == 0, &result));
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static napi_value try_shared_file_lock(napi_env env, napi_callback_info info) {
|
|
121
|
+
napi_value args[2], result;
|
|
122
|
+
int fd;
|
|
123
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &fd)) return NULL;
|
|
124
|
+
int status = flock(fd, LOCK_SH | LOCK_NB);
|
|
125
|
+
if (status < 0 && errno != EWOULDBLOCK && errno != EAGAIN) return failure(env, "flock", errno);
|
|
126
|
+
CHECK(napi_get_boolean(env, status == 0, &result));
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
static napi_value duplicate_directory(napi_env env, napi_callback_info info) {
|
|
131
|
+
napi_value args[2], result;
|
|
132
|
+
int dir;
|
|
133
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &dir)) return NULL;
|
|
134
|
+
int fd = openat(dir, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
135
|
+
if (fd < 0) return failure(env, "duplicate directory", errno);
|
|
136
|
+
if (napi_create_int32(env, fd, &result) != napi_ok) {
|
|
137
|
+
close(fd);
|
|
138
|
+
napi_throw_error(env, "NATIVE_STORAGE_API_ERROR", "could not return descriptor");
|
|
139
|
+
return NULL;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static napi_value mkdir_at(napi_env env, napi_callback_info info) {
|
|
145
|
+
napi_value args[4], result;
|
|
146
|
+
int dir, mode;
|
|
147
|
+
char name[NAME_MAX + 1];
|
|
148
|
+
if (!arguments(env, info, 3, args) || !integer(env, args[0], &dir)
|
|
149
|
+
|| !component(env, args[1], name) || !integer(env, args[2], &mode)) return NULL;
|
|
150
|
+
if (mode > 0777) return failure(env, "mkdirat mode", EINVAL);
|
|
151
|
+
if (mkdirat(dir, name, (mode_t)mode) < 0) return failure(env, "mkdirat", errno);
|
|
152
|
+
CHECK(napi_get_undefined(env, &result));
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static napi_value rename_at(napi_env env, napi_callback_info info) {
|
|
157
|
+
napi_value args[5], result;
|
|
158
|
+
int source, target;
|
|
159
|
+
char from[NAME_MAX + 1], to[NAME_MAX + 1];
|
|
160
|
+
if (!arguments(env, info, 4, args) || !integer(env, args[0], &source)
|
|
161
|
+
|| !component(env, args[1], from) || !integer(env, args[2], &target)
|
|
162
|
+
|| !component(env, args[3], to)) return NULL;
|
|
163
|
+
if (renameat(source, from, target, to) < 0) return failure(env, "renameat", errno);
|
|
164
|
+
CHECK(napi_get_undefined(env, &result));
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static napi_value unlink_at(napi_env env, napi_callback_info info) {
|
|
169
|
+
napi_value args[3], result;
|
|
170
|
+
int dir;
|
|
171
|
+
char name[NAME_MAX + 1];
|
|
172
|
+
if (!arguments(env, info, 2, args) || !integer(env, args[0], &dir)
|
|
173
|
+
|| !component(env, args[1], name)) return NULL;
|
|
174
|
+
/* Never recursive, and never remove directories through this primitive. */
|
|
175
|
+
if (unlinkat(dir, name, 0) < 0) return failure(env, "unlinkat", errno);
|
|
176
|
+
CHECK(napi_get_undefined(env, &result));
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static napi_value link_at(napi_env env, napi_callback_info info) {
|
|
181
|
+
napi_value args[5], result;
|
|
182
|
+
int source, target;
|
|
183
|
+
char from[NAME_MAX + 1], to[NAME_MAX + 1];
|
|
184
|
+
if (!arguments(env, info, 4, args) || !integer(env, args[0], &source)
|
|
185
|
+
|| !component(env, args[1], from) || !integer(env, args[2], &target)
|
|
186
|
+
|| !component(env, args[3], to)) return NULL;
|
|
187
|
+
/* No AT_SYMLINK_FOLLOW and no overwrite of an existing destination. */
|
|
188
|
+
if (linkat(source, from, target, to, 0) < 0) return failure(env, "linkat", errno);
|
|
189
|
+
CHECK(napi_get_undefined(env, &result));
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
static napi_value stat_at(napi_env env, napi_callback_info info) {
|
|
194
|
+
napi_value args[3], result, value;
|
|
195
|
+
int dir;
|
|
196
|
+
char name[NAME_MAX + 1];
|
|
197
|
+
struct stat stat;
|
|
198
|
+
if (!arguments(env, info, 2, args) || !integer(env, args[0], &dir)
|
|
199
|
+
|| !component(env, args[1], name)) return NULL;
|
|
200
|
+
if (fstatat(dir, name, &stat, AT_SYMLINK_NOFOLLOW) < 0) return failure(env, "fstatat", errno);
|
|
201
|
+
CHECK(napi_create_object(env, &result));
|
|
202
|
+
#define UNSIGNED_FIELD(key, field) \
|
|
203
|
+
CHECK(napi_create_bigint_uint64(env, (uint64_t)(field), &value)); \
|
|
204
|
+
CHECK(napi_set_named_property(env, result, key, value))
|
|
205
|
+
UNSIGNED_FIELD("dev", stat.st_dev); UNSIGNED_FIELD("ino", stat.st_ino);
|
|
206
|
+
UNSIGNED_FIELD("mode", stat.st_mode); UNSIGNED_FIELD("uid", stat.st_uid);
|
|
207
|
+
UNSIGNED_FIELD("gid", stat.st_gid); UNSIGNED_FIELD("nlink", stat.st_nlink);
|
|
208
|
+
UNSIGNED_FIELD("rdev", stat.st_rdev); UNSIGNED_FIELD("size", stat.st_size);
|
|
209
|
+
UNSIGNED_FIELD("blksize", stat.st_blksize); UNSIGNED_FIELD("blocks", stat.st_blocks);
|
|
210
|
+
#undef UNSIGNED_FIELD
|
|
211
|
+
#ifdef __APPLE__
|
|
212
|
+
struct timespec atime = stat.st_atimespec, mtime = stat.st_mtimespec, ctime = stat.st_ctimespec;
|
|
213
|
+
#else
|
|
214
|
+
struct timespec atime = stat.st_atim, mtime = stat.st_mtim, ctime = stat.st_ctim;
|
|
215
|
+
#endif
|
|
216
|
+
#define TIME_FIELD(key, field) \
|
|
217
|
+
CHECK(napi_create_bigint_int64(env, (int64_t)field.tv_sec * 1000000000LL + field.tv_nsec, &value)); \
|
|
218
|
+
CHECK(napi_set_named_property(env, result, key, value))
|
|
219
|
+
TIME_FIELD("atimeNs", atime); TIME_FIELD("mtimeNs", mtime); TIME_FIELD("ctimeNs", ctime);
|
|
220
|
+
#undef TIME_FIELD
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static napi_value directory_path(napi_env env, napi_callback_info info) {
|
|
225
|
+
napi_value args[2], result;
|
|
226
|
+
int dir;
|
|
227
|
+
struct stat stat;
|
|
228
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &dir)) return NULL;
|
|
229
|
+
if (fstat(dir, &stat) < 0) return failure(env, "directory path", errno);
|
|
230
|
+
if (!S_ISDIR(stat.st_mode)) return failure(env, "directory path", ENOTDIR);
|
|
231
|
+
char path[PATH_MAX + 1];
|
|
232
|
+
#ifdef __APPLE__
|
|
233
|
+
if (fcntl(dir, F_GETPATH, path) < 0) return failure(env, "F_GETPATH", errno);
|
|
234
|
+
#else
|
|
235
|
+
char reference[64];
|
|
236
|
+
snprintf(reference, sizeof(reference), "/proc/self/fd/%d", dir);
|
|
237
|
+
ssize_t length = readlink(reference, path, PATH_MAX);
|
|
238
|
+
if (length < 0) return failure(env, "readlink directory", errno);
|
|
239
|
+
if (length >= PATH_MAX) return failure(env, "readlink directory", ENAMETOOLONG);
|
|
240
|
+
path[length] = '\0';
|
|
241
|
+
#endif
|
|
242
|
+
CHECK(napi_create_string_utf8(env, path, NAPI_AUTO_LENGTH, &result));
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
static napi_value read_directory_impl(napi_env env, napi_callback_info info, int as_bytes) {
|
|
247
|
+
napi_value args[2], result, value;
|
|
248
|
+
int dir;
|
|
249
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &dir)) return NULL;
|
|
250
|
+
CHECK(napi_create_array(env, &result));
|
|
251
|
+
/* Open a fresh description: dup() alone would share the directory offset. */
|
|
252
|
+
int fd = openat(dir, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
|
|
253
|
+
if (fd < 0) return failure(env, "open directory for listing", errno);
|
|
254
|
+
DIR *stream = fdopendir(fd);
|
|
255
|
+
if (!stream) {
|
|
256
|
+
int error = errno;
|
|
257
|
+
close(fd);
|
|
258
|
+
return failure(env, "fdopendir", error);
|
|
259
|
+
}
|
|
260
|
+
uint32_t index = 0;
|
|
261
|
+
for (;;) {
|
|
262
|
+
errno = 0;
|
|
263
|
+
struct dirent *entry = readdir(stream);
|
|
264
|
+
if (!entry) {
|
|
265
|
+
int error = errno;
|
|
266
|
+
int closed = closedir(stream);
|
|
267
|
+
if (error) return failure(env, "readdir", error);
|
|
268
|
+
if (closed < 0) return failure(env, "closedir", errno);
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
|
|
272
|
+
if (index == UINT32_MAX) { closedir(stream); return failure(env, "readdir", EOVERFLOW); }
|
|
273
|
+
napi_status created = as_bytes
|
|
274
|
+
? napi_create_buffer_copy(env, strlen(entry->d_name), entry->d_name, NULL, &value)
|
|
275
|
+
: napi_create_string_utf8(env, entry->d_name, NAPI_AUTO_LENGTH, &value);
|
|
276
|
+
if (created != napi_ok
|
|
277
|
+
|| napi_set_element(env, result, index++, value) != napi_ok) {
|
|
278
|
+
closedir(stream);
|
|
279
|
+
napi_throw_error(env, "NATIVE_STORAGE_API_ERROR", "could not return directory entry");
|
|
280
|
+
return NULL;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
static napi_value read_directory(napi_env env, napi_callback_info info) {
|
|
286
|
+
return read_directory_impl(env, info, 0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
static napi_value read_directory_bytes(napi_env env, napi_callback_info info) {
|
|
290
|
+
return read_directory_impl(env, info, 1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#include "process-info-pipe.c"
|
|
294
|
+
#ifdef __linux__
|
|
295
|
+
#include "linux-process.c"
|
|
296
|
+
#endif
|
|
297
|
+
|
|
298
|
+
static napi_value readlink_at(napi_env env, napi_callback_info info) {
|
|
299
|
+
napi_value args[3], result;
|
|
300
|
+
int dir;
|
|
301
|
+
char name[NAME_MAX + 1], target[PATH_MAX + 1];
|
|
302
|
+
if (!arguments(env, info, 2, args) || !integer(env, args[0], &dir)
|
|
303
|
+
|| !component(env, args[1], name)) return NULL;
|
|
304
|
+
ssize_t size = readlinkat(dir, name, target, sizeof(target));
|
|
305
|
+
if (size < 0) return failure(env, "readlinkat", errno);
|
|
306
|
+
if (size >= (ssize_t)sizeof(target)) return failure(env, "readlinkat", ENAMETOOLONG);
|
|
307
|
+
CHECK(napi_create_buffer_copy(env, (size_t)size, target, NULL, &result));
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static napi_value rename_no_replace_at(napi_env env, napi_callback_info info) {
|
|
312
|
+
napi_value args[5], result;
|
|
313
|
+
int from, to, status;
|
|
314
|
+
char old_name[NAME_MAX + 1], new_name[NAME_MAX + 1];
|
|
315
|
+
if (!arguments(env, info, 4, args) || !integer(env, args[0], &from)
|
|
316
|
+
|| !component(env, args[1], old_name) || !integer(env, args[2], &to)
|
|
317
|
+
|| !component(env, args[3], new_name)) return NULL;
|
|
318
|
+
#if defined(__linux__) && defined(SYS_renameat2)
|
|
319
|
+
status = (int)syscall(SYS_renameat2, from, old_name, to, new_name, RENAME_NOREPLACE);
|
|
320
|
+
#elif defined(__APPLE__)
|
|
321
|
+
status = renameatx_np(from, old_name, to, new_name, RENAME_EXCL);
|
|
322
|
+
#else
|
|
323
|
+
return failure(env, "no-replace rename unavailable", ENOSYS);
|
|
324
|
+
#endif
|
|
325
|
+
if (status < 0) return failure(env, "no-replace rename", errno);
|
|
326
|
+
CHECK(napi_get_undefined(env, &result));
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
static napi_value symlink_at(napi_env env, napi_callback_info info) {
|
|
331
|
+
napi_value args[4], result;
|
|
332
|
+
int dir;
|
|
333
|
+
char name[NAME_MAX + 1], target[PATH_MAX + 1];
|
|
334
|
+
void *data; size_t length; bool is_buffer;
|
|
335
|
+
if (!arguments(env, info, 3, args) || !integer(env, args[1], &dir)
|
|
336
|
+
|| !component(env, args[2], name)) return NULL;
|
|
337
|
+
if (napi_is_buffer(env, args[0], &is_buffer) != napi_ok || !is_buffer
|
|
338
|
+
|| napi_get_buffer_info(env, args[0], &data, &length) != napi_ok
|
|
339
|
+
|| length == 0 || length > PATH_MAX || memchr(data, 0, length)) {
|
|
340
|
+
napi_throw_type_error(env, "INVALID_STORAGE_ARGUMENT", "symlink target must be bounded nonempty bytes without NUL");
|
|
341
|
+
return NULL;
|
|
342
|
+
}
|
|
343
|
+
memcpy(target, data, length); target[length] = 0;
|
|
344
|
+
if (symlinkat(target, dir, name) < 0) return failure(env, "symlinkat", errno);
|
|
345
|
+
CHECK(napi_get_undefined(env, &result));
|
|
346
|
+
return result;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#ifdef __APPLE__
|
|
350
|
+
#include "darwin-browser-listener.c"
|
|
351
|
+
/* Both values come from the running kernel. Never fall back to boot volume UUID,
|
|
352
|
+
* wall-clock boot time, hostname or an environment-provided machine identifier. */
|
|
353
|
+
static napi_value darwin_host_boot(napi_env env, napi_callback_info info) {
|
|
354
|
+
napi_value args[1], result, host_value, boot_value;
|
|
355
|
+
if (!arguments(env, info, 0, args)) return NULL;
|
|
356
|
+
uuid_t host, boot;
|
|
357
|
+
char host_text[37], boot_text[37];
|
|
358
|
+
size_t size = sizeof(boot_text);
|
|
359
|
+
struct timespec timeout = { .tv_sec = 1, .tv_nsec = 0 };
|
|
360
|
+
if (gethostuuid(host, &timeout) < 0) return failure(env, "gethostuuid", errno);
|
|
361
|
+
if (uuid_is_null(host)) return failure(env, "empty host UUID", EIO);
|
|
362
|
+
if (sysctlbyname("kern.bootsessionuuid", boot_text, &size, NULL, 0) < 0) {
|
|
363
|
+
return failure(env, "kernel boot-session UUID", errno);
|
|
364
|
+
}
|
|
365
|
+
if (size != sizeof(boot_text) || boot_text[36] != 0 || strlen(boot_text) != 36
|
|
366
|
+
|| uuid_parse(boot_text, boot) != 0 || uuid_is_null(boot)) {
|
|
367
|
+
return failure(env, "invalid kernel boot-session UUID", EIO);
|
|
368
|
+
}
|
|
369
|
+
uuid_unparse_lower(host, host_text);
|
|
370
|
+
uuid_unparse_lower(boot, boot_text);
|
|
371
|
+
CHECK(napi_create_object(env, &result));
|
|
372
|
+
CHECK(napi_create_string_utf8(env, host_text, 36, &host_value));
|
|
373
|
+
CHECK(napi_create_string_utf8(env, boot_text, 36, &boot_value));
|
|
374
|
+
CHECK(napi_set_named_property(env, result, "host_uuid", host_value));
|
|
375
|
+
CHECK(napi_set_named_property(env, result, "boot_session_uuid", boot_value));
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
378
|
+
#endif
|
|
379
|
+
|
|
380
|
+
static napi_value initialize(napi_env env, napi_value exports) {
|
|
381
|
+
#define METHOD(name, function) { name, NULL, function, NULL, NULL, NULL, napi_default, NULL }
|
|
382
|
+
napi_property_descriptor methods[] = {
|
|
383
|
+
METHOD("openAt", open_at), METHOD("duplicateDirectory", duplicate_directory),
|
|
384
|
+
METHOD("mkdirAt", mkdir_at), METHOD("renameAt", rename_at), METHOD("unlinkAt", unlink_at),
|
|
385
|
+
METHOD("linkAt", link_at),
|
|
386
|
+
METHOD("statAt", stat_at), METHOD("directoryPath", directory_path),
|
|
387
|
+
METHOD("readDirectory", read_directory), METHOD("readDirectoryBytes", read_directory_bytes),
|
|
388
|
+
METHOD("readlinkAt", readlink_at),
|
|
389
|
+
METHOD("renameNoReplaceAt", rename_no_replace_at), METHOD("symlinkAt", symlink_at),
|
|
390
|
+
METHOD("tryExclusiveFileLock", try_exclusive_file_lock),
|
|
391
|
+
METHOD("trySharedFileLock", try_shared_file_lock),
|
|
392
|
+
METHOD("processPipe", process_pipe), METHOD("readProcessPipe", read_process_pipe),
|
|
393
|
+
#ifdef __APPLE__
|
|
394
|
+
METHOD("darwinHostBoot", darwin_host_boot),
|
|
395
|
+
METHOD("createDarwinBrowserListener", create_darwin_browser_listener),
|
|
396
|
+
METHOD("inspectDarwinBrowserListener", inspect_darwin_browser_listener),
|
|
397
|
+
#endif
|
|
398
|
+
#ifdef __linux__
|
|
399
|
+
METHOD("openPidfd", open_pidfd), METHOD("waitPidfd", wait_pidfd), METHOD("killPidfd", kill_pidfd),
|
|
400
|
+
#endif
|
|
401
|
+
};
|
|
402
|
+
#undef METHOD
|
|
403
|
+
CHECK(napi_define_properties(env, exports, sizeof(methods) / sizeof(methods[0]), methods));
|
|
404
|
+
return exports;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
NAPI_MODULE(descriptor_fs, initialize)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* Positive-GO protocol shared by the pre-exec gate and suspended Darwin
|
|
2
|
+
* launcher. The optional owner channel is separate from the one-shot GO pipe.
|
|
3
|
+
* It is only a liveness notification, never proof of descendant termination. */
|
|
4
|
+
#ifndef CHATI_EXECUTION_ADMISSION_H
|
|
5
|
+
#define CHATI_EXECUTION_ADMISSION_H
|
|
6
|
+
#include <errno.h>
|
|
7
|
+
#include <poll.h>
|
|
8
|
+
#include <sys/stat.h>
|
|
9
|
+
#include <time.h>
|
|
10
|
+
#include <unistd.h>
|
|
11
|
+
|
|
12
|
+
static long long chati_admission_monotonic_ms(void) {
|
|
13
|
+
struct timespec now;
|
|
14
|
+
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return -1;
|
|
15
|
+
return (long long)now.tv_sec * 1000 + now.tv_nsec / 1000000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static int chati_admission_channel(int fd) {
|
|
19
|
+
struct stat state;
|
|
20
|
+
return fd >= 3 && fstat(fd, &state) == 0
|
|
21
|
+
&& (S_ISFIFO(state.st_mode) || S_ISSOCK(state.st_mode));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static const char *chati_wait_for_admission(int fd, int owner_fd, int timeout) {
|
|
25
|
+
if (!chati_admission_channel(fd)) return "channel";
|
|
26
|
+
if (owner_fd != -1 && (owner_fd == fd || !chati_admission_channel(owner_fd))) return "owner_channel";
|
|
27
|
+
if (timeout < 1 || timeout > 30000) return "timeout";
|
|
28
|
+
long long started = chati_admission_monotonic_ms();
|
|
29
|
+
if (started < 0) return "clock";
|
|
30
|
+
long long deadline = started + timeout;
|
|
31
|
+
for (;;) {
|
|
32
|
+
long long now = chati_admission_monotonic_ms();
|
|
33
|
+
if (now < 0) return "clock";
|
|
34
|
+
if (now >= deadline) return "timeout";
|
|
35
|
+
struct pollfd inputs[2] = {
|
|
36
|
+
{ .fd = fd, .events = POLLIN }, { .fd = owner_fd, .events = POLLIN }
|
|
37
|
+
};
|
|
38
|
+
int available = poll(inputs, 2, (int)(deadline - now));
|
|
39
|
+
if (available < 0 && errno == EINTR) continue;
|
|
40
|
+
if (available <= 0) return available == 0 ? "timeout" : "poll";
|
|
41
|
+
// A live owner keeps its pipe open and writes nothing, even after GO.
|
|
42
|
+
if (inputs[1].revents) return "owner_channel";
|
|
43
|
+
if (inputs[0].revents & (POLLERR | POLLNVAL)) return "poll";
|
|
44
|
+
if (!(inputs[0].revents & (POLLIN | POLLHUP))) continue;
|
|
45
|
+
char token = 0;
|
|
46
|
+
ssize_t count = read(fd, &token, 1);
|
|
47
|
+
if (count < 0 && (errno == EINTR || errno == EAGAIN)) continue;
|
|
48
|
+
if (count != 1 || token != 'G') return count == 0 ? "eof" : "token";
|
|
49
|
+
// Narrow the GO/lifetime race. The launcher's separate reaper continues
|
|
50
|
+
// observing this channel after this function returns.
|
|
51
|
+
if (owner_fd != -1) {
|
|
52
|
+
int alive;
|
|
53
|
+
do { alive = poll(&inputs[1], 1, 0); } while (alive < 0 && errno == EINTR);
|
|
54
|
+
if (alive != 0) return "owner_channel";
|
|
55
|
+
}
|
|
56
|
+
return NULL;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
#endif
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Maintainer-built pre-exec admission boundary. No provider code runs until
|
|
2
|
+
* the owning control plane sends GO through its private inherited channel.
|
|
3
|
+
* EOF, invalid input, I/O error and deadline expiration are fatal, never GO.
|
|
4
|
+
*/
|
|
5
|
+
#define _POSIX_C_SOURCE 200809L
|
|
6
|
+
#include <errno.h>
|
|
7
|
+
#include <fcntl.h>
|
|
8
|
+
#include <limits.h>
|
|
9
|
+
#include <poll.h>
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <sys/stat.h>
|
|
13
|
+
#include <time.h>
|
|
14
|
+
#include <unistd.h>
|
|
15
|
+
#include "execution-admission.h"
|
|
16
|
+
|
|
17
|
+
static int reject(const char *reason) {
|
|
18
|
+
fprintf(stderr, "CHATI_EXECUTION_GATE_REJECTED: %s\n", reason);
|
|
19
|
+
return 125;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static long integer(const char *text, long minimum, long maximum) {
|
|
23
|
+
char *end = NULL;
|
|
24
|
+
errno = 0;
|
|
25
|
+
long value = strtol(text, &end, 10);
|
|
26
|
+
if (errno || !text[0] || *end || value < minimum || value > maximum) return -1;
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
int main(int argc, char **argv) {
|
|
31
|
+
if (argc < 4 || argv[3][0] != '/') return reject("arguments");
|
|
32
|
+
long descriptor = integer(argv[1], 3, INT_MAX);
|
|
33
|
+
long timeout = integer(argv[2], 1, 30000);
|
|
34
|
+
if (descriptor < 0 || timeout < 0) return reject("arguments");
|
|
35
|
+
int fd = (int)descriptor;
|
|
36
|
+
const char *error = chati_wait_for_admission(fd, -1, (int)timeout);
|
|
37
|
+
if (error) return reject(error);
|
|
38
|
+
if (close(fd) != 0) return reject("close");
|
|
39
|
+
execv(argv[3], &argv[3]);
|
|
40
|
+
return reject("exec");
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Included by the existing Node-API boundary only on Linux. These syscalls
|
|
2
|
+
* never resolve a caller-supplied process path or signal a numeric PID. */
|
|
3
|
+
#include <signal.h>
|
|
4
|
+
#include <sys/syscall.h>
|
|
5
|
+
|
|
6
|
+
static napi_value open_pidfd(napi_env env, napi_callback_info info) {
|
|
7
|
+
napi_value args[2], result;
|
|
8
|
+
int pid;
|
|
9
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &pid)) return NULL;
|
|
10
|
+
if (pid <= 1) return failure(env, "pidfd pid", EINVAL);
|
|
11
|
+
int fd = (int)syscall(SYS_pidfd_open, pid, 0);
|
|
12
|
+
if (fd < 0) return failure(env, "pidfd_open", errno);
|
|
13
|
+
if (napi_create_int32(env, fd, &result) != napi_ok) {
|
|
14
|
+
close(fd);
|
|
15
|
+
napi_throw_error(env, "NATIVE_PROCESS_API_ERROR", "could not return pidfd");
|
|
16
|
+
return NULL;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static napi_value wait_pidfd(napi_env env, napi_callback_info info) {
|
|
22
|
+
napi_value args[3], result;
|
|
23
|
+
int fd, timeout;
|
|
24
|
+
if (!arguments(env, info, 2, args) || !integer(env, args[0], &fd)
|
|
25
|
+
|| !integer(env, args[1], &timeout)) return NULL;
|
|
26
|
+
if (timeout > 30000) return failure(env, "pidfd timeout", EINVAL);
|
|
27
|
+
int ready = process_poll(fd, timeout);
|
|
28
|
+
if (ready < 0) return failure(env, "pidfd poll", errno);
|
|
29
|
+
CHECK(napi_get_boolean(env, ready != 0, &result));
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static napi_value kill_pidfd(napi_env env, napi_callback_info info) {
|
|
34
|
+
napi_value args[2], result;
|
|
35
|
+
int fd;
|
|
36
|
+
if (!arguments(env, info, 1, args) || !integer(env, args[0], &fd)) return NULL;
|
|
37
|
+
int sent = (int)syscall(SYS_pidfd_send_signal, fd, SIGKILL, NULL, 0);
|
|
38
|
+
if (sent < 0 && errno != ESRCH) return failure(env, "pidfd_send_signal", errno);
|
|
39
|
+
CHECK(napi_get_boolean(env, sent == 0, &result));
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* Included by descriptor-fs.c on supported POSIX hosts. This bounded private
|
|
2
|
+
* transport makes no claim about the identity or lifetime of a process. */
|
|
3
|
+
#include <poll.h>
|
|
4
|
+
#include <time.h>
|
|
5
|
+
|
|
6
|
+
static long long process_monotonic_ms(void) {
|
|
7
|
+
struct timespec now;
|
|
8
|
+
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) return -1;
|
|
9
|
+
return (long long)now.tv_sec * 1000 + now.tv_nsec / 1000000;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static int process_poll(int fd, int timeout) {
|
|
13
|
+
long long started = process_monotonic_ms();
|
|
14
|
+
if (started < 0) return -1;
|
|
15
|
+
long long deadline = started + timeout;
|
|
16
|
+
for (;;) {
|
|
17
|
+
long long now = process_monotonic_ms();
|
|
18
|
+
if (now < 0) return -1;
|
|
19
|
+
int remaining = now >= deadline ? 0 : (int)(deadline - now);
|
|
20
|
+
struct pollfd input = { .fd = fd, .events = POLLIN };
|
|
21
|
+
int result = poll(&input, 1, remaining);
|
|
22
|
+
if (result < 0 && errno == EINTR) continue;
|
|
23
|
+
if (result <= 0) return result;
|
|
24
|
+
if (input.revents & POLLNVAL) { errno = EBADF; return -1; }
|
|
25
|
+
if (input.revents & POLLERR) { errno = EIO; return -1; }
|
|
26
|
+
return (input.revents & (POLLIN | POLLHUP)) ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static napi_value process_pipe(napi_env env, napi_callback_info info) {
|
|
31
|
+
napi_value args[1], result, entry;
|
|
32
|
+
int fds[2];
|
|
33
|
+
if (!arguments(env, info, 0, args)) return NULL;
|
|
34
|
+
#ifdef __linux__
|
|
35
|
+
if (pipe2(fds, O_CLOEXEC | O_NONBLOCK) < 0) return failure(env, "process pipe", errno);
|
|
36
|
+
#else
|
|
37
|
+
if (pipe(fds) < 0) return failure(env, "process pipe", errno);
|
|
38
|
+
/* macOS pipe flags are set after creation, not atomically. Launchers must
|
|
39
|
+
* still close every non-explicit descriptor in the child spawn contract. */
|
|
40
|
+
for (int index = 0; index < 2; index++) {
|
|
41
|
+
int flags = fcntl(fds[index], F_GETFL);
|
|
42
|
+
if (flags < 0 || fcntl(fds[index], F_SETFL, flags | O_NONBLOCK) < 0
|
|
43
|
+
|| fcntl(fds[index], F_SETFD, FD_CLOEXEC) < 0) {
|
|
44
|
+
int saved = errno;
|
|
45
|
+
close(fds[0]); close(fds[1]);
|
|
46
|
+
return failure(env, "process pipe flags", saved);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
#endif
|
|
50
|
+
if (napi_create_array_with_length(env, 2, &result) != napi_ok) goto failed;
|
|
51
|
+
for (uint32_t index = 0; index < 2; index++) {
|
|
52
|
+
if (napi_create_int32(env, fds[index], &entry) != napi_ok
|
|
53
|
+
|| napi_set_element(env, result, index, entry) != napi_ok) goto failed;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
failed:
|
|
57
|
+
close(fds[0]); close(fds[1]);
|
|
58
|
+
napi_throw_error(env, "NATIVE_PROCESS_API_ERROR", "could not return pipe descriptors");
|
|
59
|
+
return NULL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static napi_value read_process_pipe(napi_env env, napi_callback_info info) {
|
|
63
|
+
napi_value args[3], result;
|
|
64
|
+
int fd, timeout;
|
|
65
|
+
struct stat state;
|
|
66
|
+
if (!arguments(env, info, 2, args) || !integer(env, args[0], &fd)
|
|
67
|
+
|| !integer(env, args[1], &timeout)) return NULL;
|
|
68
|
+
if (timeout > 30000) return failure(env, "process pipe timeout", EINVAL);
|
|
69
|
+
if (fstat(fd, &state) < 0) return failure(env, "process pipe", errno);
|
|
70
|
+
if (!S_ISFIFO(state.st_mode)) return failure(env, "process pipe type", EINVAL);
|
|
71
|
+
long long started = process_monotonic_ms();
|
|
72
|
+
if (started < 0) return failure(env, "process clock", errno);
|
|
73
|
+
long long deadline = started + timeout;
|
|
74
|
+
char output[4097];
|
|
75
|
+
size_t used = 0;
|
|
76
|
+
for (;;) {
|
|
77
|
+
long long now = process_monotonic_ms();
|
|
78
|
+
if (now < 0) return failure(env, "process clock", errno);
|
|
79
|
+
int available = process_poll(fd, now >= deadline ? 0 : (int)(deadline - now));
|
|
80
|
+
if (available < 0) return failure(env, "process pipe poll", errno);
|
|
81
|
+
if (!available) return failure(env, "process pipe timeout", ETIMEDOUT);
|
|
82
|
+
ssize_t count = read(fd, output + used, sizeof(output) - used);
|
|
83
|
+
if (count < 0 && (errno == EINTR || errno == EAGAIN)) continue;
|
|
84
|
+
if (count < 0) return failure(env, "process pipe read", errno);
|
|
85
|
+
if (count == 0) break;
|
|
86
|
+
used += (size_t)count;
|
|
87
|
+
if (used > 4096) return failure(env, "process pipe size", EOVERFLOW);
|
|
88
|
+
}
|
|
89
|
+
CHECK(napi_create_string_utf8(env, output, used, &result));
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./src/index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
8
|
+
"build:native": "node scripts/build-native.js",
|
|
9
|
+
"test": "node scripts/build-native.js && node --test test/**/*.test.js"
|
|
9
10
|
},
|
|
10
11
|
"engines": {
|
|
11
12
|
"node": ">=20.0.0"
|