opencode-supertask 0.1.21 → 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 +617 -134
- package/dist/cli/index.js.map +1 -1
- package/dist/gateway/index.d.ts +3 -1
- package/dist/gateway/index.js +480 -101
- package/dist/gateway/index.js.map +1 -1
- package/dist/plugin/supertask.js +19 -2
- package/dist/plugin/supertask.js.map +1 -1
- package/dist/web/index.js +400 -36
- package/dist/web/index.js.map +1 -1
- package/dist/worker/index.js +17 -0
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/gateway/index.js
CHANGED
|
@@ -6543,6 +6543,23 @@ var init_task_service = __esm({
|
|
|
6543
6543
|
).returning();
|
|
6544
6544
|
return result.length;
|
|
6545
6545
|
}
|
|
6546
|
+
static async resetOrphanRunningToPending() {
|
|
6547
|
+
const result = await db.update(tasks2).set({
|
|
6548
|
+
status: "pending",
|
|
6549
|
+
startedAt: null,
|
|
6550
|
+
finishedAt: null
|
|
6551
|
+
}).where(
|
|
6552
|
+
and(
|
|
6553
|
+
eq(tasks2.status, "running"),
|
|
6554
|
+
sql`NOT EXISTS (
|
|
6555
|
+
SELECT 1 FROM ${taskRuns2}
|
|
6556
|
+
WHERE ${taskRuns2.taskId} = ${tasks2.id}
|
|
6557
|
+
AND ${taskRuns2.status} = 'running'
|
|
6558
|
+
)`
|
|
6559
|
+
)
|
|
6560
|
+
).returning();
|
|
6561
|
+
return result.length;
|
|
6562
|
+
}
|
|
6546
6563
|
static async cancel(id, scope = {}) {
|
|
6547
6564
|
const conditions = [
|
|
6548
6565
|
eq(tasks2.id, id),
|
|
@@ -6878,6 +6895,80 @@ var init_health = __esm({
|
|
|
6878
6895
|
}
|
|
6879
6896
|
});
|
|
6880
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
|
+
|
|
6881
6972
|
// node_modules/cron-parser/dist/fields/types.js
|
|
6882
6973
|
var require_types = __commonJS({
|
|
6883
6974
|
"node_modules/cron-parser/dist/fields/types.js"(exports) {
|
|
@@ -14091,26 +14182,26 @@ var require_CronDate = __commonJS({
|
|
|
14091
14182
|
* @param {CronDate | Date | number | string} [timestamp] - The timestamp to initialize the CronDate with.
|
|
14092
14183
|
* @param {string} [tz] - The timezone to use for the CronDate.
|
|
14093
14184
|
*/
|
|
14094
|
-
constructor(
|
|
14185
|
+
constructor(timestamp2, tz) {
|
|
14095
14186
|
const dateOpts = { zone: tz };
|
|
14096
|
-
if (!
|
|
14187
|
+
if (!timestamp2) {
|
|
14097
14188
|
this.#date = luxon_1.DateTime.local();
|
|
14098
|
-
} else if (
|
|
14099
|
-
this.#date =
|
|
14100
|
-
this.#dstStart =
|
|
14101
|
-
this.#dstEnd =
|
|
14102
|
-
} else if (
|
|
14103
|
-
this.#date = luxon_1.DateTime.fromJSDate(
|
|
14104
|
-
} else if (typeof
|
|
14105
|
-
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);
|
|
14106
14197
|
} else {
|
|
14107
|
-
this.#date = luxon_1.DateTime.fromISO(
|
|
14108
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromRFC2822(
|
|
14109
|
-
this.#date.isValid || (this.#date = luxon_1.DateTime.fromSQL(
|
|
14110
|
-
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));
|
|
14111
14202
|
}
|
|
14112
14203
|
if (!this.#date.isValid) {
|
|
14113
|
-
throw new Error(`CronDate: unhandled timestamp: ${
|
|
14204
|
+
throw new Error(`CronDate: unhandled timestamp: ${timestamp2}`);
|
|
14114
14205
|
}
|
|
14115
14206
|
if (tz && tz !== this.#date.zoneName) {
|
|
14116
14207
|
this.#date = this.#date.setZone(tz);
|
|
@@ -18502,14 +18593,346 @@ var init_html2 = __esm({
|
|
|
18502
18593
|
}
|
|
18503
18594
|
});
|
|
18504
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
|
+
|
|
18505
18928
|
// src/web/index.tsx
|
|
18506
18929
|
var web_exports = {};
|
|
18507
18930
|
__export(web_exports, {
|
|
18508
18931
|
dashboardApp: () => dashboardApp,
|
|
18509
18932
|
default: () => web_default
|
|
18510
18933
|
});
|
|
18511
|
-
import { existsSync as
|
|
18512
|
-
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";
|
|
18513
18936
|
function parsePositiveInteger(value) {
|
|
18514
18937
|
if (!/^\d+$/.test(value)) return null;
|
|
18515
18938
|
const parsed = Number(value);
|
|
@@ -18559,7 +18982,7 @@ function esc(s) {
|
|
|
18559
18982
|
}
|
|
18560
18983
|
function readCurrentConfig() {
|
|
18561
18984
|
const configPath = getConfigPath();
|
|
18562
|
-
if (!
|
|
18985
|
+
if (!existsSync4(configPath)) return {};
|
|
18563
18986
|
try {
|
|
18564
18987
|
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
18565
18988
|
} catch {
|
|
@@ -18568,11 +18991,11 @@ function readCurrentConfig() {
|
|
|
18568
18991
|
}
|
|
18569
18992
|
function writeConfig(cfg) {
|
|
18570
18993
|
const configPath = getConfigPath();
|
|
18571
|
-
const dir =
|
|
18572
|
-
if (!
|
|
18994
|
+
const dir = dirname3(configPath);
|
|
18995
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
18573
18996
|
const tempPath = `${configPath}.${process.pid}.tmp`;
|
|
18574
|
-
|
|
18575
|
-
|
|
18997
|
+
writeFileSync2(tempPath, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
|
|
18998
|
+
renameSync2(tempPath, configPath);
|
|
18576
18999
|
}
|
|
18577
19000
|
function renderLayout(title, activeTab, body) {
|
|
18578
19001
|
return `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${title} - SuperTask</title>${SHARED_STYLES}
|
|
@@ -18589,12 +19012,12 @@ async function triggerTmpl(id){if(!confirm('\u7ACB\u5373\u89E6\u53D1\u4E00\u6B21
|
|
|
18589
19012
|
function toggleLog(id){const el=document.getElementById('log-'+id);el.style.display=el.style.display==='none'?'block':'none';}
|
|
18590
19013
|
|
|
18591
19014
|
async function clearDatabase(){
|
|
18592
|
-
if(!confirm('\u786E\u5B9A\u6E05\u7A7A\u6240\u6709\u4EFB\u52A1\u6570\u636E\uFF1F\
|
|
18593
|
-
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;
|
|
18594
19017
|
try{
|
|
18595
|
-
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'})});
|
|
18596
19019
|
const d=await r.json();
|
|
18597
|
-
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();}
|
|
18598
19021
|
else{alert('\u6E05\u7A7A\u5931\u8D25: '+d.error);}
|
|
18599
19022
|
}catch(e){alert('\u6E05\u7A7A\u5931\u8D25: '+e.message);}
|
|
18600
19023
|
}
|
|
@@ -18655,6 +19078,7 @@ var init_web = __esm({
|
|
|
18655
19078
|
init_task_service();
|
|
18656
19079
|
init_task_run_service();
|
|
18657
19080
|
init_task_template_service();
|
|
19081
|
+
init_database_maintenance_service();
|
|
18658
19082
|
init_drizzle_orm();
|
|
18659
19083
|
init_db2();
|
|
18660
19084
|
init_config();
|
|
@@ -18963,7 +19387,7 @@ var init_web = __esm({
|
|
|
18963
19387
|
const stats = await TaskService.stats({});
|
|
18964
19388
|
const runningRuns = await TaskRunService.getAllRunningRuns();
|
|
18965
19389
|
const templates = await TaskTemplateService.list(100);
|
|
18966
|
-
const configExists =
|
|
19390
|
+
const configExists = existsSync4(configPath);
|
|
18967
19391
|
let runRows = "";
|
|
18968
19392
|
if (runningRuns.length > 0) {
|
|
18969
19393
|
for (const run of runningRuns) {
|
|
@@ -19037,7 +19461,7 @@ var init_web = __esm({
|
|
|
19037
19461
|
|
|
19038
19462
|
<div class="card mt16" style="border-color:var(--red)">
|
|
19039
19463
|
<h3 style="margin:0 0 12px;font-size:14px;color:var(--red)">\u5371\u9669\u64CD\u4F5C</h3>
|
|
19040
|
-
<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>
|
|
19041
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>
|
|
19042
19466
|
</div>`;
|
|
19043
19467
|
return c.html(renderLayout("\u7CFB\u7EDF\u72B6\u6001", "system", body));
|
|
@@ -19142,14 +19566,16 @@ var init_web = __esm({
|
|
|
19142
19566
|
}
|
|
19143
19567
|
});
|
|
19144
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
|
+
}
|
|
19145
19573
|
try {
|
|
19146
|
-
const
|
|
19147
|
-
|
|
19148
|
-
await db.delete(taskTemplates4);
|
|
19149
|
-
await db.delete(tasks3);
|
|
19150
|
-
return c.json({ success: true });
|
|
19574
|
+
const result = DatabaseMaintenanceService.clear({ allowCurrentGateway: true });
|
|
19575
|
+
return c.json({ success: true, ...result });
|
|
19151
19576
|
} catch (err) {
|
|
19152
|
-
|
|
19577
|
+
const status = err instanceof DatabaseMaintenanceConflictError ? 409 : 500;
|
|
19578
|
+
return c.json({ success: false, error: err instanceof Error ? err.message : String(err) }, status);
|
|
19153
19579
|
}
|
|
19154
19580
|
});
|
|
19155
19581
|
dashboardApp = app;
|
|
@@ -19168,69 +19594,8 @@ init_config();
|
|
|
19168
19594
|
init_task_service();
|
|
19169
19595
|
init_task_run_service();
|
|
19170
19596
|
init_health();
|
|
19597
|
+
init_process_control();
|
|
19171
19598
|
import { spawn } from "child_process";
|
|
19172
|
-
|
|
19173
|
-
// src/core/process-control.ts
|
|
19174
|
-
import { spawnSync } from "child_process";
|
|
19175
|
-
import { basename } from "path";
|
|
19176
|
-
function isSafePid(pid) {
|
|
19177
|
-
return Number.isInteger(pid) && pid > 1 && pid !== process.pid;
|
|
19178
|
-
}
|
|
19179
|
-
function inspectUnixProcess(pid) {
|
|
19180
|
-
const result = spawnSync("ps", ["-o", "pgid=", "-o", "command=", "-p", String(pid)], {
|
|
19181
|
-
encoding: "utf8",
|
|
19182
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
19183
|
-
});
|
|
19184
|
-
if (result.status !== 0) return null;
|
|
19185
|
-
const match2 = result.stdout.trim().match(/^(\d+)\s+(.+)$/s);
|
|
19186
|
-
if (!match2) return null;
|
|
19187
|
-
return { processGroupId: Number(match2[1]), command: match2[2] };
|
|
19188
|
-
}
|
|
19189
|
-
function commandMatches(command, expectedExecutable) {
|
|
19190
|
-
const expectedName = basename(expectedExecutable).trim().toLowerCase();
|
|
19191
|
-
if (!expectedName) return false;
|
|
19192
|
-
return command.toLowerCase().includes(expectedName);
|
|
19193
|
-
}
|
|
19194
|
-
function signalSpawnedProcessTree(pid, signal) {
|
|
19195
|
-
if (!isSafePid(pid)) return false;
|
|
19196
|
-
if (process.platform !== "win32") {
|
|
19197
|
-
try {
|
|
19198
|
-
process.kill(-pid, signal);
|
|
19199
|
-
return true;
|
|
19200
|
-
} catch {
|
|
19201
|
-
}
|
|
19202
|
-
}
|
|
19203
|
-
try {
|
|
19204
|
-
process.kill(pid, signal);
|
|
19205
|
-
return true;
|
|
19206
|
-
} catch {
|
|
19207
|
-
return false;
|
|
19208
|
-
}
|
|
19209
|
-
}
|
|
19210
|
-
function signalRecordedProcessTree(pid, signal, expectedExecutable) {
|
|
19211
|
-
if (!isSafePid(pid)) return false;
|
|
19212
|
-
if (process.platform === "win32") {
|
|
19213
|
-
const list = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
19214
|
-
encoding: "utf8",
|
|
19215
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
19216
|
-
});
|
|
19217
|
-
if (list.status !== 0 || !commandMatches(list.stdout, expectedExecutable)) return false;
|
|
19218
|
-
const args = ["/PID", String(pid), "/T"];
|
|
19219
|
-
if (signal === "SIGKILL") args.push("/F");
|
|
19220
|
-
return spawnSync("taskkill", args, { stdio: "ignore" }).status === 0;
|
|
19221
|
-
}
|
|
19222
|
-
const info = inspectUnixProcess(pid);
|
|
19223
|
-
if (!info || !commandMatches(info.command, expectedExecutable)) return false;
|
|
19224
|
-
try {
|
|
19225
|
-
if (info.processGroupId === pid) process.kill(-pid, signal);
|
|
19226
|
-
else process.kill(pid, signal);
|
|
19227
|
-
return true;
|
|
19228
|
-
} catch {
|
|
19229
|
-
return false;
|
|
19230
|
-
}
|
|
19231
|
-
}
|
|
19232
|
-
|
|
19233
|
-
// src/worker/index.ts
|
|
19234
19599
|
var DEFAULT_MAX_OUTPUT_CHARS = 64 * 1024;
|
|
19235
19600
|
var FORBIDDEN_AGENT = "supertask-runner";
|
|
19236
19601
|
var WorkerEngine = class {
|
|
@@ -19493,14 +19858,14 @@ ${output}` : ""}`;
|
|
|
19493
19858
|
if (entry.child.exitCode !== null || entry.child.signalCode !== null) {
|
|
19494
19859
|
return Promise.resolve();
|
|
19495
19860
|
}
|
|
19496
|
-
return new Promise((
|
|
19861
|
+
return new Promise((resolve2) => {
|
|
19497
19862
|
const timeout = setTimeout(() => {
|
|
19498
19863
|
this.signalEntry(entry, "SIGKILL");
|
|
19499
|
-
|
|
19864
|
+
resolve2();
|
|
19500
19865
|
}, 5e3);
|
|
19501
19866
|
entry.child.once("close", () => {
|
|
19502
19867
|
clearTimeout(timeout);
|
|
19503
|
-
|
|
19868
|
+
resolve2();
|
|
19504
19869
|
});
|
|
19505
19870
|
this.signalEntry(entry, "SIGTERM");
|
|
19506
19871
|
});
|
|
@@ -19532,6 +19897,7 @@ ${output}` : ""}`;
|
|
|
19532
19897
|
init_task_run_service();
|
|
19533
19898
|
init_task_service();
|
|
19534
19899
|
init_backoff();
|
|
19900
|
+
init_process_control();
|
|
19535
19901
|
async function checkHeartbeats(heartbeatTimeoutMs) {
|
|
19536
19902
|
const staleRuns = await TaskRunService.getStaleRuns(heartbeatTimeoutMs);
|
|
19537
19903
|
if (staleRuns.length === 0) return;
|
|
@@ -19833,6 +20199,7 @@ init_db2();
|
|
|
19833
20199
|
init_task_service();
|
|
19834
20200
|
init_task_run_service();
|
|
19835
20201
|
init_health();
|
|
20202
|
+
init_process_control();
|
|
19836
20203
|
var STALE_THRESHOLD_MS = 3e4;
|
|
19837
20204
|
function acquireLock() {
|
|
19838
20205
|
const now = Date.now();
|
|
@@ -19841,7 +20208,8 @@ function acquireLock() {
|
|
|
19841
20208
|
sqlite.exec("BEGIN IMMEDIATE");
|
|
19842
20209
|
const existing = sqlite.prepare("SELECT id, pid, heartbeat_at FROM gateway_lock WHERE id = 1").get();
|
|
19843
20210
|
if (existing) {
|
|
19844
|
-
|
|
20211
|
+
const lockHolderAlive = existing.pid !== pid && isProcessAlive(existing.pid);
|
|
20212
|
+
if (now - existing.heartbeat_at < STALE_THRESHOLD_MS && lockHolderAlive) {
|
|
19845
20213
|
sqlite.exec("ROLLBACK");
|
|
19846
20214
|
console.error(JSON.stringify({
|
|
19847
20215
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -19911,6 +20279,15 @@ async function main() {
|
|
|
19911
20279
|
const worker = new WorkerEngine(cfg);
|
|
19912
20280
|
const watchdog = new Watchdog(cfg);
|
|
19913
20281
|
const scheduler = new Scheduler(cfg);
|
|
20282
|
+
const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
|
|
20283
|
+
if (recoveredOrphans > 0) {
|
|
20284
|
+
console.log(JSON.stringify({
|
|
20285
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
20286
|
+
level: "warn",
|
|
20287
|
+
msg: "reset orphan running tasks to pending",
|
|
20288
|
+
count: recoveredOrphans
|
|
20289
|
+
}));
|
|
20290
|
+
}
|
|
19914
20291
|
initializeGatewayHealth({
|
|
19915
20292
|
workerPollIntervalMs: cfg.worker.pollIntervalMs,
|
|
19916
20293
|
schedulerEnabled: cfg.scheduler.enabled,
|
|
@@ -19981,6 +20358,8 @@ if (import.meta.main) {
|
|
|
19981
20358
|
main();
|
|
19982
20359
|
}
|
|
19983
20360
|
export {
|
|
19984
|
-
|
|
20361
|
+
acquireLock,
|
|
20362
|
+
main,
|
|
20363
|
+
releaseLock
|
|
19985
20364
|
};
|
|
19986
20365
|
//# sourceMappingURL=index.js.map
|