arcane-os 0.55.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +5 -3
  3. package/browser-runtime/ai/core-local.mjs +306 -0
  4. package/browser-runtime/core/client.mjs +75 -14
  5. package/browser-runtime/core/contracts.mjs +2 -1
  6. package/docs/consumer-updates.md +82 -0
  7. package/docs/publishing.md +41 -18
  8. package/docs/reference/README.md +31 -1
  9. package/docs/reference/availability-and-normalization.md +11 -3
  10. package/docs/reference/cli.md +9 -1
  11. package/docs/reference/core-client.md +45 -2
  12. package/docs/reference/core-native-packaging.md +195 -11
  13. package/docs/reference/core-runtime.md +10 -0
  14. package/docs/reference/inventory/package-api.json +160 -20
  15. package/docs/reference/local-ai.md +197 -0
  16. package/docs/reference/protocols.md +91 -14
  17. package/docs/reference/pwa.md +17 -0
  18. package/docs/reference/runtime-modules.md +6 -5
  19. package/docs/reference/sdk-api.md +165 -26
  20. package/package.json +5 -1
  21. package/runtime/arcane/modules/LocalAIReadiness.js +3 -1
  22. package/schemas/arcane-app.schema.json +64 -0
  23. package/schemas/arcane-package.schema.json +54 -0
  24. package/src/app-descriptor.mjs +35 -2
  25. package/src/app-layout.mjs +6 -2
  26. package/src/cli/main.mjs +8 -2
  27. package/src/core/host.mjs +23 -0
  28. package/src/core/hosts/arcane-core-loader.cjs +2 -0
  29. package/src/core/hosts/linux/arcane_core_process.c +568 -0
  30. package/src/core/hosts/linux/arcane_core_process.h +127 -0
  31. package/src/core/hosts/macos/ArcaneCoreProcess.h +85 -0
  32. package/src/core/hosts/macos/ArcaneCoreProcess.m +357 -0
  33. package/src/core/hosts/sea-launcher.cjs +17 -0
  34. package/src/core/hosts/windows/ArcaneCoreProcess.cs +416 -0
  35. package/src/core/hosts/windows/ArcaneHost.cs +695 -0
  36. package/src/core/hosts/windows/ArcaneHost.manifest +22 -0
  37. package/src/core/hosts/windows/ArcaneLauncher.cs +239 -0
  38. package/src/core/runtime.mjs +15 -3
  39. package/src/core/services/local-ai.mjs +543 -0
  40. package/src/core-classic-source.mjs +2 -2
  41. package/src/dev-server.mjs +7 -0
  42. package/src/import-map.mjs +1 -0
  43. package/src/local-ai/archive.mjs +52 -0
  44. package/src/local-ai/config.mjs +62 -0
  45. package/src/local-ai/dev.mjs +390 -0
  46. package/src/local-ai/discover.mjs +71 -0
  47. package/src/local-ai/http.mjs +106 -0
  48. package/src/local-ai/install.mjs +344 -0
  49. package/src/local-ai/server.mjs +262 -0
  50. package/src/native/portable-layout.mjs +29 -4
  51. package/src/native/portable-provider.mjs +39 -8
  52. package/src/native/windows-provider.mjs +262 -0
  53. package/src/native-provider-loader.mjs +8 -2
  54. package/src/packager/core.mjs +89 -15
  55. package/src/process.mjs +74 -12
  56. package/src/toolchain.mjs +38 -8
