@nmakarov/cli-toolkit 0.16.0 → 0.21.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.
- package/README.md +9 -0
- package/dist/cli-runner.cjs +5335 -0
- package/dist/cli-runner.cjs.map +1 -0
- package/dist/cli-runner.js +5318 -0
- package/dist/cli-runner.js.map +1 -0
- package/dist/db.cjs +1 -1
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +1 -1
- package/dist/db.js.map +1 -1
- package/dist/filedatabase.cjs +94 -13
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +94 -13
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client2.cjs +1375 -11
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +1365 -11
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1639 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1608 -29
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +25 -7
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +25 -7
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +9 -6
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +9 -6
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +192 -343
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +190 -343
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +16 -1
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +16 -1
- package/dist/params.js.map +1 -1
- package/dist/tasks.cjs +2666 -0
- package/dist/tasks.cjs.map +1 -0
- package/dist/tasks.js +2600 -0
- package/dist/tasks.js.map +1 -0
- package/dist/utils.cjs +15 -2
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +12 -1
- package/dist/utils.js.map +1 -1
- package/package.json +20 -5
- package/scripts/ssm/parse-cli.ts +35 -0
- package/scripts/ssm/ssm-admin.ts +151 -0
- package/scripts/ssm/ssm-pull.ts +150 -0
package/dist/index.js
CHANGED
|
@@ -901,11 +901,11 @@ function buildBreadcrumb(parts) {
|
|
|
901
901
|
if (parts.length === 1) return parts[0];
|
|
902
902
|
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
903
903
|
}
|
|
904
|
-
function buildDetailBreadcrumb(
|
|
905
|
-
if (
|
|
906
|
-
return suffix ? `\u2190 ${suffix}` :
|
|
904
|
+
function buildDetailBreadcrumb(path5, suffix = "") {
|
|
905
|
+
if (path5.length <= 1) {
|
|
906
|
+
return suffix ? `\u2190 ${suffix}` : path5[0] || "";
|
|
907
907
|
}
|
|
908
|
-
const breadcrumb = buildBreadcrumb(
|
|
908
|
+
const breadcrumb = buildBreadcrumb(path5);
|
|
909
909
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
910
910
|
}
|
|
911
911
|
var init_utils = __esm({
|
|
@@ -1876,7 +1876,7 @@ var Params = class _Params {
|
|
|
1876
1876
|
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
1877
1877
|
}
|
|
1878
1878
|
type = type.default(defValObj.value);
|
|
1879
|
-
} else if (str.match(
|
|
1879
|
+
} else if (str.match(/\s*required\s*/)) {
|
|
1880
1880
|
type = type.required();
|
|
1881
1881
|
} else {
|
|
1882
1882
|
type = type.optional();
|
|
@@ -1952,6 +1952,8 @@ var Params = class _Params {
|
|
|
1952
1952
|
/**
|
|
1953
1953
|
* Get all parameters from definitions (main script).
|
|
1954
1954
|
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
1955
|
+
* Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
|
|
1956
|
+
* around {@link get}) so --showUsedParams groups usage correctly.
|
|
1955
1957
|
*/
|
|
1956
1958
|
getAll(defs) {
|
|
1957
1959
|
return this.getAllForModule("script", defs);
|
|
@@ -1988,6 +1990,19 @@ var Params = class _Params {
|
|
|
1988
1990
|
this._currentModule = prev;
|
|
1989
1991
|
}
|
|
1990
1992
|
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
|
|
1995
|
+
* under the same module (for --showUsedParams / getFiguredByModule).
|
|
1996
|
+
*/
|
|
1997
|
+
runWithModule(moduleName, fn) {
|
|
1998
|
+
const prev = this._currentModule;
|
|
1999
|
+
this._currentModule = moduleName;
|
|
2000
|
+
try {
|
|
2001
|
+
return fn();
|
|
2002
|
+
} finally {
|
|
2003
|
+
this._currentModule = prev;
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
1991
2006
|
/**
|
|
1992
2007
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
1993
2008
|
*/
|
|
@@ -2001,9 +2016,9 @@ var Params = class _Params {
|
|
|
2001
2016
|
if (!parenMatch) continue;
|
|
2002
2017
|
const parts = parenMatch[1].split(":");
|
|
2003
2018
|
if (parts.length < 3) continue;
|
|
2004
|
-
const
|
|
2005
|
-
if (!
|
|
2006
|
-
const srcMatch =
|
|
2019
|
+
const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2020
|
+
if (!path5 || path5.includes(paramsIndexPath)) continue;
|
|
2021
|
+
const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2007
2022
|
if (srcMatch) return srcMatch[1];
|
|
2008
2023
|
}
|
|
2009
2024
|
return "script";
|
|
@@ -2358,7 +2373,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2358
2373
|
const versions = await this.getVersions();
|
|
2359
2374
|
while (versions.length > this.maxVersions) {
|
|
2360
2375
|
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
2361
|
-
this.logger.
|
|
2376
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
2362
2377
|
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
2363
2378
|
}
|
|
2364
2379
|
return versionName;
|
|
@@ -2636,7 +2651,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2636
2651
|
};
|
|
2637
2652
|
this.metadata.files.push(fileEntry);
|
|
2638
2653
|
this.lastFileData = null;
|
|
2639
|
-
this.logger.
|
|
2654
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
2640
2655
|
}
|
|
2641
2656
|
/**
|
|
2642
2657
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -2669,7 +2684,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2669
2684
|
const filesBeforeCreate = this.metadata.files.length;
|
|
2670
2685
|
this.makeNewFile();
|
|
2671
2686
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
2672
|
-
this.logger.
|
|
2687
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
2673
2688
|
} else if (this.metadata.files.length === 0) {
|
|
2674
2689
|
this.makeNewFile();
|
|
2675
2690
|
}
|
|
@@ -2718,7 +2733,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2718
2733
|
dataLeftOver = null;
|
|
2719
2734
|
}
|
|
2720
2735
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
2721
|
-
this.logger.
|
|
2736
|
+
this.logger.silly?.(
|
|
2722
2737
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
2723
2738
|
);
|
|
2724
2739
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -2773,7 +2788,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2773
2788
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2774
2789
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
2775
2790
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
2776
|
-
this.logger.
|
|
2791
|
+
this.logger.silly?.(
|
|
2777
2792
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
2778
2793
|
);
|
|
2779
2794
|
}
|
|
@@ -2797,7 +2812,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2797
2812
|
}
|
|
2798
2813
|
try {
|
|
2799
2814
|
await fs3.promises.writeFile(filePath, serializedData, "utf8");
|
|
2800
|
-
this.logger.
|
|
2815
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
2801
2816
|
} catch (error) {
|
|
2802
2817
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
2803
2818
|
}
|
|
@@ -2879,7 +2894,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
2879
2894
|
this.useMetadata = format.hasMetadata;
|
|
2880
2895
|
}
|
|
2881
2896
|
if (this.useMetadata) {
|
|
2882
|
-
const
|
|
2897
|
+
const destPath = this.getDestinationPath();
|
|
2898
|
+
const metadataPath = path3.join(destPath, "metadata.json");
|
|
2883
2899
|
if (fs3.existsSync(metadataPath)) {
|
|
2884
2900
|
try {
|
|
2885
2901
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -2893,7 +2909,9 @@ var FileDatabase = class _FileDatabase {
|
|
|
2893
2909
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
2894
2910
|
}
|
|
2895
2911
|
} else {
|
|
2896
|
-
throw new FileDatabaseError(
|
|
2912
|
+
throw new FileDatabaseError(
|
|
2913
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
2914
|
+
);
|
|
2897
2915
|
}
|
|
2898
2916
|
} else {
|
|
2899
2917
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -2910,6 +2928,13 @@ var FileDatabase = class _FileDatabase {
|
|
|
2910
2928
|
* Write data to the file database
|
|
2911
2929
|
*/
|
|
2912
2930
|
async write(data, options = {}) {
|
|
2931
|
+
if (options.filename) {
|
|
2932
|
+
const destPath2 = this.getDestinationPath();
|
|
2933
|
+
await ensurePath(destPath2);
|
|
2934
|
+
const filePath = path3.join(destPath2, options.filename);
|
|
2935
|
+
await this.safeWrite(filePath, data);
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2913
2938
|
if (options.forceNewVersion && !this.versioned) {
|
|
2914
2939
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2915
2940
|
}
|
|
@@ -2933,17 +2958,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
2933
2958
|
});
|
|
2934
2959
|
if (matches) {
|
|
2935
2960
|
targetFileIndex = i;
|
|
2936
|
-
this.logger.
|
|
2961
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
2937
2962
|
break;
|
|
2938
2963
|
} else {
|
|
2939
|
-
this.logger.
|
|
2964
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
2940
2965
|
}
|
|
2941
2966
|
}
|
|
2942
2967
|
if (targetFileIndex === null) {
|
|
2943
|
-
this.logger.
|
|
2968
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
2944
2969
|
}
|
|
2945
2970
|
} else {
|
|
2946
|
-
this.logger.
|
|
2971
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
2947
2972
|
}
|
|
2948
2973
|
if (targetFileIndex !== null) {
|
|
2949
2974
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -2972,7 +2997,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
2972
2997
|
* Read data from the file database
|
|
2973
2998
|
*/
|
|
2974
2999
|
async read(options = {}) {
|
|
2975
|
-
const { version, nextPage = false, pageSize } = options;
|
|
3000
|
+
const { version, nextPage = false, pageSize, filename } = options;
|
|
3001
|
+
if (filename) {
|
|
3002
|
+
const destPath = this.getDestinationPath(version);
|
|
3003
|
+
const filePath = path3.join(destPath, filename);
|
|
3004
|
+
try {
|
|
3005
|
+
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3006
|
+
return JSON.parse(rawData);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
2976
3011
|
await this.prepare({ read: true, version });
|
|
2977
3012
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
2978
3013
|
if (isNonPaginatedData) {
|
|
@@ -3053,6 +3088,67 @@ var FileDatabase = class _FileDatabase {
|
|
|
3053
3088
|
this.currentRecord = 0;
|
|
3054
3089
|
this.hasReadFirstPage = false;
|
|
3055
3090
|
}
|
|
3091
|
+
/**
|
|
3092
|
+
* List filenames in the table directory.
|
|
3093
|
+
* For catalog/key-value usage (files written with { filename }).
|
|
3094
|
+
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
3095
|
+
*/
|
|
3096
|
+
async listFilenames() {
|
|
3097
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3098
|
+
try {
|
|
3099
|
+
const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
|
|
3100
|
+
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
3101
|
+
} catch (err) {
|
|
3102
|
+
if (err?.code === "ENOENT") return [];
|
|
3103
|
+
throw new FileDatabaseError(`Failed to list files: ${err.message}`);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Remove a file from the table directory (catalog mode).
|
|
3108
|
+
* Use with listFilenames() to manage individual files.
|
|
3109
|
+
*/
|
|
3110
|
+
async removeFile(filename) {
|
|
3111
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3112
|
+
const filePath = path3.join(destPath, filename);
|
|
3113
|
+
try {
|
|
3114
|
+
await fs3.promises.unlink(filePath);
|
|
3115
|
+
} catch (err) {
|
|
3116
|
+
if (err?.code === "ENOENT") return;
|
|
3117
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Remove a file and its metadata entry (non-versioned mode with useMetadata).
|
|
3122
|
+
* Use with findData() to get fileName, then call removeFileEntry to delete.
|
|
3123
|
+
*/
|
|
3124
|
+
async removeFileEntry(filename) {
|
|
3125
|
+
if (this.versioned) {
|
|
3126
|
+
throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
|
|
3127
|
+
}
|
|
3128
|
+
await this.prepare({ read: true });
|
|
3129
|
+
const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
|
|
3130
|
+
if (idx === -1) {
|
|
3131
|
+
throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
|
|
3132
|
+
}
|
|
3133
|
+
const entry = this.metadata.files[idx];
|
|
3134
|
+
const recordsCount = entry.recordsCount || 0;
|
|
3135
|
+
this.metadata.files.splice(idx, 1);
|
|
3136
|
+
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
3137
|
+
const destPath = this.getDestinationPath();
|
|
3138
|
+
const filePath = path3.join(destPath, filename);
|
|
3139
|
+
try {
|
|
3140
|
+
await fs3.promises.unlink(filePath);
|
|
3141
|
+
} catch (err) {
|
|
3142
|
+
if (err?.code === "ENOENT") {
|
|
3143
|
+
this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
|
|
3144
|
+
} else {
|
|
3145
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
if (this.useMetadata) {
|
|
3149
|
+
await this.saveVersionMetadata(this.metadata);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3056
3152
|
/**
|
|
3057
3153
|
* Set file-level synopsis calculation function
|
|
3058
3154
|
*/
|
|
@@ -3524,7 +3620,7 @@ async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
|
3524
3620
|
dbConnectionString: "string",
|
|
3525
3621
|
dbProfile: "boolean default false"
|
|
3526
3622
|
};
|
|
3527
|
-
const paramsConfig = context.params.
|
|
3623
|
+
const paramsConfig = context.params.getAll(defs);
|
|
3528
3624
|
dbName = paramsConfig.dbName;
|
|
3529
3625
|
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3530
3626
|
dbProfile = paramsConfig.dbProfile;
|
|
@@ -3586,6 +3682,7 @@ var ALL_LEVELS = [
|
|
|
3586
3682
|
"response",
|
|
3587
3683
|
"progress"
|
|
3588
3684
|
];
|
|
3685
|
+
var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
|
|
3589
3686
|
var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
|
|
3590
3687
|
var LEVEL_COLORS = {
|
|
3591
3688
|
error: chalk.red.bold,
|
|
@@ -3671,7 +3768,7 @@ var Logger = class _Logger {
|
|
|
3671
3768
|
silent: false,
|
|
3672
3769
|
showLevel: false,
|
|
3673
3770
|
timestamp: false,
|
|
3674
|
-
levels:
|
|
3771
|
+
levels: DEFAULT_LEVELS,
|
|
3675
3772
|
progressTimes: false,
|
|
3676
3773
|
progressThrottle: void 0
|
|
3677
3774
|
};
|
|
@@ -3831,15 +3928,17 @@ var Logger = class _Logger {
|
|
|
3831
3928
|
}
|
|
3832
3929
|
normalizeLevels(levels) {
|
|
3833
3930
|
if (!levels || !levels.length) {
|
|
3834
|
-
return
|
|
3931
|
+
return DEFAULT_LEVELS;
|
|
3835
3932
|
}
|
|
3836
|
-
const
|
|
3837
|
-
const
|
|
3838
|
-
const
|
|
3933
|
+
const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
|
|
3934
|
+
const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
|
|
3935
|
+
const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
|
|
3936
|
+
const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
|
|
3937
|
+
const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
|
|
3839
3938
|
if (unknown.length) {
|
|
3840
3939
|
console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
|
|
3841
3940
|
}
|
|
3842
|
-
const base =
|
|
3941
|
+
const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
|
|
3843
3942
|
return base.filter((level) => !excludes.includes(level));
|
|
3844
3943
|
}
|
|
3845
3944
|
isValidMode(mode) {
|
|
@@ -3897,6 +3996,1456 @@ function setup(opts = {}) {
|
|
|
3897
3996
|
function setupContext(opts = {}) {
|
|
3898
3997
|
return setup(opts);
|
|
3899
3998
|
}
|
|
3999
|
+
|
|
4000
|
+
// src/utils/core-utils.ts
|
|
4001
|
+
function sleepMs(ms) {
|
|
4002
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4003
|
+
}
|
|
4004
|
+
function toJsonColumn(value) {
|
|
4005
|
+
if (value === void 0 || value === null) return null;
|
|
4006
|
+
return JSON.stringify(value);
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
// src/tasks/servicesRegistry.ts
|
|
4010
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4011
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4012
|
+
import os from "os";
|
|
4013
|
+
import path4 from "path";
|
|
4014
|
+
|
|
4015
|
+
// src/tasks/taskUtils.ts
|
|
4016
|
+
import { randomUUID } from "crypto";
|
|
4017
|
+
function getDb(context) {
|
|
4018
|
+
const db = context.db;
|
|
4019
|
+
if (!db) {
|
|
4020
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4021
|
+
}
|
|
4022
|
+
return db;
|
|
4023
|
+
}
|
|
4024
|
+
function queueToTableNames(queue) {
|
|
4025
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4026
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4027
|
+
}
|
|
4028
|
+
return {
|
|
4029
|
+
tasksTable: queue,
|
|
4030
|
+
historyTable: `${queue}_history`
|
|
4031
|
+
};
|
|
4032
|
+
}
|
|
4033
|
+
function servicesRegistryTable(queue) {
|
|
4034
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4035
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4036
|
+
}
|
|
4037
|
+
return `${queue}_services_registry`;
|
|
4038
|
+
}
|
|
4039
|
+
async function ensureTaskTables(context, options = {}) {
|
|
4040
|
+
const queue = options.queue ?? "tasks";
|
|
4041
|
+
const recreate = options.recreate ?? false;
|
|
4042
|
+
const db = getDb(context);
|
|
4043
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
4044
|
+
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4045
|
+
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4046
|
+
if (recreate) {
|
|
4047
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
4048
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
4049
|
+
}
|
|
4050
|
+
if (needsTasks) {
|
|
4051
|
+
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4052
|
+
await db.schema.createTable(tasksTable, (t) => {
|
|
4053
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4054
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4055
|
+
t.timestamp("started_at");
|
|
4056
|
+
t.timestamp("completed_at");
|
|
4057
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4058
|
+
t.text("schedule");
|
|
4059
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4060
|
+
t.text("target").notNullable();
|
|
4061
|
+
t.text("task").notNullable();
|
|
4062
|
+
t.json("params");
|
|
4063
|
+
t.text("opid");
|
|
4064
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
4065
|
+
t.text("progress");
|
|
4066
|
+
t.boolean("success");
|
|
4067
|
+
t.json("results");
|
|
4068
|
+
});
|
|
4069
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4070
|
+
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4071
|
+
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4072
|
+
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
if (needsHistory) {
|
|
4076
|
+
await db.schema.createTable(historyTable, (t) => {
|
|
4077
|
+
t.uuid("id").notNullable();
|
|
4078
|
+
t.timestamp("created_at").notNullable();
|
|
4079
|
+
t.timestamp("started_at");
|
|
4080
|
+
t.timestamp("completed_at");
|
|
4081
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4082
|
+
t.text("schedule");
|
|
4083
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4084
|
+
t.text("target").notNullable();
|
|
4085
|
+
t.text("task").notNullable();
|
|
4086
|
+
t.json("params");
|
|
4087
|
+
t.text("opid");
|
|
4088
|
+
t.text("progress");
|
|
4089
|
+
t.boolean("success");
|
|
4090
|
+
t.json("results");
|
|
4091
|
+
});
|
|
4092
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
4093
|
+
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4094
|
+
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
const registryTable = servicesRegistryTable(queue);
|
|
4098
|
+
const needsRegistry = !await db.tableExists(registryTable);
|
|
4099
|
+
if (needsRegistry) {
|
|
4100
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
4101
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4102
|
+
t.uuid("instance_id").notNullable().unique();
|
|
4103
|
+
t.text("queue").notNullable();
|
|
4104
|
+
t.text("service_group").notNullable();
|
|
4105
|
+
t.text("service_name").notNullable();
|
|
4106
|
+
t.text("target").notNullable();
|
|
4107
|
+
t.text("hostname");
|
|
4108
|
+
t.integer("pid");
|
|
4109
|
+
t.json("metadata");
|
|
4110
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4111
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
4112
|
+
t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
|
|
4113
|
+
t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
4114
|
+
t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
async function enqueueTask(context, options) {
|
|
4119
|
+
const db = getDb(context);
|
|
4120
|
+
const queue = options.queue ?? "tasks";
|
|
4121
|
+
const { tasksTable } = queueToTableNames(queue);
|
|
4122
|
+
const id = randomUUID();
|
|
4123
|
+
await db(tasksTable).insert({
|
|
4124
|
+
id,
|
|
4125
|
+
target: options.target,
|
|
4126
|
+
task: options.task,
|
|
4127
|
+
params: toJsonColumn(options.params ?? null),
|
|
4128
|
+
opid: options.opid ?? null,
|
|
4129
|
+
priority: options.priority ?? 0,
|
|
4130
|
+
schedule: options.schedule ?? null
|
|
4131
|
+
});
|
|
4132
|
+
return id;
|
|
4133
|
+
}
|
|
4134
|
+
async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
4135
|
+
const db = getDb(context);
|
|
4136
|
+
await db(tasksTable).where({ id: taskId }).update({
|
|
4137
|
+
progress: typeof progress === "string" ? progress : JSON.stringify(progress)
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
// src/tasks/servicesRegistry.ts
|
|
4142
|
+
function getDb2(context) {
|
|
4143
|
+
const db = context.db;
|
|
4144
|
+
if (!db) {
|
|
4145
|
+
throw new Error("Services registry requires context.db");
|
|
4146
|
+
}
|
|
4147
|
+
return db;
|
|
4148
|
+
}
|
|
4149
|
+
function parseMetadataColumn(value) {
|
|
4150
|
+
if (!value) return {};
|
|
4151
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
4152
|
+
if (typeof value === "string") {
|
|
4153
|
+
try {
|
|
4154
|
+
const p = JSON.parse(value);
|
|
4155
|
+
return p && typeof p === "object" && !Array.isArray(p) ? p : {};
|
|
4156
|
+
} catch {
|
|
4157
|
+
return {};
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
return {};
|
|
4161
|
+
}
|
|
4162
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
4163
|
+
intake: 1,
|
|
4164
|
+
harvest: 1,
|
|
4165
|
+
loader: 0,
|
|
4166
|
+
photos: 0,
|
|
4167
|
+
photosprocessor: 0,
|
|
4168
|
+
ingest: 0
|
|
4169
|
+
};
|
|
4170
|
+
function sanitizeNamePart(raw) {
|
|
4171
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4172
|
+
return s.slice(0, 80) || "runner";
|
|
4173
|
+
}
|
|
4174
|
+
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
4175
|
+
const safeQ = sanitizeNamePart(queue);
|
|
4176
|
+
const safeG = sanitizeNamePart(serviceGroup);
|
|
4177
|
+
return path4.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
4178
|
+
}
|
|
4179
|
+
async function readIdentityFile(filePath) {
|
|
4180
|
+
try {
|
|
4181
|
+
const text = await readFile(filePath, "utf8");
|
|
4182
|
+
const parsed = JSON.parse(text);
|
|
4183
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4184
|
+
} catch {
|
|
4185
|
+
return {};
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
async function writeIdentityFile(filePath, data) {
|
|
4189
|
+
await mkdir(path4.dirname(filePath), { recursive: true });
|
|
4190
|
+
await writeFile(filePath, `${JSON.stringify(data, null, 2)}
|
|
4191
|
+
`, "utf8");
|
|
4192
|
+
}
|
|
4193
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
4194
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
4195
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
4196
|
+
}
|
|
4197
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
4198
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
4199
|
+
}
|
|
4200
|
+
async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
|
|
4201
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4202
|
+
let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
4203
|
+
if (excludeInstanceId) {
|
|
4204
|
+
q = q.whereNot("instance_id", excludeInstanceId);
|
|
4205
|
+
}
|
|
4206
|
+
const row = await q.count("id as count").first();
|
|
4207
|
+
return Number(row?.count ?? 0);
|
|
4208
|
+
}
|
|
4209
|
+
function isUniqueViolation(error) {
|
|
4210
|
+
const code = error?.code ?? error?.errno;
|
|
4211
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
4212
|
+
}
|
|
4213
|
+
async function registerInServicesRegistry(context, options) {
|
|
4214
|
+
const db = getDb2(context);
|
|
4215
|
+
const registryTable = servicesRegistryTable(options.queue);
|
|
4216
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
4217
|
+
if (!serviceGroup) {
|
|
4218
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4219
|
+
}
|
|
4220
|
+
const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
|
|
4221
|
+
let identity = await readIdentityFile(identityPath);
|
|
4222
|
+
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
|
|
4223
|
+
identity.instanceId = instanceId;
|
|
4224
|
+
await writeIdentityFile(identityPath, identity);
|
|
4225
|
+
const hostname = os.hostname();
|
|
4226
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4227
|
+
const meta = toJsonColumn(options.metadata ?? null);
|
|
4228
|
+
const existing = await db(registryTable).where({ instance_id: instanceId }).first();
|
|
4229
|
+
if (existing) {
|
|
4230
|
+
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
4231
|
+
target: options.target,
|
|
4232
|
+
hostname,
|
|
4233
|
+
pid,
|
|
4234
|
+
metadata: meta,
|
|
4235
|
+
last_seen_at: db.fn.now()
|
|
4236
|
+
});
|
|
4237
|
+
const serviceName = String(existing.service_name);
|
|
4238
|
+
identity.serviceName = serviceName;
|
|
4239
|
+
await writeIdentityFile(identityPath, identity);
|
|
4240
|
+
const reg = {
|
|
4241
|
+
instanceId,
|
|
4242
|
+
serviceName,
|
|
4243
|
+
serviceGroup,
|
|
4244
|
+
queue: options.queue,
|
|
4245
|
+
target: options.target,
|
|
4246
|
+
rowId: String(existing.id)
|
|
4247
|
+
};
|
|
4248
|
+
context.servicesRegistry = reg;
|
|
4249
|
+
context.runnerHeartbeat = reg;
|
|
4250
|
+
context.logger.info?.(
|
|
4251
|
+
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
4252
|
+
);
|
|
4253
|
+
return {
|
|
4254
|
+
instanceId,
|
|
4255
|
+
serviceName,
|
|
4256
|
+
serviceGroup,
|
|
4257
|
+
queue: options.queue,
|
|
4258
|
+
target: options.target,
|
|
4259
|
+
rowId: String(existing.id),
|
|
4260
|
+
registryTable
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
4264
|
+
const aliveOthers = await countAliveInGroup(
|
|
4265
|
+
db,
|
|
4266
|
+
registryTable,
|
|
4267
|
+
options.queue,
|
|
4268
|
+
serviceGroup,
|
|
4269
|
+
options.staleMs,
|
|
4270
|
+
instanceId
|
|
4271
|
+
);
|
|
4272
|
+
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
4273
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
4274
|
+
if (options.enforceMaxInstances) {
|
|
4275
|
+
throw new Error(msg);
|
|
4276
|
+
}
|
|
4277
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
4278
|
+
}
|
|
4279
|
+
const explicitName = options.serviceName?.trim();
|
|
4280
|
+
const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
|
|
4281
|
+
const hostBase = sanitizeNamePart(hostname);
|
|
4282
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
4283
|
+
const baseCandidates = [];
|
|
4284
|
+
if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
|
|
4285
|
+
if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
|
|
4286
|
+
baseCandidates.push(`${groupBase}-${hostBase}`);
|
|
4287
|
+
baseCandidates.push(groupBase);
|
|
4288
|
+
function* eachServiceNameCandidate(bases) {
|
|
4289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4290
|
+
for (const rawBase of bases) {
|
|
4291
|
+
const base = sanitizeNamePart(rawBase);
|
|
4292
|
+
if (!base) continue;
|
|
4293
|
+
const seq = [base];
|
|
4294
|
+
for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
|
|
4295
|
+
for (const c of seq) {
|
|
4296
|
+
if (seen.has(c)) continue;
|
|
4297
|
+
seen.add(c);
|
|
4298
|
+
yield c;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
let inserted;
|
|
4303
|
+
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
4304
|
+
try {
|
|
4305
|
+
const rows = await db(registryTable).insert({
|
|
4306
|
+
instance_id: instanceId,
|
|
4307
|
+
queue: options.queue,
|
|
4308
|
+
service_group: serviceGroup,
|
|
4309
|
+
service_name: candidate,
|
|
4310
|
+
target: options.target,
|
|
4311
|
+
hostname,
|
|
4312
|
+
pid,
|
|
4313
|
+
metadata: meta,
|
|
4314
|
+
last_seen_at: db.fn.now()
|
|
4315
|
+
}).returning(["id", "service_name"]);
|
|
4316
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
4317
|
+
if (row) {
|
|
4318
|
+
inserted = { id: String(row.id), service_name: String(row.service_name) };
|
|
4319
|
+
break;
|
|
4320
|
+
}
|
|
4321
|
+
} catch (error) {
|
|
4322
|
+
if (!isUniqueViolation(error)) {
|
|
4323
|
+
throw error;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
if (!inserted) {
|
|
4328
|
+
throw new Error(
|
|
4329
|
+
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
identity.serviceName = inserted.service_name;
|
|
4333
|
+
await writeIdentityFile(identityPath, identity);
|
|
4334
|
+
const regNew = {
|
|
4335
|
+
instanceId,
|
|
4336
|
+
serviceName: inserted.service_name,
|
|
4337
|
+
serviceGroup,
|
|
4338
|
+
queue: options.queue,
|
|
4339
|
+
target: options.target,
|
|
4340
|
+
rowId: inserted.id
|
|
4341
|
+
};
|
|
4342
|
+
context.servicesRegistry = regNew;
|
|
4343
|
+
context.runnerHeartbeat = regNew;
|
|
4344
|
+
context.logger.info?.(
|
|
4345
|
+
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
4346
|
+
);
|
|
4347
|
+
return {
|
|
4348
|
+
instanceId,
|
|
4349
|
+
serviceName: inserted.service_name,
|
|
4350
|
+
serviceGroup,
|
|
4351
|
+
queue: options.queue,
|
|
4352
|
+
target: options.target,
|
|
4353
|
+
rowId: inserted.id,
|
|
4354
|
+
registryTable
|
|
4355
|
+
};
|
|
4356
|
+
}
|
|
4357
|
+
async function touchServicesRegistry(context, registration) {
|
|
4358
|
+
const db = getDb2(context);
|
|
4359
|
+
const hostname = os.hostname();
|
|
4360
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4361
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
4362
|
+
last_seen_at: db.fn.now(),
|
|
4363
|
+
hostname,
|
|
4364
|
+
pid
|
|
4365
|
+
});
|
|
4366
|
+
}
|
|
4367
|
+
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
4368
|
+
const db = getDb2(context);
|
|
4369
|
+
const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
|
|
4370
|
+
const prev = parseMetadataColumn(row?.metadata);
|
|
4371
|
+
const merged = { ...prev, ...patch };
|
|
4372
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
4373
|
+
metadata: toJsonColumn(merged),
|
|
4374
|
+
last_seen_at: db.fn.now()
|
|
4375
|
+
});
|
|
4376
|
+
context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
|
|
4377
|
+
}
|
|
4378
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
4379
|
+
const db = getDb2(context);
|
|
4380
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
|
|
4381
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
|
|
4382
|
+
}
|
|
4383
|
+
async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
4384
|
+
const db = getDb2(context);
|
|
4385
|
+
const staleMs = options.staleMs ?? 6e4;
|
|
4386
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4387
|
+
const table = servicesRegistryTable(options.queue);
|
|
4388
|
+
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
4389
|
+
if (options.serviceGroup?.trim()) {
|
|
4390
|
+
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
4391
|
+
}
|
|
4392
|
+
return await q;
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
// src/tasks/taskLogs.ts
|
|
4396
|
+
function getLogsState(context) {
|
|
4397
|
+
const holder = context;
|
|
4398
|
+
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
4399
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
|
|
4400
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4401
|
+
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4402
|
+
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4403
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4404
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4405
|
+
const errorDb = new FileDatabase({
|
|
4406
|
+
basePath,
|
|
4407
|
+
namespace,
|
|
4408
|
+
tableName: errorTableName,
|
|
4409
|
+
versioned: true,
|
|
4410
|
+
useMetadata: true,
|
|
4411
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4412
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4413
|
+
logger: holder.logger
|
|
4414
|
+
});
|
|
4415
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4416
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4417
|
+
if (!enabled) {
|
|
4418
|
+
const disabledState = {
|
|
4419
|
+
db: null,
|
|
4420
|
+
errorDb,
|
|
4421
|
+
queue: Promise.resolve(),
|
|
4422
|
+
initialized: true,
|
|
4423
|
+
errorInitialized: false
|
|
4424
|
+
};
|
|
4425
|
+
holder.__tasksLogsState = disabledState;
|
|
4426
|
+
return disabledState;
|
|
4427
|
+
}
|
|
4428
|
+
const db = new FileDatabase({
|
|
4429
|
+
basePath,
|
|
4430
|
+
namespace,
|
|
4431
|
+
tableName,
|
|
4432
|
+
versioned: true,
|
|
4433
|
+
useMetadata: true,
|
|
4434
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4435
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4436
|
+
logger: holder.logger
|
|
4437
|
+
});
|
|
4438
|
+
const state = {
|
|
4439
|
+
db,
|
|
4440
|
+
errorDb,
|
|
4441
|
+
queue: Promise.resolve(),
|
|
4442
|
+
initialized: false,
|
|
4443
|
+
errorInitialized: false
|
|
4444
|
+
};
|
|
4445
|
+
holder.__tasksLogsState = state;
|
|
4446
|
+
return state;
|
|
4447
|
+
}
|
|
4448
|
+
function isErrorPayload(payload) {
|
|
4449
|
+
if (!payload) return false;
|
|
4450
|
+
if (typeof payload === "object") {
|
|
4451
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4452
|
+
if (level === "error" || level === "fatal") return true;
|
|
4453
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4454
|
+
return false;
|
|
4455
|
+
}
|
|
4456
|
+
if (typeof payload === "string") {
|
|
4457
|
+
return /\berror\b/i.test(payload);
|
|
4458
|
+
}
|
|
4459
|
+
return false;
|
|
4460
|
+
}
|
|
4461
|
+
function buildLogRecord(task, payload) {
|
|
4462
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4463
|
+
return {
|
|
4464
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4465
|
+
opid: task.opid ?? null,
|
|
4466
|
+
taskId: task.id,
|
|
4467
|
+
taskName: task.task,
|
|
4468
|
+
target: task.target,
|
|
4469
|
+
source: typeof params.source === "string" ? params.source : null,
|
|
4470
|
+
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4471
|
+
payload
|
|
4472
|
+
};
|
|
4473
|
+
}
|
|
4474
|
+
function appendTaskIpcLog(context, task, payload) {
|
|
4475
|
+
const state = getLogsState(context);
|
|
4476
|
+
if (!state.db && !state.errorDb) return;
|
|
4477
|
+
const record = buildLogRecord(task, payload);
|
|
4478
|
+
state.queue = state.queue.then(async () => {
|
|
4479
|
+
if (state.db) {
|
|
4480
|
+
await state.db.write([record], { forceNewVersion: !state.initialized });
|
|
4481
|
+
state.initialized = true;
|
|
4482
|
+
}
|
|
4483
|
+
if (state.errorDb && isErrorPayload(payload)) {
|
|
4484
|
+
await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
|
|
4485
|
+
state.errorInitialized = true;
|
|
4486
|
+
}
|
|
4487
|
+
}).catch((error) => {
|
|
4488
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4489
|
+
});
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
// src/tasks/time-matcher.ts
|
|
4493
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4494
|
+
function resolveAsterisks(field, range) {
|
|
4495
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4496
|
+
}
|
|
4497
|
+
function resolveRanges(field) {
|
|
4498
|
+
const regex = /(\d+)-(\d+)/;
|
|
4499
|
+
let current = field;
|
|
4500
|
+
while (true) {
|
|
4501
|
+
const match = regex.exec(current);
|
|
4502
|
+
if (!match) break;
|
|
4503
|
+
const raw = match[0];
|
|
4504
|
+
let first = Number(match[1]);
|
|
4505
|
+
let last = Number(match[2]);
|
|
4506
|
+
if (last < first) {
|
|
4507
|
+
[first, last] = [last, first];
|
|
4508
|
+
}
|
|
4509
|
+
const values = [];
|
|
4510
|
+
for (let i = first; i <= last; i += 1) {
|
|
4511
|
+
values.push(i);
|
|
4512
|
+
}
|
|
4513
|
+
current = current.replace(raw, values.join(","));
|
|
4514
|
+
}
|
|
4515
|
+
return current;
|
|
4516
|
+
}
|
|
4517
|
+
function resolveSteps(field) {
|
|
4518
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4519
|
+
if (!match) return field;
|
|
4520
|
+
const base = match[1];
|
|
4521
|
+
const step = Number(match[2]);
|
|
4522
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4523
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4524
|
+
}
|
|
4525
|
+
function convertPattern(pattern) {
|
|
4526
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4527
|
+
if (parts.length !== 6) {
|
|
4528
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4529
|
+
}
|
|
4530
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4531
|
+
}
|
|
4532
|
+
function fieldMatches(field, value) {
|
|
4533
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4534
|
+
return allowed.includes(value);
|
|
4535
|
+
}
|
|
4536
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4537
|
+
const parsed = convertPattern(pattern);
|
|
4538
|
+
return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// src/tasks/TaskMaster.ts
|
|
4542
|
+
var TaskMaster = class {
|
|
4543
|
+
context;
|
|
4544
|
+
task;
|
|
4545
|
+
constructor(context, task) {
|
|
4546
|
+
this.context = context;
|
|
4547
|
+
this.task = task;
|
|
4548
|
+
}
|
|
4549
|
+
cantRunReason() {
|
|
4550
|
+
return false;
|
|
4551
|
+
}
|
|
4552
|
+
requestStop(_allowanceMs) {
|
|
4553
|
+
}
|
|
4554
|
+
};
|
|
4555
|
+
|
|
4556
|
+
// src/tasks/coreTasks/TaskPing.ts
|
|
4557
|
+
var TaskPing = class extends TaskMaster {
|
|
4558
|
+
async run() {
|
|
4559
|
+
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4560
|
+
return { success: true, results: "pong" };
|
|
4561
|
+
}
|
|
4562
|
+
};
|
|
4563
|
+
|
|
4564
|
+
// src/tasks/coreTasks/TaskSampleProcess.ts
|
|
4565
|
+
var TaskSampleProcess = class extends TaskMaster {
|
|
4566
|
+
stopRequested = false;
|
|
4567
|
+
stopAllowanceMs = 0;
|
|
4568
|
+
stopDecisionLogged = false;
|
|
4569
|
+
requestStop(allowanceMs) {
|
|
4570
|
+
this.stopRequested = true;
|
|
4571
|
+
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
4572
|
+
this.context.logger.warn?.(
|
|
4573
|
+
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
async run(reportProgress) {
|
|
4577
|
+
const totalRaw = this.task?.params?.total ?? 10;
|
|
4578
|
+
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
4579
|
+
const nameRaw = this.task?.params?.name;
|
|
4580
|
+
const total = Number(totalRaw);
|
|
4581
|
+
const delay = Number(delayRaw);
|
|
4582
|
+
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
|
|
4583
|
+
const errors = [];
|
|
4584
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4585
|
+
errors.push('param "total" must be a positive integer');
|
|
4586
|
+
}
|
|
4587
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4588
|
+
errors.push('param "delay" must be an integer >= 0');
|
|
4589
|
+
}
|
|
4590
|
+
if (errors.length > 0) {
|
|
4591
|
+
return {
|
|
4592
|
+
success: false,
|
|
4593
|
+
results: {
|
|
4594
|
+
error: `Validation failed: ${errors.join(", ")}`,
|
|
4595
|
+
received: { total: totalRaw, delay: delayRaw, name: nameRaw }
|
|
4596
|
+
}
|
|
4597
|
+
};
|
|
4598
|
+
}
|
|
4599
|
+
const startedAt = Date.now();
|
|
4600
|
+
for (let i = 1; i <= total; i += 1) {
|
|
4601
|
+
if (this.stopRequested) {
|
|
4602
|
+
const remainingMs = Math.max(0, (total - i + 1) * delay);
|
|
4603
|
+
if (remainingMs <= this.stopAllowanceMs) {
|
|
4604
|
+
if (!this.stopDecisionLogged) {
|
|
4605
|
+
this.stopDecisionLogged = true;
|
|
4606
|
+
this.context.logger.warn?.(
|
|
4607
|
+
`[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
|
|
4608
|
+
);
|
|
4609
|
+
}
|
|
4610
|
+
} else {
|
|
4611
|
+
this.context.logger.warn?.(
|
|
4612
|
+
`[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
|
|
4613
|
+
);
|
|
4614
|
+
return {
|
|
4615
|
+
success: false,
|
|
4616
|
+
results: {
|
|
4617
|
+
message: `Stopped before completion at iteration ${i}/${total}`,
|
|
4618
|
+
completed: i - 1,
|
|
4619
|
+
total,
|
|
4620
|
+
name,
|
|
4621
|
+
remainingMs,
|
|
4622
|
+
allowanceMs: this.stopAllowanceMs
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
const elapsed = Date.now() - startedAt;
|
|
4628
|
+
const remaining = Math.max(0, (total - i) * delay);
|
|
4629
|
+
const progress = {
|
|
4630
|
+
name,
|
|
4631
|
+
count: i,
|
|
4632
|
+
total,
|
|
4633
|
+
elapsedMs: elapsed,
|
|
4634
|
+
remainingMs: remaining,
|
|
4635
|
+
status: `running ${name}: ${i}/${total}`
|
|
4636
|
+
};
|
|
4637
|
+
this.context.logger.progress("running", {
|
|
4638
|
+
prefix: name,
|
|
4639
|
+
count: i,
|
|
4640
|
+
total
|
|
4641
|
+
});
|
|
4642
|
+
await reportProgress(progress);
|
|
4643
|
+
await sleepMs(delay);
|
|
4644
|
+
}
|
|
4645
|
+
return {
|
|
4646
|
+
success: true,
|
|
4647
|
+
results: {
|
|
4648
|
+
message: `Completed ${total} iterations`,
|
|
4649
|
+
total,
|
|
4650
|
+
delay,
|
|
4651
|
+
name
|
|
4652
|
+
}
|
|
4653
|
+
};
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
|
|
4657
|
+
// src/tasks/coreTasks/TaskShellCommand.ts
|
|
4658
|
+
import { spawn } from "child_process";
|
|
4659
|
+
function runShellCommand(command, cwd) {
|
|
4660
|
+
return new Promise((resolve2, reject) => {
|
|
4661
|
+
const child = spawn(command, {
|
|
4662
|
+
shell: true,
|
|
4663
|
+
cwd: cwd || process.cwd(),
|
|
4664
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4665
|
+
});
|
|
4666
|
+
let output = "";
|
|
4667
|
+
let stderr = "";
|
|
4668
|
+
child.stdout.on("data", (chunk) => {
|
|
4669
|
+
output += String(chunk);
|
|
4670
|
+
});
|
|
4671
|
+
child.stderr.on("data", (chunk) => {
|
|
4672
|
+
stderr += String(chunk);
|
|
4673
|
+
});
|
|
4674
|
+
child.on("error", (error) => {
|
|
4675
|
+
reject(error);
|
|
4676
|
+
});
|
|
4677
|
+
child.on("close", (exitCode, signal) => {
|
|
4678
|
+
resolve2({
|
|
4679
|
+
exitCode,
|
|
4680
|
+
output: output.trim(),
|
|
4681
|
+
stderr: stderr.trim(),
|
|
4682
|
+
signal
|
|
4683
|
+
});
|
|
4684
|
+
});
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
var TaskShellCommand = class extends TaskMaster {
|
|
4688
|
+
async run() {
|
|
4689
|
+
const params = this.task?.params;
|
|
4690
|
+
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
4691
|
+
const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
|
|
4692
|
+
const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
|
|
4693
|
+
const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
|
|
4694
|
+
if (!command) {
|
|
4695
|
+
return {
|
|
4696
|
+
success: false,
|
|
4697
|
+
results: {
|
|
4698
|
+
error: 'Validation failed: param "command" must be a non-empty string',
|
|
4699
|
+
received: this.task?.params ?? null
|
|
4700
|
+
}
|
|
4701
|
+
};
|
|
4702
|
+
}
|
|
4703
|
+
try {
|
|
4704
|
+
const result = await runShellCommand(command, cwd);
|
|
4705
|
+
const success = result.exitCode === 0;
|
|
4706
|
+
this.context.logger.info?.(
|
|
4707
|
+
`[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
|
|
4708
|
+
);
|
|
4709
|
+
return {
|
|
4710
|
+
success,
|
|
4711
|
+
results: {
|
|
4712
|
+
command,
|
|
4713
|
+
cwd: cwd ?? process.cwd(),
|
|
4714
|
+
output: result.output,
|
|
4715
|
+
stderr: result.stderr,
|
|
4716
|
+
exitCode: result.exitCode,
|
|
4717
|
+
signal: result.signal
|
|
4718
|
+
}
|
|
4719
|
+
};
|
|
4720
|
+
} catch (error) {
|
|
4721
|
+
return {
|
|
4722
|
+
success: false,
|
|
4723
|
+
results: {
|
|
4724
|
+
command,
|
|
4725
|
+
cwd: cwd ?? process.cwd(),
|
|
4726
|
+
output: "",
|
|
4727
|
+
stderr: "",
|
|
4728
|
+
exitCode: null,
|
|
4729
|
+
error: error?.message ?? String(error)
|
|
4730
|
+
}
|
|
4731
|
+
};
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
};
|
|
4735
|
+
|
|
4736
|
+
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
4737
|
+
import os2 from "os";
|
|
4738
|
+
import fs4 from "fs/promises";
|
|
4739
|
+
function toGb(valueBytes) {
|
|
4740
|
+
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4741
|
+
}
|
|
4742
|
+
function toMb(valueBytes) {
|
|
4743
|
+
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4744
|
+
}
|
|
4745
|
+
async function getDiskStats() {
|
|
4746
|
+
const stats = await fs4.statfs("/");
|
|
4747
|
+
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4748
|
+
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4749
|
+
const used = total - free;
|
|
4750
|
+
return {
|
|
4751
|
+
total: toGb(total),
|
|
4752
|
+
used: toGb(used),
|
|
4753
|
+
free: toGb(free)
|
|
4754
|
+
};
|
|
4755
|
+
}
|
|
4756
|
+
var TaskSystemInfo = class extends TaskMaster {
|
|
4757
|
+
async run() {
|
|
4758
|
+
try {
|
|
4759
|
+
const totalMemory = os2.totalmem();
|
|
4760
|
+
const freeMemory = os2.freemem();
|
|
4761
|
+
const usedMemory = totalMemory - freeMemory;
|
|
4762
|
+
const cpus = os2.cpus();
|
|
4763
|
+
const cpuUtilization = cpus.map((cpu) => {
|
|
4764
|
+
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4765
|
+
const usage = (total - cpu.times.idle) / total * 100;
|
|
4766
|
+
return Number(usage.toFixed(2));
|
|
4767
|
+
});
|
|
4768
|
+
const processMemory = process.memoryUsage();
|
|
4769
|
+
const disk = await getDiskStats();
|
|
4770
|
+
const results = {
|
|
4771
|
+
memory: {
|
|
4772
|
+
total: toGb(totalMemory),
|
|
4773
|
+
used: toGb(usedMemory),
|
|
4774
|
+
free: toGb(freeMemory)
|
|
4775
|
+
},
|
|
4776
|
+
processMemory: {
|
|
4777
|
+
rss: toMb(processMemory.rss),
|
|
4778
|
+
heapTotal: toMb(processMemory.heapTotal),
|
|
4779
|
+
heapUsed: toMb(processMemory.heapUsed),
|
|
4780
|
+
external: toMb(processMemory.external)
|
|
4781
|
+
},
|
|
4782
|
+
disk,
|
|
4783
|
+
cpu: {
|
|
4784
|
+
cores: cpuUtilization.length,
|
|
4785
|
+
utilization: cpuUtilization
|
|
4786
|
+
},
|
|
4787
|
+
runtime: {
|
|
4788
|
+
platform: os2.platform(),
|
|
4789
|
+
arch: os2.arch(),
|
|
4790
|
+
uptimeSec: os2.uptime(),
|
|
4791
|
+
hostname: os2.hostname()
|
|
4792
|
+
}
|
|
4793
|
+
};
|
|
4794
|
+
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
4795
|
+
return { success: true, results };
|
|
4796
|
+
} catch (error) {
|
|
4797
|
+
return {
|
|
4798
|
+
success: false,
|
|
4799
|
+
results: {
|
|
4800
|
+
error: "Can't collect system stats",
|
|
4801
|
+
message: error?.message ?? String(error)
|
|
4802
|
+
}
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
}
|
|
4806
|
+
};
|
|
4807
|
+
|
|
4808
|
+
// src/tasks/coreTasks/TaskSumAB.ts
|
|
4809
|
+
var TaskSumAB = class extends TaskMaster {
|
|
4810
|
+
async run() {
|
|
4811
|
+
const a = this.task?.params?.a;
|
|
4812
|
+
const b = this.task?.params?.b;
|
|
4813
|
+
if (typeof a !== "number" || Number.isNaN(a)) {
|
|
4814
|
+
return {
|
|
4815
|
+
success: false,
|
|
4816
|
+
results: {
|
|
4817
|
+
error: 'Validation failed: param "a" must be a valid number',
|
|
4818
|
+
received: { a, b }
|
|
4819
|
+
}
|
|
4820
|
+
};
|
|
4821
|
+
}
|
|
4822
|
+
if (typeof b !== "number" || Number.isNaN(b)) {
|
|
4823
|
+
return {
|
|
4824
|
+
success: false,
|
|
4825
|
+
results: {
|
|
4826
|
+
error: 'Validation failed: param "b" must be a valid number',
|
|
4827
|
+
received: { a, b }
|
|
4828
|
+
}
|
|
4829
|
+
};
|
|
4830
|
+
}
|
|
4831
|
+
const sum = a + b;
|
|
4832
|
+
this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
|
|
4833
|
+
return {
|
|
4834
|
+
success: true,
|
|
4835
|
+
results: { a, b, sum }
|
|
4836
|
+
};
|
|
4837
|
+
}
|
|
4838
|
+
};
|
|
4839
|
+
|
|
4840
|
+
// src/tasks/coreTasks/TaskStopRunner.ts
|
|
4841
|
+
var TaskStopRunner = class extends TaskMaster {
|
|
4842
|
+
async run() {
|
|
4843
|
+
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4844
|
+
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
4845
|
+
return {
|
|
4846
|
+
success: true,
|
|
4847
|
+
results: {
|
|
4848
|
+
stopRunner: true,
|
|
4849
|
+
allowanceMs,
|
|
4850
|
+
message: "Runner stop requested"
|
|
4851
|
+
}
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
};
|
|
4855
|
+
|
|
4856
|
+
// src/tasks/TasksRegistry.ts
|
|
4857
|
+
var TasksRegistry = class _TasksRegistry {
|
|
4858
|
+
map = {};
|
|
4859
|
+
constructor(initial) {
|
|
4860
|
+
if (initial) {
|
|
4861
|
+
this.addMany(initial);
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4864
|
+
static withCoreTasks() {
|
|
4865
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
4866
|
+
}
|
|
4867
|
+
add(taskName, taskClass) {
|
|
4868
|
+
this.map[taskName] = taskClass;
|
|
4869
|
+
return this;
|
|
4870
|
+
}
|
|
4871
|
+
addMany(entries) {
|
|
4872
|
+
for (const [name, klass] of Object.entries(entries)) {
|
|
4873
|
+
this.add(name, klass);
|
|
4874
|
+
}
|
|
4875
|
+
return this;
|
|
4876
|
+
}
|
|
4877
|
+
get(taskName) {
|
|
4878
|
+
return this.map[taskName];
|
|
4879
|
+
}
|
|
4880
|
+
listSupportedTasks() {
|
|
4881
|
+
return Object.keys(this.map).sort();
|
|
4882
|
+
}
|
|
4883
|
+
toObject() {
|
|
4884
|
+
return { ...this.map };
|
|
4885
|
+
}
|
|
4886
|
+
};
|
|
4887
|
+
|
|
4888
|
+
// src/tasks/taskScriptRunner.ts
|
|
4889
|
+
import { spawn as spawn2 } from "child_process";
|
|
4890
|
+
function toCliArgs(args = []) {
|
|
4891
|
+
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
4892
|
+
}
|
|
4893
|
+
function formatChildLogPrefix(task) {
|
|
4894
|
+
return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
4895
|
+
}
|
|
4896
|
+
async function runNodeTaskScript(context, options) {
|
|
4897
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
4898
|
+
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
4899
|
+
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
4900
|
+
const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
|
|
4901
|
+
const child = spawn2(
|
|
4902
|
+
process.execPath,
|
|
4903
|
+
nodeArgs,
|
|
4904
|
+
{
|
|
4905
|
+
cwd: options.cwd || process.cwd(),
|
|
4906
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
4907
|
+
env: {
|
|
4908
|
+
...process.env,
|
|
4909
|
+
TASK_ID: options.task.id,
|
|
4910
|
+
TASK_NAME: options.task.task,
|
|
4911
|
+
TASK_OPID: options.task.opid || ""
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
);
|
|
4915
|
+
let stdout = "";
|
|
4916
|
+
let stderr = "";
|
|
4917
|
+
let workerResult = null;
|
|
4918
|
+
let hadErrorMessage = false;
|
|
4919
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
4920
|
+
const db = context.db;
|
|
4921
|
+
const tasksTable = context.params?.get?.("table") || "tasks";
|
|
4922
|
+
let progressWriteChain = Promise.resolve();
|
|
4923
|
+
let progressCallbackChain = Promise.resolve();
|
|
4924
|
+
const updateProgress = (text) => {
|
|
4925
|
+
if (!db || !text || !text.trim()) return;
|
|
4926
|
+
progressWriteChain = progressWriteChain.then(async () => {
|
|
4927
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
4928
|
+
}).catch((error) => {
|
|
4929
|
+
context.logger.warn?.(
|
|
4930
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4931
|
+
);
|
|
4932
|
+
});
|
|
4933
|
+
if (options.onProgress) {
|
|
4934
|
+
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
4935
|
+
await options.onProgress?.(text.slice(0, 4e3));
|
|
4936
|
+
}).catch((error) => {
|
|
4937
|
+
context.logger.warn?.(
|
|
4938
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4939
|
+
);
|
|
4940
|
+
});
|
|
4941
|
+
}
|
|
4942
|
+
};
|
|
4943
|
+
const payloadToProgressText = (payload) => {
|
|
4944
|
+
if (!payload) return "";
|
|
4945
|
+
if (typeof payload === "string") return payload;
|
|
4946
|
+
if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
4947
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
4948
|
+
return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
|
|
4949
|
+
}
|
|
4950
|
+
if (typeof payload.message === "string") return payload.message;
|
|
4951
|
+
if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
4952
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
4953
|
+
return `${pfx}${payload.count}/${payload.total}`;
|
|
4954
|
+
}
|
|
4955
|
+
return "";
|
|
4956
|
+
};
|
|
4957
|
+
child.stdout.on("data", (chunk) => {
|
|
4958
|
+
const text = String(chunk);
|
|
4959
|
+
stdout += text;
|
|
4960
|
+
if (text.trim()) {
|
|
4961
|
+
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4962
|
+
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
4963
|
+
}
|
|
4964
|
+
});
|
|
4965
|
+
child.stderr.on("data", (chunk) => {
|
|
4966
|
+
const text = String(chunk);
|
|
4967
|
+
stderr += text;
|
|
4968
|
+
if (text.trim()) {
|
|
4969
|
+
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4970
|
+
}
|
|
4971
|
+
});
|
|
4972
|
+
child.on("message", (message) => {
|
|
4973
|
+
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
4974
|
+
workerResult = message.__taskWorkerResult;
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
if (message && typeof message === "object") {
|
|
4978
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
4979
|
+
if (level === "error" || level === "fatal") {
|
|
4980
|
+
hadErrorMessage = true;
|
|
4981
|
+
}
|
|
4982
|
+
}
|
|
4983
|
+
appendTaskIpcLog(context, options.task, message);
|
|
4984
|
+
const progressText = payloadToProgressText(message);
|
|
4985
|
+
if (progressText) {
|
|
4986
|
+
updateProgress(progressText);
|
|
4987
|
+
if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
|
|
4988
|
+
const countNum = Number(String(message.count).trim());
|
|
4989
|
+
const totalNum = Number(message.total);
|
|
4990
|
+
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
4991
|
+
context.logger.progress(message.message || "progress", {
|
|
4992
|
+
prefix: message.prefix || prefix,
|
|
4993
|
+
count: countNum,
|
|
4994
|
+
total: totalNum
|
|
4995
|
+
});
|
|
4996
|
+
} else {
|
|
4997
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4998
|
+
}
|
|
4999
|
+
} else {
|
|
5000
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
});
|
|
5004
|
+
return await new Promise((resolve2, reject) => {
|
|
5005
|
+
child.on("error", (error) => reject(error));
|
|
5006
|
+
child.on("close", (exitCode, signal) => {
|
|
5007
|
+
Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
|
|
5008
|
+
resolve2({
|
|
5009
|
+
exitCode,
|
|
5010
|
+
signal,
|
|
5011
|
+
stdout: stdout.trim(),
|
|
5012
|
+
stderr: stderr.trim(),
|
|
5013
|
+
workerResult,
|
|
5014
|
+
hadErrorMessage
|
|
5015
|
+
});
|
|
5016
|
+
});
|
|
5017
|
+
});
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
|
|
5021
|
+
// src/tasks/index.ts
|
|
5022
|
+
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
5023
|
+
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
5024
|
+
function getDb3(context) {
|
|
5025
|
+
const db = context.db;
|
|
5026
|
+
if (!db) {
|
|
5027
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
5028
|
+
}
|
|
5029
|
+
return db;
|
|
5030
|
+
}
|
|
5031
|
+
function normalizeRegistry(registry) {
|
|
5032
|
+
if (!registry) return defaultTasksRegistry;
|
|
5033
|
+
if (registry instanceof TasksRegistry) return registry;
|
|
5034
|
+
return new TasksRegistry().addMany(registry);
|
|
5035
|
+
}
|
|
5036
|
+
function normalizeAllowedTasks(value) {
|
|
5037
|
+
if (!value) return void 0;
|
|
5038
|
+
if (Array.isArray(value)) {
|
|
5039
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5040
|
+
return out2.length ? out2 : void 0;
|
|
5041
|
+
}
|
|
5042
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5043
|
+
return out.length ? out : void 0;
|
|
5044
|
+
}
|
|
5045
|
+
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
5046
|
+
return enqueueTask(context, {
|
|
5047
|
+
queue,
|
|
5048
|
+
target,
|
|
5049
|
+
task: "stopRunner",
|
|
5050
|
+
params: { allowanceMs },
|
|
5051
|
+
priority: 1e6
|
|
5052
|
+
});
|
|
5053
|
+
}
|
|
5054
|
+
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
5055
|
+
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
5056
|
+
for (const [, taskInstance] of runningTaskInstances) {
|
|
5057
|
+
if (typeof taskInstance.requestStop === "function") {
|
|
5058
|
+
try {
|
|
5059
|
+
await taskInstance.requestStop(allowanceMs);
|
|
5060
|
+
} catch (error) {
|
|
5061
|
+
context.logger.warn?.("[tasks] task requestStop failed:", error);
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
context.emitter.emit("stop", allowanceMs);
|
|
5066
|
+
}
|
|
5067
|
+
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
5068
|
+
const db = getDb3(context);
|
|
5069
|
+
const taskName = row.task;
|
|
5070
|
+
const TaskClass = registry.get(taskName);
|
|
5071
|
+
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
5072
|
+
if (!TaskClass) {
|
|
5073
|
+
const err = { message: `Unknown task "${taskName}"` };
|
|
5074
|
+
await db(historyTable).insert({
|
|
5075
|
+
...rowForHistory,
|
|
5076
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5077
|
+
success: false,
|
|
5078
|
+
params: toJsonColumn(row.params),
|
|
5079
|
+
results: toJsonColumn(err)
|
|
5080
|
+
});
|
|
5081
|
+
if (row.schedule) {
|
|
5082
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5083
|
+
started_at: null,
|
|
5084
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5085
|
+
success: false,
|
|
5086
|
+
results: toJsonColumn(err),
|
|
5087
|
+
past_due: null,
|
|
5088
|
+
paused_at: db.fn.now(),
|
|
5089
|
+
progress: LOCKED_BY_ERROR_MESSAGE
|
|
5090
|
+
});
|
|
5091
|
+
} else {
|
|
5092
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
5093
|
+
}
|
|
5094
|
+
return { stopRunnerRequested: false, stopAllowanceMs: 0 };
|
|
5095
|
+
}
|
|
5096
|
+
let success = false;
|
|
5097
|
+
let results = null;
|
|
5098
|
+
let taskInstance = null;
|
|
5099
|
+
try {
|
|
5100
|
+
taskInstance = new TaskClass(context, row);
|
|
5101
|
+
runningTaskInstances.set(row.id, taskInstance);
|
|
5102
|
+
const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
|
|
5103
|
+
success = !!runResult?.success;
|
|
5104
|
+
results = runResult?.results ?? null;
|
|
5105
|
+
} catch (error) {
|
|
5106
|
+
success = false;
|
|
5107
|
+
results = {
|
|
5108
|
+
message: error?.message ?? String(error),
|
|
5109
|
+
name: error?.name ?? "Error",
|
|
5110
|
+
stack: error?.stack ?? null
|
|
5111
|
+
};
|
|
5112
|
+
} finally {
|
|
5113
|
+
runningTaskInstances.delete(row.id);
|
|
5114
|
+
}
|
|
5115
|
+
await db(historyTable).insert({
|
|
5116
|
+
...rowForHistory,
|
|
5117
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5118
|
+
success,
|
|
5119
|
+
params: toJsonColumn(row.params),
|
|
5120
|
+
results: toJsonColumn(results)
|
|
5121
|
+
});
|
|
5122
|
+
if (!success) {
|
|
5123
|
+
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
5124
|
+
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
5125
|
+
const fallbackRecoverCommand = [
|
|
5126
|
+
"npx",
|
|
5127
|
+
"tsx",
|
|
5128
|
+
"examples/tasks/recover-task.ts",
|
|
5129
|
+
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
5130
|
+
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
5131
|
+
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
5132
|
+
].join(" ");
|
|
5133
|
+
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5134
|
+
appendTaskIpcLog(context, row, {
|
|
5135
|
+
level: "error",
|
|
5136
|
+
message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5137
|
+
details: results
|
|
5138
|
+
});
|
|
5139
|
+
}
|
|
5140
|
+
if (row.schedule) {
|
|
5141
|
+
if (success) {
|
|
5142
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5143
|
+
started_at: null,
|
|
5144
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5145
|
+
success,
|
|
5146
|
+
results: toJsonColumn(results),
|
|
5147
|
+
progress: null,
|
|
5148
|
+
past_due: null
|
|
5149
|
+
});
|
|
5150
|
+
} else {
|
|
5151
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5152
|
+
started_at: null,
|
|
5153
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5154
|
+
success,
|
|
5155
|
+
results: toJsonColumn(results),
|
|
5156
|
+
paused_at: db.fn.now(),
|
|
5157
|
+
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5158
|
+
past_due: null
|
|
5159
|
+
});
|
|
5160
|
+
}
|
|
5161
|
+
} else {
|
|
5162
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
5163
|
+
}
|
|
5164
|
+
const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
|
|
5165
|
+
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5166
|
+
return { stopRunnerRequested, stopAllowanceMs };
|
|
5167
|
+
}
|
|
5168
|
+
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
5169
|
+
const db = getDb3(context);
|
|
5170
|
+
let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
|
|
5171
|
+
if (taskNames && taskNames.length > 0) {
|
|
5172
|
+
query = query.whereIn("task", taskNames);
|
|
5173
|
+
}
|
|
5174
|
+
const candidates = await query;
|
|
5175
|
+
for (const row of candidates) {
|
|
5176
|
+
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5177
|
+
continue;
|
|
5178
|
+
}
|
|
5179
|
+
const TaskClass = registry.get(row.task);
|
|
5180
|
+
if (TaskClass) {
|
|
5181
|
+
const taskInstance = new TaskClass(context, row);
|
|
5182
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5183
|
+
if (reason) {
|
|
5184
|
+
if (!row.past_due) {
|
|
5185
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5186
|
+
past_due: db.fn.now(),
|
|
5187
|
+
progress: String(reason)
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
continue;
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
|
|
5194
|
+
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5195
|
+
if (claimed) return claimed;
|
|
5196
|
+
}
|
|
5197
|
+
return null;
|
|
5198
|
+
}
|
|
5199
|
+
async function runTasksLoop(context, options) {
|
|
5200
|
+
const queue = options.queue ?? "tasks";
|
|
5201
|
+
const target = options.target;
|
|
5202
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
5203
|
+
const maxParallel = options.maxParallel ?? 1;
|
|
5204
|
+
const scanLimit = options.scanLimit ?? 100;
|
|
5205
|
+
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5206
|
+
const registry = normalizeRegistry(options.registry);
|
|
5207
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5208
|
+
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5209
|
+
const runningPromises = /* @__PURE__ */ new Set();
|
|
5210
|
+
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5211
|
+
let runningStopControlPromise = null;
|
|
5212
|
+
let stopRequested = false;
|
|
5213
|
+
let stopAllowanceMs = 5e3;
|
|
5214
|
+
context.__tasksRunnerStop = false;
|
|
5215
|
+
let registryReg = null;
|
|
5216
|
+
let registryInterval = null;
|
|
5217
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5218
|
+
if (hbGroup) {
|
|
5219
|
+
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5220
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5221
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5222
|
+
const defaultMeta = {
|
|
5223
|
+
component: "tasks-runner",
|
|
5224
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5225
|
+
};
|
|
5226
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
5227
|
+
queue,
|
|
5228
|
+
target,
|
|
5229
|
+
serviceGroup: hbGroup,
|
|
5230
|
+
serviceName: options.runnerServiceName,
|
|
5231
|
+
identityDir,
|
|
5232
|
+
staleMs,
|
|
5233
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5234
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5235
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
5236
|
+
});
|
|
5237
|
+
registryInterval = setInterval(() => {
|
|
5238
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5239
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
5240
|
+
});
|
|
5241
|
+
}, hbIntervalMs);
|
|
5242
|
+
}
|
|
5243
|
+
try {
|
|
5244
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
5245
|
+
if (!runningStopControlPromise) {
|
|
5246
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
5247
|
+
context,
|
|
5248
|
+
tasksTable,
|
|
5249
|
+
target,
|
|
5250
|
+
registry,
|
|
5251
|
+
10,
|
|
5252
|
+
["stopRunner", "stop"]
|
|
5253
|
+
);
|
|
5254
|
+
if (claimedStopTask) {
|
|
5255
|
+
runningStopControlPromise = executeClaimedTask(
|
|
5256
|
+
context,
|
|
5257
|
+
tasksTable,
|
|
5258
|
+
historyTable,
|
|
5259
|
+
claimedStopTask,
|
|
5260
|
+
registry,
|
|
5261
|
+
runningTaskInstances
|
|
5262
|
+
).then(async (outcome) => {
|
|
5263
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5264
|
+
stopRequested = true;
|
|
5265
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5266
|
+
context.__tasksRunnerStop = true;
|
|
5267
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5268
|
+
}
|
|
5269
|
+
}).finally(() => {
|
|
5270
|
+
runningStopControlPromise = null;
|
|
5271
|
+
});
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
while (runningPromises.size < maxParallel) {
|
|
5275
|
+
const claimed = await claimNextRunnableTask(
|
|
5276
|
+
context,
|
|
5277
|
+
tasksTable,
|
|
5278
|
+
target,
|
|
5279
|
+
registry,
|
|
5280
|
+
scanLimit,
|
|
5281
|
+
allowedTasks
|
|
5282
|
+
);
|
|
5283
|
+
if (!claimed) break;
|
|
5284
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5285
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5286
|
+
stopRequested = true;
|
|
5287
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5288
|
+
context.__tasksRunnerStop = true;
|
|
5289
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5290
|
+
}
|
|
5291
|
+
}).finally(() => {
|
|
5292
|
+
runningPromises.delete(p);
|
|
5293
|
+
});
|
|
5294
|
+
runningPromises.add(p);
|
|
5295
|
+
}
|
|
5296
|
+
await sleepMs(pollMs);
|
|
5297
|
+
}
|
|
5298
|
+
if (context.isStop() && !stopRequested) {
|
|
5299
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
5300
|
+
}
|
|
5301
|
+
if (runningPromises.size > 0) {
|
|
5302
|
+
if (stopRequested) {
|
|
5303
|
+
await Promise.race([
|
|
5304
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
5305
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
5306
|
+
context.logger.warn?.(
|
|
5307
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
5308
|
+
);
|
|
5309
|
+
})
|
|
5310
|
+
]);
|
|
5311
|
+
} else {
|
|
5312
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
} finally {
|
|
5316
|
+
if (registryInterval) {
|
|
5317
|
+
clearInterval(registryInterval);
|
|
5318
|
+
registryInterval = null;
|
|
5319
|
+
}
|
|
5320
|
+
if (registryReg) {
|
|
5321
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
5322
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
5323
|
+
});
|
|
5324
|
+
registryReg = null;
|
|
5325
|
+
delete context.servicesRegistry;
|
|
5326
|
+
delete context.runnerHeartbeat;
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5331
|
+
const db = getDb3(context);
|
|
5332
|
+
const queue = options.queue ?? "tasks";
|
|
5333
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5334
|
+
const pollMs = options.pollMs ?? 500;
|
|
5335
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5336
|
+
const deadline = Date.now() + timeoutMs;
|
|
5337
|
+
while (Date.now() <= deadline) {
|
|
5338
|
+
const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5339
|
+
if (done) return done;
|
|
5340
|
+
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5341
|
+
if (!pending) {
|
|
5342
|
+
const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5343
|
+
return maybeDone ?? null;
|
|
5344
|
+
}
|
|
5345
|
+
await sleepMs(pollMs);
|
|
5346
|
+
}
|
|
5347
|
+
return null;
|
|
5348
|
+
}
|
|
5349
|
+
var TasksManager = class _TasksManager {
|
|
5350
|
+
context;
|
|
5351
|
+
queue;
|
|
5352
|
+
target;
|
|
5353
|
+
recreateTaskTables;
|
|
5354
|
+
pollMs;
|
|
5355
|
+
maxParallel;
|
|
5356
|
+
scanLimit;
|
|
5357
|
+
allowedTasks;
|
|
5358
|
+
registry;
|
|
5359
|
+
runnerServiceGroup;
|
|
5360
|
+
runnerServiceName;
|
|
5361
|
+
runnerIdentityDir;
|
|
5362
|
+
runnerHeartbeatIntervalMs;
|
|
5363
|
+
runnerHeartbeatStaleMs;
|
|
5364
|
+
runnerGroupMaxInstances;
|
|
5365
|
+
runnerEnforceMaxInstances;
|
|
5366
|
+
runnerMetadata;
|
|
5367
|
+
constructor(context, options = {}) {
|
|
5368
|
+
this.context = context;
|
|
5369
|
+
this.queue = options.queue ?? "tasks";
|
|
5370
|
+
this.target = options.target ?? "localRunner";
|
|
5371
|
+
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5372
|
+
this.pollMs = options.pollMs ?? 1e3;
|
|
5373
|
+
this.maxParallel = options.maxParallel ?? 1;
|
|
5374
|
+
this.scanLimit = options.scanLimit ?? 100;
|
|
5375
|
+
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5376
|
+
this.registry = normalizeRegistry(options.registry);
|
|
5377
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5378
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
5379
|
+
this.runnerIdentityDir = options.runnerIdentityDir;
|
|
5380
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5381
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5382
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5383
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5384
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
5385
|
+
}
|
|
5386
|
+
static init(context, options = {}) {
|
|
5387
|
+
const defs = {
|
|
5388
|
+
table: "string default tasks",
|
|
5389
|
+
target: "string default localRunner",
|
|
5390
|
+
recreateTaskTables: "boolean default false",
|
|
5391
|
+
pollMs: "number default 1000",
|
|
5392
|
+
maxParallel: "number default 1",
|
|
5393
|
+
scanLimit: "number default 100",
|
|
5394
|
+
allowedTasks: "string",
|
|
5395
|
+
runnerServiceGroup: "string",
|
|
5396
|
+
runnerServiceName: "string",
|
|
5397
|
+
runnerIdentityDir: "string default ./data/runner-identities",
|
|
5398
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5399
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
5400
|
+
runnerGroupMaxInstances: "number",
|
|
5401
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
5402
|
+
};
|
|
5403
|
+
const discovered = context.params.getAllForModule(defs);
|
|
5404
|
+
const resolved = {
|
|
5405
|
+
queue: discovered.table,
|
|
5406
|
+
target: discovered.target,
|
|
5407
|
+
recreateTaskTables: discovered.recreateTaskTables,
|
|
5408
|
+
pollMs: discovered.pollMs,
|
|
5409
|
+
maxParallel: discovered.maxParallel,
|
|
5410
|
+
scanLimit: discovered.scanLimit,
|
|
5411
|
+
allowedTasks: discovered.allowedTasks,
|
|
5412
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5413
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
5414
|
+
runnerIdentityDir: discovered.runnerIdentityDir,
|
|
5415
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5416
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5417
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
5418
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
5419
|
+
...options
|
|
5420
|
+
};
|
|
5421
|
+
return new _TasksManager(context, resolved);
|
|
5422
|
+
}
|
|
5423
|
+
async ensureTaskTables(options = {}) {
|
|
5424
|
+
await ensureTaskTables(this.context, {
|
|
5425
|
+
queue: this.queue,
|
|
5426
|
+
recreate: options.recreate ?? this.recreateTaskTables
|
|
5427
|
+
});
|
|
5428
|
+
}
|
|
5429
|
+
async runTasksLoop(options = {}) {
|
|
5430
|
+
await runTasksLoop(this.context, {
|
|
5431
|
+
queue: options.queue ?? this.queue,
|
|
5432
|
+
target: options.target ?? this.target,
|
|
5433
|
+
pollMs: options.pollMs ?? this.pollMs,
|
|
5434
|
+
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5435
|
+
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5436
|
+
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5437
|
+
registry: options.registry ?? this.registry,
|
|
5438
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5439
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5440
|
+
runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
|
|
5441
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5442
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5443
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
5444
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
5445
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
5446
|
+
});
|
|
5447
|
+
}
|
|
5448
|
+
};
|
|
3900
5449
|
export {
|
|
3901
5450
|
Args,
|
|
3902
5451
|
Box5 as Box,
|
|
@@ -3919,8 +5468,18 @@ export {
|
|
|
3919
5468
|
ScreenFooter,
|
|
3920
5469
|
ScreenRow,
|
|
3921
5470
|
ScreenTitle,
|
|
5471
|
+
TaskMaster,
|
|
5472
|
+
TaskPing,
|
|
5473
|
+
TaskSampleProcess,
|
|
5474
|
+
TaskShellCommand,
|
|
5475
|
+
TaskStopRunner,
|
|
5476
|
+
TaskSumAB,
|
|
5477
|
+
TaskSystemInfo,
|
|
5478
|
+
TasksManager,
|
|
5479
|
+
TasksRegistry,
|
|
3922
5480
|
Text5 as Text,
|
|
3923
5481
|
TextBlock,
|
|
5482
|
+
appendTaskIpcLog,
|
|
3924
5483
|
buildBreadcrumb,
|
|
3925
5484
|
buildDetailBreadcrumb,
|
|
3926
5485
|
buildFooter,
|
|
@@ -3928,15 +5487,28 @@ export {
|
|
|
3928
5487
|
dbFindAndConnect,
|
|
3929
5488
|
dbInit,
|
|
3930
5489
|
defaultFileSynopsisFunction,
|
|
5490
|
+
defaultTasksRegistry,
|
|
3931
5491
|
defaultVersionSynopsisFunction,
|
|
5492
|
+
enqueueStopTask,
|
|
5493
|
+
enqueueTask,
|
|
5494
|
+
ensureTaskTables,
|
|
3932
5495
|
getArgsInstance,
|
|
3933
5496
|
createElement2 as h,
|
|
3934
5497
|
joiEdateType,
|
|
3935
5498
|
joiStringArrayType,
|
|
5499
|
+
listServicesRegistry as listAliveRunnerHeartbeats,
|
|
5500
|
+
listServicesRegistry,
|
|
3936
5501
|
listSources,
|
|
3937
5502
|
listTables,
|
|
3938
5503
|
load,
|
|
3939
5504
|
organizeFooterMessages,
|
|
5505
|
+
queueToTableNames,
|
|
5506
|
+
registerInServicesRegistry,
|
|
5507
|
+
registerInServicesRegistry as registerRunnerHeartbeat,
|
|
5508
|
+
runNodeTaskScript,
|
|
5509
|
+
runTasksLoop,
|
|
5510
|
+
servicesRegistryTable as runnerHeartbeatsTable,
|
|
5511
|
+
servicesRegistryTable,
|
|
3940
5512
|
setupContext,
|
|
3941
5513
|
showListScreen,
|
|
3942
5514
|
showMenuScreen,
|
|
@@ -3944,11 +5516,18 @@ export {
|
|
|
3944
5516
|
showMultiColumnListWithPreviewScreen,
|
|
3945
5517
|
showScreen,
|
|
3946
5518
|
showWordGridScreen,
|
|
5519
|
+
touchServicesRegistry as touchRunnerHeartbeat,
|
|
5520
|
+
touchServicesRegistry,
|
|
5521
|
+
unregisterServicesRegistry as unregisterRunnerHeartbeat,
|
|
5522
|
+
unregisterServicesRegistry,
|
|
5523
|
+
updateServicesRegistryMetadata,
|
|
5524
|
+
updateTaskProgress,
|
|
3947
5525
|
useCallback,
|
|
3948
5526
|
useEffect3 as useEffect,
|
|
3949
5527
|
useInput2 as useInput,
|
|
3950
5528
|
useMemo,
|
|
3951
5529
|
useRef3 as useRef,
|
|
3952
|
-
useState3 as useState
|
|
5530
|
+
useState3 as useState,
|
|
5531
|
+
waitForTaskResult
|
|
3953
5532
|
};
|
|
3954
5533
|
//# sourceMappingURL=index.js.map
|