@ai-ecoverse/slicc-kernel 1.108.5 → 1.109.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.
@@ -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.109.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
@@ -6467,245 +6467,115 @@ function binfmtOf(binfmts, path) {
6467
6467
  return best === void 0 ? void 0 : binfmts.get(best);
6468
6468
  }
6469
6469
 
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
- }
6470
+ // src/cron/command.ts
6471
+ var CRONTAB_PATH = "/usr/bin/crontab";
6472
+ var CRONTAB_COMMAND = {
6473
+ name: "crontab",
6474
+ abi: "js",
6475
+ glue: CRONTAB_PATH,
6476
+ wasm: CRONTAB_PATH,
6477
+ argv0: "crontab"
6500
6478
  };
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
- };
6479
+ var CRONTAB_SOURCE = `export default async function crontab(ctx) {
6480
+ const say = (fd, text) => ctx.write(fd, text).catch(() => undefined);
6481
+ const usage = async () => {
6482
+ await say(2, "usage: crontab [-u user] { -l | -r | -e | file | - }\\n");
6483
+ return 1;
6484
+ };
6485
+ const args = ctx.argv.slice(1);
6486
+ let user;
6487
+ let action;
6488
+ let file;
6489
+ for (let i = 0; i < args.length; i++) {
6490
+ const arg = args[i];
6491
+ if (arg === "-u") {
6492
+ user = args[++i];
6493
+ if (user === undefined) return usage();
6494
+ } else if (arg === "-l" || arg === "-r" || arg === "-e") {
6495
+ if (action !== undefined) return usage();
6496
+ action = arg.slice(1);
6497
+ } else if (arg === "-" || !arg.startsWith("-")) {
6498
+ if (file !== undefined) return usage();
6499
+ file = arg;
6500
+ } else return usage();
6501
+ }
6502
+ if ((action === undefined) === (file === undefined)) return usage();
6503
+ const call = (c) => ctx.crontab(user === undefined ? c : { ...c, user });
6504
+ const failed = async (answer) => {
6505
+ await say(2, answer.error + "\\n");
6506
+ return 1;
6507
+ };
6508
+ const decode = (bytes) => new TextDecoder().decode(bytes);
6509
+ if (action === "l") {
6510
+ const answer = await call({ action: "list" });
6511
+ if (answer.error !== undefined) return failed(answer);
6512
+ await say(1, answer.text);
6513
+ return 0;
6514
+ }
6515
+ if (action === "r") {
6516
+ const answer = await call({ action: "remove" });
6517
+ return answer.error !== undefined ? failed(answer) : 0;
6518
+ }
6519
+ if (action === "e") return edit();
6520
+ let text;
6521
+ try {
6522
+ if (file === "-") {
6523
+ const parts = [];
6524
+ for (;;) {
6525
+ const chunk = await ctx.read(0);
6526
+ if (chunk.length === 0) break;
6527
+ parts.push(decode(chunk));
6528
+ }
6529
+ text = parts.join("");
6530
+ } else text = decode(await ctx.fs.readFile(ctx.resolve(file)));
6531
+ } catch (err) {
6532
+ await say(2, "crontab: " + file + ": " + (err && err.code ? err.code : String(err)) + "\\n");
6533
+ return 1;
6560
6534
  }
6561
- async settle(pinned, op) {
6535
+ const answer = await call({ action: "install", text });
6536
+ return answer.error !== undefined ? failed(answer) : 0;
6537
+
6538
+ async function edit() {
6539
+ const editor = ctx.env.VISUAL || ctx.env.EDITOR;
6540
+ if (!editor) {
6541
+ await say(2, "crontab: set VISUAL or EDITOR to edit, or install a file with crontab <file>\\n");
6542
+ return 1;
6543
+ }
6544
+ const current = await call({ action: "list" });
6545
+ const before = current.error === undefined ? current.text : "";
6546
+ if (current.error !== undefined && !current.error.startsWith("no crontab for ")) return failed(current);
6547
+ const tmp = "/tmp/crontab." + ctx.pid + "." + crypto.randomUUID();
6562
6548
  try {
6563
- await op();
6549
+ const file = await ctx.open(tmp, { write: true, create: true, exclusive: true, mode: 0o600 });
6550
+ await file.write(0, new TextEncoder().encode(before));
6551
+ await file.close();
6564
6552
  } catch (err) {
6565
- await pinned.rollback();
6566
- throw err;
6553
+ await say(2, "crontab: " + tmp + ": " + (err && err.code ? err.code : String(err)) + "\\n");
6554
+ return 1;
6567
6555
  }
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
- }
6556
+ const child = await ctx.spawn({ argv: ["/bin/bash", "-c", editor + ' "$1"', "crontab", tmp] });
6557
+ const ended = await ctx.wait(child.pid);
6558
+ if (ended.status !== 0) {
6559
+ await say(2, "crontab: the editor exited with status " + ended.status + ", crontab not changed\\n");
6560
+ await ctx.fs.rm(tmp).catch(() => undefined);
6561
+ return 1;
6578
6562
  }
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);
6563
+ const after = decode(await ctx.fs.readFile(tmp));
6564
+ if (after === before) {
6565
+ await say(2, "crontab: no changes made to crontab\\n");
6566
+ await ctx.fs.rm(tmp).catch(() => undefined);
6567
+ return 0;
6586
6568
  }
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);
6569
+ const answer = await call({ action: "install", text: after });
6570
+ if (answer.error !== undefined) {
6571
+ await say(2, answer.error + "\\ncrontab: your edit is kept in " + tmp + "\\n");
6572
+ return 1;
6596
6573
  }
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;
6647
- }
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);
6574
+ await ctx.fs.rm(tmp).catch(() => undefined);
6575
+ return 0;
6659
6576
  }
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
- };
6577
+ }
6578
+ `;
6709
6579
 
