gitoriaLog in with ident

antcolony

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Main branchmain3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmremain/plugins/proc/proc.zig

23.3 KB

  1. // hl:proc — CHILD PROCESSES as instance events (creator API ruling 2026-08-17:
  2. // no eventloop plumbing in app code — `spawn()` hands back a Process the app
  3. // subscribes with `on process.line` / `on process.exit`, and `process.kill()`
  4. // is the stop button).
  5. //
  6. // Exports:
  7. // hl_proc_spawn(argv_list) → { pid, events } — events is a LOOP SOURCE
  8. // (wake_fd bell, the fetch-completions pattern)
  9. // delivering { line, stream } per output line and
  10. // a final { exit } when the child is gone
  11. // hl_proc_kill(pid) → SIGTERM the child; the exit event still arrives
  12. //
  13. // One worker thread per child reads BOTH pipes with poll(), line-buffers, and
  14. // posts events into the spawn's OWN queue — nothing global, nothing shared
  15. // between children. No shell anywhere: argv is exec'd verbatim.
  16. const std = @import("std");
  17. const api = @import("plugin_api");
  18. const linux = std.os.linux;
  19. const libc = std.c;
  20. const PthreadMutex = libc.pthread_mutex_t;
  21. fn mutexLock(m: *PthreadMutex) void {
  22. _ = libc.pthread_mutex_lock(m);
  23. }
  24. fn mutexUnlock(m: *PthreadMutex) void {
  25. _ = libc.pthread_mutex_unlock(m);
  26. }
  27. const HlValue = api.HlValue;
  28. const HlObject = api.HlObject;
  29. const HlField = api.HlField;
  30. const HlIterator = api.HlIterator;
  31. const HlString = api.HlString;
  32. var gpa = std.heap.DebugAllocator(.{ .stack_trace_frames = 0 }){};
  33. const allocator = gpa.allocator();
  34. // SCRIPT-RELATIVE program paths (the Hybriel path rule — imports, hl:fs and
  35. // now spawn all resolve against the SCRIPT, not the process cwd): the loader
  36. // installs the script dir right after dlopen.
  37. var script_dir: ?[]u8 = null;
  38. export fn hl_proc_set_script_dir(ptr: [*]const u8, len: usize) callconv(.c) void {
  39. if (script_dir) |old| allocator.free(old);
  40. script_dir = allocator.dupe(u8, ptr[0..len]) catch null;
  41. }
  42. const c = struct {
  43. extern "c" fn fork() c_int;
  44. extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
  45. extern "c" fn pipe(fds: *[2]c_int) c_int;
  46. extern "c" fn close(fd: c_int) c_int;
  47. extern "c" fn dup2(old: c_int, new: c_int) c_int;
  48. extern "c" fn kill(pid: c_int, sig: c_int) c_int;
  49. extern "c" fn waitpid(pid: c_int, status: ?*c_int, options: c_int) c_int;
  50. extern "c" fn _exit(code: c_int) noreturn;
  51. extern "c" fn eventfd(initval: c_uint, flags: c_int) c_int;
  52. extern "c" fn fcntl(fd: c_int, cmd: c_int, arg: c_int) c_int;
  53. extern "c" fn strerror(errnum: c_int) [*:0]const u8;
  54. extern "c" fn write(fd: c_int, buf: [*]const u8, n: usize) isize;
  55. extern "c" fn read(fd: c_int, buf: [*]u8, n: usize) isize;
  56. extern "c" fn poll(fds: [*]PollFd, n: c_ulong, timeout: c_int) c_int;
  57. const PollFd = extern struct { fd: c_int, events: c_short, revents: c_short };
  58. };
  59. const POLLIN: c_short = 0x001;
  60. const EFD_NONBLOCK: c_int = 0o4000;
  61. fn hlStr(s: []const u8) HlString {
  62. return .{ .ptr = s.ptr, .len = s.len };
  63. }
  64. // ── one spawned child ────────────────────────────────────────────────────────
  65. const Event = struct {
  66. line: ?[]u8 = null, // owned
  67. stream: []const u8 = "", // "out" | "err"
  68. exit_code: ?i32 = null,
  69. /// exec NEVER HAPPENED: the errno text. The child has no exit — this is
  70. /// the ONLY event, delivered as an hl_error so the loop routes it through
  71. /// `on x.Error` → `on Error` → located crash (creator ruling 2026-08-17).
  72. spawn_err: ?[]u8 = null,
  73. };
  74. const Child = struct {
  75. mutex: PthreadMutex = libc.PTHREAD_MUTEX_INITIALIZER,
  76. queue: std.ArrayListUnmanaged(Event) = .empty,
  77. wake_fd: i32 = -1,
  78. pid: i32 = 0,
  79. out_fd: i32 = -1,
  80. err_fd: i32 = -1,
  81. exec_fd: i32 = -1,
  82. argv0: []const u8 = "",
  83. thread: ?std.Thread = null,
  84. /// the iterator was closed by the loop — the worker frees the Child when done
  85. done: bool = false,
  86. fn post(self: *Child, ev: Event) void {
  87. mutexLock(&self.mutex);
  88. self.queue.append(allocator, ev) catch {};
  89. mutexUnlock(&self.mutex);
  90. const one: u64 = 1;
  91. _ = c.write(self.wake_fd, @ptrCast(&one), 8);
  92. }
  93. };
  94. fn workerMain(ch: *Child) void {
  95. // the exec verdict first: BYTES on the CLOEXEC pipe = exec never happened
  96. {
  97. var eno: i32 = 0;
  98. const got = c.read(ch.exec_fd, @ptrCast(&eno), 4);
  99. _ = c.close(ch.exec_fd);
  100. if (got == 4) {
  101. var status: c_int = 0;
  102. _ = c.waitpid(ch.pid, &status, 0); // reap the stillborn child
  103. _ = c.close(ch.out_fd);
  104. _ = c.close(ch.err_fd);
  105. const msg = std.fmt.allocPrint(allocator, "hl:proc spawn: cannot start '{s}': {s}", .{ ch.argv0, std.mem.span(c.strerror(eno)) }) catch null;
  106. ch.post(.{ .spawn_err = msg orelse @constCast("hl:proc spawn failed") });
  107. return;
  108. }
  109. }
  110. var bufs = [2]std.ArrayListUnmanaged(u8){ .empty, .empty };
  111. defer for (&bufs) |*b| b.deinit(allocator);
  112. const names = [2][]const u8{ "stdout", "stderr" };
  113. var fds = [2]i32{ ch.out_fd, ch.err_fd };
  114. var open_count: usize = 2;
  115. var rd: [4096]u8 = undefined;
  116. while (open_count > 0) {
  117. var pfds: [2]c.PollFd = undefined;
  118. var map: [2]usize = undefined;
  119. var n: usize = 0;
  120. for (fds, 0..) |fd, i| {
  121. if (fd < 0) continue;
  122. pfds[n] = .{ .fd = fd, .events = POLLIN, .revents = 0 };
  123. map[n] = i;
  124. n += 1;
  125. }
  126. const pr = c.poll(&pfds, n, -1);
  127. if (pr <= 0) continue;
  128. for (pfds[0..n], 0..) |pfd, pi| {
  129. if (pfd.revents == 0) continue;
  130. const i = map[pi];
  131. const got = c.read(pfd.fd, &rd, rd.len);
  132. if (got <= 0) {
  133. // EOF on this stream: flush a trailing unterminated line
  134. if (bufs[i].items.len > 0) {
  135. const line = allocator.dupe(u8, bufs[i].items) catch null;
  136. if (line) |l| ch.post(.{ .line = l, .stream = names[i] });
  137. bufs[i].clearRetainingCapacity();
  138. }
  139. _ = c.close(pfd.fd);
  140. fds[i] = -1;
  141. open_count -= 1;
  142. continue;
  143. }
  144. const chunk = rd[0..@intCast(got)];
  145. var start: usize = 0;
  146. for (chunk, 0..) |ch2, k| {
  147. if (ch2 != '\n') continue;
  148. bufs[i].appendSlice(allocator, chunk[start..k]) catch {};
  149. const line = allocator.dupe(u8, bufs[i].items) catch null;
  150. if (line) |l| ch.post(.{ .line = l, .stream = names[i] });
  151. bufs[i].clearRetainingCapacity();
  152. start = k + 1;
  153. }
  154. bufs[i].appendSlice(allocator, chunk[start..]) catch {};
  155. }
  156. }
  157. var status: c_int = 0;
  158. _ = c.waitpid(ch.pid, &status, 0);
  159. // POSIX status decode: exited → code, signalled → 128+sig (the shell rule)
  160. const code: i32 = if ((status & 0x7f) == 0) @intCast((status >> 8) & 0xff) else 128 + @as(i32, @intCast(status & 0x7f));
  161. ch.post(.{ .exit_code = code });
  162. }
  163. // ── the loop source ──────────────────────────────────────────────────────────
  164. fn eventsTryNext(ctx: ?*anyopaque) callconv(.c) HlValue {
  165. const ch: *Child = @ptrCast(@alignCast(ctx orelse return api.makeNull()));
  166. // drain the bell (read may fail with EAGAIN — fine)
  167. var eat: [8]u8 = undefined;
  168. _ = c.read(ch.wake_fd, &eat, 8);
  169. mutexLock(&ch.mutex);
  170. if (ch.queue.items.len == 0) {
  171. mutexUnlock(&ch.mutex);
  172. return api.makeNull();
  173. }
  174. const ev = ch.queue.orderedRemove(0);
  175. mutexUnlock(&ch.mutex);
  176. if (ev.spawn_err) |msg| {
  177. return api.makeError(msg);
  178. }
  179. if (ev.exit_code) |code| {
  180. const fields = allocator.alloc(HlField, 2) catch return api.makeNull();
  181. fields[0] = .{ .key = hlStr("exit"), .value = api.makeNumber(@floatFromInt(code)) };
  182. fields[1] = .{ .key = hlStr("pid"), .value = api.makeNumber(@floatFromInt(ch.pid)) };
  183. const obj = allocator.create(HlObject) catch return api.makeNull();
  184. obj.* = .{ .fields = fields.ptr, .field_count = 2, .deinit_fn = null };
  185. return api.makeObject(obj);
  186. }
  187. const fields = allocator.alloc(HlField, 2) catch return api.makeNull();
  188. fields[0] = .{ .key = hlStr("line"), .value = api.makeString(ev.line orelse "") };
  189. fields[1] = .{ .key = hlStr("stream"), .value = api.makeString(ev.stream) };
  190. const obj = allocator.create(HlObject) catch return api.makeNull();
  191. obj.* = .{ .fields = fields.ptr, .field_count = 2, .deinit_fn = null };
  192. return api.makeObject(obj);
  193. }
  194. fn eventsDeinit(ctx: ?*anyopaque) callconv(.c) void {
  195. const ch: *Child = @ptrCast(@alignCast(ctx orelse return));
  196. ch.done = true;
  197. }
  198. // ── exports ──────────────────────────────────────────────────────────────────
  199. /// hl_proc_spawn(argvList) → { pid, events }
  200. export fn hl_proc_spawn(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  201. // Accepts BOTH forms: a STRING (split on ANY whitespace run — spaces,
  202. // tabs, NEWLINES: a long command written across lines, the multi-line-
  203. // string way, is one command) or a LIST (an object with numeric keys —
  204. // the loader's valueArrayToHl contract) for arguments that contain spaces.
  205. if (argc < 1) return api.makeError("hl:proc spawn: pass a command string or an argv list");
  206. var parts = std.ArrayListUnmanaged([]const u8).empty;
  207. defer parts.deinit(allocator);
  208. if (argv[0].type == .hl_string) {
  209. // whitespace-run tokens, with QUOTE GROUPING: a token opening with
  210. // ' or " runs (whitespace included) to the matching close, quotes
  211. // stripped — one argument. NOT a shell: no expansion, no nesting,
  212. // no substitution; just grouping, the way a command line reads.
  213. const sv0 = argv[0].data.string;
  214. const text0 = sv0.ptr[0..sv0.len];
  215. var pos: usize = 0;
  216. while (pos < text0.len) {
  217. const chx = text0[pos];
  218. if (chx == ' ' or chx == '\t' or chx == '\r' or chx == '\n') {
  219. pos += 1;
  220. continue;
  221. }
  222. if (chx == '\'' or chx == '"') {
  223. const close = std.mem.indexOfScalarPos(u8, text0, pos + 1, chx) orelse
  224. return api.makeError("hl:proc spawn: unclosed quote in the command string");
  225. parts.append(allocator, text0[pos + 1 .. close]) catch return api.makeError("hl:proc: out of memory");
  226. pos = close + 1;
  227. continue;
  228. }
  229. var end = pos;
  230. while (end < text0.len and text0[end] != ' ' and text0[end] != '\t' and text0[end] != '\r' and text0[end] != '\n') end += 1;
  231. parts.append(allocator, text0[pos..end]) catch return api.makeError("hl:proc: out of memory");
  232. pos = end;
  233. }
  234. } else if (argv[0].type == .hl_object) {
  235. const obj_in = argv[0].data.object;
  236. for (obj_in.fields[0..obj_in.field_count]) |field| {
  237. if (field.value.type != .hl_string) return api.makeError("hl:proc spawn: argv entries must be strings");
  238. const sv = field.value.data.string;
  239. parts.append(allocator, sv.ptr[0..sv.len]) catch return api.makeError("hl:proc: out of memory");
  240. }
  241. } else {
  242. return api.makeError("hl:proc spawn: pass a command string or an argv list");
  243. }
  244. const n_args: usize = parts.items.len;
  245. if (n_args == 0) return api.makeError("hl:proc spawn: empty command");
  246. // NUL-terminated argv for exec, before the fork (no allocation after fork)
  247. var cargv = allocator.alloc(?[*:0]const u8, n_args + 1) catch return api.makeError("hl:proc: out of memory");
  248. for (parts.items, 0..) |part, i| {
  249. var text: []const u8 = part;
  250. // THE PROGRAM (argv[0]): a path containing '/' that is not absolute
  251. // resolves against the SCRIPT's directory — the same rule every other
  252. // Hybriel path follows. A bare name searches PATH (exec semantics).
  253. var resolved: ?[]u8 = null;
  254. defer if (resolved) |r| allocator.free(r);
  255. if (i == 0 and text.len > 0 and text[0] != '/' and std.mem.indexOfScalar(u8, text, '/') != null) {
  256. if (script_dir) |sd| {
  257. resolved = std.fmt.allocPrint(allocator, "{s}/{s}", .{ sd, text }) catch null;
  258. if (resolved) |r| text = r;
  259. }
  260. }
  261. const z = allocator.dupeZ(u8, text) catch return api.makeError("hl:proc: out of memory");
  262. cargv[i] = z.ptr;
  263. }
  264. cargv[n_args] = null;
  265. var out_pipe: [2]c_int = undefined;
  266. var err_pipe: [2]c_int = undefined;
  267. var exec_pipe: [2]c_int = undefined; // the exec-failure channel (CLOEXEC)
  268. if (c.pipe(&out_pipe) != 0 or c.pipe(&err_pipe) != 0 or c.pipe(&exec_pipe) != 0) {
  269. return api.makeError("hl:proc spawn: pipe() failed");
  270. }
  271. _ = c.fcntl(exec_pipe[1], 2, 1); // F_SETFD FD_CLOEXEC: closes ITSELF on a successful exec
  272. const pid = c.fork();
  273. if (pid < 0) return api.makeError("hl:proc spawn: fork() failed");
  274. if (pid == 0) {
  275. // DIE WITH THE PARENT (2026-08-17): a supervisor that crashes or is
  276. // Ctrl-C'd must not leave orphaned llama-servers squatting on ports —
  277. // the kernel sends the child SIGTERM when hybriel exits, any exit.
  278. _ = linux.prctl(1, 15, 0, 0, 0); // PR_SET_PDEATHSIG = 1, SIGTERM = 15
  279. // the CHILD: pipes onto stdout/stderr, exec verbatim — no shell
  280. _ = c.dup2(out_pipe[1], 1);
  281. _ = c.dup2(err_pipe[1], 2);
  282. _ = c.close(out_pipe[0]);
  283. _ = c.close(out_pipe[1]);
  284. _ = c.close(err_pipe[0]);
  285. _ = c.close(err_pipe[1]);
  286. _ = c.close(exec_pipe[0]);
  287. _ = c.execvp(cargv[0].?, @ptrCast(cargv.ptr));
  288. // exec failed: write errno through the CLOEXEC pipe — a successful
  289. // exec closed it, so the parent reading BYTES means "never started"
  290. const e: i32 = std.c._errno().*;
  291. _ = c.write(exec_pipe[1], @ptrCast(&e), 4);
  292. c._exit(127);
  293. }
  294. _ = c.close(out_pipe[1]);
  295. _ = c.close(err_pipe[1]);
  296. _ = c.close(exec_pipe[1]);
  297. const ch = allocator.create(Child) catch return api.makeError("hl:proc: out of memory");
  298. ch.* = .{
  299. .wake_fd = c.eventfd(0, EFD_NONBLOCK),
  300. .pid = pid,
  301. .out_fd = out_pipe[0],
  302. .err_fd = err_pipe[0],
  303. .exec_fd = exec_pipe[0],
  304. .argv0 = allocator.dupe(u8, std.mem.span(cargv[0].?)) catch "",
  305. };
  306. ch.thread = std.Thread.spawn(.{}, workerMain, .{ch}) catch {
  307. return api.makeError("hl:proc spawn: worker thread failed");
  308. };
  309. if (ch.thread) |t| t.detach();
  310. const iter = allocator.create(HlIterator) catch return api.makeError("hl:proc: out of memory");
  311. iter.* = .{
  312. .context = @ptrCast(ch),
  313. .next_fn = &eventsTryNext,
  314. .deinit_fn = &eventsDeinit,
  315. .try_next_fn = &eventsTryNext,
  316. .wake_fd = ch.wake_fd,
  317. };
  318. const fields = allocator.alloc(HlField, 2) catch return api.makeNull();
  319. fields[0] = .{ .key = hlStr("pid"), .value = api.makeNumber(@floatFromInt(pid)) };
  320. fields[1] = .{ .key = hlStr("events"), .value = api.makeIterator(iter) };
  321. const obj = allocator.create(HlObject) catch return api.makeNull();
  322. obj.* = .{ .fields = fields.ptr, .field_count = 2, .deinit_fn = null };
  323. return api.makeObject(obj);
  324. }
  325. /// hl_proc_kill(pid, signal?) — SIGTERM by default; the exit event still arrives
  326. export fn hl_proc_kill(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  327. if (argc < 1 or argv[0].type != .hl_number) {
  328. return api.makeError("hl:proc kill: pass the pid spawn() returned");
  329. }
  330. const pid: c_int = @intFromFloat(argv[0].data.number);
  331. var sig: c_int = 15; // SIGTERM
  332. if (argc >= 2 and argv[1].type == .hl_number) sig = @intFromFloat(argv[1].data.number);
  333. if (pid <= 1) return api.makeError("hl:proc kill: refusing pid <= 1");
  334. const r = c.kill(pid, sig);
  335. return api.makeBool(r == 0);
  336. }
  337. /// hl_proc_env(name) — one environment variable, null when unset. The
  338. /// environment is PROCESS surface, which is why it lives in hl:proc.
  339. export fn hl_proc_env(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  340. if (argc < 1 or argv[0].type != .hl_string) {
  341. return api.makeError("hl:proc env: pass the variable name");
  342. }
  343. const name = argv[0].data.string;
  344. const z = allocator.dupeZ(u8, name.ptr[0..name.len]) catch return api.makeNull();
  345. defer allocator.free(z);
  346. const v = std.c.getenv(z.ptr) orelse return api.makeNull();
  347. const copy = allocator.dupe(u8, std.mem.span(v)) catch return api.makeNull();
  348. return api.makeString(copy);
  349. }
  350. // ── the program's own arguments (mission 317) ────────────────────────────────
  351. // The host installs them right after dlopen, the same way it installs the
  352. // script directory: one NUL-separated blob and a count. WHAT they are is the
  353. // host's ruling, not this plugin's — the interpreter hands over the positionals
  354. // that follow a FILE entry, and a compiled binary hands over its own argv minus
  355. // argv[0]. A program started with none simply gets none.
  356. var arg_blob: ?[]u8 = null;
  357. var arg_count: usize = 0;
  358. export fn hl_proc_set_args(ptr: [*]const u8, len: usize, count: usize) callconv(.c) void {
  359. if (arg_blob) |old| allocator.free(old);
  360. arg_blob = allocator.dupe(u8, ptr[0..len]) catch null;
  361. arg_count = if (arg_blob == null) 0 else count;
  362. }
  363. /// hl_proc_argc() — how many arguments the program was given.
  364. export fn hl_proc_argc(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  365. _ = argc;
  366. _ = argv;
  367. return api.makeNumber(@floatFromInt(arg_count));
  368. }
  369. /// hl_proc_arg(i) — the i-th argument, null when there is none. `args()` in
  370. /// server.hl walks these into the list the language hands back; the pair exists
  371. /// because a plugin's return value is one value, and a LIST is what the caller
  372. /// wants.
  373. export fn hl_proc_arg(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  374. if (argc < 1 or argv[0].type != .hl_number) {
  375. return api.makeError("hl:proc arg: pass the index");
  376. }
  377. const n = argv[0].data.number;
  378. if (n < 0 or n != @floor(n)) return api.makeNull();
  379. const want: usize = @intFromFloat(n);
  380. if (want >= arg_count) return api.makeNull();
  381. const blob = arg_blob orelse return api.makeNull();
  382. var seen: usize = 0;
  383. var start: usize = 0;
  384. for (blob, 0..) |ch, i| {
  385. if (ch != 0) continue;
  386. if (seen == want) {
  387. const copy = allocator.dupe(u8, blob[start..i]) catch return api.makeNull();
  388. return api.makeString(copy);
  389. }
  390. seen += 1;
  391. start = i + 1;
  392. }
  393. return api.makeNull();
  394. }
  395. /// hl_proc_cwd() — the process's current working directory, absolute. NOTE the
  396. /// language's paths stay SCRIPT-relative; this is for tools that act where the
  397. /// USER is standing (a CLI operating on "here"), not for resolving your own files.
  398. export fn hl_proc_cwd(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  399. _ = argc;
  400. _ = argv;
  401. var buf: [4096]u8 = undefined;
  402. const p = std.c.getcwd(&buf, buf.len) orelse return api.makeError("hl:proc cwd: getcwd failed");
  403. const copy = allocator.dupe(u8, std.mem.sliceTo(p, 0)) catch return api.makeNull();
  404. return api.makeString(copy);
  405. }
  406. // ── stdin as a LOOP SOURCE (realms, 2026-09-01) ──────────────────────────────
  407. // hl_proc_stdin() → loop source of { line, eof }: one reader thread on fd 0,
  408. // line-buffered, posting under a mutex with an eventfd bell — the same shape
  409. // as a child's pipes above. A joined terminal realm reads its keyboard here.
  410. const StdinSrc = struct {
  411. mutex: PthreadMutex = libc.PTHREAD_MUTEX_INITIALIZER,
  412. queue: std.ArrayListUnmanaged([]u8) = .empty,
  413. wake_fd: i32 = -1,
  414. eof: bool = false,
  415. eof_reported: bool = false,
  416. fn post(self: *StdinSrc, line: ?[]u8) void {
  417. mutexLock(&self.mutex);
  418. if (line) |l| {
  419. self.queue.append(allocator, l) catch {};
  420. } else {
  421. self.eof = true;
  422. }
  423. mutexUnlock(&self.mutex);
  424. const one: u64 = 1;
  425. _ = stdin_c.write(self.wake_fd, @ptrCast(&one), 8);
  426. }
  427. };
  428. const stdin_c = struct {
  429. extern "c" fn read(fd: c_int, buf: [*]u8, n: usize) isize;
  430. extern "c" fn write(fd: c_int, buf: [*]const u8, n: usize) isize;
  431. extern "c" fn eventfd(initval: c_uint, flags: c_int) c_int;
  432. };
  433. var stdin_src: ?*StdinSrc = null;
  434. fn stdinReaderMain(s: *StdinSrc) void {
  435. var buf = std.ArrayListUnmanaged(u8).empty;
  436. defer buf.deinit(allocator);
  437. var rd: [4096]u8 = undefined;
  438. while (true) {
  439. const got = stdin_c.read(0, &rd, rd.len);
  440. if (got <= 0) break;
  441. const chunk = rd[0..@intCast(got)];
  442. var start: usize = 0;
  443. for (chunk, 0..) |ch, k| {
  444. if (ch != '\n') continue;
  445. buf.appendSlice(allocator, chunk[start..k]) catch {};
  446. const line = allocator.dupe(u8, buf.items) catch null;
  447. if (line) |l| s.post(l);
  448. buf.clearRetainingCapacity();
  449. start = k + 1;
  450. }
  451. buf.appendSlice(allocator, chunk[start..]) catch {};
  452. }
  453. if (buf.items.len > 0) {
  454. const line = allocator.dupe(u8, buf.items) catch null;
  455. if (line) |l| s.post(l);
  456. }
  457. s.post(null);
  458. }
  459. fn stdinTryNext(ctx: ?*anyopaque) callconv(.c) HlValue {
  460. const s: *StdinSrc = @ptrCast(@alignCast(ctx orelse return api.makeNull()));
  461. var eat: [8]u8 = undefined;
  462. _ = stdin_c.read(s.wake_fd, &eat, 8);
  463. mutexLock(&s.mutex);
  464. var line: ?[]u8 = null;
  465. var eof_now = false;
  466. if (s.queue.items.len > 0) {
  467. line = s.queue.orderedRemove(0);
  468. } else if (s.eof and !s.eof_reported) {
  469. s.eof_reported = true;
  470. eof_now = true;
  471. }
  472. mutexUnlock(&s.mutex);
  473. if (line == null and !eof_now) return api.makeNull();
  474. const fields = allocator.alloc(HlField, 2) catch return api.makeNull();
  475. fields[0] = .{ .key = .{ .ptr = "line".ptr, .len = 4 }, .value = if (line) |l| api.makeString(l) else api.makeNull() };
  476. fields[1] = .{ .key = .{ .ptr = "eof".ptr, .len = 3 }, .value = api.makeBool(eof_now) };
  477. const obj = allocator.create(HlObject) catch return api.makeNull();
  478. obj.* = .{ .fields = fields.ptr, .field_count = 2, .deinit_fn = null };
  479. return api.makeObject(obj);
  480. }
  481. fn stdinDeinit(ctx: ?*anyopaque) callconv(.c) void {
  482. _ = ctx;
  483. }
  484. /// hl_proc_stdin() → loop source
  485. export fn hl_proc_stdin(argc: u32, argv: [*]const HlValue) callconv(.c) HlValue {
  486. _ = argc;
  487. _ = argv;
  488. if (stdin_src != null) return api.makeError("hl:proc stdin: this process already reads its terminal");
  489. const s = allocator.create(StdinSrc) catch return api.makeError("hl:proc: out of memory");
  490. s.* = .{ .wake_fd = stdin_c.eventfd(0, 0o4000) };
  491. const t = std.Thread.spawn(.{}, stdinReaderMain, .{s}) catch {
  492. allocator.destroy(s);
  493. return api.makeError("hl:proc stdin: cannot start the reader thread");
  494. };
  495. t.detach();
  496. stdin_src = s;
  497. const iter = allocator.create(HlIterator) catch return api.makeError("hl:proc: out of memory");
  498. iter.* = .{
  499. .context = @ptrCast(s),
  500. .next_fn = &stdinTryNext,
  501. .deinit_fn = &stdinDeinit,
  502. .try_next_fn = &stdinTryNext,
  503. .wake_fd = s.wake_fd,
  504. };
  505. return api.makeIterator(iter);
  506. }

Branches

Latest commits

  • 3a4d0324antcolony#37: a too-long report gets up to 3 fix tries, finished work is never thrown away for lengthmre
  • a6af7883tracker: worker box sees calendar.worldapi.org (login to copy)mre
  • c613d26btemplates: bridges to external components (login.js for ident's selector) are allowed (creator 2026-09-27)mre
  • 9062978ctracker: worker box sees /media/STORAGE/projects/old-tracker read-only (tracker#2 source data)mre
  • 7f9660eeState of 2026-09-27, before the move to gitoriamre