@@ -0,0 +1,568 @@
1
+ #define _GNU_SOURCE
2
+ #define _POSIX_C_SOURCE 200809L
3
+
4
+ #include "arcane_core_process.h"
5
+
6
+ #include <errno.h>
7
+ #include <fcntl.h>
8
+ #include <pthread.h>
9
+ #include <stdatomic.h>
10
+ #include <stdint.h>
11
+ #include <stdio.h>
12
+ #include <stdlib.h>
13
+ #include <string.h>
14
+ #include <strings.h>
15
+ #include <sys/socket.h>
16
+ #include <sys/wait.h>
17
+ #include <unistd.h>
18
+
19
+ extern char **environ;
20
+
21
+ typedef struct CoreWrite {
22
+ struct CoreWrite *next;
23
+ void *data;
24
+ size_t length;
25
+ void *context;
26
+ } CoreWrite;
27
+
28
+ struct ArcaneCoreProcess {
29
+ atomic_uint references;
30
+ pthread_mutex_t mutex;
31
+ pthread_cond_t changed;
32
+ ArcaneCoreProcessCallbacks callbacks;
33
+ void *context;
34
+ char *executable;
35
+ char **argv;
36
+ char *working_directory;
37
+ CoreWrite *first;
38
+ CoreWrite *last;
39
+ int closing;
40
+ int launch_done;
41
+ int launch_error;
42
+ int input;
43
+ int output;
44
+ int errors;
45
+ ArcaneCoreProcessResult result;
46
+ };
47
+
48
+ typedef struct {
49
+ int operation;
50
+ int error_number;
51
+ } LaunchFailure;
52
+
53
+ typedef struct {
54
+ ArcaneCoreProcess *process;
55
+ unsigned char chunk[16384];
56
+ size_t position;
57
+ size_t available;
58
+ int error_number;
59
+ } CoreReader;
60
+
61
+ enum { LAUNCH_CHDIR = 1, LAUNCH_DUP, LAUNCH_EXEC };
62
+
63
+ static void *run_core(void *data);
64
+ static void *write_core(void *data);
65
+ static void *read_core(void *data);
66
+ static void *read_errors(void *data);
67
+
68
+ static void free_process(ArcaneCoreProcess *process) {
69
+ if (process->argv) {
70
+ for (size_t index = 0; process->argv[index]; ++index) free(process->argv[index]);
71
+ free(process->argv);
72
+ }
73
+ free(process->executable);
74
+ free(process->working_directory);
75
+ pthread_cond_destroy(&process->changed);
76
+ pthread_mutex_destroy(&process->mutex);
77
+ free(process);
78
+ }
79
+
80
+ void arcane_core_process_retain(ArcaneCoreProcess *process) {
81
+ if (process) atomic_fetch_add_explicit(&process->references, 1, memory_order_relaxed);
82
+ }
83
+
84
+ void arcane_core_process_release(ArcaneCoreProcess *process) {
85
+ if (process && atomic_fetch_sub_explicit(&process->references, 1, memory_order_acq_rel) == 1)
86
+ free_process(process);
87
+ }
88
+
89
+ static void report_error(ArcaneCoreProcess *process, const char *operation, int error_number) {
90
+ pthread_mutex_lock(&process->mutex);
91
+ process->result.had_error = 1;
92
+ pthread_mutex_unlock(&process->mutex);
93
+ process->callbacks.error(process, operation, error_number, process->context);
94
+ }
95
+
96
+ static void close_owned(ArcaneCoreProcess *process, int descriptor, const char *operation) {
97
+ if (descriptor >= 0 && close(descriptor) != 0) report_error(process, operation, errno);
98
+ }
99
+
100
+ void arcane_core_process_close(ArcaneCoreProcess *process) {
101
+ if (!process) return;
102
+ pthread_mutex_lock(&process->mutex);
103
+ process->closing = 1;
104
+ pthread_cond_broadcast(&process->changed);
105
+ pthread_mutex_unlock(&process->mutex);
106
+ }
107
+
108
+ int arcane_core_process_send(ArcaneCoreProcess *process, const void *json,
109
+ size_t length, void *write_context) {
110
+ if (!process || (!json && length)) return EINVAL;
111
+ CoreWrite *write = calloc(1, sizeof(*write));
112
+ if (!write) return ENOMEM;
113
+ write->data = malloc(length ? length : 1);
114
+ if (!write->data) { free(write); return ENOMEM; }
115
+ if (length) memcpy(write->data, json, length);
116
+ write->length = length;
117
+ write->context = write_context;
118
+ pthread_mutex_lock(&process->mutex);
119
+ if (process->closing) {
120
+ pthread_mutex_unlock(&process->mutex);
121
+ free(write->data);
122
+ free(write);
123
+ return EPIPE;
124
+ }
125
+ if (process->last) process->last->next = write;
126
+ else process->first = write;
127
+ process->last = write;
128
+ pthread_cond_broadcast(&process->changed);
129
+ pthread_mutex_unlock(&process->mutex);
130
+ return 0;
131
+ }
132
+
133
+ ArcaneCoreProcess *arcane_core_process_start(
134
+ const char *executable, const char *const argv[], const char *working_directory,
135
+ const ArcaneCoreProcessCallbacks *callbacks, void *context) {
136
+ if (!executable || !*executable || !argv || !argv[0]
137
+ || !working_directory || !*working_directory || !callbacks
138
+ || !callbacks->frame || !callbacks->output || !callbacks->written
139
+ || !callbacks->error || !callbacks->exited || !callbacks->complete) {
140
+ errno = EINVAL;
141
+ return NULL;
142
+ }
143
+ ArcaneCoreProcess *process = calloc(1, sizeof(*process));
144
+ if (!process) return NULL;
145
+ int failure = pthread_mutex_init(&process->mutex, NULL);
146
+ if (failure) { free(process); errno = failure; return NULL; }
147
+ failure = pthread_cond_init(&process->changed, NULL);
148
+ if (failure) {
149
+ pthread_mutex_destroy(&process->mutex);
150
+ free(process);
151
+ errno = failure;
152
+ return NULL;
153
+ }
154
+ atomic_init(&process->references, 2); /* caller and detached lifecycle owner */
155
+ process->callbacks = *callbacks;
156
+ process->context = context;
157
+ process->input = process->output = process->errors = -1;
158
+ process->result.pid = -1;
159
+ process->result.exit_code = -1;
160
+ process->executable = strdup(executable);
161
+ process->working_directory = strdup(working_directory);
162
+ size_t count = 0;
163
+ while (argv[count]) {
164
+ if (count == SIZE_MAX / sizeof(char *) - 1) {
165
+ free_process(process);
166
+ errno = EOVERFLOW;
167
+ return NULL;
168
+ }
169
+ ++count;
170
+ }
171
+ process->argv = calloc(count + 1, sizeof(char *));
172
+ if (!process->executable || !process->working_directory || !process->argv) {
173
+ free_process(process);
174
+ errno = ENOMEM;
175
+ return NULL;
176
+ }
177
+ for (size_t index = 0; index < count; ++index) {
178
+ process->argv[index] = strdup(argv[index]);
179
+ if (!process->argv[index]) { free_process(process); errno = ENOMEM; return NULL; }
180
+ }
181
+ pthread_attr_t attributes;
182
+ failure = pthread_attr_init(&attributes);
183
+ if (failure) { free_process(process); errno = failure; return NULL; }
184
+ failure = pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
185
+ pthread_t thread;
186
+ if (!failure) failure = pthread_create(&thread, &attributes, run_core, process);
187
+ pthread_attr_destroy(&attributes);
188
+ if (failure) { free_process(process); errno = failure; return NULL; }
189
+ return process;
190
+ }
191
+
192
+ static int await_launch(ArcaneCoreProcess *process) {
193
+ pthread_mutex_lock(&process->mutex);
194
+ while (!process->launch_done) pthread_cond_wait(&process->changed, &process->mutex);
195
+ int launched = process->result.launched;
196
+ pthread_mutex_unlock(&process->mutex);
197
+ return launched;
198
+ }
199
+
200
+ static void acknowledge_write(ArcaneCoreProcess *process, CoreWrite *write, int failure) {
201
+ process->callbacks.written(process, write->context, failure, process->context);
202
+ free(write->data);
203
+ free(write);
204
+ }
205
+
206
+ static int send_complete(int descriptor, const void *data, size_t length) {
207
+ const unsigned char *cursor = data;
208
+ while (length) {
209
+ /* A chunk is transport work, never a content limit. MSG_NOSIGNAL avoids
210
+ * modifying SIGPIPE behavior for the host or any other thread. */
211
+ size_t chunk = length > 16384 ? 16384 : length;
212
+ ssize_t sent = send(descriptor, cursor, chunk, MSG_NOSIGNAL);
213
+ if (sent < 0 && errno == EINTR) continue;
214
+ if (sent < 0) return errno;
215
+ if (!sent) return EPIPE;
216
+ cursor += sent;
217
+ length -= (size_t)sent;
218
+ }
219
+ return 0;
220
+ }
221
+
222
+ static void *write_core(void *data) {
223
+ ArcaneCoreProcess *process = data;
224
+ int failure = await_launch(process) ? 0 : process->launch_error;
225
+ for (;;) {
226
+ pthread_mutex_lock(&process->mutex);
227
+ while (!process->first && !process->closing)
228
+ pthread_cond_wait(&process->changed, &process->mutex);
229
+ CoreWrite *write = process->first;
230
+ if (write) {
231
+ process->first = write->next;
232
+ if (!process->first) process->last = NULL;
233
+ }
234
+ pthread_mutex_unlock(&process->mutex);
235
+ if (!write) break;
236
+ if (!failure) {
237
+ char header[sizeof(size_t) * 3 + sizeof("Content-Length: \r\n\r\n")];
238
+ int length = snprintf(header, sizeof(header), "Content-Length: %zu\r\n\r\n", write->length);
239
+ failure = length < 0 || (size_t)length >= sizeof(header) ? EOVERFLOW
240
+ : send_complete(process->input, header, (size_t)length);
241
+ if (!failure) failure = send_complete(process->input, write->data, write->length);
242
+ if (failure) {
243
+ arcane_core_process_close(process);
244
+ report_error(process, "stdin.write", failure);
245
+ }
246
+ }
247
+ acknowledge_write(process, write, failure);
248
+ }
249
+ if (process->input >= 0) close_owned(process, process->input, "stdin.close");
250
+ return NULL;
251
+ }
252
+
253
+ /* Streaming chunks carry the complete stream, without imposing content limits. */
254
+ static int fill_stdout(CoreReader *reader) {
255
+ if (reader->position == reader->available) {
256
+ ssize_t count;
257
+ do { count = read(reader->process->output, reader->chunk, sizeof(reader->chunk)); }
258
+ while (count < 0 && errno == EINTR);
259
+ if (count < 0) {
260
+ reader->error_number = errno;
261
+ arcane_core_process_close(reader->process);
262
+ report_error(reader->process, "stdout.read", reader->error_number);
263
+ return -1;
264
+ }
265
+ if (!count) return 0;
266
+ reader->position = 0;
267
+ reader->available = (size_t)count;
268
+ reader->process->callbacks.output(reader->process, ARCANE_CORE_STDOUT,
269
+ reader->chunk, (size_t)count, reader->process->context);
270
+ }
271
+ return 1;
272
+ }
273
+
274
+ /* Returns 1 for a character, 0 for EOF, or -1 after a reported read failure. */
275
+ static int next_character(CoreReader *reader, unsigned char *character) {
276
+ int status = fill_stdout(reader);
277
+ if (status != 1) return status;
278
+ *character = reader->chunk[reader->position++];
279
+ return 1;
280
+ }
281
+
282
+ static int append_header(char **header, size_t *length, size_t *capacity, unsigned char character) {
283
+ if (*length == *capacity) {
284
+ if (*capacity > SIZE_MAX / 2) return EOVERFLOW;
285
+ size_t next = *capacity ? *capacity * 2 : 128;
286
+ char *replacement = realloc(*header, next);
287
+ if (!replacement) return ENOMEM;
288
+ *header = replacement;
289
+ *capacity = next;
290
+ }
291
+ (*header)[(*length)++] = (char)character;
292
+ return 0;
293
+ }
294
+
295
+ static int content_length(const char *header, size_t length, size_t *body_length) {
296
+ size_t cursor = 0;
297
+ int found = 0;
298
+ while (cursor < length) {
299
+ size_t end = cursor;
300
+ while (end + 1 < length && !(header[end] == '\r' && header[end + 1] == '\n')) ++end;
301
+ if (end + 1 >= length) return EPROTO;
302
+ if (end == cursor) break;
303
+ size_t colon = cursor;
304
+ while (colon < end && header[colon] != ':') ++colon;
305
+ if (colon == end) return EPROTO;
306
+ if (colon - cursor == 14 && strncasecmp(header + cursor, "Content-Length", 14) == 0) {
307
+ if (found) return EPROTO;
308
+ found = 1;
309
+ size_t number = colon + 1;
310
+ while (number < end && (header[number] == ' ' || header[number] == '\t')) ++number;
311
+ if (number == end || header[number] < '0' || header[number] > '9') return EPROTO;
312
+ size_t value = 0;
313
+ while (number < end && header[number] >= '0' && header[number] <= '9') {
314
+ unsigned digit = (unsigned)(header[number++] - '0');
315
+ if (value > (SIZE_MAX - digit) / 10) return EOVERFLOW;
316
+ value = value * 10 + digit;
317
+ }
318
+ while (number < end && (header[number] == ' ' || header[number] == '\t')) ++number;
319
+ if (number != end) return EPROTO;
320
+ *body_length = value;
321
+ }
322
+ cursor = end + 2;
323
+ }
324
+ return found ? 0 : EPROTO;
325
+ }
326
+
327
+ static void *read_core(void *data) {
328
+ ArcaneCoreProcess *process = data;
329
+ if (!await_launch(process)) return NULL;
330
+ CoreReader reader = {.process = process};
331
+ int parsing = 1;
332
+ while (parsing) {
333
+ char *header = NULL;
334
+ size_t length = 0, capacity = 0, body_length = 0;
335
+ int read_status = 1, failure = 0;
336
+ unsigned char character;
337
+ while ((read_status = next_character(&reader, &character)) == 1) {
338
+ failure = append_header(&header, &length, &capacity, character);
339
+ if (failure || (length >= 4 && memcmp(header + length - 4, "\r\n\r\n", 4) == 0)) break;
340
+ }
341
+ if (read_status <= 0) {
342
+ if (!read_status && length) {
343
+ arcane_core_process_close(process);
344
+ report_error(process, "stdout.incomplete-header", EPROTO);
345
+ }
346
+ free(header);
347
+ break;
348
+ }
349
+ if (!failure) failure = content_length(header, length, &body_length);
350
+ free(header);
351
+ if (failure) {
352
+ arcane_core_process_close(process);
353
+ report_error(process, "stdout.frame-header", failure);
354
+ parsing = 0;
355
+ break;
356
+ }
357
+ unsigned char *body = malloc(body_length ? body_length : 1);
358
+ if (!body) {
359
+ arcane_core_process_close(process);
360
+ report_error(process, "stdout.frame-allocation", ENOMEM);
361
+ parsing = 0;
362
+ break;
363
+ }
364
+ size_t received = 0;
365
+ while (received < body_length) {
366
+ read_status = fill_stdout(&reader);
367
+ if (read_status != 1) break;
368
+ size_t amount = reader.available - reader.position;
369
+ if (amount > body_length - received) amount = body_length - received;
370
+ memcpy(body + received, reader.chunk + reader.position, amount);
371
+ reader.position += amount;
372
+ received += amount;
373
+ }
374
+ if (received == body_length) process->callbacks.frame(process, body, body_length, process->context);
375
+ else if (!reader.error_number) {
376
+ arcane_core_process_close(process);
377
+ report_error(process, "stdout.incomplete-body", EPROTO);
378
+ }
379
+ free(body);
380
+ if (received != body_length) break;
381
+ }
382
+ if (!parsing) {
383
+ /* Every chunk was already delivered to output(). A malformed frame is
384
+ * not permission to drop the remaining child's diagnostic stream. */
385
+ reader.position = reader.available;
386
+ while (fill_stdout(&reader) == 1) reader.position = reader.available;
387
+ }
388
+ arcane_core_process_close(process);
389
+ close_owned(process, process->output, "stdout.close");
390
+ return NULL;
391
+ }
392
+
393
+ static void *read_errors(void *data) {
394
+ ArcaneCoreProcess *process = data;
395
+ if (!await_launch(process)) return NULL;
396
+ unsigned char chunk[16384];
397
+ for (;;) {
398
+ ssize_t count = read(process->errors, chunk, sizeof(chunk));
399
+ if (count < 0 && errno == EINTR) continue;
400
+ if (count < 0) {
401
+ int failure = errno;
402
+ arcane_core_process_close(process);
403
+ report_error(process, "stderr.read", failure);
404
+ }
405
+ if (count <= 0) break;
406
+ process->callbacks.output(process, ARCANE_CORE_STDERR, chunk, (size_t)count, process->context);
407
+ }
408
+ close_owned(process, process->errors, "stderr.close");
409
+ return NULL;
410
+ }
411
+
412
+ static int prepare_descriptor(int descriptor) {
413
+ if (descriptor < 3) {
414
+ int replacement = fcntl(descriptor, F_DUPFD_CLOEXEC, 3);
415
+ int saved = errno;
416
+ close(descriptor);
417
+ errno = saved;
418
+ return replacement;
419
+ }
420
+ return descriptor;
421
+ }
422
+
423
+ static int create_pair(int pair[2], int input) {
424
+ /* Atomic close-on-exec prevents concurrent launches retaining another
425
+ * process's channel and indefinitely holding its EOF open. */
426
+ if ((input ? socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair)
427
+ : pipe2(pair, O_CLOEXEC)) != 0) return errno;
428
+ pair[0] = prepare_descriptor(pair[0]);
429
+ if (pair[0] < 0) {
430
+ int failure = errno;
431
+ close(pair[1]);
432
+ pair[1] = -1;
433
+ return failure;
434
+ }
435
+ pair[1] = prepare_descriptor(pair[1]);
436
+ if (pair[1] < 0) {
437
+ int failure = errno;
438
+ close(pair[0]);
439
+ pair[0] = -1;
440
+ return failure;
441
+ }
442
+ return 0;
443
+ }
444
+
445
+ static void child_failed(int descriptor, int operation) {
446
+ LaunchFailure failure = {operation, errno};
447
+ const unsigned char *cursor = (const unsigned char *)&failure;
448
+ size_t remaining = sizeof(failure);
449
+ while (remaining) {
450
+ ssize_t count = write(descriptor, cursor, remaining);
451
+ if (count < 0 && errno == EINTR) continue;
452
+ if (count <= 0) break;
453
+ cursor += count;
454
+ remaining -= (size_t)count;
455
+ }
456
+ _exit(127);
457
+ }
458
+
459
+ static void exec_child(ArcaneCoreProcess *process, int pairs[4][2]) {
460
+ close(pairs[3][0]);
461
+ if (chdir(process->working_directory) != 0) child_failed(pairs[3][1], LAUNCH_CHDIR);
462
+ if (dup2(pairs[0][1], STDIN_FILENO) < 0
463
+ || dup2(pairs[1][1], STDOUT_FILENO) < 0
464
+ || dup2(pairs[2][1], STDERR_FILENO) < 0) child_failed(pairs[3][1], LAUNCH_DUP);
465
+ for (size_t index = 0; index < 3; ++index) {
466
+ close(pairs[index][0]);
467
+ close(pairs[index][1]);
468
+ }
469
+ execve(process->executable, process->argv, environ);
470
+ child_failed(pairs[3][1], LAUNCH_EXEC);
471
+ }
472
+
473
+ static int observe_launch(int descriptor, const char **operation) {
474
+ LaunchFailure failure = {0};
475
+ unsigned char *cursor = (unsigned char *)&failure;
476
+ size_t received = 0;
477
+ while (received < sizeof(failure)) {
478
+ ssize_t count = read(descriptor, cursor + received, sizeof(failure) - received);
479
+ if (count < 0 && errno == EINTR) continue;
480
+ if (count < 0) return errno;
481
+ if (!count) return received ? EPROTO : 0;
482
+ received += (size_t)count;
483
+ }
484
+ *operation = failure.operation == LAUNCH_CHDIR ? "launch.chdir"
485
+ : failure.operation == LAUNCH_DUP ? "launch.stdio" : "launch.exec";
486
+ return failure.error_number;
487
+ }
488
+
489
+ static void *run_core(void *data) {
490
+ ArcaneCoreProcess *process = data;
491
+ int pairs[4][2] = {{-1, -1}, {-1, -1}, {-1, -1}, {-1, -1}};
492
+ pthread_t threads[3];
493
+ void *(*workers[3])(void *) = {write_core, read_core, read_errors};
494
+ size_t started = 0;
495
+ int failure = 0;
496
+ const char *operation = "launch.pipe";
497
+ for (size_t index = 0; index < 4 && !failure; ++index)
498
+ failure = create_pair(pairs[index], index == 0);
499
+ if (!failure) {
500
+ operation = "thread.start";
501
+ for (; started < 3; ++started) {
502
+ failure = pthread_create(&threads[started], NULL, workers[started], process);
503
+ if (failure) break;
504
+ }
505
+ }
506
+ pid_t child = -1;
507
+ if (!failure) {
508
+ operation = "launch.fork";
509
+ child = fork();
510
+ if (!child) exec_child(process, pairs);
511
+ if (child < 0) failure = errno;
512
+ else {
513
+ process->result.pid = child;
514
+ for (size_t index = 0; index < 4; ++index) {
515
+ close_owned(process, pairs[index][1], "launch.parent-close");
516
+ pairs[index][1] = -1;
517
+ }
518
+ operation = "launch.handshake";
519
+ failure = observe_launch(pairs[3][0], &operation);
520
+ }
521
+ }
522
+ if (failure) report_error(process, operation, failure);
523
+ if (!failure) {
524
+ process->input = pairs[0][0];
525
+ process->output = pairs[1][0];
526
+ process->errors = pairs[2][0];
527
+ for (size_t index = 0; index < 3; ++index) pairs[index][0] = -1;
528
+ if (process->callbacks.started) process->callbacks.started(process, child, process->context);
529
+ }
530
+ for (size_t index = 0; index < 4; ++index) {
531
+ close_owned(process, pairs[index][0], "launch.pipe-close");
532
+ close_owned(process, pairs[index][1], "launch.pipe-close");
533
+ }
534
+ pthread_mutex_lock(&process->mutex);
535
+ process->result.launched = !failure;
536
+ process->launch_error = failure;
537
+ process->launch_done = 1;
538
+ if (failure) process->closing = 1;
539
+ pthread_cond_broadcast(&process->changed);
540
+ pthread_mutex_unlock(&process->mutex);
541
+ if (!started) write_core(process); /* Return outcomes even before a writer exists. */
542
+ if (child > 0) {
543
+ int status = 0;
544
+ pid_t waited;
545
+ do { waited = waitpid(child, &status, 0); } while (waited < 0 && errno == EINTR);
546
+ if (waited < 0) report_error(process, "child.wait", errno);
547
+ pthread_mutex_lock(&process->mutex);
548
+ process->closing = 1;
549
+ if (waited == child) {
550
+ process->result.exited = WIFEXITED(status) || WIFSIGNALED(status);
551
+ if (WIFEXITED(status)) process->result.exit_code = WEXITSTATUS(status);
552
+ if (WIFSIGNALED(status)) process->result.term_signal = WTERMSIG(status);
553
+ if (process->result.term_signal || process->result.exit_code != 0)
554
+ process->result.had_error = 1;
555
+ }
556
+ ArcaneCoreProcessResult result = process->result;
557
+ pthread_cond_broadcast(&process->changed);
558
+ pthread_mutex_unlock(&process->mutex);
559
+ if (waited == child && (result.term_signal || result.exit_code != 0))
560
+ process->callbacks.error(process, result.term_signal ? "child.signal" : "child.exit-status",
561
+ 0, process->context);
562
+ process->callbacks.exited(process, &result, process->context);
563
+ }
564
+ for (size_t index = 0; index < started; ++index) pthread_join(threads[index], NULL);
565
+ process->callbacks.complete(process, &process->result, process->context);
566
+ arcane_core_process_release(process);
567
+ return NULL;
568
+ }
@@ -0,0 +1,127 @@
1
+ #ifndef ARCANE_CORE_PROCESS_H
2
+ #define ARCANE_CORE_PROCESS_H
3
+
4
+ #include <stddef.h>
5
+ #include <sys/types.h>
6
+
7
+ #ifdef __cplusplus
8
+ extern "C" {
9
+ #endif
10
+
11
+ typedef struct ArcaneCoreProcess ArcaneCoreProcess;
12
+
13
+ typedef enum {
14
+ ARCANE_CORE_STDOUT,
15
+ ARCANE_CORE_STDERR
16
+ } ArcaneCoreStream;
17
+
18
+ typedef struct {
19
+ pid_t pid;
20
+ int launched;
21
+ /* A terminal child status was observed, including termination by signal. */
22
+ int exited;
23
+ /* -1 unless the child exited normally; term_signal is zero unless signaled. */
24
+ int exit_code;
25
+ int term_signal;
26
+ int had_error;
27
+ } ArcaneCoreProcessResult;
28
+
29
+ /*
30
+ * All callbacks run on owner-managed background threads, never a UI thread.
31
+ * stdout, stderr, writes and lifecycle callbacks can run concurrently. Each
32
+ * individual stream and the write acknowledgements retain their original order.
33
+ * Marshal UI work at the host; keep context alive through complete() returning.
34
+ * Callback arguments and payloads are borrowed for the duration of the call.
35
+ * Callbacks must return normally and must not wait for another owner callback.
36
+ * They may call send(), close(), retain() or release() with an owned reference.
37
+ * A borrowed callback process remains valid through that callback's return.
38
+ *
39
+ * output() receives every unmodified stdout/stderr chunk. stdout includes its
40
+ * transport headers; frame() additionally receives each complete opaque JSON
41
+ * body. No JSON is parsed, reformatted or selected here. After a framing error,
42
+ * stdout continues through output() until EOF, without guessing a new boundary.
43
+ * error() identifies the operation and its POSIX error. For child.exit-status
44
+ * or child.signal, error_number is zero and exited() supplies the actual status.
45
+ * Complete raw output is
46
+ * retained by the output callbacks, including incomplete/malformed frames.
47
+ *
48
+ * written() fires exactly once for each send() returning zero. A zero error
49
+ * means the complete header and body reached the child's input transport, not
50
+ * that Core accepted or completed the requested operation. Core response frames
51
+ * own that distinction. A nonzero error reports an undelivered/partial write.
52
+ * No payload is retried after a partial write.
53
+ *
54
+ * started() is optional and runs after exec succeeds, before frame delivery.
55
+ * exited() runs when waitpid observes the child, independently of output EOF.
56
+ * complete() runs last, after every accepted write has an outcome, both output
57
+ * streams have ended (or reported a read error), and the child has been reaped
58
+ * (or waitpid has reported its error). It runs even when launch fails.
59
+ * All callbacks except started() are required so outcomes cannot disappear.
60
+ * A framing/read failure requests graceful close while preserving queued writes
61
+ * and continuing every still-readable output stream through EOF.
62
+ */
63
+ typedef struct {
64
+ void (*started)(ArcaneCoreProcess *process, pid_t pid, void *context);
65
+ void (*frame)(ArcaneCoreProcess *process, const void *json, size_t length, void *context);
66
+ void (*output)(ArcaneCoreProcess *process, ArcaneCoreStream stream,
67
+ const void *data, size_t length, void *context);
68
+ void (*written)(ArcaneCoreProcess *process, void *write_context,
69
+ int error_number, void *context);
70
+ void (*error)(ArcaneCoreProcess *process, const char *operation,
71
+ int error_number, void *context);
72
+ void (*exited)(ArcaneCoreProcess *process, const ArcaneCoreProcessResult *result,
73
+ void *context);
74
+ void (*complete)(ArcaneCoreProcess *process, const ArcaneCoreProcessResult *result,
75
+ void *context);
76
+ } ArcaneCoreProcessCallbacks;
77
+
78
+ /*
79
+ * Start one executable, with an explicit NULL-terminated argv (including argv[0])
80
+ * and working directory. Relative executable paths resolve after chdir; PATH
81
+ * search, shell commands and OS/product defaults are not used. The child inherits
82
+ * the host environment. This call copies its arguments and starts background
83
+ * ownership; it does not wait for launch, I/O or child exit. Callbacks may begin
84
+ * before this function returns. A NULL result sets errno and emits no callbacks.
85
+ * Once a handle is returned, asynchronous launch failures use error/complete.
86
+ * The caller owns one reference. No global signal disposition is changed.
87
+ */
88
+ ArcaneCoreProcess *arcane_core_process_start(
89
+ const char *executable,
90
+ const char *const argv[],
91
+ const char *working_directory,
92
+ const ArcaneCoreProcessCallbacks *callbacks,
93
+ void *context);
94
+
95
+ /*
96
+ * Copy and queue a complete opaque JSON body. Length exists only for transport
97
+ * framing and is not a product limit. The caller may release its input after
98
+ * return. Zero accepts the write; a POSIX error rejects it synchronously and
99
+ * written() is then not called. Concurrent calls are ordered when queued.
100
+ */
101
+ int arcane_core_process_send(ArcaneCoreProcess *process, const void *json,
102
+ size_t length, void *write_context);
103
+
104
+ /*
105
+ * Idempotent, nonblocking graceful close: reject new sends, finish the accepted
106
+ * write queue, then close child stdin. stdout/stderr remain open until EOF and
107
+ * the child is observed to exit. There is no forced termination or deadline.
108
+ * Accepted service work may continue after the renderer closes. Renderer request
109
+ * cancellation is a host decision sent through the public Core control protocol.
110
+ */
111
+ void arcane_core_process_close(ArcaneCoreProcess *process);
112
+
113
+ /*
114
+ * References permit host/UI ownership independent of background completion.
115
+ * retain() requires an already-live reference. release() relinquishes one; the
116
+ * last external release does not cancel work or close input. Call close() when
117
+ * the host intends shutdown, and keep callback context alive until completion.
118
+ * No function may race use of its own final reference against release().
119
+ */
120
+ void arcane_core_process_retain(ArcaneCoreProcess *process);
121
+ void arcane_core_process_release(ArcaneCoreProcess *process);
122
+
123
+ #ifdef __cplusplus
124
+ }
125
+ #endif
126
+
127
+ #endif