6710
6580
  // src/cron/schedule.ts
6711
6581
  var MISSED_CAP = 1e5;
@@ -6951,6 +6821,305 @@ function parseCrontab(text, path, system = false) {
6951
6821
  return { jobs, errors };
6952
6822
  }
6953
6823
 
6824
+ // src/cron/crontab.ts
6825
+ var CRONTAB_MAX = 256 * 1024;
6826
+ var ACTIONS = /* @__PURE__ */ new Set(["list", "remove", "install"]);
6827
+ function crontabCall(req) {
6828
+ const { action, user, text } = req;
6829
+ if (typeof action !== "string" || !ACTIONS.has(action)) return void 0;
6830
+ if (user !== void 0 && typeof user !== "string") return void 0;
6831
+ if (action === "install" && typeof text !== "string") return void 0;
6832
+ return {
6833
+ op: "proc-crontab",
6834
+ action,
6835
+ ...user !== void 0 ? { user } : {},
6836
+ ...action === "install" ? { text } : {}
6837
+ };
6838
+ }
6839
+ var Crontabs = class {
6840
+ o;
6841
+ constructor(options) {
6842
+ this.o = options;
6843
+ }
6844
+ async request(cred, call) {
6845
+ const caller = await this.o.lookup(cred.ruid);
6846
+ if (!caller) return { error: `crontab: your uid ${cred.ruid} is not in /etc/passwd` };
6847
+ let target = caller;
6848
+ if (call.user !== void 0 && call.user !== caller.name) {
6849
+ if (cred.euid !== 0) return { error: "crontab: must be privileged to use -u" };
6850
+ const found = await this.o.lookup(call.user);
6851
+ if (!found) return { error: `crontab: user ${call.user} unknown` };
6852
+ target = found;
6853
+ }
6854
+ const path = `${SPOOL_DIR}/${target.name}`;
6855
+ const none = { error: `no crontab for ${target.name}` };
6856
+ const { fs } = this.o;
6857
+ if (call.action === "install") return this.install(path, target, call.text);
6858
+ if (!await fs.exists(path)) return none;
6859
+ if (call.action === "list") return { text: await fs.readFile(path) };
6860
+ await fs.rm(path);
6861
+ return { text: "" };
6862
+ }
6863
+ async install(path, target, text) {
6864
+ if (text.length > CRONTAB_MAX) return { error: "crontab: the table is too long" };
6865
+ const { errors } = parseCrontab(text, path);
6866
+ if (errors.length > 0) {
6867
+ const lines2 = errors.map(({ line, message: message3 }) => `"-":${line}: ${message3}`);
6868
+ return { error: ["crontab: errors in crontab file, cannot install", ...lines2].join("\n") };
6869
+ }
6870
+ const { fs, owners } = this.o;
6871
+ if (!await fs.exists(SPOOL_DIR)) {
6872
+ await fs.mkdir(SPOOL_DIR, { recursive: true });
6873
+ if (owners) await fs.chown?.(SPOOL_DIR, { uid: 0, gid: 0, mode: 448, follow: false });
6874
+ }
6875
+ const fresh2 = `${SPOOL_DIR}/.${target.name}.${crypto.randomUUID()}.new`;
6876
+ const owner = owners ? { uid: target.uid, gid: target.gid, mode: 384 } : void 0;
6877
+ await fs.writeFile(fresh2, text, owner);
6878
+ await fs.rename(fresh2, path);
6879
+ return { text: "" };
6880
+ }
6881
+ };
6882
+
6883
+ // src/fs/unlinked.ts
6884
+ var UNLINKED_DIR = ".slicc-unlinked";
6885
+ function webLocks() {
6886
+ return globalThis.navigator?.locks;
6887
+ }
6888
+ var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
6889
+ var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
6890
+ var lockName = (instance) => `slicc-unlinked:${instance}`;
6891
+ var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
6892
+ var PathGate = class {
6893
+ tails = /* @__PURE__ */ new Map();
6894
+ async acquire(path) {
6895
+ const prev = this.tails.get(path) ?? Promise.resolve();
6896
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
6897
+ const tail2 = prev.then(() => promise);
6898
+ this.tails.set(path, tail2);
6899
+ await prev;
6900
+ return () => {
6901
+ resolve2();
6902
+ if (this.tails.get(path) === tail2) this.tails.delete(path);
6903
+ };
6904
+ }
6905
+ async run(path, op) {
6906
+ const release = await this.acquire(path);
6907
+ try {
6908
+ return await op();
6909
+ } finally {
6910
+ release();
6911
+ }
6912
+ }
6913
+ };
6914
+ var UnlinkedKeeper = class {
6915
+ fs;
6916
+ holders;
6917
+ rootOf;
6918
+ locks;
6919
+ busy;
6920
+ waiting = /* @__PURE__ */ new Set();
6921
+ owner;
6922
+ instance;
6923
+ gate = new PathGate();
6924
+ queue = Promise.resolve();
6925
+ count = 0;
6926
+ links = /* @__PURE__ */ new Map();
6927
+ linked = /* @__PURE__ */ new Map();
6928
+ dirOwner;
6929
+ constructor(fs, holders, rootOf, options = {}) {
6930
+ const { locks, owner } = options;
6931
+ this.dirOwner = options.dirOwner;
6932
+ this.fs = fs;
6933
+ this.holders = holders;
6934
+ this.rootOf = rootOf;
6935
+ this.locks = locks;
6936
+ this.busy = options.busy ?? (() => false);
6937
+ this.owner = owner;
6938
+ this.instance = owner ? `${owner.id}~${crypto.randomUUID()}` : crypto.randomUUID();
6939
+ void locks?.request(lockName(this.instance), () => new Promise(() => void 0));
6940
+ }
6941
+ serial(op) {
6942
+ const next3 = this.queue.then(op);
6943
+ this.queue = next3.catch(() => void 0);
6944
+ return next3;
6945
+ }
6946
+ wrap(fs = this.fs) {
6947
+ const keeper = this;
6948
+ const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
6949
+ const counted2 = (st) => {
6950
+ const hidden = keeper.links.get(`${st.dev ?? 0}:${st.ino}`) ?? 0;
6951
+ return hidden === 0 || st.nlink === void 0 ? st : { ...st, nlink: Math.max(0, st.nlink - hidden) };
6952
+ };
6953
+ return {
6954
+ ...fs,
6955
+ links: (path) => mayLink(fs, path),
6956
+ stat: async (path) => counted2(await fs.stat(path)),
6957
+ lstat: async (path) => counted2(await fs.lstat(path)),
6958
+ async rm(path, options) {
6959
+ const target = fs.resolvePath("/", path);
6960
+ const pinned = await keeper.pin(target);
6961
+ if (pinned.moved.has(target)) pinned.commit();
6962
+ else await keeper.settle(pinned, () => fs.rm(path, options));
6963
+ },
6964
+ async rename(from, to) {
6965
+ const target = fs.resolvePath("/", to);
6966
+ if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
6967
+ const pinned = await keeper.pin(target);
6968
+ await keeper.settle(pinned, () => fs.rename(from, to));
6969
+ },
6970
+ readdir: async (path) => visible(await fs.readdir(path)),
6971
+ readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])
6972
+ };
6973
+ }
6974
+ async settle(pinned, op) {
6975
+ try {
6976
+ await op();
6977
+ } catch (err) {
6978
+ await pinned.rollback();
6979
+ throw err;
6980
+ }
6981
+ pinned.commit();
6982
+ }
6983
+ async pin(target) {
6984
+ const held = /* @__PURE__ */ new Map();
6985
+ for (const holder of this.holders) {
6986
+ if (holder.owns(target)) continue;
6987
+ for (const path of holder.held()) {
6988
+ if (!within4(path, target) || holder.isKept(path)) continue;
6989
+ held.set(path, [...held.get(path) ?? [], holder]);
6990
+ }
6991
+ }
6992
+ const paths = [...held.keys()].sort();
6993
+ const releases = [];
6994
+ for (const path of paths) releases.push(await this.gate.acquire(path));
6995
+ const hidden = /* @__PURE__ */ new Map();
6996
+ for (const path of paths) {
6997
+ const at = await this.hide(path);
6998
+ if (at !== void 0) hidden.set(path, at);
6999
+ }
7000
+ const done4 = () => {
7001
+ for (const release of releases) release();
7002
+ };
7003
+ const files = /* @__PURE__ */ new Map();
7004
+ for (const [path, at] of hidden) {
7005
+ const holders = held.get(path);
7006
+ const file = this.keptFile(at, { refs: holders.length + 1 });
7007
+ files.set(path, file);
7008
+ for (const holder of holders) holder.keep(path, file);
7009
+ }
7010
+ return {
7011
+ moved: new Set(hidden.keys()),
7012
+ commit: () => {
7013
+ for (const file of files.values()) file.release();
7014
+ done4();
7015
+ },
7016
+ rollback: async () => {
7017
+ for (const [path, file] of files) {
7018
+ for (const holder of held.get(path)) holder.unkeep(path, file);
7019
+ await this.serial(async () => {
7020
+ await Promise.allSettled([this.fs.rename(file.hidden, path)]);
7021
+ this.unlinkedName(file.hidden);
7022
+ await this.tidy(parentOf5(file.hidden));
7023
+ });
7024
+ }
7025
+ done4();
7026
+ }
7027
+ };
7028
+ }
7029
+ keptFile(hidden, entry) {
7030
+ return {
7031
+ hidden,
7032
+ release: () => {
7033
+ if (--entry.refs === 0) void this.serial(() => this.drop(hidden));
7034
+ }
7035
+ };
7036
+ }
7037
+ hide(path) {
7038
+ return this.serial(async () => {
7039
+ const dir = dirOf(this.rootOf(path));
7040
+ const hidden = `${dir}/${this.instance}.${++this.count}`;
7041
+ try {
7042
+ const owner = this.dirOwner;
7043
+ await this.fs.mkdir(dir, { recursive: true, ...owner ? { owner } : {} });
7044
+ await this.fs.rename(path, hidden);
7045
+ await this.linkedName(hidden);
7046
+ return hidden;
7047
+ } catch {
7048
+ await this.tidy(dir);
7049
+ return void 0;
7050
+ }
7051
+ });
7052
+ }
7053
+ closed(path) {
7054
+ if (this.waiting.delete(path)) void this.serial(() => this.drop(path));
7055
+ }
7056
+ async drop(hidden) {
7057
+ if (this.busy(hidden)) {
7058
+ this.waiting.add(hidden);
7059
+ return;
7060
+ }
7061
+ await this.fs.rm(hidden).catch(() => void 0);
7062
+ this.unlinkedName(hidden);
7063
+ await this.tidy(parentOf5(hidden));
7064
+ }
7065
+ async linkedName(hidden) {
7066
+ if (!mayLink(this.fs, hidden)) return;
7067
+ const st = await this.fs.lstat(hidden).catch(() => void 0);
7068
+ if (st?.nlink === void 0) return;
7069
+ const id2 = `${st.dev ?? 0}:${st.ino}`;
7070
+ this.linked.set(hidden, id2);
7071
+ this.links.set(id2, (this.links.get(id2) ?? 0) + 1);
7072
+ }
7073
+ unlinkedName(hidden) {
7074
+ const id2 = this.linked.get(hidden);
7075
+ if (id2 === void 0) return;
7076
+ this.linked.delete(hidden);
7077
+ const left = (this.links.get(id2) ?? 1) - 1;
7078
+ if (left > 0) this.links.set(id2, left);
7079
+ else this.links.delete(id2);
7080
+ }
7081
+ async tidy(dir) {
7082
+ const left = await this.fs.readdir(dir).catch(() => [UNLINKED_DIR]);
7083
+ if (left.length === 0) await this.fs.rm(dir, { recursive: true }).catch(() => void 0);
7084
+ }
7085
+ sweep(root) {
7086
+ const query = this.locks?.query?.bind(this.locks);
7087
+ const owner = this.owner;
7088
+ if (!query && !owner) return Promise.resolve();
7089
+ return this.serial(async () => {
7090
+ const dir = dirOf(root);
7091
+ const names = await this.fs.readdir(dir).catch(() => void 0);
7092
+ if (!names) return;
7093
+ const held = query && new Set(((await query()).held ?? []).map((lock) => lock.name));
7094
+ for (const name2 of names) {
7095
+ const id2 = name2.slice(0, name2.lastIndexOf("."));
7096
+ const at = id2.indexOf("~");
7097
+ const dead = id2 !== this.instance && (at < 0 ? held !== void 0 && !held.has(lockName(id2)) : owner?.alive(id2.slice(0, at)) === false);
7098
+ if (dead) await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
7099
+ }
7100
+ await this.tidy(dir);
7101
+ });
7102
+ }
7103
+ };
7104
+
7105
+ // src/cron/clock.ts
7106
+ var systemCronClock = {
7107
+ now: () => Date.now(),
7108
+ sleep(until, signal) {
7109
+ if (signal.aborted) return Promise.resolve();
7110
+ return new Promise((resolve2) => {
7111
+ const done4 = () => {
7112
+ clearTimeout(timer);
7113
+ signal.removeEventListener("abort", done4);
7114
+ resolve2();
7115
+ };
7116
+ const timer = setTimeout(done4, Math.max(0, until - Date.now()));
7117
+ timer.unref?.();
7118
+ signal.addEventListener("abort", done4, { once: true });
7119
+ });
7120
+ }
7121
+ };
7122
+
6954
7123
  // src/cron/service.ts
