opencode-supertask 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/cli/index.js +498 -142
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.js +449 -108
- package/dist/gateway/index.js.map +1 -1
- package/dist/web/index.js +383 -36
- package/dist/web/index.js.map +1 -1
- package/package.json +1 -1
package/dist/gateway/index.js
CHANGED
|
@@ -6895,6 +6895,80 @@ var init_health = __esm({
|
|
|
6895
6895
|
}
|
|
6896
6896
|
});
|
|
6897
6897
|
|
|
6898
|
+
// src/core/process-control.ts
|
|
6899
|
+
import { spawnSync } from "child_process";
|
|
6900
|
+
import { basename } from "path";
|
|
6901
|
+
function isSafePid(pid) {
|
|
6902
|
+
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
6903
|
+
}
|
|
6904
|
+
function isProcessAlive(pid) {
|
|
6905
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
6906
|
+
try {
|
|
6907
|
+
process.kill(pid, 0);
|
|
6908
|
+
return true;
|
|
6909
|
+
} catch (error) {
|
|
6910
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
6911
|
+
}
|
|
6912
|
+
}
|
|
6913
|
+
function inspectUnixProcess(pid) {
|
|
6914
|
+
const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
6915
|
+
encoding: "utf8",
|
|
6916
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6917
|
+
});
|
|
6918
|
+
if (result.status !== 0) return null;
|
|
6919
|
+
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
6920
|
+
if (!match2) return null;
|
|
6921
|
+
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
6922
|
+
}
|
|
6923
|
+
function commandMatches(command, expectedExecutable) {
|
|
6924
|
+
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
6925
|
+
if (!expectedName) return false;
|
|
6926
|
+
return command.toLowerCase().includes(expectedName);
|
|
6927
|
+
}
|
|
6928
|
+
function signalSpawnedProcessTree(pid, signal) {
|
|
6929
|
+
if (!isSafePid(pid)) return false;
|
|
6930
|
+
if (process.platform !== "win32") {
|
|
6931
|
+
try {
|
|
6932
|
+
process.kill(-pid, signal);
|
|
6933
|
+
return true;
|
|
6934
|
+
} catch {
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6937
|
+
try {
|
|
6938
|
+
process.kill(pid, signal);
|
|
6939
|
+
return true;
|
|
6940
|
+
} catch {
|
|
6941
|
+
return false;
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
6945
|
+
if (!isSafePid(pid)) return false;
|
|
6946
|
+
if (process.platform === "win32") {
|
|
6947
|
+
const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
6948
|
+
encoding: "utf8",
|
|
6949
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
6950
|
+
});
|
|
6951
|
+
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
6952
|
+
const args = ["/PID", String(pid), "/T"];
|
|
6953
|
+
if (signal === "SIGKILL") args.push("/F");
|
|
6954
|
+
return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
6955
|
+
}
|
|
6956
|
+
const info = inspectUnixProcess(pid);
|
|
6957
|
+
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
6958
|
+
try {
|
|
6959
|
+
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
6960
|
+
else process.kill(pid, signal);
|
|
6961
|
+
return true;
|
|
6962
|
+
} catch {
|
|
6963
|
+
return false;
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
var init_process_control = __esm({
|
|
6967
|
+
"src/core/process-control.ts"() {
|
|
6968
|
+
"use strict";
|
|
6969
|
+
}
|
|
6970
|
+
});
|
|
6971
|
+
|
|
6898
6972
|
// node_modules/cron-parser/dist/fields/types.js
|
|
6899
6973
|
var require_types = __commonJS({
|
|
6900
6974
|
"node_modules/cron-parser/dist/fields/types.js"(exports) {
|
|
@@ -14108,26 +14182,26 @@ var require_CronDate = __commonJS({
|
|
|
14108
14182
|
* @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
|
|
14109
14183
|
* @param {string} [tz] - The timezone to use for the CronDate.
|
|
14110
14184
|
*/
|
|
14111
|
-
constructor(
|
|
14185
|
+
constructor(timestamp2, tz) {
|
|
14112
14186
|
const dateOpts = { zone: tz };
|
|
14113
|
-
if (!
|
|
14187
|
+
if (!timestamp2) {
|
|
14114
14188
|
this.#date = luxon_1.DateTime.local();
|
|
14115
|
-
} else if (
|
|
14116
|
-
this.#date =
|
|
14117
|
-
this.#dstStart =
|
|
14118
|
-
this.#dstEnd =
|
|
14119
|
-
} else if (
|
|
14120
|
-
this.#date = luxon_1.DateTime.fromJSDate(
|
|
14121
|
-
} else if (typeof
|
|
14122
|
-
this.#date = luxon_1.DateTime.fromMillis(
|
|
14189
|
+
} else if (timestamp2 instanceof _CronDate) {
|
|
14190
|
+
this.#date = timestamp2.#date;
|
|
14191
|
+
this.#dstStart = timestamp2.#dstStart;
|
|
14192
|
+
this.#dstEnd = timestamp2.#dstEnd;
|
|
14193
|
+
} else if (timestamp2 instanceof Date) {
|
|
14194
|
+
this.#date = luxon_1.DateTime.fromJSDate(timestamp2, dateOpts);
|
|
14195
|
+
} else if (typeof timestamp2 === "number") {
|
|
14196
|
+
this.#date = luxon_1.DateTime.fromMillis(timestamp2, dateOpts);
|
|
14123
14197
|
} else {
|
|
14124
|
-
this.#date = luxon_1.DateTime.fromISO(
|
|
14125
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(
|
|
14126
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(
|
|
14127
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(
|
|
14198
|
+
this.#date = luxon_1.DateTime.fromISO(timestamp2, dateOpts);
|
|
14199
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(timestamp2, dateOpts));
|
|
14200
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(timestamp2, dateOpts));
|
|
14201
|
+
this.#date.isValid || (this.#date = luxon_1.DateTime.fromFormat(timestamp2, "EEE, d MMM yyyy HH:mm:ss", dateOpts));
|
|
14128
14202
|
}
|
|
14129
14203
|
if (!this.#date.isValid) {
|
|
14130
|
-
throw new Error(`CronDate: unhandled timestamp: ${
|
|
14204
|
+
throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
|
|
14131
14205
|
}
|
|
14132
14206
|
if (tz && tz !== this.#date.zoneName) {
|
|
14133
14207
|
this.#date = this.#date.setZone(tz);
|
|
@@ -18519,14 +18593,346 @@ var init_html2 = __esm({
|
|
|
18519
18593
|
}
|
|
18520
18594
|
});
|
|
18521
18595
|
|
|
18596
|
+
// src/core/services/database-maintenance.service.ts
|
|
18597
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
18598
|
+
import { randomUUID } from "crypto";
|
|
18599
|
+
import {
|
|
18600
|
+
chmodSync,
|
|
18601
|
+
constants,
|
|
18602
|
+
copyFileSync,
|
|
18603
|
+
existsSync as existsSync3,
|
|
18604
|
+
mkdirSync as mkdirSync2,
|
|
18605
|
+
renameSync,
|
|
18606
|
+
statSync,
|
|
18607
|
+
unlinkSync,
|
|
18608
|
+
writeFileSync
|
|
18609
|
+
} from "fs";
|
|
18610
|
+
import { tmpdir } from "os";
|
|
18611
|
+
import { basename as basename2, dirname as dirname2, resolve } from "path";
|
|
18612
|
+
function timestamp() {
|
|
18613
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
18614
|
+
}
|
|
18615
|
+
function normalizedPath(path) {
|
|
18616
|
+
return path === ":memory:" ? path : resolve(path);
|
|
18617
|
+
}
|
|
18618
|
+
function safeUnlink(path) {
|
|
18619
|
+
try {
|
|
18620
|
+
unlinkSync(path);
|
|
18621
|
+
} catch (error) {
|
|
18622
|
+
const code = error instanceof Error && "code" in error ? error.code : void 0;
|
|
18623
|
+
if (code !== "ENOENT") throw error;
|
|
18624
|
+
}
|
|
18625
|
+
}
|
|
18626
|
+
function errorMessage(error) {
|
|
18627
|
+
return error instanceof Error ? error.message : String(error);
|
|
18628
|
+
}
|
|
18629
|
+
var REQUIRED_TABLES, DatabaseMaintenanceConflictError, DatabaseMaintenanceService;
|
|
18630
|
+
var init_database_maintenance_service = __esm({
|
|
18631
|
+
"src/core/services/database-maintenance.service.ts"() {
|
|
18632
|
+
"use strict";
|
|
18633
|
+
init_db2();
|
|
18634
|
+
init_process_control();
|
|
18635
|
+
REQUIRED_TABLES = ["gateway_lock", "tasks", "task_runs", "task_templates"];
|
|
18636
|
+
DatabaseMaintenanceConflictError = class extends Error {
|
|
18637
|
+
constructor(message) {
|
|
18638
|
+
super(message);
|
|
18639
|
+
this.name = "DatabaseMaintenanceConflictError";
|
|
18640
|
+
}
|
|
18641
|
+
};
|
|
18642
|
+
DatabaseMaintenanceService = class {
|
|
18643
|
+
static check() {
|
|
18644
|
+
return this.inspect(getSqlite(), DB_FILE_PATH);
|
|
18645
|
+
}
|
|
18646
|
+
static backup(outputPath) {
|
|
18647
|
+
const sqlite2 = getSqlite();
|
|
18648
|
+
try {
|
|
18649
|
+
sqlite2.query("PRAGMA wal_checkpoint(PASSIVE)").get();
|
|
18650
|
+
} catch {
|
|
18651
|
+
}
|
|
18652
|
+
return this.writeSnapshot(sqlite2, outputPath ?? this.createBackupPath("backup"));
|
|
18653
|
+
}
|
|
18654
|
+
static clear(options = {}) {
|
|
18655
|
+
const sqlite2 = getSqlite();
|
|
18656
|
+
this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
|
|
18657
|
+
this.assertNoRunningWork(sqlite2);
|
|
18658
|
+
sqlite2.exec("BEGIN IMMEDIATE");
|
|
18659
|
+
let backup = null;
|
|
18660
|
+
try {
|
|
18661
|
+
this.assertGatewaySafe(sqlite2, options.allowCurrentGateway ?? false);
|
|
18662
|
+
this.assertNoRunningWork(sqlite2);
|
|
18663
|
+
const before = this.readCounts(sqlite2);
|
|
18664
|
+
backup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-clear"));
|
|
18665
|
+
sqlite2.exec("DELETE FROM task_runs");
|
|
18666
|
+
sqlite2.exec("DELETE FROM tasks");
|
|
18667
|
+
sqlite2.exec("DELETE FROM task_templates");
|
|
18668
|
+
sqlite2.exec("COMMIT");
|
|
18669
|
+
return {
|
|
18670
|
+
backupPath: backup.path,
|
|
18671
|
+
deleted: before,
|
|
18672
|
+
check: this.inspect(sqlite2, DB_FILE_PATH)
|
|
18673
|
+
};
|
|
18674
|
+
} catch (error) {
|
|
18675
|
+
if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
|
|
18676
|
+
if (error instanceof DatabaseMaintenanceConflictError) throw error;
|
|
18677
|
+
const backupHint = backup ? `\uFF1B\u4E8B\u52A1\u5DF2\u56DE\u6EDA\uFF0C\u5907\u4EFD\u4FDD\u7559\u5728 ${backup.path}` : "";
|
|
18678
|
+
throw new Error(`\u6E05\u7A7A\u6570\u636E\u5E93\u5931\u8D25${backupHint}\uFF1A${errorMessage(error)}`);
|
|
18679
|
+
}
|
|
18680
|
+
}
|
|
18681
|
+
static restore(sourcePath) {
|
|
18682
|
+
if (DB_FILE_PATH === ":memory:") {
|
|
18683
|
+
throw new Error("\u5185\u5B58\u6570\u636E\u5E93\u4E0D\u652F\u6301 restore");
|
|
18684
|
+
}
|
|
18685
|
+
const source = normalizedPath(sourcePath);
|
|
18686
|
+
const livePath = normalizedPath(DB_FILE_PATH);
|
|
18687
|
+
if (source === livePath) throw new Error("\u6062\u590D\u6765\u6E90\u4E0D\u80FD\u662F\u5F53\u524D\u6570\u636E\u5E93\u6587\u4EF6");
|
|
18688
|
+
const current = getSqlite();
|
|
18689
|
+
this.assertGatewaySafe(current, false);
|
|
18690
|
+
this.assertNoRunningWork(current);
|
|
18691
|
+
if (!existsSync3(source) || !statSync(source).isFile()) {
|
|
18692
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${source}`);
|
|
18693
|
+
}
|
|
18694
|
+
let sourceCheck;
|
|
18695
|
+
try {
|
|
18696
|
+
sourceCheck = this.inspectFile(source);
|
|
18697
|
+
} catch (error) {
|
|
18698
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u65E0\u6548\uFF1A${errorMessage(error)}`);
|
|
18699
|
+
}
|
|
18700
|
+
if (!sourceCheck.ok) {
|
|
18701
|
+
throw new Error(`\u5907\u4EFD\u6587\u4EF6\u6821\u9A8C\u5931\u8D25\uFF1A${sourceCheck.integrityMessages.join("; ") || "schema/foreign key error"}`);
|
|
18702
|
+
}
|
|
18703
|
+
const stagePath = `${livePath}.restore-${process.pid}-${randomUUID()}.tmp`;
|
|
18704
|
+
const rollbackPath = `${livePath}.rollback-${process.pid}-${randomUUID()}.tmp`;
|
|
18705
|
+
let safetyBackup = null;
|
|
18706
|
+
let liveMoved = false;
|
|
18707
|
+
try {
|
|
18708
|
+
copyFileSync(source, stagePath, constants.COPYFILE_EXCL);
|
|
18709
|
+
chmodSync(stagePath, 384);
|
|
18710
|
+
const staged = new Database3(stagePath);
|
|
18711
|
+
try {
|
|
18712
|
+
const stagedCheck = this.inspect(staged, stagePath);
|
|
18713
|
+
if (!stagedCheck.ok) throw new Error("\u6682\u5B58\u6062\u590D\u6587\u4EF6\u6821\u9A8C\u5931\u8D25");
|
|
18714
|
+
const now = Date.now();
|
|
18715
|
+
staged.query(
|
|
18716
|
+
"INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
|
|
18717
|
+
).run(process.pid, now, now);
|
|
18718
|
+
} finally {
|
|
18719
|
+
staged.close();
|
|
18720
|
+
}
|
|
18721
|
+
const sqlite2 = getSqlite();
|
|
18722
|
+
sqlite2.exec("BEGIN IMMEDIATE");
|
|
18723
|
+
try {
|
|
18724
|
+
this.assertGatewaySafe(sqlite2, false);
|
|
18725
|
+
this.assertNoRunningWork(sqlite2);
|
|
18726
|
+
safetyBackup = this.writeSnapshot(sqlite2, this.createBackupPath("pre-restore"));
|
|
18727
|
+
const now = Date.now();
|
|
18728
|
+
sqlite2.query(
|
|
18729
|
+
"INSERT OR REPLACE INTO gateway_lock (id, pid, acquired_at, heartbeat_at, ready_at) VALUES (1, ?, ?, ?, NULL)"
|
|
18730
|
+
).run(process.pid, now, now);
|
|
18731
|
+
sqlite2.exec("COMMIT");
|
|
18732
|
+
} catch (error) {
|
|
18733
|
+
if (sqlite2.inTransaction) sqlite2.exec("ROLLBACK");
|
|
18734
|
+
throw error;
|
|
18735
|
+
}
|
|
18736
|
+
const checkpoint = sqlite2.query("PRAGMA wal_checkpoint(TRUNCATE)").get();
|
|
18737
|
+
if (checkpoint && checkpoint.busy !== 0) {
|
|
18738
|
+
throw new DatabaseMaintenanceConflictError("\u6570\u636E\u5E93\u4ECD\u88AB\u5176\u4ED6\u8FDE\u63A5\u5360\u7528\uFF0C\u65E0\u6CD5\u5B89\u5168\u6062\u590D");
|
|
18739
|
+
}
|
|
18740
|
+
closeDb();
|
|
18741
|
+
safeUnlink(`${livePath}-wal`);
|
|
18742
|
+
safeUnlink(`${livePath}-shm`);
|
|
18743
|
+
renameSync(livePath, rollbackPath);
|
|
18744
|
+
liveMoved = true;
|
|
18745
|
+
renameSync(stagePath, livePath);
|
|
18746
|
+
const restored = getSqlite();
|
|
18747
|
+
restored.exec("BEGIN IMMEDIATE");
|
|
18748
|
+
let recoveredRunningTasks = 0;
|
|
18749
|
+
let closedRunningRuns = 0;
|
|
18750
|
+
try {
|
|
18751
|
+
recoveredRunningTasks = this.scalar(
|
|
18752
|
+
restored,
|
|
18753
|
+
"SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'"
|
|
18754
|
+
);
|
|
18755
|
+
closedRunningRuns = this.scalar(
|
|
18756
|
+
restored,
|
|
18757
|
+
"SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'"
|
|
18758
|
+
);
|
|
18759
|
+
const finishedAt = Math.floor(Date.now() / 1e3);
|
|
18760
|
+
restored.query(`
|
|
18761
|
+
UPDATE task_runs
|
|
18762
|
+
SET status = 'failed', finished_at = ?,
|
|
18763
|
+
log = CASE
|
|
18764
|
+
WHEN log IS NULL OR log = '' THEN '\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
|
|
18765
|
+
ELSE log || '
|
|
18766
|
+
\u6570\u636E\u5E93\u6062\u590D\u65F6\u5173\u95ED\u9057\u7559\u8FD0\u884C\u8BB0\u5F55'
|
|
18767
|
+
END
|
|
18768
|
+
WHERE status = 'running'
|
|
18769
|
+
`).run(finishedAt);
|
|
18770
|
+
restored.exec(`
|
|
18771
|
+
UPDATE tasks
|
|
18772
|
+
SET status = 'pending', started_at = NULL, finished_at = NULL
|
|
18773
|
+
WHERE status = 'running'
|
|
18774
|
+
`);
|
|
18775
|
+
restored.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
18776
|
+
restored.exec("COMMIT");
|
|
18777
|
+
} catch (error) {
|
|
18778
|
+
if (restored.inTransaction) restored.exec("ROLLBACK");
|
|
18779
|
+
throw error;
|
|
18780
|
+
}
|
|
18781
|
+
const check = this.inspect(restored, livePath);
|
|
18782
|
+
if (!check.ok) throw new Error("\u6062\u590D\u540E\u7684\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
|
|
18783
|
+
safeUnlink(rollbackPath);
|
|
18784
|
+
return {
|
|
18785
|
+
sourcePath: source,
|
|
18786
|
+
safetyBackupPath: safetyBackup.path,
|
|
18787
|
+
recoveredRunningTasks,
|
|
18788
|
+
closedRunningRuns,
|
|
18789
|
+
check
|
|
18790
|
+
};
|
|
18791
|
+
} catch (error) {
|
|
18792
|
+
closeDb();
|
|
18793
|
+
safeUnlink(`${livePath}-wal`);
|
|
18794
|
+
safeUnlink(`${livePath}-shm`);
|
|
18795
|
+
safeUnlink(stagePath);
|
|
18796
|
+
safeUnlink(`${stagePath}-journal`);
|
|
18797
|
+
safeUnlink(`${stagePath}-wal`);
|
|
18798
|
+
safeUnlink(`${stagePath}-shm`);
|
|
18799
|
+
if (liveMoved && existsSync3(rollbackPath)) {
|
|
18800
|
+
safeUnlink(livePath);
|
|
18801
|
+
renameSync(rollbackPath, livePath);
|
|
18802
|
+
try {
|
|
18803
|
+
const original = getSqlite();
|
|
18804
|
+
original.query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
18805
|
+
} catch {
|
|
18806
|
+
}
|
|
18807
|
+
} else if (existsSync3(rollbackPath)) {
|
|
18808
|
+
safeUnlink(rollbackPath);
|
|
18809
|
+
} else {
|
|
18810
|
+
try {
|
|
18811
|
+
getSqlite().query("DELETE FROM gateway_lock WHERE pid = ?").run(process.pid);
|
|
18812
|
+
} catch {
|
|
18813
|
+
}
|
|
18814
|
+
}
|
|
18815
|
+
const backupHint = safetyBackup ? `\uFF1B\u5F53\u524D\u5E93\u5B89\u5168\u5907\u4EFD\uFF1A${safetyBackup.path}` : "";
|
|
18816
|
+
throw new Error(`\u6062\u590D\u6570\u636E\u5E93\u5931\u8D25\uFF0C\u5DF2\u5C1D\u8BD5\u56DE\u6EDA${backupHint}\uFF1A${errorMessage(error)}`);
|
|
18817
|
+
}
|
|
18818
|
+
}
|
|
18819
|
+
static inspectFile(path) {
|
|
18820
|
+
let sqlite2;
|
|
18821
|
+
try {
|
|
18822
|
+
sqlite2 = new Database3(path, { readonly: true, strict: true });
|
|
18823
|
+
} catch (error) {
|
|
18824
|
+
throw new Error(`\u65E0\u6CD5\u6253\u5F00\u5907\u4EFD\u6587\u4EF6\uFF1A${errorMessage(error)}`);
|
|
18825
|
+
}
|
|
18826
|
+
try {
|
|
18827
|
+
return this.inspect(sqlite2, path);
|
|
18828
|
+
} finally {
|
|
18829
|
+
sqlite2.close();
|
|
18830
|
+
}
|
|
18831
|
+
}
|
|
18832
|
+
static inspect(sqlite2, path) {
|
|
18833
|
+
const tableRows = sqlite2.query(
|
|
18834
|
+
"SELECT name FROM sqlite_master WHERE type = 'table'"
|
|
18835
|
+
).all();
|
|
18836
|
+
const tableNames = new Set(tableRows.map((row) => row.name));
|
|
18837
|
+
const missingTables = REQUIRED_TABLES.filter((table) => !tableNames.has(table));
|
|
18838
|
+
const integrityRows = sqlite2.query("PRAGMA integrity_check").all();
|
|
18839
|
+
const integrityMessages = integrityRows.map((row) => row.integrity_check);
|
|
18840
|
+
const foreignKeyViolations = sqlite2.query("PRAGMA foreign_key_check").all().length;
|
|
18841
|
+
const journal = sqlite2.query("PRAGMA journal_mode").get();
|
|
18842
|
+
const counts = missingTables.length === 0 ? this.readCounts(sqlite2) : { tasks: 0, taskRuns: 0, taskTemplates: 0 };
|
|
18843
|
+
const runningTasks = missingTables.includes("tasks") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
|
|
18844
|
+
const runningRuns = missingTables.includes("task_runs") ? 0 : this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
|
|
18845
|
+
const sizeBytes = path === ":memory:" || !existsSync3(path) ? sqlite2.serialize().byteLength : statSync(path).size;
|
|
18846
|
+
return {
|
|
18847
|
+
ok: integrityMessages.length === 1 && integrityMessages[0] === "ok" && foreignKeyViolations === 0 && missingTables.length === 0,
|
|
18848
|
+
path: normalizedPath(path),
|
|
18849
|
+
sizeBytes,
|
|
18850
|
+
journalMode: journal?.journal_mode ?? "unknown",
|
|
18851
|
+
integrityMessages,
|
|
18852
|
+
foreignKeyViolations,
|
|
18853
|
+
missingTables,
|
|
18854
|
+
counts,
|
|
18855
|
+
runningTasks,
|
|
18856
|
+
runningRuns
|
|
18857
|
+
};
|
|
18858
|
+
}
|
|
18859
|
+
static writeSnapshot(sqlite2, outputPath) {
|
|
18860
|
+
const liveCheck = this.inspect(sqlite2, DB_FILE_PATH);
|
|
18861
|
+
if (!liveCheck.ok) throw new Error("\u5F53\u524D\u6570\u636E\u5E93\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C\uFF0C\u5DF2\u62D2\u7EDD\u521B\u5EFA\u5907\u4EFD");
|
|
18862
|
+
const output = normalizedPath(outputPath);
|
|
18863
|
+
if (DB_FILE_PATH !== ":memory:" && output === normalizedPath(DB_FILE_PATH)) {
|
|
18864
|
+
throw new Error("\u5907\u4EFD\u8DEF\u5F84\u4E0D\u80FD\u8986\u76D6\u5F53\u524D\u6570\u636E\u5E93");
|
|
18865
|
+
}
|
|
18866
|
+
if (existsSync3(output)) throw new Error(`\u5907\u4EFD\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${output}`);
|
|
18867
|
+
mkdirSync2(dirname2(output), { recursive: true });
|
|
18868
|
+
const temporary = `${output}.tmp-${process.pid}-${randomUUID()}`;
|
|
18869
|
+
try {
|
|
18870
|
+
writeFileSync(temporary, sqlite2.serialize(), { flag: "wx", mode: 384 });
|
|
18871
|
+
const standalone = new Database3(temporary, { readwrite: true, create: false });
|
|
18872
|
+
try {
|
|
18873
|
+
standalone.query("PRAGMA journal_mode = DELETE").get();
|
|
18874
|
+
} finally {
|
|
18875
|
+
standalone.close();
|
|
18876
|
+
}
|
|
18877
|
+
safeUnlink(`${temporary}-wal`);
|
|
18878
|
+
safeUnlink(`${temporary}-shm`);
|
|
18879
|
+
const check = this.inspectFile(temporary);
|
|
18880
|
+
if (!check.ok) throw new Error("\u65B0\u5907\u4EFD\u672A\u901A\u8FC7\u5B8C\u6574\u6027\u6821\u9A8C");
|
|
18881
|
+
renameSync(temporary, output);
|
|
18882
|
+
const finalCheck = { ...check, path: output, sizeBytes: statSync(output).size };
|
|
18883
|
+
return { path: output, sizeBytes: finalCheck.sizeBytes, check: finalCheck };
|
|
18884
|
+
} catch (error) {
|
|
18885
|
+
safeUnlink(temporary);
|
|
18886
|
+
safeUnlink(`${temporary}-wal`);
|
|
18887
|
+
safeUnlink(`${temporary}-shm`);
|
|
18888
|
+
throw error;
|
|
18889
|
+
}
|
|
18890
|
+
}
|
|
18891
|
+
static createBackupPath(kind) {
|
|
18892
|
+
const directory = DB_FILE_PATH === ":memory:" ? tmpdir() : dirname2(normalizedPath(DB_FILE_PATH));
|
|
18893
|
+
const base = DB_FILE_PATH === ":memory:" ? "supertask-memory" : basename2(DB_FILE_PATH, ".db");
|
|
18894
|
+
return resolve(directory, `${base}.${kind}-${timestamp()}-${randomUUID().slice(0, 8)}.db`);
|
|
18895
|
+
}
|
|
18896
|
+
static readCounts(sqlite2) {
|
|
18897
|
+
return {
|
|
18898
|
+
tasks: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks"),
|
|
18899
|
+
taskRuns: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs"),
|
|
18900
|
+
taskTemplates: this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_templates")
|
|
18901
|
+
};
|
|
18902
|
+
}
|
|
18903
|
+
static scalar(sqlite2, statement) {
|
|
18904
|
+
const row = sqlite2.query(statement).get();
|
|
18905
|
+
return Number(row?.count ?? 0);
|
|
18906
|
+
}
|
|
18907
|
+
static assertGatewaySafe(sqlite2, allowCurrentGateway) {
|
|
18908
|
+
const lock = sqlite2.query("SELECT pid FROM gateway_lock WHERE id = 1").get();
|
|
18909
|
+
if (!lock || !isProcessAlive(lock.pid)) return;
|
|
18910
|
+
if (allowCurrentGateway && lock.pid === process.pid) return;
|
|
18911
|
+
throw new DatabaseMaintenanceConflictError(
|
|
18912
|
+
`Gateway PID ${lock.pid} \u4ECD\u5728\u8FD0\u884C\uFF0C\u8BF7\u5148\u6267\u884C pm2 stop supertask-gateway`
|
|
18913
|
+
);
|
|
18914
|
+
}
|
|
18915
|
+
static assertNoRunningWork(sqlite2) {
|
|
18916
|
+
const runningTasks = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM tasks WHERE status = 'running'");
|
|
18917
|
+
const runningRuns = this.scalar(sqlite2, "SELECT COUNT(*) AS count FROM task_runs WHERE status = 'running'");
|
|
18918
|
+
if (runningTasks > 0 || runningRuns > 0) {
|
|
18919
|
+
throw new DatabaseMaintenanceConflictError(
|
|
18920
|
+
`\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08tasks=${runningTasks}, task_runs=${runningRuns}\uFF09\uFF0C\u5DF2\u62D2\u7EDD\u5371\u9669\u64CD\u4F5C`
|
|
18921
|
+
);
|
|
18922
|
+
}
|
|
18923
|
+
}
|
|
18924
|
+
};
|
|
18925
|
+
}
|
|
18926
|
+
});
|
|
18927
|
+
|
|
18522
18928
|
// src/web/index.tsx
|
|
18523
18929
|
var web_exports = {};
|
|
18524
18930
|
__export(web_exports, {
|
|
18525
18931
|
dashboardApp: () => dashboardApp,
|
|
18526
18932
|
default: () => web_default
|
|
18527
18933
|
});
|
|
18528
|
-
import { existsSync as
|
|
18529
|
-
import { dirname as
|
|
18934
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
|
|
18935
|
+
import { dirname as dirname3 } from "path";
|
|
18530
18936
|
function parsePositiveInteger(value) {
|
|
18531
18937
|
if (!/^\d+$/.test(value)) return null;
|
|
18532
18938
|
const parsed = Number(value);
|
|
@@ -18576,7 +18982,7 @@ function esc(s) {
|
|
|
18576
18982
|
}
|
|
18577
18983
|
function readCurrentConfig() {
|
|
18578
18984
|
const configPath = getConfigPath();
|
|
18579
|
-
if (!
|
|
18985
|
+
if (!existsSync4(configPath)) return {};
|
|
18580
18986
|
try {
|
|
18581
18987
|
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
18582
18988
|
} catch {
|
|
@@ -18585,11 +18991,11 @@ function readCurrentConfig() {
|
|
|
18585
18991
|
}
|
|
18586
18992
|
function writeConfig(cfg) {
|
|
18587
18993
|
const configPath = getConfigPath();
|
|
18588
|
-
const dir =
|
|
18589
|
-
if (!
|
|
18994
|
+
const dir = dirname3(configPath);
|
|
18995
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
18590
18996
|
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
18591
|
-
|
|
18592
|
-
|
|
18997
|
+
writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
18998
|
+
renameSync2(tempPath, configPath);
|
|
18593
18999
|
}
|
|
18594
19000
|
function renderLayout(title, activeTab, body) {
|
|
18595
19001
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -18606,12 +19012,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
|
|
|
18606
19012
|
function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
|
|
18607
19013
|
|
|
18608
19014
|
async function clearDatabase(){
|
|
18609
|
-
if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\
|
|
18610
|
-
if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
|
|
19015
|
+
if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\u7CFB\u7EDF\u4F1A\u5148\u81EA\u52A8\u521B\u5EFA\u5907\u4EFD\u3002'))return;
|
|
19016
|
+
if(!confirm('\u518D\u6B21\u786E\u8BA4\uFF1A\u5C06\u4E8B\u52A1\u6027\u5220\u9664\u6240\u6709\u4EFB\u52A1\u3001\u6267\u884C\u8BB0\u5F55\u548C\u8C03\u5EA6\u6A21\u677F\u3002'))return;
|
|
18611
19017
|
try{
|
|
18612
|
-
const r=await fetch('/api/database/clear',{method:'POST'});
|
|
19018
|
+
const r=await fetch('/api/database/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({confirmation:'CLEAR'})});
|
|
18613
19019
|
const d=await r.json();
|
|
18614
|
-
if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A');location.reload();}
|
|
19020
|
+
if(d.success){alert('\u6570\u636E\u5E93\u5DF2\u6E05\u7A7A\uFF0C\u81EA\u52A8\u5907\u4EFD\uFF1A'+d.backupPath);location.reload();}
|
|
18615
19021
|
else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
|
|
18616
19022
|
}catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
|
|
18617
19023
|
}
|
|
@@ -18672,6 +19078,7 @@ var init_web = __esm({
|
|
|
18672
19078
|
init_task_service();
|
|
18673
19079
|
init_task_run_service();
|
|
18674
19080
|
init_task_template_service();
|
|
19081
|
+
init_database_maintenance_service();
|
|
18675
19082
|
init_drizzle_orm();
|
|
18676
19083
|
init_db2();
|
|
18677
19084
|
init_config();
|
|
@@ -18980,7 +19387,7 @@ var init_web = __esm({
|
|
|
18980
19387
|
const stats = await TaskService.stats({});
|
|
18981
19388
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
18982
19389
|
const templates = await TaskTemplateService.list(100);
|
|
18983
|
-
const configExists =
|
|
19390
|
+
const configExists = existsSync4(configPath);
|
|
18984
19391
|
let runRows = "";
|
|
18985
19392
|
if (runningRuns.length > 0) {
|
|
18986
19393
|
for (const run of runningRuns) {
|
|
@@ -19054,7 +19461,7 @@ var init_web = __esm({
|
|
|
19054
19461
|
|
|
19055
19462
|
<div class="card mt16" style="border-color:var(--red)">
|
|
19056
19463
|
<h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
|
|
19057
|
-
<p class="sm mu" style="margin:0 0 12px">\
|
|
19464
|
+
<p class="sm mu" style="margin:0 0 12px">\u81EA\u52A8\u5907\u4EFD\u540E\u4E8B\u52A1\u6027\u6E05\u7A7A tasks\u3001task_runs \u548C task_templates\uFF1B\u5B58\u5728\u8FD0\u884C\u4E2D\u4EFB\u52A1\u65F6\u4F1A\u62D2\u7EDD\u6267\u884C\u3002</p>
|
|
19058
19465
|
<button class="btn btn-danger" style="border-color:var(--red);color:var(--red);padding:6px 16px" onclick="clearDatabase()">\u6E05\u7A7A\u6570\u636E\u5E93</button>
|
|
19059
19466
|
</div>`;
|
|
19060
19467
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
@@ -19159,14 +19566,16 @@ var init_web = __esm({
|
|
|
19159
19566
|
}
|
|
19160
19567
|
});
|
|
19161
19568
|
app.post("/api/database/clear", async (c) => {
|
|
19569
|
+
const body = await c.req.json().catch(() => ({}));
|
|
19570
|
+
if (body.confirmation !== "CLEAR") {
|
|
19571
|
+
return c.json({ success: false, error: "confirmation must be CLEAR" }, 400);
|
|
19572
|
+
}
|
|
19162
19573
|
try {
|
|
19163
|
-
const
|
|
19164
|
-
|
|
19165
|
-
await db.delete(taskTemplates4);
|
|
19166
|
-
await db.delete(tasks3);
|
|
19167
|
-
return c.json({ success: true });
|
|
19574
|
+
const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
|
|
19575
|
+
return c.json({ success: true, ...result });
|
|
19168
19576
|
} catch (err) {
|
|
19169
|
-
|
|
19577
|
+
const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
|
|
19578
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
|
|
19170
19579
|
}
|
|
19171
19580
|
});
|
|
19172
19581
|
dashboardApp = app;
|
|
@@ -19185,78 +19594,8 @@ init_config();
|
|
|
19185
19594
|
init_task_service();
|
|
19186
19595
|
init_task_run_service();
|
|
19187
19596
|
init_health();
|
|
19597
|
+
init_process_control();
|
|
19188
19598
|
import { spawn } from "child_process";
|
|
19189
|
-
|
|
19190
|
-
// src/core/process-control.ts
|
|
19191
|
-
import { spawnSync } from "child_process";
|
|
19192
|
-
import { basename } from "path";
|
|
19193
|
-
function isSafePid(pid) {
|
|
19194
|
-
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
19195
|
-
}
|
|
19196
|
-
function isProcessAlive(pid) {
|
|
19197
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
19198
|
-
try {
|
|
19199
|
-
process.kill(pid, 0);
|
|
19200
|
-
return true;
|
|
19201
|
-
} catch (error) {
|
|
19202
|
-
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
19203
|
-
}
|
|
19204
|
-
}
|
|
19205
|
-
function inspectUnixProcess(pid) {
|
|
19206
|
-
const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
19207
|
-
encoding: "utf8",
|
|
19208
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
19209
|
-
});
|
|
19210
|
-
if (result.status !== 0) return null;
|
|
19211
|
-
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
19212
|
-
if (!match2) return null;
|
|
19213
|
-
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
19214
|
-
}
|
|
19215
|
-
function commandMatches(command, expectedExecutable) {
|
|
19216
|
-
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
19217
|
-
if (!expectedName) return false;
|
|
19218
|
-
return command.toLowerCase().includes(expectedName);
|
|
19219
|
-
}
|
|
19220
|
-
function signalSpawnedProcessTree(pid, signal) {
|
|
19221
|
-
if (!isSafePid(pid)) return false;
|
|
19222
|
-
if (process.platform !== "win32") {
|
|
19223
|
-
try {
|
|
19224
|
-
process.kill(-pid, signal);
|
|
19225
|
-
return true;
|
|
19226
|
-
} catch {
|
|
19227
|
-
}
|
|
19228
|
-
}
|
|
19229
|
-
try {
|
|
19230
|
-
process.kill(pid, signal);
|
|
19231
|
-
return true;
|
|
19232
|
-
} catch {
|
|
19233
|
-
return false;
|
|
19234
|
-
}
|
|
19235
|
-
}
|
|
19236
|
-
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
19237
|
-
if (!isSafePid(pid)) return false;
|
|
19238
|
-
if (process.platform === "win32") {
|
|
19239
|
-
const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
19240
|
-
encoding: "utf8",
|
|
19241
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
19242
|
-
});
|
|
19243
|
-
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
19244
|
-
const args = ["/PID", String(pid), "/T"];
|
|
19245
|
-
if (signal === "SIGKILL") args.push("/F");
|
|
19246
|
-
return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
19247
|
-
}
|
|
19248
|
-
const info = inspectUnixProcess(pid);
|
|
19249
|
-
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
19250
|
-
try {
|
|
19251
|
-
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
19252
|
-
else process.kill(pid, signal);
|
|
19253
|
-
return true;
|
|
19254
|
-
} catch {
|
|
19255
|
-
return false;
|
|
19256
|
-
}
|
|
19257
|
-
}
|
|
19258
|
-
|
|
19259
|
-
// src/worker/index.ts
|
|
19260
19599
|
var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
|
|
19261
19600
|
var FORBIDDEN_AGENT = "supertask-runner";
|
|
19262
19601
|
var WorkerEngine = class {
|
|
@@ -19519,14 +19858,14 @@ ${output}` : ""}`;
|
|
|
19519
19858
|
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
19520
19859
|
return Promise.resolve();
|
|
19521
19860
|
}
|
|
19522
|
-
return new Promise((
|
|
19861
|
+
return new Promise((resolve2) => {
|
|
19523
19862
|
const timeout = setTimeout(() => {
|
|
19524
19863
|
this.signalEntry(entry, "SIGKILL");
|
|
19525
|
-
|
|
19864
|
+
resolve2();
|
|
19526
19865
|
}, 5e3);
|
|
19527
19866
|
entry.child.once("close", () => {
|
|
19528
19867
|
clearTimeout(timeout);
|
|
19529
|
-
|
|
19868
|
+
resolve2();
|
|
19530
19869
|
});
|
|
19531
19870
|
this.signalEntry(entry, "SIGTERM");
|
|
19532
19871
|
});
|
|
@@ -19558,6 +19897,7 @@ ${output}` : ""}`;
|
|
|
19558
19897
|
init_task_run_service();
|
|
19559
19898
|
init_task_service();
|
|
19560
19899
|
init_backoff();
|
|
19900
|
+
init_process_control();
|
|
19561
19901
|
async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
19562
19902
|
const staleRuns = await TaskRunService.getStaleRuns(heartbeatTimeoutMs);
|
|
19563
19903
|
if (staleRuns.length === 0) return;
|
|
@@ -19859,6 +20199,7 @@ init_db2();
|
|
|
19859
20199
|
init_task_service();
|
|
19860
20200
|
init_task_run_service();
|
|
19861
20201
|
init_health();
|
|
20202
|
+
init_process_control();
|
|
19862
20203
|
var STALE_THRESHOLD_MS = 3e4;
|
|
19863
20204
|
function acquireLock() {
|
|
19864
20205
|
const now = Date.now();
|