@ai-ecoverse/slicc-kernel 1.108.5 → 1.110.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.
@@ -2645,14 +2645,14 @@ var WsReader = class {
2645
2645
  }
2646
2646
  };
2647
2647
  function switchingHead(accept2, protocol = "") {
2648
- const chosen = protocol ? `Sec-WebSocket-Protocol: ${protocol}\r
2648
+ const chosen2 = protocol ? `Sec-WebSocket-Protocol: ${protocol}\r
2649
2649
  ` : "";
2650
2650
  return latin1Bytes(
2651
2651
  `HTTP/1.1 101 Switching Protocols\r
2652
2652
  Upgrade: websocket\r
2653
2653
  Connection: Upgrade\r
2654
2654
  Sec-WebSocket-Accept: ${accept2}\r
2655
- ${chosen}\r
2655
+ ${chosen2}\r
2656
2656
  `
2657
2657
  );
2658
2658
  }
@@ -6108,7 +6108,7 @@ function satisfies(version, range2) {
6108
6108
  }
6109
6109
 
6110
6110
  // src/version.ts
6111
- var KERNEL_VERSION = true ? "1.108.5" : "0.0.0-dev";
6111
+ var KERNEL_VERSION = true ? "1.110.0" : "0.0.0-dev";
6112
6112
  var OS_RELEASE = "6.0.0-slicc";
6113
6113
  function procVersion() {
6114
6114
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -6156,6 +6156,37 @@ function argsOf(pkg, raw) {
6156
6156
  if (!raw.every((arg) => typeof arg === "string")) return void 0;
6157
6157
  return raw.map((arg) => arg.replace(PACKAGE, pkg));
6158
6158
  }
6159
+ var warnedVariants = /* @__PURE__ */ new Set();
6160
+ function variantOf(pkg, raw) {
6161
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
6162
+ const { env, wasm } = raw;
6163
+ const path = inside(pkg, wasm);
6164
+ if (!path || !env || typeof env !== "object" || Array.isArray(env)) return void 0;
6165
+ const pairs = Object.entries(env);
6166
+ if (pairs.length === 0) return void 0;
6167
+ if (!pairs.every(([key, value2]) => ENV_KEY.test(key) && typeof value2 === "string")) {
6168
+ return void 0;
6169
+ }
6170
+ return { env: Object.fromEntries(pairs), wasm: path };
6171
+ }
6172
+ function variantsOf(pkg, name2, raw) {
6173
+ if (raw === void 0) return void 0;
6174
+ const variants = Array.isArray(raw) ? raw.map((one) => variantOf(pkg, one)) : [void 0];
6175
+ if (variants.every((one) => one !== void 0)) {
6176
+ return variants.length > 0 ? variants : void 0;
6177
+ }
6178
+ const key = `${pkg}:${name2}:${JSON.stringify(raw)}`;
6179
+ if (!warnedVariants.has(key)) {
6180
+ warnedVariants.add(key);
6181
+ console.warn(`slicc-kernel: ${pkg}: ${name2}: ignoring malformed "variants", running its wasm`);
6182
+ }
6183
+ return void 0;
6184
+ }
6185
+ function variantFor(variants, env) {
6186
+ return variants?.find(
6187
+ (one) => Object.entries(one.env).every(([key, value2]) => env[key] === value2)
6188
+ )?.wasm;
6189
+ }
6159
6190
  function abiOf(raw, fallback) {
6160
6191
  const abi = raw ?? fallback;
6161
6192
  return abi === "emscripten" || abi === "wasi" || abi === "js" ? abi : void 0;
@@ -6189,6 +6220,7 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
6189
6220
  const glue = abi === "wasi" ? wasm : inside(pkg, raw.glue);
6190
6221
  if (!abi || !glue || !wasm) return void 0;
6191
6222
  const imports = abi === "wasi" ? inside(pkg, raw.imports) : void 0;
6223
+ const variants = variantsOf(pkg, name2, raw.variants);
6192
6224
  return {
6193
6225
  name: name2,
6194
6226
  abi,
@@ -6200,7 +6232,8 @@ function commandOf(pkg, name2, raw, packageAbi, shared) {
6200
6232
  ...abi === "wasi" && raw.preopenRoot === true ? { preopenRoot: true } : {},
6201
6233
  ...args ? { args } : {},
6202
6234
  ...withEnv,
6203
- ...imports ? { imports } : {}
6235
+ ...imports ? { imports } : {},
6236
+ ...variants ? { variants } : {}
6204
6237
  };
6205
6238
  }
6206
6239
  function commandsOf(pkg, manifest) {
@@ -6467,245 +6500,116 @@ function binfmtOf(binfmts, path) {
6467
6500
  return best === void 0 ? void 0 : binfmts.get(best);
6468
6501
  }
6469
6502
 
6470
- // src/fs/unlinked.ts
6471
- var UNLINKED_DIR = ".slicc-unlinked";
6472
- function webLocks() {
6473
- return globalThis.navigator?.locks;
6474
- }
6475
- var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
6476
- var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
6477
- var lockName = (instance) => `slicc-unlinked:${instance}`;
6478
- var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
6479
- var PathGate = class {
6480
- tails = /* @__PURE__ */ new Map();
6481
- async acquire(path) {
6482
- const prev = this.tails.get(path) ?? Promise.resolve();
6483
- const { promise, resolve: resolve2 } = Promise.withResolvers();
6484
- const tail2 = prev.then(() => promise);
6485
- this.tails.set(path, tail2);
6486
- await prev;
6487
- return () => {
6488
- resolve2();
6489
- if (this.tails.get(path) === tail2) this.tails.delete(path);
6490
- };
6491
- }
6492
- async run(path, op) {
6493
- const release = await this.acquire(path);
6494
- try {
6495
- return await op();
6496
- } finally {
6497
- release();
6498
- }
6499
- }
6503
+ // src/cron/command.ts
6504
+ var CRONTAB_PATH = "/usr/bin/crontab";
6505
+ var CRONTAB_COMMAND = {
6506
+ name: "crontab",
6507
+ abi: "js",
6508
+ glue: CRONTAB_PATH,
6509
+ wasm: CRONTAB_PATH,
6510
+ argv0: "crontab"
6500
6511
  };
6501
- var UnlinkedKeeper = class {
6502
- fs;
6503
- holders;
6504
- rootOf;
6505
- locks;
6506
- busy;
6507
- waiting = /* @__PURE__ */ new Set();
6508
- owner;
6509
- instance;
6510
- gate = new PathGate();
6511
- queue = Promise.resolve();
6512
- count = 0;
6513
- links = /* @__PURE__ */ new Map();
6514
- linked = /* @__PURE__ */ new Map();
6515
- dirOwner;
6516
- constructor(fs, holders, rootOf, options = {}) {
6517
- const { locks, owner } = options;
6518
- this.dirOwner = options.dirOwner;
6519
- this.fs = fs;
6520
- this.holders = holders;
6521
- this.rootOf = rootOf;
6522
- this.locks = locks;
6523
- this.busy = options.busy ?? (() => false);
6524
- this.owner = owner;
6525
- this.instance = owner ? `${owner.id}~${crypto.randomUUID()}` : crypto.randomUUID();
6526
- void locks?.request(lockName(this.instance), () => new Promise(() => void 0));
6527
- }
6528
- serial(op) {
6529
- const next3 = this.queue.then(op);
6530
- this.queue = next3.catch(() => void 0);
6531
- return next3;
6532
- }
6533
- wrap(fs = this.fs) {
6534
- const keeper = this;
6535
- const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
6536
- const counted2 = (st) => {
6537
- const hidden = keeper.links.get(`${st.dev ?? 0}:${st.ino}`) ?? 0;
6538
- return hidden === 0 || st.nlink === void 0 ? st : { ...st, nlink: Math.max(0, st.nlink - hidden) };
6539
- };
6540
- return {
6541
- ...fs,
6542
- links: (path) => mayLink(fs, path),
6543
- stat: async (path) => counted2(await fs.stat(path)),
6544
- lstat: async (path) => counted2(await fs.lstat(path)),
6545
- async rm(path, options) {
6546
- const target = fs.resolvePath("/", path);
6547
- const pinned = await keeper.pin(target);
6548
- if (pinned.moved.has(target)) pinned.commit();
6549
- else await keeper.settle(pinned, () => fs.rm(path, options));
6550
- },
6551
- async rename(from, to) {
6552
- const target = fs.resolvePath("/", to);
6553
- if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
6554
- const pinned = await keeper.pin(target);
6555
- await keeper.settle(pinned, () => fs.rename(from, to));
6556
- },
6557
- readdir: async (path) => visible(await fs.readdir(path)),
6558
- readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])
6559
- };
6512
+ var CRONTAB_SOURCE = `export default async function crontab(ctx) {
6513
+ const say = (fd, text) => ctx.write(fd, text).catch(() => undefined);
6514
+ const usage = async () => {
6515
+ await say(2, "usage: crontab [-u user] { -l | -r | -e | file | - }\\n");
6516
+ return 1;
6517
+ };
6518
+ const args = ctx.argv.slice(1);
6519
+ let user;
6520
+ let action;
6521
+ let file;
6522
+ for (let i = 0; i < args.length; i++) {
6523
+ const arg = args[i];
6524
+ if (arg === "-u") {
6525
+ user = args[++i];
6526
+ if (user === undefined) return usage();
6527
+ } else if (arg === "-l" || arg === "-r" || arg === "-e") {
6528
+ if (action !== undefined) return usage();
6529
+ action = arg.slice(1);
6530
+ } else if (arg === "-" || !arg.startsWith("-")) {
6531
+ if (file !== undefined) return usage();
6532
+ file = arg;
6533
+ } else return usage();
6534
+ }
6535
+ if ((action === undefined) === (file === undefined)) return usage();
6536
+ const call = (c) => ctx.crontab(user === undefined ? c : { ...c, user });
6537
+ const failed = async (answer) => {
6538
+ await say(2, answer.error + "\\n");
6539
+ return 1;
6540
+ };
6541
+ const decode = (bytes) => new TextDecoder().decode(bytes);
6542
+ if (action === "l") {
6543
+ const answer = await call({ action: "list" });
6544
+ if (answer.error !== undefined) return failed(answer);
6545
+ await say(1, answer.text);
6546
+ return 0;
6547
+ }
6548
+ if (action === "r") {
6549
+ const answer = await call({ action: "remove" });
6550
+ return answer.error !== undefined ? failed(answer) : 0;
6551
+ }
6552
+ if (action === "e") return edit();
6553
+ let text;
6554
+ try {
6555
+ if (file === "-") {
6556
+ const stream = new TextDecoder();
6557
+ const parts = [];
6558
+ for (;;) {
6559
+ const chunk = await ctx.read(0);
6560
+ if (chunk.length === 0) break;
6561
+ parts.push(stream.decode(chunk, { stream: true }));
6562
+ }
6563
+ text = parts.join("") + stream.decode();
6564
+ } else text = decode(await ctx.fs.readFile(ctx.resolve(file)));
6565
+ } catch (err) {
6566
+ await say(2, "crontab: " + file + ": " + (err && err.code ? err.code : String(err)) + "\\n");
6567
+ return 1;
6560
6568
  }
6561
- async settle(pinned, op) {
6569
+ const answer = await call({ action: "install", text });
6570
+ return answer.error !== undefined ? failed(answer) : 0;
6571
+
6572
+ async function edit() {
6573
+ const editor = ctx.env.VISUAL || ctx.env.EDITOR;
6574
+ if (!editor) {
6575
+ await say(2, "crontab: set VISUAL or EDITOR to edit, or install a file with crontab <file>\\n");
6576
+ return 1;
6577
+ }
6578
+ const current = await call({ action: "list" });
6579
+ const before = current.error === undefined ? current.text : "";
6580
+ if (current.error !== undefined && !current.error.startsWith("no crontab for ")) return failed(current);
6581
+ const tmp = "/tmp/crontab." + ctx.pid + "." + crypto.randomUUID();
6562
6582
  try {
6563
- await op();
6583
+ const file = await ctx.open(tmp, { write: true, create: true, exclusive: true, mode: 0o600 });
6584
+ await file.write(0, new TextEncoder().encode(before));
6585
+ await file.close();
6564
6586
  } catch (err) {
6565
- await pinned.rollback();
6566
- throw err;
6587
+ await say(2, "crontab: " + tmp + ": " + (err && err.code ? err.code : String(err)) + "\\n");
6588
+ return 1;
6567
6589
  }
6568
- pinned.commit();
6569
- }
6570
- async pin(target) {
6571
- const held = /* @__PURE__ */ new Map();
6572
- for (const holder of this.holders) {
6573
- if (holder.owns(target)) continue;
6574
- for (const path of holder.held()) {
6575
- if (!within4(path, target) || holder.isKept(path)) continue;
6576
- held.set(path, [...held.get(path) ?? [], holder]);
6577
- }
6590
+ const child = await ctx.spawn({ argv: ["/bin/bash", "-c", editor + ' "$1"', "crontab", tmp] });
6591
+ const ended = await ctx.wait(child.pid);
6592
+ if (ended.status !== 0) {
6593
+ await say(2, "crontab: the editor exited with status " + ended.status + ", crontab not changed\\n");
6594
+ await ctx.fs.rm(tmp).catch(() => undefined);
6595
+ return 1;
6578
6596
  }
6579
- const paths = [...held.keys()].sort();
6580
- const releases = [];
6581
- for (const path of paths) releases.push(await this.gate.acquire(path));
6582
- const hidden = /* @__PURE__ */ new Map();
6583
- for (const path of paths) {
6584
- const at = await this.hide(path);
6585
- if (at !== void 0) hidden.set(path, at);
6586
- }
6587
- const done4 = () => {
6588
- for (const release of releases) release();
6589
- };
6590
- const files = /* @__PURE__ */ new Map();
6591
- for (const [path, at] of hidden) {
6592
- const holders = held.get(path);
6593
- const file = this.keptFile(at, { refs: holders.length + 1 });
6594
- files.set(path, file);
6595
- for (const holder of holders) holder.keep(path, file);
6597
+ const after = decode(await ctx.fs.readFile(tmp));
6598
+ if (after === before) {
6599
+ await say(2, "crontab: no changes made to crontab\\n");
6600
+ await ctx.fs.rm(tmp).catch(() => undefined);
6601
+ return 0;
6596
6602
  }
6597
- return {
6598
- moved: new Set(hidden.keys()),
6599
- commit: () => {
6600
- for (const file of files.values()) file.release();
6601
- done4();
6602
- },
6603
- rollback: async () => {
6604
- for (const [path, file] of files) {
6605
- for (const holder of held.get(path)) holder.unkeep(path, file);
6606
- await this.serial(async () => {
6607
- await Promise.allSettled([this.fs.rename(file.hidden, path)]);
6608
- this.unlinkedName(file.hidden);
6609
- await this.tidy(parentOf5(file.hidden));
6610
- });
6611
- }
6612
- done4();
6613
- }
6614
- };
6615
- }
6616
- keptFile(hidden, entry) {
6617
- return {
6618
- hidden,
6619
- release: () => {
6620
- if (--entry.refs === 0) void this.serial(() => this.drop(hidden));
6621
- }
6622
- };
6623
- }
6624
- hide(path) {
6625
- return this.serial(async () => {
6626
- const dir = dirOf(this.rootOf(path));
6627
- const hidden = `${dir}/${this.instance}.${++this.count}`;
6628
- try {
6629
- const owner = this.dirOwner;
6630
- await this.fs.mkdir(dir, { recursive: true, ...owner ? { owner } : {} });
6631
- await this.fs.rename(path, hidden);
6632
- await this.linkedName(hidden);
6633
- return hidden;
6634
- } catch {
6635
- await this.tidy(dir);
6636
- return void 0;
6637
- }
6638
- });
6639
- }
6640
- closed(path) {
6641
- if (this.waiting.delete(path)) void this.serial(() => this.drop(path));
6642
- }
6643
- async drop(hidden) {
6644
- if (this.busy(hidden)) {
6645
- this.waiting.add(hidden);
6646
- return;
6603
+ const answer = await call({ action: "install", text: after });
6604
+ if (answer.error !== undefined) {
6605
+ await say(2, answer.error + "\\ncrontab: your edit is kept in " + tmp + "\\n");
6606
+ return 1;
6647
6607
  }
6648
- await this.fs.rm(hidden).catch(() => void 0);
6649
- this.unlinkedName(hidden);
6650
- await this.tidy(parentOf5(hidden));
6651
- }
6652
- async linkedName(hidden) {
6653
- if (!mayLink(this.fs, hidden)) return;
6654
- const st = await this.fs.lstat(hidden).catch(() => void 0);
6655
- if (st?.nlink === void 0) return;
6656
- const id2 = `${st.dev ?? 0}:${st.ino}`;
6657
- this.linked.set(hidden, id2);
6658
- this.links.set(id2, (this.links.get(id2) ?? 0) + 1);
6608
+ await ctx.fs.rm(tmp).catch(() => undefined);
6609
+ return 0;
6659
6610
  }
6660
- unlinkedName(hidden) {
6661
- const id2 = this.linked.get(hidden);
6662
- if (id2 === void 0) return;
6663
- this.linked.delete(hidden);
6664
- const left = (this.links.get(id2) ?? 1) - 1;
6665
- if (left > 0) this.links.set(id2, left);
6666
- else this.links.delete(id2);
6667
- }
6668
- async tidy(dir) {
6669
- const left = await this.fs.readdir(dir).catch(() => [UNLINKED_DIR]);
6670
- if (left.length === 0) await this.fs.rm(dir, { recursive: true }).catch(() => void 0);
6671
- }
6672
- sweep(root) {
6673
- const query = this.locks?.query?.bind(this.locks);
6674
- const owner = this.owner;
6675
- if (!query && !owner) return Promise.resolve();
6676
- return this.serial(async () => {
6677
- const dir = dirOf(root);
6678
- const names = await this.fs.readdir(dir).catch(() => void 0);
6679
- if (!names) return;
6680
- const held = query && new Set(((await query()).held ?? []).map((lock) => lock.name));
6681
- for (const name2 of names) {
6682
- const id2 = name2.slice(0, name2.lastIndexOf("."));
6683
- const at = id2.indexOf("~");
6684
- const dead = id2 !== this.instance && (at < 0 ? held !== void 0 && !held.has(lockName(id2)) : owner?.alive(id2.slice(0, at)) === false);
6685
- if (dead) await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
6686
- }
6687
- await this.tidy(dir);
6688
- });
6689
- }
6690
- };
6691
-
6692
- // src/cron/clock.ts
6693
- var systemCronClock = {
6694
- now: () => Date.now(),
6695
- sleep(until, signal) {
6696
- if (signal.aborted) return Promise.resolve();
6697
- return new Promise((resolve2) => {
6698
- const done4 = () => {
6699
- clearTimeout(timer);
6700
- signal.removeEventListener("abort", done4);
6701
- resolve2();
6702
- };
6703
- const timer = setTimeout(done4, Math.max(0, until - Date.now()));
6704
- timer.unref?.();
6705
- signal.addEventListener("abort", done4, { once: true });
6706
- });
6707
- }
6708
- };
6611
+ }
6612
+ `;
6709
6613
 
6710
6614
  // src/cron/schedule.ts
6711
6615
  var MISSED_CAP = 1e5;
@@ -6877,79 +6781,378 @@ function hashId(text) {
6877
6781
  a = Math.imul(a ^ c, 16777619);
6878
6782
  b = Math.imul(b ^ c, 1540483477);
6879
6783
  }
6880
- const hex3 = (n) => (n >>> 0).toString(16).padStart(8, "0");
6881
- return hex3(a) + hex3(b);
6882
- }
6883
- function split2(text, count2) {
6884
- const words2 = [];
6885
- let rest2 = text;
6886
- for (let i = 0; i < count2; i++) {
6887
- const match = /^(\S+)\s+/.exec(rest2);
6888
- if (!match) return void 0;
6889
- words2.push(match[1]);
6890
- rest2 = rest2.slice(match[0].length);
6784
+ const hex3 = (n) => (n >>> 0).toString(16).padStart(8, "0");
6785
+ return hex3(a) + hex3(b);
6786
+ }
6787
+ function split2(text, count2) {
6788
+ const words2 = [];
6789
+ let rest2 = text;
6790
+ for (let i = 0; i < count2; i++) {
6791
+ const match = /^(\S+)\s+/.exec(rest2);
6792
+ if (!match) return void 0;
6793
+ words2.push(match[1]);
6794
+ rest2 = rest2.slice(match[0].length);
6795
+ }
6796
+ return [words2, rest2.trim()];
6797
+ }
6798
+ function jobOf(content, pending, ctx) {
6799
+ const fields = content.startsWith("@") ? 1 : 5;
6800
+ const parts2 = split2(content, fields + (ctx.system ? 1 : 0));
6801
+ if (!parts2 || parts2[1] === "") throw new Error("no command");
6802
+ const [words2, command] = parts2;
6803
+ const schedule = words2.slice(0, fields).join(" ");
6804
+ const when = parseWhen(schedule);
6805
+ const user = ctx.system ? words2[fields] : void 0;
6806
+ if (user !== void 0 && !USER_NAME.test(user)) throw new Error(`bad user name ${user}`);
6807
+ if (pending !== void 0 && !JOB_NAME.test(pending))
6808
+ throw new Error(`bad job name "${pending}"`);
6809
+ if (pending !== void 0 && ctx.names.has(pending))
6810
+ throw new Error(`job name ${pending} is taken`);
6811
+ const hashed = hashId(`${ctx.path}
6812
+ ${user ?? ""}
6813
+ ${schedule}
6814
+ ${command}`);
6815
+ let id2 = pending ?? hashed;
6816
+ for (let n = 2; pending === void 0 && ctx.names.has(id2); n++) id2 = `${hashed}.${n}`;
6817
+ ctx.names.add(id2);
6818
+ return {
6819
+ id: id2,
6820
+ ...pending !== void 0 ? { name: pending } : {},
6821
+ schedule,
6822
+ when,
6823
+ ...user !== void 0 ? { user } : {},
6824
+ command
6825
+ };
6826
+ }
6827
+ function parseCrontab(text, path, system = false) {
6828
+ const jobs = [];
6829
+ const errors = [];
6830
+ const env = {};
6831
+ const ctx = { path, system, names: /* @__PURE__ */ new Set() };
6832
+ let name2;
6833
+ for (const [index, raw] of text.split("\n").entries()) {
6834
+ const line = index + 1;
6835
+ const content = raw.trim();
6836
+ const named = NAME_LINE.exec(content);
6837
+ if (named) {
6838
+ name2 = named[1];
6839
+ continue;
6840
+ }
6841
+ const pending = name2;
6842
+ name2 = void 0;
6843
+ if (content === "" || content.startsWith("#")) continue;
6844
+ const assign = ENV_LINE.exec(content);
6845
+ if (assign) {
6846
+ env[assign[1]] = unquote(assign[2]);
6847
+ continue;
6848
+ }
6849
+ try {
6850
+ jobs.push({ line, ...jobOf(content, pending, ctx), env: { ...env } });
6851
+ } catch (err) {
6852
+ errors.push({ line, message: err.message });
6853
+ }
6854
+ }
6855
+ return { jobs, errors };
6856
+ }
6857
+
6858
+ // src/cron/crontab.ts
6859
+ var CRONTAB_MAX = 256 * 1024;
6860
+ var ACTIONS = /* @__PURE__ */ new Set(["list", "remove", "install"]);
6861
+ function crontabCall(req) {
6862
+ const { action, user, text } = req;
6863
+ if (typeof action !== "string" || !ACTIONS.has(action)) return void 0;
6864
+ if (user !== void 0 && typeof user !== "string") return void 0;
6865
+ if (action === "install" && typeof text !== "string") return void 0;
6866
+ return {
6867
+ op: "proc-crontab",
6868
+ action,
6869
+ ...user !== void 0 ? { user } : {},
6870
+ ...action === "install" ? { text } : {}
6871
+ };
6872
+ }
6873
+ var Crontabs = class {
6874
+ o;
6875
+ constructor(options) {
6876
+ this.o = options;
6877
+ }
6878
+ async request(cred, call) {
6879
+ const caller = await this.o.lookup(cred.ruid);
6880
+ if (!caller) return { error: `crontab: your uid ${cred.ruid} is not in /etc/passwd` };
6881
+ let target = caller;
6882
+ if (call.user !== void 0 && call.user !== caller.name) {
6883
+ if (cred.euid !== 0) return { error: "crontab: must be privileged to use -u" };
6884
+ const found = await this.o.lookup(call.user);
6885
+ if (!found) return { error: `crontab: user ${call.user} unknown` };
6886
+ target = found;
6887
+ }
6888
+ const path = `${SPOOL_DIR}/${target.name}`;
6889
+ const none = { error: `no crontab for ${target.name}` };
6890
+ const { fs } = this.o;
6891
+ if (call.action === "install") return this.install(path, target, call.text);
6892
+ if (!await fs.exists(path)) return none;
6893
+ if (call.action === "list") return { text: await fs.readFile(path) };
6894
+ await fs.rm(path);
6895
+ return { text: "" };
6896
+ }
6897
+ async install(path, target, text) {
6898
+ if (text.length > CRONTAB_MAX) return { error: "crontab: the table is too long" };
6899
+ const { errors } = parseCrontab(text, path);
6900
+ if (errors.length > 0) {
6901
+ const lines2 = errors.map(({ line, message: message3 }) => `"-":${line}: ${message3}`);
6902
+ return { error: ["crontab: errors in crontab file, cannot install", ...lines2].join("\n") };
6903
+ }
6904
+ const { fs, owners } = this.o;
6905
+ if (!await fs.exists(SPOOL_DIR)) {
6906
+ await fs.mkdir(SPOOL_DIR, { recursive: true });
6907
+ if (owners) await fs.chown?.(SPOOL_DIR, { uid: 0, gid: 0, mode: 448, follow: false });
6908
+ }
6909
+ const fresh2 = `${SPOOL_DIR}/.${target.name}.${crypto.randomUUID()}.new`;
6910
+ const owner = owners ? { uid: target.uid, gid: target.gid, mode: 384 } : void 0;
6911
+ await fs.writeFile(fresh2, text, owner);
6912
+ await fs.rename(fresh2, path);
6913
+ return { text: "" };
6914
+ }
6915
+ };
6916
+
6917
+ // src/fs/unlinked.ts
6918
+ var UNLINKED_DIR = ".slicc-unlinked";
6919
+ function webLocks() {
6920
+ return globalThis.navigator?.locks;
6921
+ }
6922
+ var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
6923
+ var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
6924
+ var lockName = (instance) => `slicc-unlinked:${instance}`;
6925
+ var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
6926
+ var PathGate = class {
6927
+ tails = /* @__PURE__ */ new Map();
6928
+ async acquire(path) {
6929
+ const prev = this.tails.get(path) ?? Promise.resolve();
6930
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
6931
+ const tail2 = prev.then(() => promise);
6932
+ this.tails.set(path, tail2);
6933
+ await prev;
6934
+ return () => {
6935
+ resolve2();
6936
+ if (this.tails.get(path) === tail2) this.tails.delete(path);
6937
+ };
6938
+ }
6939
+ async run(path, op) {
6940
+ const release = await this.acquire(path);
6941
+ try {
6942
+ return await op();
6943
+ } finally {
6944
+ release();
6945
+ }
6946
+ }
6947
+ };
6948
+ var UnlinkedKeeper = class {
6949
+ fs;
6950
+ holders;
6951
+ rootOf;
6952
+ locks;
6953
+ busy;
6954
+ waiting = /* @__PURE__ */ new Set();
6955
+ owner;
6956
+ instance;
6957
+ gate = new PathGate();
6958
+ queue = Promise.resolve();
6959
+ count = 0;
6960
+ links = /* @__PURE__ */ new Map();
6961
+ linked = /* @__PURE__ */ new Map();
6962
+ dirOwner;
6963
+ constructor(fs, holders, rootOf, options = {}) {
6964
+ const { locks, owner } = options;
6965
+ this.dirOwner = options.dirOwner;
6966
+ this.fs = fs;
6967
+ this.holders = holders;
6968
+ this.rootOf = rootOf;
6969
+ this.locks = locks;
6970
+ this.busy = options.busy ?? (() => false);
6971
+ this.owner = owner;
6972
+ this.instance = owner ? `${owner.id}~${crypto.randomUUID()}` : crypto.randomUUID();
6973
+ void locks?.request(lockName(this.instance), () => new Promise(() => void 0));
6974
+ }
6975
+ serial(op) {
6976
+ const next3 = this.queue.then(op);
6977
+ this.queue = next3.catch(() => void 0);
6978
+ return next3;
6979
+ }
6980
+ wrap(fs = this.fs) {
6981
+ const keeper = this;
6982
+ const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
6983
+ const counted2 = (st) => {
6984
+ const hidden = keeper.links.get(`${st.dev ?? 0}:${st.ino}`) ?? 0;
6985
+ return hidden === 0 || st.nlink === void 0 ? st : { ...st, nlink: Math.max(0, st.nlink - hidden) };
6986
+ };
6987
+ return {
6988
+ ...fs,
6989
+ links: (path) => mayLink(fs, path),
6990
+ stat: async (path) => counted2(await fs.stat(path)),
6991
+ lstat: async (path) => counted2(await fs.lstat(path)),
6992
+ async rm(path, options) {
6993
+ const target = fs.resolvePath("/", path);
6994
+ const pinned = await keeper.pin(target);
6995
+ if (pinned.moved.has(target)) pinned.commit();
6996
+ else await keeper.settle(pinned, () => fs.rm(path, options));
6997
+ },
6998
+ async rename(from, to) {
6999
+ const target = fs.resolvePath("/", to);
7000
+ if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
7001
+ const pinned = await keeper.pin(target);
7002
+ await keeper.settle(pinned, () => fs.rename(from, to));
7003
+ },
7004
+ readdir: async (path) => visible(await fs.readdir(path)),
7005
+ readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])
7006
+ };
6891
7007
  }
6892
- return [words2, rest2.trim()];
6893
- }
6894
- function jobOf(content, pending, ctx) {
6895
- const fields = content.startsWith("@") ? 1 : 5;
6896
- const parts2 = split2(content, fields + (ctx.system ? 1 : 0));
6897
- if (!parts2 || parts2[1] === "") throw new Error("no command");
6898
- const [words2, command] = parts2;
6899
- const schedule = words2.slice(0, fields).join(" ");
6900
- const when = parseWhen(schedule);
6901
- const user = ctx.system ? words2[fields] : void 0;
6902
- if (user !== void 0 && !USER_NAME.test(user)) throw new Error(`bad user name ${user}`);
6903
- if (pending !== void 0 && !JOB_NAME.test(pending))
6904
- throw new Error(`bad job name "${pending}"`);
6905
- if (pending !== void 0 && ctx.names.has(pending))
6906
- throw new Error(`job name ${pending} is taken`);
6907
- const hashed = hashId(`${ctx.path}
6908
- ${user ?? ""}
6909
- ${schedule}
6910
- ${command}`);
6911
- let id2 = pending ?? hashed;
6912
- for (let n = 2; pending === void 0 && ctx.names.has(id2); n++) id2 = `${hashed}.${n}`;
6913
- ctx.names.add(id2);
6914
- return {
6915
- id: id2,
6916
- ...pending !== void 0 ? { name: pending } : {},
6917
- schedule,
6918
- when,
6919
- ...user !== void 0 ? { user } : {},
6920
- command
6921
- };
6922
- }
6923
- function parseCrontab(text, path, system = false) {
6924
- const jobs = [];
6925
- const errors = [];
6926
- const env = {};
6927
- const ctx = { path, system, names: /* @__PURE__ */ new Set() };
6928
- let name2;
6929
- for (const [index, raw] of text.split("\n").entries()) {
6930
- const line = index + 1;
6931
- const content = raw.trim();
6932
- const named = NAME_LINE.exec(content);
6933
- if (named) {
6934
- name2 = named[1];
6935
- continue;
6936
- }
6937
- const pending = name2;
6938
- name2 = void 0;
6939
- if (content === "" || content.startsWith("#")) continue;
6940
- const assign = ENV_LINE.exec(content);
6941
- if (assign) {
6942
- env[assign[1]] = unquote(assign[2]);
6943
- continue;
6944
- }
7008
+ async settle(pinned, op) {
6945
7009
  try {
6946
- jobs.push({ line, ...jobOf(content, pending, ctx), env: { ...env } });
7010
+ await op();
6947
7011
  } catch (err) {
6948
- errors.push({ line, message: err.message });
7012
+ await pinned.rollback();
7013
+ throw err;
6949
7014
  }
7015
+ pinned.commit();
6950
7016
  }
6951
- return { jobs, errors };
6952
- }
7017
+ async pin(target) {
7018
+ const held = /* @__PURE__ */ new Map();
7019
+ for (const holder of this.holders) {
7020
+ if (holder.owns(target)) continue;
7021
+ for (const path of holder.held()) {
7022
+ if (!within4(path, target) || holder.isKept(path)) continue;
7023
+ held.set(path, [...held.get(path) ?? [], holder]);
7024
+ }
7025
+ }
7026
+ const paths = [...held.keys()].sort();
7027
+ const releases = [];
7028
+ for (const path of paths) releases.push(await this.gate.acquire(path));
7029
+ const hidden = /* @__PURE__ */ new Map();
7030
+ for (const path of paths) {
7031
+ const at = await this.hide(path);
7032
+ if (at !== void 0) hidden.set(path, at);
7033
+ }
7034
+ const done4 = () => {
7035
+ for (const release of releases) release();
7036
+ };
7037
+ const files = /* @__PURE__ */ new Map();
7038
+ for (const [path, at] of hidden) {
7039
+ const holders = held.get(path);
7040
+ const file = this.keptFile(at, { refs: holders.length + 1 });
7041
+ files.set(path, file);
7042
+ for (const holder of holders) holder.keep(path, file);
7043
+ }
7044
+ return {
7045
+ moved: new Set(hidden.keys()),
7046
+ commit: () => {
7047
+ for (const file of files.values()) file.release();
7048
+ done4();
7049
+ },
7050
+ rollback: async () => {
7051
+ for (const [path, file] of files) {
7052
+ for (const holder of held.get(path)) holder.unkeep(path, file);
7053
+ await this.serial(async () => {
7054
+ await Promise.allSettled([this.fs.rename(file.hidden, path)]);
7055
+ this.unlinkedName(file.hidden);
7056
+ await this.tidy(parentOf5(file.hidden));
7057
+ });
7058
+ }
7059
+ done4();
7060
+ }
7061
+ };
7062
+ }
7063
+ keptFile(hidden, entry) {
7064
+ return {
7065
+ hidden,
7066
+ release: () => {
7067
+ if (--entry.refs === 0) void this.serial(() => this.drop(hidden));
7068
+ }
7069
+ };
7070
+ }
7071
+ hide(path) {
7072
+ return this.serial(async () => {
7073
+ const dir = dirOf(this.rootOf(path));
7074
+ const hidden = `${dir}/${this.instance}.${++this.count}`;
7075
+ try {
7076
+ const owner = this.dirOwner;
7077
+ await this.fs.mkdir(dir, { recursive: true, ...owner ? { owner } : {} });
7078
+ await this.fs.rename(path, hidden);
7079
+ await this.linkedName(hidden);
7080
+ return hidden;
7081
+ } catch {
7082
+ await this.tidy(dir);
7083
+ return void 0;
7084
+ }
7085
+ });
7086
+ }
7087
+ closed(path) {
7088
+ if (this.waiting.delete(path)) void this.serial(() => this.drop(path));
7089
+ }
7090
+ async drop(hidden) {
7091
+ if (this.busy(hidden)) {
7092
+ this.waiting.add(hidden);
7093
+ return;
7094
+ }
7095
+ await this.fs.rm(hidden).catch(() => void 0);
7096
+ this.unlinkedName(hidden);
7097
+ await this.tidy(parentOf5(hidden));
7098
+ }
7099
+ async linkedName(hidden) {
7100
+ if (!mayLink(this.fs, hidden)) return;
7101
+ const st = await this.fs.lstat(hidden).catch(() => void 0);
7102
+ if (st?.nlink === void 0) return;
7103
+ const id2 = `${st.dev ?? 0}:${st.ino}`;
7104
+ this.linked.set(hidden, id2);
7105
+ this.links.set(id2, (this.links.get(id2) ?? 0) + 1);
7106
+ }
7107
+ unlinkedName(hidden) {
7108
+ const id2 = this.linked.get(hidden);
7109
+ if (id2 === void 0) return;
7110
+ this.linked.delete(hidden);
7111
+ const left = (this.links.get(id2) ?? 1) - 1;
7112
+ if (left > 0) this.links.set(id2, left);
7113
+ else this.links.delete(id2);
7114
+ }
7115
+ async tidy(dir) {
7116
+ const left = await this.fs.readdir(dir).catch(() => [UNLINKED_DIR]);
7117
+ if (left.length === 0) await this.fs.rm(dir, { recursive: true }).catch(() => void 0);
7118
+ }
7119
+ sweep(root) {
7120
+ const query = this.locks?.query?.bind(this.locks);
7121
+ const owner = this.owner;
7122
+ if (!query && !owner) return Promise.resolve();
7123
+ return this.serial(async () => {
7124
+ const dir = dirOf(root);
7125
+ const names = await this.fs.readdir(dir).catch(() => void 0);
7126
+ if (!names) return;
7127
+ const held = query && new Set(((await query()).held ?? []).map((lock) => lock.name));
7128
+ for (const name2 of names) {
7129
+ const id2 = name2.slice(0, name2.lastIndexOf("."));
7130
+ const at = id2.indexOf("~");
7131
+ const dead = id2 !== this.instance && (at < 0 ? held !== void 0 && !held.has(lockName(id2)) : owner?.alive(id2.slice(0, at)) === false);
7132
+ if (dead) await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
7133
+ }
7134
+ await this.tidy(dir);
7135
+ });
7136
+ }
7137
+ };
7138
+
7139
+ // src/cron/clock.ts
7140
+ var systemCronClock = {
7141
+ now: () => Date.now(),
7142
+ sleep(until, signal) {
7143
+ if (signal.aborted) return Promise.resolve();
7144
+ return new Promise((resolve2) => {
7145
+ const done4 = () => {
7146
+ clearTimeout(timer);
7147
+ signal.removeEventListener("abort", done4);
7148
+ resolve2();
7149
+ };
7150
+ const timer = setTimeout(done4, Math.max(0, until - Date.now()));
7151
+ timer.unref?.();
7152
+ signal.addEventListener("abort", done4, { once: true });
7153
+ });
7154
+ }
7155
+ };
6953
7156
 
6954
7157
  // src/cron/service.ts
6955
7158
  var CRON_DIR = "/var/spool/cron";
@@ -7125,9 +7328,12 @@ var CronService = class {
7125
7328
  const account = await this.host.lookup(job.user);
7126
7329
  if (account) out.push({ source: "system", account, job });
7127
7330
  }
7128
- const names = await fs.readdir(SPOOL_DIR).catch(() => []);
7331
+ const names = await fs.readdir(SPOOL_DIR).catch((err) => {
7332
+ if (err?.code === "ENOENT") return [];
7333
+ throw err;
7334
+ });
7129
7335
  for (const name2 of names.sort()) {
7130
- if (name2.startsWith(".") || name2.endsWith("~") || name2.endsWith(".new")) continue;
7336
+ if (name2.startsWith(".") || name2.endsWith("~")) continue;
7131
7337
  const path = `${SPOOL_DIR}/${name2}`;
7132
7338
  const account = await this.host.lookup(name2);
7133
7339
  if (!account) {
@@ -16637,6 +16843,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
16637
16843
  "proc-wait",
16638
16844
  "proc-captured",
16639
16845
  "proc-sudo",
16846
+ "proc-crontab",
16640
16847
  "proc-run",
16641
16848
  "proc-exit",
16642
16849
  "proc-kill-self",
@@ -17826,8 +18033,17 @@ var WasmProcess = class {
17826
18033
  return { ok: true, kind: "bytes", bytes: this.children.captured(req.pid, req.slot) };
17827
18034
  case "proc-sudo":
17828
18035
  return { ok: true, kind: "json", json: await this.sudo(req, cancelled) };
18036
+ case "proc-crontab":
18037
+ return { ok: true, kind: "json", json: await this.crontab(req) };
17829
18038
  }
17830
18039
  }
18040
+ async crontab(req) {
18041
+ const { crontab } = this.options;
18042
+ if (!crontab) throw new KernelError("ENOSYS");
18043
+ const call = crontabCall(req);
18044
+ if (!call) throw new KernelError("EINVAL");
18045
+ return crontab(call);
18046
+ }
17831
18047
  signalSyscall(req) {
17832
18048
  if (req.op === "sig-inherited") {
17833
18049
  return { ok: true, kind: "json", json: this.inherited & ~this.caught };
@@ -18230,6 +18446,7 @@ function processOptions(opts, header3, hooks, created3) {
18230
18446
  ...opts.writesBack ? { writesBack: opts.writesBack } : {},
18231
18447
  processes: opts.processes,
18232
18448
  ...opts.sudo ? { sudo: opts.sudo } : {},
18449
+ ...opts.crontab ? { crontab: opts.crontab } : {},
18233
18450
  openFiles: opts.openFiles,
18234
18451
  ...opts.nodes ? { nodes: opts.nodes } : {},
18235
18452
  mounts: opts.mounts,
@@ -21697,6 +21914,7 @@ var LAYOUT = [
21697
21914
  ["/usr", 493],
21698
21915
  ["/usr/local", 493]
21699
21916
  ];
21917
+ var IN_TREE = /* @__PURE__ */ new Set([SUDO_PATH, CRONTAB_PATH]);
21700
21918
  var LOAD_SAMPLE_MS = 5e3;
21701
21919
  var LOAD_WINDOWS_MS = [6e4, 3e5, 9e5];
21702
21920
  var encoder8 = new TextEncoder();
@@ -21795,9 +22013,18 @@ function targetOf(command) {
21795
22013
  ...command.env ? { env: command.env } : {},
21796
22014
  ...command.unset ? { unset: command.unset } : {},
21797
22015
  ...command.imports ? { imports: command.imports } : {},
21798
- ...command.preopenRoot ? { preopenRoot: true } : {}
22016
+ ...command.preopenRoot ? { preopenRoot: true } : {},
22017
+ ...command.variants ? { variants: command.variants } : {}
21799
22018
  };
21800
22019
  }
22020
+ function chosen(planned, env) {
22021
+ const { target } = planned;
22022
+ const wasm = variantFor(target.variants, env);
22023
+ if (wasm === void 0) return planned;
22024
+ const { asked: _asked, ...rest2 } = target;
22025
+ const glue = target.abi === "wasi" ? wasm : target.glue;
22026
+ return { ...planned, target: { ...rest2, wasm, glue } };
22027
+ }
21801
22028
  function withScriptEnv(target, command) {
21802
22029
  if (!command) return target;
21803
22030
  return {
@@ -21927,6 +22154,7 @@ var Launcher = class {
21927
22154
  boot = Date.now();
21928
22155
  bootId = crypto.randomUUID();
21929
22156
  cron;
22157
+ crontabs;
21930
22158
  terminals = 0;
21931
22159
  constructor(options) {
21932
22160
  this.usersMode = usersMode(options.users);
@@ -22127,7 +22355,9 @@ var Launcher = class {
22127
22355
  found = this.trustedAppended().then(async (appended2) => {
22128
22356
  const tiers = await this.tiers(scope, appended2);
22129
22357
  return scanCommands(this.scanned, tiers, tiers.length - appended2.length);
22130
- }).then((commands) => this.refusals(commands).set(SUDO_COMMAND.name, SUDO_COMMAND));
22358
+ }).then(
22359
+ (commands) => this.refusals(commands).set(SUDO_COMMAND.name, SUDO_COMMAND).set(CRONTAB_COMMAND.name, CRONTAB_COMMAND)
22360
+ );
22131
22361
  this.catalogs.set(scope, found);
22132
22362
  }
22133
22363
  return found;
@@ -22532,7 +22762,7 @@ var Launcher = class {
22532
22762
  const root = packageRootOf(at, roots) ?? PACKAGE_ROOT.exec(at)?.[1];
22533
22763
  if (root !== void 0) files.push(`${root}/package.json`);
22534
22764
  }
22535
- return [...new Set(files)].filter((path) => path !== "" && path !== SUDO_PATH);
22765
+ return [...new Set(files)].filter((path) => path !== "" && !IN_TREE.has(path));
22536
22766
  }
22537
22767
  async unrunnable(file, cwd, scope) {
22538
22768
  const path = this.fs.resolvePath(cwd, file);
@@ -22672,6 +22902,9 @@ var Launcher = class {
22672
22902
  if (target.abi === "js" && target.wasm === SUDO_PATH) {
22673
22903
  return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
22674
22904
  }
22905
+ if (target.abi === "js" && target.wasm === CRONTAB_PATH) {
22906
+ return { abi: "js", glue: CRONTAB_SOURCE, path: CRONTAB_PATH };
22907
+ }
22675
22908
  if (target.abi === "js" && target.wasm === REFUSED_PATH) {
22676
22909
  return { abi: "js", glue: REFUSED_SOURCE, path: REFUSED_PATH };
22677
22910
  }
@@ -22702,14 +22935,15 @@ var Launcher = class {
22702
22935
  ...target.preopenRoot ? { preopenRoot: true } : {}
22703
22936
  };
22704
22937
  }
22705
- async launch(planned, req) {
22706
- const { target, args } = planned;
22938
+ async launch(asked, req) {
22707
22939
  const cwd = await physicalPathAsync(this.base, req.cwd);
22708
22940
  const seen = { ...req.env, HOSTNAME: this.hostname, cwd };
22709
- const env = { ...expandDefaults(target.env, seen), ...req.env };
22710
- for (const key of target.unset ?? []) delete env[key];
22941
+ const env = { ...expandDefaults(asked.target.env, seen), ...req.env };
22942
+ for (const key of asked.target.unset ?? []) delete env[key];
22711
22943
  delete env[TICKET_ENV];
22712
22944
  env.HOSTNAME = this.hostname;
22945
+ const planned = chosen(asked, env);
22946
+ const { target, args } = planned;
22713
22947
  let program;
22714
22948
  await this.pnpmReady();
22715
22949
  try {
@@ -22827,6 +23061,7 @@ var Launcher = class {
22827
23061
  cpu: this.cpuTotal()
22828
23062
  }),
22829
23063
  sudo: (call, signal) => this.sudo(pid, call, signal),
23064
+ crontab: (call) => this.crontab(pid, call),
22830
23065
  openFiles: this.openFiles,
22831
23066
  nodes: stack.nodes,
22832
23067
  held: this.held,
@@ -23045,6 +23280,14 @@ var Launcher = class {
23045
23280
  const threads = this.stateOf(pid) === "S" ? this.processes.get(pid)?.threads?.() : void 0;
23046
23281
  return threads ? { threads } : {};
23047
23282
  }
23283
+ crontab(pid, call) {
23284
+ this.crontabs ??= new Crontabs({
23285
+ fs: this.fs,
23286
+ owners: this.owners,
23287
+ lookup: (user) => this.users.lookup(user)
23288
+ });
23289
+ return this.crontabs.request(this.credOf(pid), call);
23290
+ }
23048
23291
  sudo(pid, call, signal) {
23049
23292
  const caller = {
23050
23293
  cred: this.credOf(pid),