6955
7124
  var CRON_DIR = "/var/spool/cron";
6956
7125
  var STAMP_PATH = `${CRON_DIR}/stamp`;
@@ -7127,7 +7296,7 @@ var CronService = class {
7127
7296
  }
7128
7297
  const names = await fs.readdir(SPOOL_DIR).catch(() => []);
7129
7298
  for (const name2 of names.sort()) {
7130
- if (name2.startsWith(".") || name2.endsWith("~") || name2.endsWith(".new")) continue;
7299
+ if (name2.startsWith(".") || name2.endsWith("~")) continue;
7131
7300
  const path = `${SPOOL_DIR}/${name2}`;
7132
7301
  const account = await this.host.lookup(name2);
7133
7302
  if (!account) {
@@ -16637,6 +16806,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
16637
16806
  "proc-wait",
16638
16807
  "proc-captured",
16639
16808
  "proc-sudo",
16809
+ "proc-crontab",
16640
16810
  "proc-run",
16641
16811
  "proc-exit",
16642
16812
  "proc-kill-self",
@@ -17826,8 +17996,17 @@ var WasmProcess = class {
17826
17996
  return { ok: true, kind: "bytes", bytes: this.children.captured(req.pid, req.slot) };
17827
17997
  case "proc-sudo":
17828
17998
  return { ok: true, kind: "json", json: await this.sudo(req, cancelled) };
17999
+ case "proc-crontab":
18000
+ return { ok: true, kind: "json", json: await this.crontab(req) };
17829
18001
  }
17830
18002
  }
18003
+ async crontab(req) {
18004
+ const { crontab } = this.options;
18005
+ if (!crontab) throw new KernelError("ENOSYS");
18006
+ const call = crontabCall(req);
18007
+ if (!call) throw new KernelError("EINVAL");
18008
+ return crontab(call);
18009
+ }
17831
18010
  signalSyscall(req) {
17832
18011
  if (req.op === "sig-inherited") {
17833
18012
  return { ok: true, kind: "json", json: this.inherited & ~this.caught };
@@ -18230,6 +18409,7 @@ function processOptions(opts, header3, hooks, created3) {
18230
18409
  ...opts.writesBack ? { writesBack: opts.writesBack } : {},
18231
18410
  processes: opts.processes,
18232
18411
  ...opts.sudo ? { sudo: opts.sudo } : {},
18412
+ ...opts.crontab ? { crontab: opts.crontab } : {},
18233
18413
  openFiles: opts.openFiles,
18234
18414
  ...opts.nodes ? { nodes: opts.nodes } : {},
18235
18415
  mounts: opts.mounts,
@@ -21697,6 +21877,7 @@ var LAYOUT = [
21697
21877
  ["/usr", 493],
21698
21878
  ["/usr/local", 493]
21699
21879
  ];
21880
+ var IN_TREE = /* @__PURE__ */ new Set([SUDO_PATH, CRONTAB_PATH]);
21700
21881
  var LOAD_SAMPLE_MS = 5e3;
21701
21882
  var LOAD_WINDOWS_MS = [6e4, 3e5, 9e5];
21702
21883
  var encoder8 = new TextEncoder();
@@ -21927,6 +22108,7 @@ var Launcher = class {
21927
22108
  boot = Date.now();
21928
22109
  bootId = crypto.randomUUID();
21929
22110
  cron;
22111
+ crontabs;
21930
22112
  terminals = 0;
21931
22113
  constructor(options) {
21932
22114
  this.usersMode = usersMode(options.users);
@@ -22127,7 +22309,9 @@ var Launcher = class {
22127
22309
  found = this.trustedAppended().then(async (appended2) => {
22128
22310
  const tiers = await this.tiers(scope, appended2);
22129
22311
  return scanCommands(this.scanned, tiers, tiers.length - appended2.length);
22130
- }).then((commands) => this.refusals(commands).set(SUDO_COMMAND.name, SUDO_COMMAND));
22312
+ }).then(
22313
+ (commands) => this.refusals(commands).set(SUDO_COMMAND.name, SUDO_COMMAND).set(CRONTAB_COMMAND.name, CRONTAB_COMMAND)
22314
+ );
22131
22315
  this.catalogs.set(scope, found);
22132
22316
  }
22133
22317
  return found;
@@ -22532,7 +22716,7 @@ var Launcher = class {
22532
22716
  const root = packageRootOf(at, roots) ?? PACKAGE_ROOT.exec(at)?.[1];
22533
22717
  if (root !== void 0) files.push(`${root}/package.json`);
22534
22718
  }
22535
- return [...new Set(files)].filter((path) => path !== "" && path !== SUDO_PATH);
22719
+ return [...new Set(files)].filter((path) => path !== "" && !IN_TREE.has(path));
22536
22720
  }
22537
22721
  async unrunnable(file, cwd, scope) {
22538
22722
  const path = this.fs.resolvePath(cwd, file);
@@ -22672,6 +22856,9 @@ var Launcher = class {
22672
22856
  if (target.abi === "js" && target.wasm === SUDO_PATH) {
22673
22857
  return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
22674
22858
  }
22859
+ if (target.abi === "js" && target.wasm === CRONTAB_PATH) {
22860
+ return { abi: "js", glue: CRONTAB_SOURCE, path: CRONTAB_PATH };
22861
+ }
22675
22862
  if (target.abi === "js" && target.wasm === REFUSED_PATH) {
22676
22863
  return { abi: "js", glue: REFUSED_SOURCE, path: REFUSED_PATH };
22677
22864
  }
@@ -22827,6 +23014,7 @@ var Launcher = class {
22827
23014
  cpu: this.cpuTotal()
22828
23015
  }),
22829
23016
  sudo: (call, signal) => this.sudo(pid, call, signal),
23017
+ crontab: (call) => this.crontab(pid, call),
22830
23018
  openFiles: this.openFiles,
22831
23019
  nodes: stack.nodes,
22832
23020
  held: this.held,
@@ -23045,6 +23233,14 @@ var Launcher = class {
23045
23233
  const threads = this.stateOf(pid) === "S" ? this.processes.get(pid)?.threads?.() : void 0;
23046
23234
  return threads ? { threads } : {};
23047
23235
  }
23236
+ crontab(pid, call) {
23237
+ this.crontabs ??= new Crontabs({
23238
+ fs: this.fs,
23239
+ owners: this.owners,
23240
+ lookup: (user) => this.users.lookup(user)
23241
+ });
23242
+ return this.crontabs.request(this.credOf(pid), call);
23243
+ }
23048
23244
  sudo(pid, call, signal) {
23049
23245
  const caller = {
23050
23246
  cred: this.credOf(pid),