@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.cjs
CHANGED
|
@@ -932,11 +932,11 @@ function buildBreadcrumb(parts) {
|
|
|
932
932
|
if (parts.length === 1) return parts[0];
|
|
933
933
|
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
934
934
|
}
|
|
935
|
-
function buildDetailBreadcrumb(
|
|
936
|
-
if (
|
|
937
|
-
return suffix ? `\u2190 ${suffix}` :
|
|
935
|
+
function buildDetailBreadcrumb(path5, suffix = "") {
|
|
936
|
+
if (path5.length <= 1) {
|
|
937
|
+
return suffix ? `\u2190 ${suffix}` : path5[0] || "";
|
|
938
938
|
}
|
|
939
|
-
const breadcrumb = buildBreadcrumb(
|
|
939
|
+
const breadcrumb = buildBreadcrumb(path5);
|
|
940
940
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
941
941
|
}
|
|
942
942
|
var init_utils = __esm({
|
|
@@ -1107,8 +1107,18 @@ __export(src_exports, {
|
|
|
1107
1107
|
ScreenFooter: () => ScreenFooter,
|
|
1108
1108
|
ScreenRow: () => ScreenRow,
|
|
1109
1109
|
ScreenTitle: () => ScreenTitle,
|
|
1110
|
+
TaskMaster: () => TaskMaster,
|
|
1111
|
+
TaskPing: () => TaskPing,
|
|
1112
|
+
TaskSampleProcess: () => TaskSampleProcess,
|
|
1113
|
+
TaskShellCommand: () => TaskShellCommand,
|
|
1114
|
+
TaskStopRunner: () => TaskStopRunner,
|
|
1115
|
+
TaskSumAB: () => TaskSumAB,
|
|
1116
|
+
TaskSystemInfo: () => TaskSystemInfo,
|
|
1117
|
+
TasksManager: () => TasksManager,
|
|
1118
|
+
TasksRegistry: () => TasksRegistry,
|
|
1110
1119
|
Text: () => import_ink5.Text,
|
|
1111
1120
|
TextBlock: () => TextBlock,
|
|
1121
|
+
appendTaskIpcLog: () => appendTaskIpcLog,
|
|
1112
1122
|
buildBreadcrumb: () => buildBreadcrumb,
|
|
1113
1123
|
buildDetailBreadcrumb: () => buildDetailBreadcrumb,
|
|
1114
1124
|
buildFooter: () => buildFooter,
|
|
@@ -1116,15 +1126,28 @@ __export(src_exports, {
|
|
|
1116
1126
|
dbFindAndConnect: () => dbFindAndConnect,
|
|
1117
1127
|
dbInit: () => dbInit,
|
|
1118
1128
|
defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
|
|
1129
|
+
defaultTasksRegistry: () => defaultTasksRegistry,
|
|
1119
1130
|
defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
|
|
1131
|
+
enqueueStopTask: () => enqueueStopTask,
|
|
1132
|
+
enqueueTask: () => enqueueTask,
|
|
1133
|
+
ensureTaskTables: () => ensureTaskTables,
|
|
1120
1134
|
getArgsInstance: () => getArgsInstance,
|
|
1121
1135
|
h: () => import_react5.createElement,
|
|
1122
1136
|
joiEdateType: () => joiEdateType,
|
|
1123
1137
|
joiStringArrayType: () => joiStringArrayType,
|
|
1138
|
+
listAliveRunnerHeartbeats: () => listServicesRegistry,
|
|
1139
|
+
listServicesRegistry: () => listServicesRegistry,
|
|
1124
1140
|
listSources: () => listSources,
|
|
1125
1141
|
listTables: () => listTables,
|
|
1126
1142
|
load: () => load,
|
|
1127
1143
|
organizeFooterMessages: () => organizeFooterMessages,
|
|
1144
|
+
queueToTableNames: () => queueToTableNames,
|
|
1145
|
+
registerInServicesRegistry: () => registerInServicesRegistry,
|
|
1146
|
+
registerRunnerHeartbeat: () => registerInServicesRegistry,
|
|
1147
|
+
runNodeTaskScript: () => runNodeTaskScript,
|
|
1148
|
+
runTasksLoop: () => runTasksLoop,
|
|
1149
|
+
runnerHeartbeatsTable: () => servicesRegistryTable,
|
|
1150
|
+
servicesRegistryTable: () => servicesRegistryTable,
|
|
1128
1151
|
setupContext: () => setupContext,
|
|
1129
1152
|
showListScreen: () => showListScreen,
|
|
1130
1153
|
showMenuScreen: () => showMenuScreen,
|
|
@@ -1132,12 +1155,19 @@ __export(src_exports, {
|
|
|
1132
1155
|
showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
|
|
1133
1156
|
showScreen: () => showScreen,
|
|
1134
1157
|
showWordGridScreen: () => showWordGridScreen,
|
|
1158
|
+
touchRunnerHeartbeat: () => touchServicesRegistry,
|
|
1159
|
+
touchServicesRegistry: () => touchServicesRegistry,
|
|
1160
|
+
unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
|
|
1161
|
+
unregisterServicesRegistry: () => unregisterServicesRegistry,
|
|
1162
|
+
updateServicesRegistryMetadata: () => updateServicesRegistryMetadata,
|
|
1163
|
+
updateTaskProgress: () => updateTaskProgress,
|
|
1135
1164
|
useCallback: () => import_react5.useCallback,
|
|
1136
1165
|
useEffect: () => import_react5.useEffect,
|
|
1137
1166
|
useInput: () => import_ink5.useInput,
|
|
1138
1167
|
useMemo: () => import_react5.useMemo,
|
|
1139
1168
|
useRef: () => import_react5.useRef,
|
|
1140
|
-
useState: () => import_react5.useState
|
|
1169
|
+
useState: () => import_react5.useState,
|
|
1170
|
+
waitForTaskResult: () => waitForTaskResult
|
|
1141
1171
|
});
|
|
1142
1172
|
module.exports = __toCommonJS(src_exports);
|
|
1143
1173
|
|
|
@@ -1962,7 +1992,7 @@ var Params = class _Params {
|
|
|
1962
1992
|
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
1963
1993
|
}
|
|
1964
1994
|
type = type.default(defValObj.value);
|
|
1965
|
-
} else if (str.match(
|
|
1995
|
+
} else if (str.match(/\s*required\s*/)) {
|
|
1966
1996
|
type = type.required();
|
|
1967
1997
|
} else {
|
|
1968
1998
|
type = type.optional();
|
|
@@ -2038,6 +2068,8 @@ var Params = class _Params {
|
|
|
2038
2068
|
/**
|
|
2039
2069
|
* Get all parameters from definitions (main script).
|
|
2040
2070
|
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
2071
|
+
* Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
|
|
2072
|
+
* around {@link get}) so --showUsedParams groups usage correctly.
|
|
2041
2073
|
*/
|
|
2042
2074
|
getAll(defs) {
|
|
2043
2075
|
return this.getAllForModule("script", defs);
|
|
@@ -2074,6 +2106,19 @@ var Params = class _Params {
|
|
|
2074
2106
|
this._currentModule = prev;
|
|
2075
2107
|
}
|
|
2076
2108
|
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
|
|
2111
|
+
* under the same module (for --showUsedParams / getFiguredByModule).
|
|
2112
|
+
*/
|
|
2113
|
+
runWithModule(moduleName, fn) {
|
|
2114
|
+
const prev = this._currentModule;
|
|
2115
|
+
this._currentModule = moduleName;
|
|
2116
|
+
try {
|
|
2117
|
+
return fn();
|
|
2118
|
+
} finally {
|
|
2119
|
+
this._currentModule = prev;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2077
2122
|
/**
|
|
2078
2123
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2079
2124
|
*/
|
|
@@ -2087,9 +2132,9 @@ var Params = class _Params {
|
|
|
2087
2132
|
if (!parenMatch) continue;
|
|
2088
2133
|
const parts = parenMatch[1].split(":");
|
|
2089
2134
|
if (parts.length < 3) continue;
|
|
2090
|
-
const
|
|
2091
|
-
if (!
|
|
2092
|
-
const srcMatch =
|
|
2135
|
+
const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2136
|
+
if (!path5 || path5.includes(paramsIndexPath)) continue;
|
|
2137
|
+
const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2093
2138
|
if (srcMatch) return srcMatch[1];
|
|
2094
2139
|
}
|
|
2095
2140
|
return "script";
|
|
@@ -2444,7 +2489,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2444
2489
|
const versions = await this.getVersions();
|
|
2445
2490
|
while (versions.length > this.maxVersions) {
|
|
2446
2491
|
const versionToDelete = import_path4.default.resolve(this.getDestinationPath(), versions.shift());
|
|
2447
|
-
this.logger.
|
|
2492
|
+
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
2448
2493
|
await import_fs4.default.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
2449
2494
|
}
|
|
2450
2495
|
return versionName;
|
|
@@ -2722,7 +2767,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2722
2767
|
};
|
|
2723
2768
|
this.metadata.files.push(fileEntry);
|
|
2724
2769
|
this.lastFileData = null;
|
|
2725
|
-
this.logger.
|
|
2770
|
+
this.logger.silly?.(`[FileDatabase] Created new file: ${fileEntry.fileName}, fileNumber: ${this.currentFileNumber}`);
|
|
2726
2771
|
}
|
|
2727
2772
|
/**
|
|
2728
2773
|
* Figure out what data to write and which file to use (for pagination)
|
|
@@ -2755,7 +2800,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2755
2800
|
const filesBeforeCreate = this.metadata.files.length;
|
|
2756
2801
|
this.makeNewFile();
|
|
2757
2802
|
newlyCreatedFileIndex = filesBeforeCreate;
|
|
2758
|
-
this.logger.
|
|
2803
|
+
this.logger.silly?.(`[FileDatabase] Creating new file for unique custom metadata combination, fileNumber: ${this.currentFileNumber}`);
|
|
2759
2804
|
} else if (this.metadata.files.length === 0) {
|
|
2760
2805
|
this.makeNewFile();
|
|
2761
2806
|
}
|
|
@@ -2804,7 +2849,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2804
2849
|
dataLeftOver = null;
|
|
2805
2850
|
}
|
|
2806
2851
|
const fileName = this.metadata.files[this.metadata.files.length - 1].fileName;
|
|
2807
|
-
this.logger.
|
|
2852
|
+
this.logger.silly?.(
|
|
2808
2853
|
`[FileDatabase] figureOutDataAndFileToWrite: filename=${fileName}, forceNewFile=${forceNewFile}, targetFileIndex=${targetFileIndex}, dataToWrite.length=${Array.isArray(dataToWrite) ? dataToWrite.length : "N/A"}, lastFileRecordsCount=${lastFileRecordsCount}`
|
|
2809
2854
|
);
|
|
2810
2855
|
return { dataToWrite, dataLeftOver, fileName };
|
|
@@ -2859,7 +2904,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2859
2904
|
this.metadata.modifiedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2860
2905
|
this.metadata.dataType = detectDataType(dataToWrite);
|
|
2861
2906
|
this.metadata.totalRecords = this.metadata.files.reduce((sum, file) => sum + (file.recordsCount || 0), 0);
|
|
2862
|
-
this.logger.
|
|
2907
|
+
this.logger.silly?.(
|
|
2863
2908
|
`[FileDatabase] Updated metadata for file ${currentFile.fileName}: recordsCount=${recordsCount}, totalRecords=${this.metadata.totalRecords}`
|
|
2864
2909
|
);
|
|
2865
2910
|
}
|
|
@@ -2883,7 +2928,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2883
2928
|
}
|
|
2884
2929
|
try {
|
|
2885
2930
|
await import_fs4.default.promises.writeFile(filePath, serializedData, "utf8");
|
|
2886
|
-
this.logger.
|
|
2931
|
+
this.logger.silly?.(`[FileDatabase] Wrote ${bytesToHumanReadable(requiredBytes)} to ${filePath}`);
|
|
2887
2932
|
} catch (error) {
|
|
2888
2933
|
throw new FileDatabaseError(`Failed to write file ${filePath}: ${error.message}`);
|
|
2889
2934
|
}
|
|
@@ -2965,7 +3010,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
2965
3010
|
this.useMetadata = format.hasMetadata;
|
|
2966
3011
|
}
|
|
2967
3012
|
if (this.useMetadata) {
|
|
2968
|
-
const
|
|
3013
|
+
const destPath = this.getDestinationPath();
|
|
3014
|
+
const metadataPath = import_path4.default.join(destPath, "metadata.json");
|
|
2969
3015
|
if (import_fs4.default.existsSync(metadataPath)) {
|
|
2970
3016
|
try {
|
|
2971
3017
|
const rawData = await import_fs4.default.promises.readFile(metadataPath, "utf8");
|
|
@@ -2979,7 +3025,9 @@ var FileDatabase = class _FileDatabase {
|
|
|
2979
3025
|
throw new FileDatabaseError(`Failed to read metadata: ${e.message}`);
|
|
2980
3026
|
}
|
|
2981
3027
|
} else {
|
|
2982
|
-
throw new FileDatabaseError(
|
|
3028
|
+
throw new FileDatabaseError(
|
|
3029
|
+
`[FileDatabase] No metadata found in non-versioned mode. Looked for: ${metadataPath} (table path: ${destPath})`
|
|
3030
|
+
);
|
|
2983
3031
|
}
|
|
2984
3032
|
} else {
|
|
2985
3033
|
this.metadata = await this.figureMetadataFromVersionFiles("");
|
|
@@ -2996,6 +3044,13 @@ var FileDatabase = class _FileDatabase {
|
|
|
2996
3044
|
* Write data to the file database
|
|
2997
3045
|
*/
|
|
2998
3046
|
async write(data, options = {}) {
|
|
3047
|
+
if (options.filename) {
|
|
3048
|
+
const destPath2 = this.getDestinationPath();
|
|
3049
|
+
await ensurePath(destPath2);
|
|
3050
|
+
const filePath = import_path4.default.join(destPath2, options.filename);
|
|
3051
|
+
await this.safeWrite(filePath, data);
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
2999
3054
|
if (options.forceNewVersion && !this.versioned) {
|
|
3000
3055
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
3001
3056
|
}
|
|
@@ -3019,17 +3074,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3019
3074
|
});
|
|
3020
3075
|
if (matches) {
|
|
3021
3076
|
targetFileIndex = i;
|
|
3022
|
-
this.logger.
|
|
3077
|
+
this.logger.silly?.(`[FileDatabase] Found existing file with matching custom metadata: ${fileEntry.fileName}, metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
3023
3078
|
break;
|
|
3024
3079
|
} else {
|
|
3025
|
-
this.logger.
|
|
3080
|
+
this.logger.silly?.(`[FileDatabase] File ${fileEntry.fileName} does not match custom metadata: ${JSON.stringify(options.customMetadata)}`);
|
|
3026
3081
|
}
|
|
3027
3082
|
}
|
|
3028
3083
|
if (targetFileIndex === null) {
|
|
3029
|
-
this.logger.
|
|
3084
|
+
this.logger.silly?.(`[FileDatabase] No existing file found with custom metadata: ${JSON.stringify(options.customMetadata)}, will create new file`);
|
|
3030
3085
|
}
|
|
3031
3086
|
} else {
|
|
3032
|
-
this.logger.
|
|
3087
|
+
this.logger.silly?.(`[FileDatabase] No custom metadata provided, will create new file`);
|
|
3033
3088
|
}
|
|
3034
3089
|
if (targetFileIndex !== null) {
|
|
3035
3090
|
const targetFile = this.metadata.files[targetFileIndex];
|
|
@@ -3058,7 +3113,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3058
3113
|
* Read data from the file database
|
|
3059
3114
|
*/
|
|
3060
3115
|
async read(options = {}) {
|
|
3061
|
-
const { version, nextPage = false, pageSize } = options;
|
|
3116
|
+
const { version, nextPage = false, pageSize, filename } = options;
|
|
3117
|
+
if (filename) {
|
|
3118
|
+
const destPath = this.getDestinationPath(version);
|
|
3119
|
+
const filePath = import_path4.default.join(destPath, filename);
|
|
3120
|
+
try {
|
|
3121
|
+
const rawData = await import_fs4.default.promises.readFile(filePath, "utf8");
|
|
3122
|
+
return JSON.parse(rawData);
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
throw new FileDatabaseError(`Failed to read file ${filename}: ${error.message}`);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3062
3127
|
await this.prepare({ read: true, version });
|
|
3063
3128
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
3064
3129
|
if (isNonPaginatedData) {
|
|
@@ -3139,6 +3204,67 @@ var FileDatabase = class _FileDatabase {
|
|
|
3139
3204
|
this.currentRecord = 0;
|
|
3140
3205
|
this.hasReadFirstPage = false;
|
|
3141
3206
|
}
|
|
3207
|
+
/**
|
|
3208
|
+
* List filenames in the table directory.
|
|
3209
|
+
* For catalog/key-value usage (files written with { filename }).
|
|
3210
|
+
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
3211
|
+
*/
|
|
3212
|
+
async listFilenames() {
|
|
3213
|
+
const destPath = this.versioned && this.currentVersion ? import_path4.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3214
|
+
try {
|
|
3215
|
+
const entries = await import_fs4.default.promises.readdir(destPath, { withFileTypes: true });
|
|
3216
|
+
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
if (err?.code === "ENOENT") return [];
|
|
3219
|
+
throw new FileDatabaseError(`Failed to list files: ${err.message}`);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
/**
|
|
3223
|
+
* Remove a file from the table directory (catalog mode).
|
|
3224
|
+
* Use with listFilenames() to manage individual files.
|
|
3225
|
+
*/
|
|
3226
|
+
async removeFile(filename) {
|
|
3227
|
+
const destPath = this.versioned && this.currentVersion ? import_path4.default.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
3228
|
+
const filePath = import_path4.default.join(destPath, filename);
|
|
3229
|
+
try {
|
|
3230
|
+
await import_fs4.default.promises.unlink(filePath);
|
|
3231
|
+
} catch (err) {
|
|
3232
|
+
if (err?.code === "ENOENT") return;
|
|
3233
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* Remove a file and its metadata entry (non-versioned mode with useMetadata).
|
|
3238
|
+
* Use with findData() to get fileName, then call removeFileEntry to delete.
|
|
3239
|
+
*/
|
|
3240
|
+
async removeFileEntry(filename) {
|
|
3241
|
+
if (this.versioned) {
|
|
3242
|
+
throw new FileDatabaseError("removeFileEntry is only supported in non-versioned mode");
|
|
3243
|
+
}
|
|
3244
|
+
await this.prepare({ read: true });
|
|
3245
|
+
const idx = this.metadata.files.findIndex((f) => f.fileName === filename);
|
|
3246
|
+
if (idx === -1) {
|
|
3247
|
+
throw new FileDatabaseError(`File entry ${filename} not found in metadata`);
|
|
3248
|
+
}
|
|
3249
|
+
const entry = this.metadata.files[idx];
|
|
3250
|
+
const recordsCount = entry.recordsCount || 0;
|
|
3251
|
+
this.metadata.files.splice(idx, 1);
|
|
3252
|
+
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
3253
|
+
const destPath = this.getDestinationPath();
|
|
3254
|
+
const filePath = import_path4.default.join(destPath, filename);
|
|
3255
|
+
try {
|
|
3256
|
+
await import_fs4.default.promises.unlink(filePath);
|
|
3257
|
+
} catch (err) {
|
|
3258
|
+
if (err?.code === "ENOENT") {
|
|
3259
|
+
this.logger.warn?.(`[FileDatabase] File ${filename} already missing on disk`);
|
|
3260
|
+
} else {
|
|
3261
|
+
throw new FileDatabaseError(`Failed to remove file ${filename}: ${err.message}`);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
if (this.useMetadata) {
|
|
3265
|
+
await this.saveVersionMetadata(this.metadata);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3142
3268
|
/**
|
|
3143
3269
|
* Set file-level synopsis calculation function
|
|
3144
3270
|
*/
|
|
@@ -3610,7 +3736,7 @@ async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
|
3610
3736
|
dbConnectionString: "string",
|
|
3611
3737
|
dbProfile: "boolean default false"
|
|
3612
3738
|
};
|
|
3613
|
-
const paramsConfig = context.params.
|
|
3739
|
+
const paramsConfig = context.params.getAll(defs);
|
|
3614
3740
|
dbName = paramsConfig.dbName;
|
|
3615
3741
|
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3616
3742
|
dbProfile = paramsConfig.dbProfile;
|
|
@@ -3672,6 +3798,7 @@ var ALL_LEVELS = [
|
|
|
3672
3798
|
"response",
|
|
3673
3799
|
"progress"
|
|
3674
3800
|
];
|
|
3801
|
+
var DEFAULT_LEVELS = ALL_LEVELS.filter((l) => l !== "silly");
|
|
3675
3802
|
var MAX_LEVEL_LENGTH = Math.max(...ALL_LEVELS.map((level) => level.toUpperCase().length));
|
|
3676
3803
|
var LEVEL_COLORS = {
|
|
3677
3804
|
error: import_chalk.default.red.bold,
|
|
@@ -3757,7 +3884,7 @@ var Logger = class _Logger {
|
|
|
3757
3884
|
silent: false,
|
|
3758
3885
|
showLevel: false,
|
|
3759
3886
|
timestamp: false,
|
|
3760
|
-
levels:
|
|
3887
|
+
levels: DEFAULT_LEVELS,
|
|
3761
3888
|
progressTimes: false,
|
|
3762
3889
|
progressThrottle: void 0
|
|
3763
3890
|
};
|
|
@@ -3917,15 +4044,17 @@ var Logger = class _Logger {
|
|
|
3917
4044
|
}
|
|
3918
4045
|
normalizeLevels(levels) {
|
|
3919
4046
|
if (!levels || !levels.length) {
|
|
3920
|
-
return
|
|
4047
|
+
return DEFAULT_LEVELS;
|
|
3921
4048
|
}
|
|
3922
|
-
const
|
|
3923
|
-
const
|
|
3924
|
-
const
|
|
4049
|
+
const tokens = levels.map((t) => String(t).trim()).filter(Boolean);
|
|
4050
|
+
const explicitIncludes = tokens.filter((t) => !t.startsWith("+") && !t.startsWith("-")).map((t) => t);
|
|
4051
|
+
const addIncludes = tokens.filter((t) => t.startsWith("+")).map((t) => t.slice(1));
|
|
4052
|
+
const excludes = tokens.filter((t) => t.startsWith("-")).map((t) => t.slice(1));
|
|
4053
|
+
const unknown = [...explicitIncludes, ...addIncludes, ...excludes].filter((level) => !ALL_LEVELS.includes(level));
|
|
3925
4054
|
if (unknown.length) {
|
|
3926
4055
|
console.warn(`[Logger] Unknown level(s): ${unknown.join(", ")}`);
|
|
3927
4056
|
}
|
|
3928
|
-
const base =
|
|
4057
|
+
const base = explicitIncludes.length ? explicitIncludes : Array.from(/* @__PURE__ */ new Set([...DEFAULT_LEVELS, ...addIncludes]));
|
|
3929
4058
|
return base.filter((level) => !excludes.includes(level));
|
|
3930
4059
|
}
|
|
3931
4060
|
isValidMode(mode) {
|
|
@@ -3983,6 +4112,1456 @@ function setup(opts = {}) {
|
|
|
3983
4112
|
function setupContext(opts = {}) {
|
|
3984
4113
|
return setup(opts);
|
|
3985
4114
|
}
|
|
4115
|
+
|
|
4116
|
+
// src/utils/core-utils.ts
|
|
4117
|
+
function sleepMs(ms) {
|
|
4118
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4119
|
+
}
|
|
4120
|
+
function toJsonColumn(value) {
|
|
4121
|
+
if (value === void 0 || value === null) return null;
|
|
4122
|
+
return JSON.stringify(value);
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
// src/tasks/servicesRegistry.ts
|
|
4126
|
+
var import_node_crypto2 = require("crypto");
|
|
4127
|
+
var import_promises = require("fs/promises");
|
|
4128
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
4129
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
4130
|
+
|
|
4131
|
+
// src/tasks/taskUtils.ts
|
|
4132
|
+
var import_node_crypto = require("crypto");
|
|
4133
|
+
function getDb(context) {
|
|
4134
|
+
const db = context.db;
|
|
4135
|
+
if (!db) {
|
|
4136
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
4137
|
+
}
|
|
4138
|
+
return db;
|
|
4139
|
+
}
|
|
4140
|
+
function queueToTableNames(queue) {
|
|
4141
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4142
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4143
|
+
}
|
|
4144
|
+
return {
|
|
4145
|
+
tasksTable: queue,
|
|
4146
|
+
historyTable: `${queue}_history`
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
function servicesRegistryTable(queue) {
|
|
4150
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4151
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4152
|
+
}
|
|
4153
|
+
return `${queue}_services_registry`;
|
|
4154
|
+
}
|
|
4155
|
+
async function ensureTaskTables(context, options = {}) {
|
|
4156
|
+
const queue = options.queue ?? "tasks";
|
|
4157
|
+
const recreate = options.recreate ?? false;
|
|
4158
|
+
const db = getDb(context);
|
|
4159
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
4160
|
+
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4161
|
+
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4162
|
+
if (recreate) {
|
|
4163
|
+
await db.schema.dropTableIfExists(historyTable);
|
|
4164
|
+
await db.schema.dropTableIfExists(tasksTable);
|
|
4165
|
+
}
|
|
4166
|
+
if (needsTasks) {
|
|
4167
|
+
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4168
|
+
await db.schema.createTable(tasksTable, (t) => {
|
|
4169
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4170
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4171
|
+
t.timestamp("started_at");
|
|
4172
|
+
t.timestamp("completed_at");
|
|
4173
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4174
|
+
t.text("schedule");
|
|
4175
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4176
|
+
t.text("target").notNullable();
|
|
4177
|
+
t.text("task").notNullable();
|
|
4178
|
+
t.json("params");
|
|
4179
|
+
t.text("opid");
|
|
4180
|
+
t.timestamp("paused_at").defaultTo(null);
|
|
4181
|
+
t.text("progress");
|
|
4182
|
+
t.boolean("success");
|
|
4183
|
+
t.json("results");
|
|
4184
|
+
});
|
|
4185
|
+
await db.schema.alterTable(tasksTable, (t) => {
|
|
4186
|
+
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4187
|
+
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4188
|
+
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4191
|
+
if (needsHistory) {
|
|
4192
|
+
await db.schema.createTable(historyTable, (t) => {
|
|
4193
|
+
t.uuid("id").notNullable();
|
|
4194
|
+
t.timestamp("created_at").notNullable();
|
|
4195
|
+
t.timestamp("started_at");
|
|
4196
|
+
t.timestamp("completed_at");
|
|
4197
|
+
t.integer("priority").notNullable().defaultTo(0);
|
|
4198
|
+
t.text("schedule");
|
|
4199
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4200
|
+
t.text("target").notNullable();
|
|
4201
|
+
t.text("task").notNullable();
|
|
4202
|
+
t.json("params");
|
|
4203
|
+
t.text("opid");
|
|
4204
|
+
t.text("progress");
|
|
4205
|
+
t.boolean("success");
|
|
4206
|
+
t.json("results");
|
|
4207
|
+
});
|
|
4208
|
+
await db.schema.alterTable(historyTable, (t) => {
|
|
4209
|
+
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4210
|
+
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
const registryTable = servicesRegistryTable(queue);
|
|
4214
|
+
const needsRegistry = !await db.tableExists(registryTable);
|
|
4215
|
+
if (needsRegistry) {
|
|
4216
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
4217
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4218
|
+
t.uuid("instance_id").notNullable().unique();
|
|
4219
|
+
t.text("queue").notNullable();
|
|
4220
|
+
t.text("service_group").notNullable();
|
|
4221
|
+
t.text("service_name").notNullable();
|
|
4222
|
+
t.text("target").notNullable();
|
|
4223
|
+
t.text("hostname");
|
|
4224
|
+
t.integer("pid");
|
|
4225
|
+
t.json("metadata");
|
|
4226
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4227
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
4228
|
+
t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
|
|
4229
|
+
t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
4230
|
+
t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
async function enqueueTask(context, options) {
|
|
4235
|
+
const db = getDb(context);
|
|
4236
|
+
const queue = options.queue ?? "tasks";
|
|
4237
|
+
const { tasksTable } = queueToTableNames(queue);
|
|
4238
|
+
const id = (0, import_node_crypto.randomUUID)();
|
|
4239
|
+
await db(tasksTable).insert({
|
|
4240
|
+
id,
|
|
4241
|
+
target: options.target,
|
|
4242
|
+
task: options.task,
|
|
4243
|
+
params: toJsonColumn(options.params ?? null),
|
|
4244
|
+
opid: options.opid ?? null,
|
|
4245
|
+
priority: options.priority ?? 0,
|
|
4246
|
+
schedule: options.schedule ?? null
|
|
4247
|
+
});
|
|
4248
|
+
return id;
|
|
4249
|
+
}
|
|
4250
|
+
async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
4251
|
+
const db = getDb(context);
|
|
4252
|
+
await db(tasksTable).where({ id: taskId }).update({
|
|
4253
|
+
progress: typeof progress === "string" ? progress : JSON.stringify(progress)
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
// src/tasks/servicesRegistry.ts
|
|
4258
|
+
function getDb2(context) {
|
|
4259
|
+
const db = context.db;
|
|
4260
|
+
if (!db) {
|
|
4261
|
+
throw new Error("Services registry requires context.db");
|
|
4262
|
+
}
|
|
4263
|
+
return db;
|
|
4264
|
+
}
|
|
4265
|
+
function parseMetadataColumn(value) {
|
|
4266
|
+
if (!value) return {};
|
|
4267
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
4268
|
+
if (typeof value === "string") {
|
|
4269
|
+
try {
|
|
4270
|
+
const p = JSON.parse(value);
|
|
4271
|
+
return p && typeof p === "object" && !Array.isArray(p) ? p : {};
|
|
4272
|
+
} catch {
|
|
4273
|
+
return {};
|
|
4274
|
+
}
|
|
4275
|
+
}
|
|
4276
|
+
return {};
|
|
4277
|
+
}
|
|
4278
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
4279
|
+
intake: 1,
|
|
4280
|
+
harvest: 1,
|
|
4281
|
+
loader: 0,
|
|
4282
|
+
photos: 0,
|
|
4283
|
+
photosprocessor: 0,
|
|
4284
|
+
ingest: 0
|
|
4285
|
+
};
|
|
4286
|
+
function sanitizeNamePart(raw) {
|
|
4287
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4288
|
+
return s.slice(0, 80) || "runner";
|
|
4289
|
+
}
|
|
4290
|
+
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
4291
|
+
const safeQ = sanitizeNamePart(queue);
|
|
4292
|
+
const safeG = sanitizeNamePart(serviceGroup);
|
|
4293
|
+
return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
4294
|
+
}
|
|
4295
|
+
async function readIdentityFile(filePath) {
|
|
4296
|
+
try {
|
|
4297
|
+
const text = await (0, import_promises.readFile)(filePath, "utf8");
|
|
4298
|
+
const parsed = JSON.parse(text);
|
|
4299
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4300
|
+
} catch {
|
|
4301
|
+
return {};
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
async function writeIdentityFile(filePath, data) {
|
|
4305
|
+
await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
|
|
4306
|
+
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
|
|
4307
|
+
`, "utf8");
|
|
4308
|
+
}
|
|
4309
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
4310
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
4311
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
4312
|
+
}
|
|
4313
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
4314
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
4315
|
+
}
|
|
4316
|
+
async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
|
|
4317
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4318
|
+
let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
4319
|
+
if (excludeInstanceId) {
|
|
4320
|
+
q = q.whereNot("instance_id", excludeInstanceId);
|
|
4321
|
+
}
|
|
4322
|
+
const row = await q.count("id as count").first();
|
|
4323
|
+
return Number(row?.count ?? 0);
|
|
4324
|
+
}
|
|
4325
|
+
function isUniqueViolation(error) {
|
|
4326
|
+
const code = error?.code ?? error?.errno;
|
|
4327
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
4328
|
+
}
|
|
4329
|
+
async function registerInServicesRegistry(context, options) {
|
|
4330
|
+
const db = getDb2(context);
|
|
4331
|
+
const registryTable = servicesRegistryTable(options.queue);
|
|
4332
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
4333
|
+
if (!serviceGroup) {
|
|
4334
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4335
|
+
}
|
|
4336
|
+
const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
|
|
4337
|
+
let identity = await readIdentityFile(identityPath);
|
|
4338
|
+
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
|
|
4339
|
+
identity.instanceId = instanceId;
|
|
4340
|
+
await writeIdentityFile(identityPath, identity);
|
|
4341
|
+
const hostname = import_node_os.default.hostname();
|
|
4342
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4343
|
+
const meta = toJsonColumn(options.metadata ?? null);
|
|
4344
|
+
const existing = await db(registryTable).where({ instance_id: instanceId }).first();
|
|
4345
|
+
if (existing) {
|
|
4346
|
+
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
4347
|
+
target: options.target,
|
|
4348
|
+
hostname,
|
|
4349
|
+
pid,
|
|
4350
|
+
metadata: meta,
|
|
4351
|
+
last_seen_at: db.fn.now()
|
|
4352
|
+
});
|
|
4353
|
+
const serviceName = String(existing.service_name);
|
|
4354
|
+
identity.serviceName = serviceName;
|
|
4355
|
+
await writeIdentityFile(identityPath, identity);
|
|
4356
|
+
const reg = {
|
|
4357
|
+
instanceId,
|
|
4358
|
+
serviceName,
|
|
4359
|
+
serviceGroup,
|
|
4360
|
+
queue: options.queue,
|
|
4361
|
+
target: options.target,
|
|
4362
|
+
rowId: String(existing.id)
|
|
4363
|
+
};
|
|
4364
|
+
context.servicesRegistry = reg;
|
|
4365
|
+
context.runnerHeartbeat = reg;
|
|
4366
|
+
context.logger.info?.(
|
|
4367
|
+
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
4368
|
+
);
|
|
4369
|
+
return {
|
|
4370
|
+
instanceId,
|
|
4371
|
+
serviceName,
|
|
4372
|
+
serviceGroup,
|
|
4373
|
+
queue: options.queue,
|
|
4374
|
+
target: options.target,
|
|
4375
|
+
rowId: String(existing.id),
|
|
4376
|
+
registryTable
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
4380
|
+
const aliveOthers = await countAliveInGroup(
|
|
4381
|
+
db,
|
|
4382
|
+
registryTable,
|
|
4383
|
+
options.queue,
|
|
4384
|
+
serviceGroup,
|
|
4385
|
+
options.staleMs,
|
|
4386
|
+
instanceId
|
|
4387
|
+
);
|
|
4388
|
+
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
4389
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
4390
|
+
if (options.enforceMaxInstances) {
|
|
4391
|
+
throw new Error(msg);
|
|
4392
|
+
}
|
|
4393
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
4394
|
+
}
|
|
4395
|
+
const explicitName = options.serviceName?.trim();
|
|
4396
|
+
const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
|
|
4397
|
+
const hostBase = sanitizeNamePart(hostname);
|
|
4398
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
4399
|
+
const baseCandidates = [];
|
|
4400
|
+
if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
|
|
4401
|
+
if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
|
|
4402
|
+
baseCandidates.push(`${groupBase}-${hostBase}`);
|
|
4403
|
+
baseCandidates.push(groupBase);
|
|
4404
|
+
function* eachServiceNameCandidate(bases) {
|
|
4405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4406
|
+
for (const rawBase of bases) {
|
|
4407
|
+
const base = sanitizeNamePart(rawBase);
|
|
4408
|
+
if (!base) continue;
|
|
4409
|
+
const seq = [base];
|
|
4410
|
+
for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
|
|
4411
|
+
for (const c of seq) {
|
|
4412
|
+
if (seen.has(c)) continue;
|
|
4413
|
+
seen.add(c);
|
|
4414
|
+
yield c;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
let inserted;
|
|
4419
|
+
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
4420
|
+
try {
|
|
4421
|
+
const rows = await db(registryTable).insert({
|
|
4422
|
+
instance_id: instanceId,
|
|
4423
|
+
queue: options.queue,
|
|
4424
|
+
service_group: serviceGroup,
|
|
4425
|
+
service_name: candidate,
|
|
4426
|
+
target: options.target,
|
|
4427
|
+
hostname,
|
|
4428
|
+
pid,
|
|
4429
|
+
metadata: meta,
|
|
4430
|
+
last_seen_at: db.fn.now()
|
|
4431
|
+
}).returning(["id", "service_name"]);
|
|
4432
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
4433
|
+
if (row) {
|
|
4434
|
+
inserted = { id: String(row.id), service_name: String(row.service_name) };
|
|
4435
|
+
break;
|
|
4436
|
+
}
|
|
4437
|
+
} catch (error) {
|
|
4438
|
+
if (!isUniqueViolation(error)) {
|
|
4439
|
+
throw error;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
if (!inserted) {
|
|
4444
|
+
throw new Error(
|
|
4445
|
+
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
4446
|
+
);
|
|
4447
|
+
}
|
|
4448
|
+
identity.serviceName = inserted.service_name;
|
|
4449
|
+
await writeIdentityFile(identityPath, identity);
|
|
4450
|
+
const regNew = {
|
|
4451
|
+
instanceId,
|
|
4452
|
+
serviceName: inserted.service_name,
|
|
4453
|
+
serviceGroup,
|
|
4454
|
+
queue: options.queue,
|
|
4455
|
+
target: options.target,
|
|
4456
|
+
rowId: inserted.id
|
|
4457
|
+
};
|
|
4458
|
+
context.servicesRegistry = regNew;
|
|
4459
|
+
context.runnerHeartbeat = regNew;
|
|
4460
|
+
context.logger.info?.(
|
|
4461
|
+
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
4462
|
+
);
|
|
4463
|
+
return {
|
|
4464
|
+
instanceId,
|
|
4465
|
+
serviceName: inserted.service_name,
|
|
4466
|
+
serviceGroup,
|
|
4467
|
+
queue: options.queue,
|
|
4468
|
+
target: options.target,
|
|
4469
|
+
rowId: inserted.id,
|
|
4470
|
+
registryTable
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
async function touchServicesRegistry(context, registration) {
|
|
4474
|
+
const db = getDb2(context);
|
|
4475
|
+
const hostname = import_node_os.default.hostname();
|
|
4476
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4477
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
4478
|
+
last_seen_at: db.fn.now(),
|
|
4479
|
+
hostname,
|
|
4480
|
+
pid
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
4484
|
+
const db = getDb2(context);
|
|
4485
|
+
const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
|
|
4486
|
+
const prev = parseMetadataColumn(row?.metadata);
|
|
4487
|
+
const merged = { ...prev, ...patch };
|
|
4488
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
4489
|
+
metadata: toJsonColumn(merged),
|
|
4490
|
+
last_seen_at: db.fn.now()
|
|
4491
|
+
});
|
|
4492
|
+
context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
|
|
4493
|
+
}
|
|
4494
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
4495
|
+
const db = getDb2(context);
|
|
4496
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
|
|
4497
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
|
|
4498
|
+
}
|
|
4499
|
+
async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
4500
|
+
const db = getDb2(context);
|
|
4501
|
+
const staleMs = options.staleMs ?? 6e4;
|
|
4502
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4503
|
+
const table = servicesRegistryTable(options.queue);
|
|
4504
|
+
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
4505
|
+
if (options.serviceGroup?.trim()) {
|
|
4506
|
+
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
4507
|
+
}
|
|
4508
|
+
return await q;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// src/tasks/taskLogs.ts
|
|
4512
|
+
function getLogsState(context) {
|
|
4513
|
+
const holder = context;
|
|
4514
|
+
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
4515
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
|
|
4516
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4517
|
+
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4518
|
+
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4519
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4520
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4521
|
+
const errorDb = new FileDatabase({
|
|
4522
|
+
basePath,
|
|
4523
|
+
namespace,
|
|
4524
|
+
tableName: errorTableName,
|
|
4525
|
+
versioned: true,
|
|
4526
|
+
useMetadata: true,
|
|
4527
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4528
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4529
|
+
logger: holder.logger
|
|
4530
|
+
});
|
|
4531
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4532
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4533
|
+
if (!enabled) {
|
|
4534
|
+
const disabledState = {
|
|
4535
|
+
db: null,
|
|
4536
|
+
errorDb,
|
|
4537
|
+
queue: Promise.resolve(),
|
|
4538
|
+
initialized: true,
|
|
4539
|
+
errorInitialized: false
|
|
4540
|
+
};
|
|
4541
|
+
holder.__tasksLogsState = disabledState;
|
|
4542
|
+
return disabledState;
|
|
4543
|
+
}
|
|
4544
|
+
const db = new FileDatabase({
|
|
4545
|
+
basePath,
|
|
4546
|
+
namespace,
|
|
4547
|
+
tableName,
|
|
4548
|
+
versioned: true,
|
|
4549
|
+
useMetadata: true,
|
|
4550
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4551
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4552
|
+
logger: holder.logger
|
|
4553
|
+
});
|
|
4554
|
+
const state = {
|
|
4555
|
+
db,
|
|
4556
|
+
errorDb,
|
|
4557
|
+
queue: Promise.resolve(),
|
|
4558
|
+
initialized: false,
|
|
4559
|
+
errorInitialized: false
|
|
4560
|
+
};
|
|
4561
|
+
holder.__tasksLogsState = state;
|
|
4562
|
+
return state;
|
|
4563
|
+
}
|
|
4564
|
+
function isErrorPayload(payload) {
|
|
4565
|
+
if (!payload) return false;
|
|
4566
|
+
if (typeof payload === "object") {
|
|
4567
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4568
|
+
if (level === "error" || level === "fatal") return true;
|
|
4569
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4570
|
+
return false;
|
|
4571
|
+
}
|
|
4572
|
+
if (typeof payload === "string") {
|
|
4573
|
+
return /\berror\b/i.test(payload);
|
|
4574
|
+
}
|
|
4575
|
+
return false;
|
|
4576
|
+
}
|
|
4577
|
+
function buildLogRecord(task, payload) {
|
|
4578
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4579
|
+
return {
|
|
4580
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4581
|
+
opid: task.opid ?? null,
|
|
4582
|
+
taskId: task.id,
|
|
4583
|
+
taskName: task.task,
|
|
4584
|
+
target: task.target,
|
|
4585
|
+
source: typeof params.source === "string" ? params.source : null,
|
|
4586
|
+
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4587
|
+
payload
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
function appendTaskIpcLog(context, task, payload) {
|
|
4591
|
+
const state = getLogsState(context);
|
|
4592
|
+
if (!state.db && !state.errorDb) return;
|
|
4593
|
+
const record = buildLogRecord(task, payload);
|
|
4594
|
+
state.queue = state.queue.then(async () => {
|
|
4595
|
+
if (state.db) {
|
|
4596
|
+
await state.db.write([record], { forceNewVersion: !state.initialized });
|
|
4597
|
+
state.initialized = true;
|
|
4598
|
+
}
|
|
4599
|
+
if (state.errorDb && isErrorPayload(payload)) {
|
|
4600
|
+
await state.errorDb.write([record], { forceNewVersion: !state.errorInitialized });
|
|
4601
|
+
state.errorInitialized = true;
|
|
4602
|
+
}
|
|
4603
|
+
}).catch((error) => {
|
|
4604
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4605
|
+
});
|
|
4606
|
+
}
|
|
4607
|
+
|
|
4608
|
+
// src/tasks/time-matcher.ts
|
|
4609
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4610
|
+
function resolveAsterisks(field, range) {
|
|
4611
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4612
|
+
}
|
|
4613
|
+
function resolveRanges(field) {
|
|
4614
|
+
const regex = /(\d+)-(\d+)/;
|
|
4615
|
+
let current = field;
|
|
4616
|
+
while (true) {
|
|
4617
|
+
const match = regex.exec(current);
|
|
4618
|
+
if (!match) break;
|
|
4619
|
+
const raw = match[0];
|
|
4620
|
+
let first = Number(match[1]);
|
|
4621
|
+
let last = Number(match[2]);
|
|
4622
|
+
if (last < first) {
|
|
4623
|
+
[first, last] = [last, first];
|
|
4624
|
+
}
|
|
4625
|
+
const values = [];
|
|
4626
|
+
for (let i = first; i <= last; i += 1) {
|
|
4627
|
+
values.push(i);
|
|
4628
|
+
}
|
|
4629
|
+
current = current.replace(raw, values.join(","));
|
|
4630
|
+
}
|
|
4631
|
+
return current;
|
|
4632
|
+
}
|
|
4633
|
+
function resolveSteps(field) {
|
|
4634
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4635
|
+
if (!match) return field;
|
|
4636
|
+
const base = match[1];
|
|
4637
|
+
const step = Number(match[2]);
|
|
4638
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4639
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4640
|
+
}
|
|
4641
|
+
function convertPattern(pattern) {
|
|
4642
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4643
|
+
if (parts.length !== 6) {
|
|
4644
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4645
|
+
}
|
|
4646
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4647
|
+
}
|
|
4648
|
+
function fieldMatches(field, value) {
|
|
4649
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4650
|
+
return allowed.includes(value);
|
|
4651
|
+
}
|
|
4652
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4653
|
+
const parsed = convertPattern(pattern);
|
|
4654
|
+
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());
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
// src/tasks/TaskMaster.ts
|
|
4658
|
+
var TaskMaster = class {
|
|
4659
|
+
context;
|
|
4660
|
+
task;
|
|
4661
|
+
constructor(context, task) {
|
|
4662
|
+
this.context = context;
|
|
4663
|
+
this.task = task;
|
|
4664
|
+
}
|
|
4665
|
+
cantRunReason() {
|
|
4666
|
+
return false;
|
|
4667
|
+
}
|
|
4668
|
+
requestStop(_allowanceMs) {
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
|
|
4672
|
+
// src/tasks/coreTasks/TaskPing.ts
|
|
4673
|
+
var TaskPing = class extends TaskMaster {
|
|
4674
|
+
async run() {
|
|
4675
|
+
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4676
|
+
return { success: true, results: "pong" };
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
|
|
4680
|
+
// src/tasks/coreTasks/TaskSampleProcess.ts
|
|
4681
|
+
var TaskSampleProcess = class extends TaskMaster {
|
|
4682
|
+
stopRequested = false;
|
|
4683
|
+
stopAllowanceMs = 0;
|
|
4684
|
+
stopDecisionLogged = false;
|
|
4685
|
+
requestStop(allowanceMs) {
|
|
4686
|
+
this.stopRequested = true;
|
|
4687
|
+
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
4688
|
+
this.context.logger.warn?.(
|
|
4689
|
+
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
4692
|
+
async run(reportProgress) {
|
|
4693
|
+
const totalRaw = this.task?.params?.total ?? 10;
|
|
4694
|
+
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
4695
|
+
const nameRaw = this.task?.params?.name;
|
|
4696
|
+
const total = Number(totalRaw);
|
|
4697
|
+
const delay = Number(delayRaw);
|
|
4698
|
+
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
|
|
4699
|
+
const errors = [];
|
|
4700
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4701
|
+
errors.push('param "total" must be a positive integer');
|
|
4702
|
+
}
|
|
4703
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4704
|
+
errors.push('param "delay" must be an integer >= 0');
|
|
4705
|
+
}
|
|
4706
|
+
if (errors.length > 0) {
|
|
4707
|
+
return {
|
|
4708
|
+
success: false,
|
|
4709
|
+
results: {
|
|
4710
|
+
error: `Validation failed: ${errors.join(", ")}`,
|
|
4711
|
+
received: { total: totalRaw, delay: delayRaw, name: nameRaw }
|
|
4712
|
+
}
|
|
4713
|
+
};
|
|
4714
|
+
}
|
|
4715
|
+
const startedAt = Date.now();
|
|
4716
|
+
for (let i = 1; i <= total; i += 1) {
|
|
4717
|
+
if (this.stopRequested) {
|
|
4718
|
+
const remainingMs = Math.max(0, (total - i + 1) * delay);
|
|
4719
|
+
if (remainingMs <= this.stopAllowanceMs) {
|
|
4720
|
+
if (!this.stopDecisionLogged) {
|
|
4721
|
+
this.stopDecisionLogged = true;
|
|
4722
|
+
this.context.logger.warn?.(
|
|
4723
|
+
`[TaskSampleProcess] continue to finish (${this.task.id}): remainingMs=${remainingMs} <= allowanceMs=${this.stopAllowanceMs}`
|
|
4724
|
+
);
|
|
4725
|
+
}
|
|
4726
|
+
} else {
|
|
4727
|
+
this.context.logger.warn?.(
|
|
4728
|
+
`[TaskSampleProcess] stopping gracefully at iteration ${i}/${total} (${this.task.id}), remainingMs=${remainingMs} > allowanceMs=${this.stopAllowanceMs}`
|
|
4729
|
+
);
|
|
4730
|
+
return {
|
|
4731
|
+
success: false,
|
|
4732
|
+
results: {
|
|
4733
|
+
message: `Stopped before completion at iteration ${i}/${total}`,
|
|
4734
|
+
completed: i - 1,
|
|
4735
|
+
total,
|
|
4736
|
+
name,
|
|
4737
|
+
remainingMs,
|
|
4738
|
+
allowanceMs: this.stopAllowanceMs
|
|
4739
|
+
}
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
const elapsed = Date.now() - startedAt;
|
|
4744
|
+
const remaining = Math.max(0, (total - i) * delay);
|
|
4745
|
+
const progress = {
|
|
4746
|
+
name,
|
|
4747
|
+
count: i,
|
|
4748
|
+
total,
|
|
4749
|
+
elapsedMs: elapsed,
|
|
4750
|
+
remainingMs: remaining,
|
|
4751
|
+
status: `running ${name}: ${i}/${total}`
|
|
4752
|
+
};
|
|
4753
|
+
this.context.logger.progress("running", {
|
|
4754
|
+
prefix: name,
|
|
4755
|
+
count: i,
|
|
4756
|
+
total
|
|
4757
|
+
});
|
|
4758
|
+
await reportProgress(progress);
|
|
4759
|
+
await sleepMs(delay);
|
|
4760
|
+
}
|
|
4761
|
+
return {
|
|
4762
|
+
success: true,
|
|
4763
|
+
results: {
|
|
4764
|
+
message: `Completed ${total} iterations`,
|
|
4765
|
+
total,
|
|
4766
|
+
delay,
|
|
4767
|
+
name
|
|
4768
|
+
}
|
|
4769
|
+
};
|
|
4770
|
+
}
|
|
4771
|
+
};
|
|
4772
|
+
|
|
4773
|
+
// src/tasks/coreTasks/TaskShellCommand.ts
|
|
4774
|
+
var import_node_child_process = require("child_process");
|
|
4775
|
+
function runShellCommand(command, cwd) {
|
|
4776
|
+
return new Promise((resolve2, reject) => {
|
|
4777
|
+
const child = (0, import_node_child_process.spawn)(command, {
|
|
4778
|
+
shell: true,
|
|
4779
|
+
cwd: cwd || process.cwd(),
|
|
4780
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4781
|
+
});
|
|
4782
|
+
let output = "";
|
|
4783
|
+
let stderr = "";
|
|
4784
|
+
child.stdout.on("data", (chunk) => {
|
|
4785
|
+
output += String(chunk);
|
|
4786
|
+
});
|
|
4787
|
+
child.stderr.on("data", (chunk) => {
|
|
4788
|
+
stderr += String(chunk);
|
|
4789
|
+
});
|
|
4790
|
+
child.on("error", (error) => {
|
|
4791
|
+
reject(error);
|
|
4792
|
+
});
|
|
4793
|
+
child.on("close", (exitCode, signal) => {
|
|
4794
|
+
resolve2({
|
|
4795
|
+
exitCode,
|
|
4796
|
+
output: output.trim(),
|
|
4797
|
+
stderr: stderr.trim(),
|
|
4798
|
+
signal
|
|
4799
|
+
});
|
|
4800
|
+
});
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4803
|
+
var TaskShellCommand = class extends TaskMaster {
|
|
4804
|
+
async run() {
|
|
4805
|
+
const params = this.task?.params;
|
|
4806
|
+
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
4807
|
+
const cwdRaw = typeof params === "string" ? void 0 : params?.cwd;
|
|
4808
|
+
const command = typeof commandRaw === "string" ? commandRaw.trim() : "";
|
|
4809
|
+
const cwd = typeof cwdRaw === "string" && cwdRaw.trim() ? cwdRaw.trim() : void 0;
|
|
4810
|
+
if (!command) {
|
|
4811
|
+
return {
|
|
4812
|
+
success: false,
|
|
4813
|
+
results: {
|
|
4814
|
+
error: 'Validation failed: param "command" must be a non-empty string',
|
|
4815
|
+
received: this.task?.params ?? null
|
|
4816
|
+
}
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
try {
|
|
4820
|
+
const result = await runShellCommand(command, cwd);
|
|
4821
|
+
const success = result.exitCode === 0;
|
|
4822
|
+
this.context.logger.info?.(
|
|
4823
|
+
`[TaskShellCommand] command="${command}" exitCode=${String(result.exitCode)} (${this.task.id})`
|
|
4824
|
+
);
|
|
4825
|
+
return {
|
|
4826
|
+
success,
|
|
4827
|
+
results: {
|
|
4828
|
+
command,
|
|
4829
|
+
cwd: cwd ?? process.cwd(),
|
|
4830
|
+
output: result.output,
|
|
4831
|
+
stderr: result.stderr,
|
|
4832
|
+
exitCode: result.exitCode,
|
|
4833
|
+
signal: result.signal
|
|
4834
|
+
}
|
|
4835
|
+
};
|
|
4836
|
+
} catch (error) {
|
|
4837
|
+
return {
|
|
4838
|
+
success: false,
|
|
4839
|
+
results: {
|
|
4840
|
+
command,
|
|
4841
|
+
cwd: cwd ?? process.cwd(),
|
|
4842
|
+
output: "",
|
|
4843
|
+
stderr: "",
|
|
4844
|
+
exitCode: null,
|
|
4845
|
+
error: error?.message ?? String(error)
|
|
4846
|
+
}
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
};
|
|
4851
|
+
|
|
4852
|
+
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
4853
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
4854
|
+
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
4855
|
+
function toGb(valueBytes) {
|
|
4856
|
+
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4857
|
+
}
|
|
4858
|
+
function toMb(valueBytes) {
|
|
4859
|
+
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4860
|
+
}
|
|
4861
|
+
async function getDiskStats() {
|
|
4862
|
+
const stats = await import_promises2.default.statfs("/");
|
|
4863
|
+
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4864
|
+
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4865
|
+
const used = total - free;
|
|
4866
|
+
return {
|
|
4867
|
+
total: toGb(total),
|
|
4868
|
+
used: toGb(used),
|
|
4869
|
+
free: toGb(free)
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
var TaskSystemInfo = class extends TaskMaster {
|
|
4873
|
+
async run() {
|
|
4874
|
+
try {
|
|
4875
|
+
const totalMemory = import_node_os2.default.totalmem();
|
|
4876
|
+
const freeMemory = import_node_os2.default.freemem();
|
|
4877
|
+
const usedMemory = totalMemory - freeMemory;
|
|
4878
|
+
const cpus = import_node_os2.default.cpus();
|
|
4879
|
+
const cpuUtilization = cpus.map((cpu) => {
|
|
4880
|
+
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4881
|
+
const usage = (total - cpu.times.idle) / total * 100;
|
|
4882
|
+
return Number(usage.toFixed(2));
|
|
4883
|
+
});
|
|
4884
|
+
const processMemory = process.memoryUsage();
|
|
4885
|
+
const disk = await getDiskStats();
|
|
4886
|
+
const results = {
|
|
4887
|
+
memory: {
|
|
4888
|
+
total: toGb(totalMemory),
|
|
4889
|
+
used: toGb(usedMemory),
|
|
4890
|
+
free: toGb(freeMemory)
|
|
4891
|
+
},
|
|
4892
|
+
processMemory: {
|
|
4893
|
+
rss: toMb(processMemory.rss),
|
|
4894
|
+
heapTotal: toMb(processMemory.heapTotal),
|
|
4895
|
+
heapUsed: toMb(processMemory.heapUsed),
|
|
4896
|
+
external: toMb(processMemory.external)
|
|
4897
|
+
},
|
|
4898
|
+
disk,
|
|
4899
|
+
cpu: {
|
|
4900
|
+
cores: cpuUtilization.length,
|
|
4901
|
+
utilization: cpuUtilization
|
|
4902
|
+
},
|
|
4903
|
+
runtime: {
|
|
4904
|
+
platform: import_node_os2.default.platform(),
|
|
4905
|
+
arch: import_node_os2.default.arch(),
|
|
4906
|
+
uptimeSec: import_node_os2.default.uptime(),
|
|
4907
|
+
hostname: import_node_os2.default.hostname()
|
|
4908
|
+
}
|
|
4909
|
+
};
|
|
4910
|
+
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
4911
|
+
return { success: true, results };
|
|
4912
|
+
} catch (error) {
|
|
4913
|
+
return {
|
|
4914
|
+
success: false,
|
|
4915
|
+
results: {
|
|
4916
|
+
error: "Can't collect system stats",
|
|
4917
|
+
message: error?.message ?? String(error)
|
|
4918
|
+
}
|
|
4919
|
+
};
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
};
|
|
4923
|
+
|
|
4924
|
+
// src/tasks/coreTasks/TaskSumAB.ts
|
|
4925
|
+
var TaskSumAB = class extends TaskMaster {
|
|
4926
|
+
async run() {
|
|
4927
|
+
const a = this.task?.params?.a;
|
|
4928
|
+
const b = this.task?.params?.b;
|
|
4929
|
+
if (typeof a !== "number" || Number.isNaN(a)) {
|
|
4930
|
+
return {
|
|
4931
|
+
success: false,
|
|
4932
|
+
results: {
|
|
4933
|
+
error: 'Validation failed: param "a" must be a valid number',
|
|
4934
|
+
received: { a, b }
|
|
4935
|
+
}
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
if (typeof b !== "number" || Number.isNaN(b)) {
|
|
4939
|
+
return {
|
|
4940
|
+
success: false,
|
|
4941
|
+
results: {
|
|
4942
|
+
error: 'Validation failed: param "b" must be a valid number',
|
|
4943
|
+
received: { a, b }
|
|
4944
|
+
}
|
|
4945
|
+
};
|
|
4946
|
+
}
|
|
4947
|
+
const sum = a + b;
|
|
4948
|
+
this.context.logger.info?.(`[TaskSumAB] ${a} + ${b} = ${sum} (${this.task.id})`);
|
|
4949
|
+
return {
|
|
4950
|
+
success: true,
|
|
4951
|
+
results: { a, b, sum }
|
|
4952
|
+
};
|
|
4953
|
+
}
|
|
4954
|
+
};
|
|
4955
|
+
|
|
4956
|
+
// src/tasks/coreTasks/TaskStopRunner.ts
|
|
4957
|
+
var TaskStopRunner = class extends TaskMaster {
|
|
4958
|
+
async run() {
|
|
4959
|
+
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4960
|
+
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
4961
|
+
return {
|
|
4962
|
+
success: true,
|
|
4963
|
+
results: {
|
|
4964
|
+
stopRunner: true,
|
|
4965
|
+
allowanceMs,
|
|
4966
|
+
message: "Runner stop requested"
|
|
4967
|
+
}
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
};
|
|
4971
|
+
|
|
4972
|
+
// src/tasks/TasksRegistry.ts
|
|
4973
|
+
var TasksRegistry = class _TasksRegistry {
|
|
4974
|
+
map = {};
|
|
4975
|
+
constructor(initial) {
|
|
4976
|
+
if (initial) {
|
|
4977
|
+
this.addMany(initial);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
static withCoreTasks() {
|
|
4981
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
4982
|
+
}
|
|
4983
|
+
add(taskName, taskClass) {
|
|
4984
|
+
this.map[taskName] = taskClass;
|
|
4985
|
+
return this;
|
|
4986
|
+
}
|
|
4987
|
+
addMany(entries) {
|
|
4988
|
+
for (const [name, klass] of Object.entries(entries)) {
|
|
4989
|
+
this.add(name, klass);
|
|
4990
|
+
}
|
|
4991
|
+
return this;
|
|
4992
|
+
}
|
|
4993
|
+
get(taskName) {
|
|
4994
|
+
return this.map[taskName];
|
|
4995
|
+
}
|
|
4996
|
+
listSupportedTasks() {
|
|
4997
|
+
return Object.keys(this.map).sort();
|
|
4998
|
+
}
|
|
4999
|
+
toObject() {
|
|
5000
|
+
return { ...this.map };
|
|
5001
|
+
}
|
|
5002
|
+
};
|
|
5003
|
+
|
|
5004
|
+
// src/tasks/taskScriptRunner.ts
|
|
5005
|
+
var import_node_child_process2 = require("child_process");
|
|
5006
|
+
function toCliArgs(args = []) {
|
|
5007
|
+
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
5008
|
+
}
|
|
5009
|
+
function formatChildLogPrefix(task) {
|
|
5010
|
+
return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
5011
|
+
}
|
|
5012
|
+
async function runNodeTaskScript(context, options) {
|
|
5013
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
5014
|
+
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
5015
|
+
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
5016
|
+
const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
|
|
5017
|
+
const child = (0, import_node_child_process2.spawn)(
|
|
5018
|
+
process.execPath,
|
|
5019
|
+
nodeArgs,
|
|
5020
|
+
{
|
|
5021
|
+
cwd: options.cwd || process.cwd(),
|
|
5022
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
5023
|
+
env: {
|
|
5024
|
+
...process.env,
|
|
5025
|
+
TASK_ID: options.task.id,
|
|
5026
|
+
TASK_NAME: options.task.task,
|
|
5027
|
+
TASK_OPID: options.task.opid || ""
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
);
|
|
5031
|
+
let stdout = "";
|
|
5032
|
+
let stderr = "";
|
|
5033
|
+
let workerResult = null;
|
|
5034
|
+
let hadErrorMessage = false;
|
|
5035
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
5036
|
+
const db = context.db;
|
|
5037
|
+
const tasksTable = context.params?.get?.("table") || "tasks";
|
|
5038
|
+
let progressWriteChain = Promise.resolve();
|
|
5039
|
+
let progressCallbackChain = Promise.resolve();
|
|
5040
|
+
const updateProgress = (text) => {
|
|
5041
|
+
if (!db || !text || !text.trim()) return;
|
|
5042
|
+
progressWriteChain = progressWriteChain.then(async () => {
|
|
5043
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
5044
|
+
}).catch((error) => {
|
|
5045
|
+
context.logger.warn?.(
|
|
5046
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5047
|
+
);
|
|
5048
|
+
});
|
|
5049
|
+
if (options.onProgress) {
|
|
5050
|
+
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
5051
|
+
await options.onProgress?.(text.slice(0, 4e3));
|
|
5052
|
+
}).catch((error) => {
|
|
5053
|
+
context.logger.warn?.(
|
|
5054
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5055
|
+
);
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
5058
|
+
};
|
|
5059
|
+
const payloadToProgressText = (payload) => {
|
|
5060
|
+
if (!payload) return "";
|
|
5061
|
+
if (typeof payload === "string") return payload;
|
|
5062
|
+
if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
5063
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
5064
|
+
return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
|
|
5065
|
+
}
|
|
5066
|
+
if (typeof payload.message === "string") return payload.message;
|
|
5067
|
+
if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
|
|
5068
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : "";
|
|
5069
|
+
return `${pfx}${payload.count}/${payload.total}`;
|
|
5070
|
+
}
|
|
5071
|
+
return "";
|
|
5072
|
+
};
|
|
5073
|
+
child.stdout.on("data", (chunk) => {
|
|
5074
|
+
const text = String(chunk);
|
|
5075
|
+
stdout += text;
|
|
5076
|
+
if (text.trim()) {
|
|
5077
|
+
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
5078
|
+
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
5079
|
+
}
|
|
5080
|
+
});
|
|
5081
|
+
child.stderr.on("data", (chunk) => {
|
|
5082
|
+
const text = String(chunk);
|
|
5083
|
+
stderr += text;
|
|
5084
|
+
if (text.trim()) {
|
|
5085
|
+
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
5086
|
+
}
|
|
5087
|
+
});
|
|
5088
|
+
child.on("message", (message) => {
|
|
5089
|
+
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
5090
|
+
workerResult = message.__taskWorkerResult;
|
|
5091
|
+
return;
|
|
5092
|
+
}
|
|
5093
|
+
if (message && typeof message === "object") {
|
|
5094
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
5095
|
+
if (level === "error" || level === "fatal") {
|
|
5096
|
+
hadErrorMessage = true;
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
appendTaskIpcLog(context, options.task, message);
|
|
5100
|
+
const progressText = payloadToProgressText(message);
|
|
5101
|
+
if (progressText) {
|
|
5102
|
+
updateProgress(progressText);
|
|
5103
|
+
if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
|
|
5104
|
+
const countNum = Number(String(message.count).trim());
|
|
5105
|
+
const totalNum = Number(message.total);
|
|
5106
|
+
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
5107
|
+
context.logger.progress(message.message || "progress", {
|
|
5108
|
+
prefix: message.prefix || prefix,
|
|
5109
|
+
count: countNum,
|
|
5110
|
+
total: totalNum
|
|
5111
|
+
});
|
|
5112
|
+
} else {
|
|
5113
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5114
|
+
}
|
|
5115
|
+
} else {
|
|
5116
|
+
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
});
|
|
5120
|
+
return await new Promise((resolve2, reject) => {
|
|
5121
|
+
child.on("error", (error) => reject(error));
|
|
5122
|
+
child.on("close", (exitCode, signal) => {
|
|
5123
|
+
Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
|
|
5124
|
+
resolve2({
|
|
5125
|
+
exitCode,
|
|
5126
|
+
signal,
|
|
5127
|
+
stdout: stdout.trim(),
|
|
5128
|
+
stderr: stderr.trim(),
|
|
5129
|
+
workerResult,
|
|
5130
|
+
hadErrorMessage
|
|
5131
|
+
});
|
|
5132
|
+
});
|
|
5133
|
+
});
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
// src/tasks/index.ts
|
|
5138
|
+
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
5139
|
+
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
5140
|
+
function getDb3(context) {
|
|
5141
|
+
const db = context.db;
|
|
5142
|
+
if (!db) {
|
|
5143
|
+
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
5144
|
+
}
|
|
5145
|
+
return db;
|
|
5146
|
+
}
|
|
5147
|
+
function normalizeRegistry(registry) {
|
|
5148
|
+
if (!registry) return defaultTasksRegistry;
|
|
5149
|
+
if (registry instanceof TasksRegistry) return registry;
|
|
5150
|
+
return new TasksRegistry().addMany(registry);
|
|
5151
|
+
}
|
|
5152
|
+
function normalizeAllowedTasks(value) {
|
|
5153
|
+
if (!value) return void 0;
|
|
5154
|
+
if (Array.isArray(value)) {
|
|
5155
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5156
|
+
return out2.length ? out2 : void 0;
|
|
5157
|
+
}
|
|
5158
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5159
|
+
return out.length ? out : void 0;
|
|
5160
|
+
}
|
|
5161
|
+
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
5162
|
+
return enqueueTask(context, {
|
|
5163
|
+
queue,
|
|
5164
|
+
target,
|
|
5165
|
+
task: "stopRunner",
|
|
5166
|
+
params: { allowanceMs },
|
|
5167
|
+
priority: 1e6
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
5171
|
+
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
5172
|
+
for (const [, taskInstance] of runningTaskInstances) {
|
|
5173
|
+
if (typeof taskInstance.requestStop === "function") {
|
|
5174
|
+
try {
|
|
5175
|
+
await taskInstance.requestStop(allowanceMs);
|
|
5176
|
+
} catch (error) {
|
|
5177
|
+
context.logger.warn?.("[tasks] task requestStop failed:", error);
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
context.emitter.emit("stop", allowanceMs);
|
|
5182
|
+
}
|
|
5183
|
+
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
5184
|
+
const db = getDb3(context);
|
|
5185
|
+
const taskName = row.task;
|
|
5186
|
+
const TaskClass = registry.get(taskName);
|
|
5187
|
+
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
5188
|
+
if (!TaskClass) {
|
|
5189
|
+
const err = { message: `Unknown task "${taskName}"` };
|
|
5190
|
+
await db(historyTable).insert({
|
|
5191
|
+
...rowForHistory,
|
|
5192
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5193
|
+
success: false,
|
|
5194
|
+
params: toJsonColumn(row.params),
|
|
5195
|
+
results: toJsonColumn(err)
|
|
5196
|
+
});
|
|
5197
|
+
if (row.schedule) {
|
|
5198
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5199
|
+
started_at: null,
|
|
5200
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5201
|
+
success: false,
|
|
5202
|
+
results: toJsonColumn(err),
|
|
5203
|
+
past_due: null,
|
|
5204
|
+
paused_at: db.fn.now(),
|
|
5205
|
+
progress: LOCKED_BY_ERROR_MESSAGE
|
|
5206
|
+
});
|
|
5207
|
+
} else {
|
|
5208
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
5209
|
+
}
|
|
5210
|
+
return { stopRunnerRequested: false, stopAllowanceMs: 0 };
|
|
5211
|
+
}
|
|
5212
|
+
let success = false;
|
|
5213
|
+
let results = null;
|
|
5214
|
+
let taskInstance = null;
|
|
5215
|
+
try {
|
|
5216
|
+
taskInstance = new TaskClass(context, row);
|
|
5217
|
+
runningTaskInstances.set(row.id, taskInstance);
|
|
5218
|
+
const runResult = await taskInstance.run((progress) => updateTaskProgress(context, tasksTable, row.id, progress));
|
|
5219
|
+
success = !!runResult?.success;
|
|
5220
|
+
results = runResult?.results ?? null;
|
|
5221
|
+
} catch (error) {
|
|
5222
|
+
success = false;
|
|
5223
|
+
results = {
|
|
5224
|
+
message: error?.message ?? String(error),
|
|
5225
|
+
name: error?.name ?? "Error",
|
|
5226
|
+
stack: error?.stack ?? null
|
|
5227
|
+
};
|
|
5228
|
+
} finally {
|
|
5229
|
+
runningTaskInstances.delete(row.id);
|
|
5230
|
+
}
|
|
5231
|
+
await db(historyTable).insert({
|
|
5232
|
+
...rowForHistory,
|
|
5233
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5234
|
+
success,
|
|
5235
|
+
params: toJsonColumn(row.params),
|
|
5236
|
+
results: toJsonColumn(results)
|
|
5237
|
+
});
|
|
5238
|
+
if (!success) {
|
|
5239
|
+
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
5240
|
+
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
5241
|
+
const fallbackRecoverCommand = [
|
|
5242
|
+
"npx",
|
|
5243
|
+
"tsx",
|
|
5244
|
+
"examples/tasks/recover-task.ts",
|
|
5245
|
+
`--dbName='${dbName.replace(/'/g, `'\\''`)}'`,
|
|
5246
|
+
`--table='${tableName.replace(/'/g, `'\\''`)}'`,
|
|
5247
|
+
`--id='${String(row.id).replace(/'/g, `'\\''`)}'`
|
|
5248
|
+
].join(" ");
|
|
5249
|
+
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5250
|
+
appendTaskIpcLog(context, row, {
|
|
5251
|
+
level: "error",
|
|
5252
|
+
message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5253
|
+
details: results
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
if (row.schedule) {
|
|
5257
|
+
if (success) {
|
|
5258
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5259
|
+
started_at: null,
|
|
5260
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5261
|
+
success,
|
|
5262
|
+
results: toJsonColumn(results),
|
|
5263
|
+
progress: null,
|
|
5264
|
+
past_due: null
|
|
5265
|
+
});
|
|
5266
|
+
} else {
|
|
5267
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5268
|
+
started_at: null,
|
|
5269
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5270
|
+
success,
|
|
5271
|
+
results: toJsonColumn(results),
|
|
5272
|
+
paused_at: db.fn.now(),
|
|
5273
|
+
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5274
|
+
past_due: null
|
|
5275
|
+
});
|
|
5276
|
+
}
|
|
5277
|
+
} else {
|
|
5278
|
+
await db(tasksTable).where({ id: row.id }).delete();
|
|
5279
|
+
}
|
|
5280
|
+
const stopRunnerRequested = !!(results && typeof results === "object" && results.stopRunner === true);
|
|
5281
|
+
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5282
|
+
return { stopRunnerRequested, stopAllowanceMs };
|
|
5283
|
+
}
|
|
5284
|
+
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
5285
|
+
const db = getDb3(context);
|
|
5286
|
+
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);
|
|
5287
|
+
if (taskNames && taskNames.length > 0) {
|
|
5288
|
+
query = query.whereIn("task", taskNames);
|
|
5289
|
+
}
|
|
5290
|
+
const candidates = await query;
|
|
5291
|
+
for (const row of candidates) {
|
|
5292
|
+
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5293
|
+
continue;
|
|
5294
|
+
}
|
|
5295
|
+
const TaskClass = registry.get(row.task);
|
|
5296
|
+
if (TaskClass) {
|
|
5297
|
+
const taskInstance = new TaskClass(context, row);
|
|
5298
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5299
|
+
if (reason) {
|
|
5300
|
+
if (!row.past_due) {
|
|
5301
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5302
|
+
past_due: db.fn.now(),
|
|
5303
|
+
progress: String(reason)
|
|
5304
|
+
});
|
|
5305
|
+
}
|
|
5306
|
+
continue;
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
|
|
5310
|
+
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5311
|
+
if (claimed) return claimed;
|
|
5312
|
+
}
|
|
5313
|
+
return null;
|
|
5314
|
+
}
|
|
5315
|
+
async function runTasksLoop(context, options) {
|
|
5316
|
+
const queue = options.queue ?? "tasks";
|
|
5317
|
+
const target = options.target;
|
|
5318
|
+
const pollMs = options.pollMs ?? 1e3;
|
|
5319
|
+
const maxParallel = options.maxParallel ?? 1;
|
|
5320
|
+
const scanLimit = options.scanLimit ?? 100;
|
|
5321
|
+
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5322
|
+
const registry = normalizeRegistry(options.registry);
|
|
5323
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5324
|
+
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5325
|
+
const runningPromises = /* @__PURE__ */ new Set();
|
|
5326
|
+
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5327
|
+
let runningStopControlPromise = null;
|
|
5328
|
+
let stopRequested = false;
|
|
5329
|
+
let stopAllowanceMs = 5e3;
|
|
5330
|
+
context.__tasksRunnerStop = false;
|
|
5331
|
+
let registryReg = null;
|
|
5332
|
+
let registryInterval = null;
|
|
5333
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5334
|
+
if (hbGroup) {
|
|
5335
|
+
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5336
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5337
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5338
|
+
const defaultMeta = {
|
|
5339
|
+
component: "tasks-runner",
|
|
5340
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5341
|
+
};
|
|
5342
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
5343
|
+
queue,
|
|
5344
|
+
target,
|
|
5345
|
+
serviceGroup: hbGroup,
|
|
5346
|
+
serviceName: options.runnerServiceName,
|
|
5347
|
+
identityDir,
|
|
5348
|
+
staleMs,
|
|
5349
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5350
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5351
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
5352
|
+
});
|
|
5353
|
+
registryInterval = setInterval(() => {
|
|
5354
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5355
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
5356
|
+
});
|
|
5357
|
+
}, hbIntervalMs);
|
|
5358
|
+
}
|
|
5359
|
+
try {
|
|
5360
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
5361
|
+
if (!runningStopControlPromise) {
|
|
5362
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
5363
|
+
context,
|
|
5364
|
+
tasksTable,
|
|
5365
|
+
target,
|
|
5366
|
+
registry,
|
|
5367
|
+
10,
|
|
5368
|
+
["stopRunner", "stop"]
|
|
5369
|
+
);
|
|
5370
|
+
if (claimedStopTask) {
|
|
5371
|
+
runningStopControlPromise = executeClaimedTask(
|
|
5372
|
+
context,
|
|
5373
|
+
tasksTable,
|
|
5374
|
+
historyTable,
|
|
5375
|
+
claimedStopTask,
|
|
5376
|
+
registry,
|
|
5377
|
+
runningTaskInstances
|
|
5378
|
+
).then(async (outcome) => {
|
|
5379
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5380
|
+
stopRequested = true;
|
|
5381
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5382
|
+
context.__tasksRunnerStop = true;
|
|
5383
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5384
|
+
}
|
|
5385
|
+
}).finally(() => {
|
|
5386
|
+
runningStopControlPromise = null;
|
|
5387
|
+
});
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
while (runningPromises.size < maxParallel) {
|
|
5391
|
+
const claimed = await claimNextRunnableTask(
|
|
5392
|
+
context,
|
|
5393
|
+
tasksTable,
|
|
5394
|
+
target,
|
|
5395
|
+
registry,
|
|
5396
|
+
scanLimit,
|
|
5397
|
+
allowedTasks
|
|
5398
|
+
);
|
|
5399
|
+
if (!claimed) break;
|
|
5400
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5401
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5402
|
+
stopRequested = true;
|
|
5403
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5404
|
+
context.__tasksRunnerStop = true;
|
|
5405
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5406
|
+
}
|
|
5407
|
+
}).finally(() => {
|
|
5408
|
+
runningPromises.delete(p);
|
|
5409
|
+
});
|
|
5410
|
+
runningPromises.add(p);
|
|
5411
|
+
}
|
|
5412
|
+
await sleepMs(pollMs);
|
|
5413
|
+
}
|
|
5414
|
+
if (context.isStop() && !stopRequested) {
|
|
5415
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
5416
|
+
}
|
|
5417
|
+
if (runningPromises.size > 0) {
|
|
5418
|
+
if (stopRequested) {
|
|
5419
|
+
await Promise.race([
|
|
5420
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
5421
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
5422
|
+
context.logger.warn?.(
|
|
5423
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
5424
|
+
);
|
|
5425
|
+
})
|
|
5426
|
+
]);
|
|
5427
|
+
} else {
|
|
5428
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
5429
|
+
}
|
|
5430
|
+
}
|
|
5431
|
+
} finally {
|
|
5432
|
+
if (registryInterval) {
|
|
5433
|
+
clearInterval(registryInterval);
|
|
5434
|
+
registryInterval = null;
|
|
5435
|
+
}
|
|
5436
|
+
if (registryReg) {
|
|
5437
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
5438
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
5439
|
+
});
|
|
5440
|
+
registryReg = null;
|
|
5441
|
+
delete context.servicesRegistry;
|
|
5442
|
+
delete context.runnerHeartbeat;
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
}
|
|
5446
|
+
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5447
|
+
const db = getDb3(context);
|
|
5448
|
+
const queue = options.queue ?? "tasks";
|
|
5449
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5450
|
+
const pollMs = options.pollMs ?? 500;
|
|
5451
|
+
const { tasksTable, historyTable } = queueToTableNames(queue);
|
|
5452
|
+
const deadline = Date.now() + timeoutMs;
|
|
5453
|
+
while (Date.now() <= deadline) {
|
|
5454
|
+
const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5455
|
+
if (done) return done;
|
|
5456
|
+
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5457
|
+
if (!pending) {
|
|
5458
|
+
const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
5459
|
+
return maybeDone ?? null;
|
|
5460
|
+
}
|
|
5461
|
+
await sleepMs(pollMs);
|
|
5462
|
+
}
|
|
5463
|
+
return null;
|
|
5464
|
+
}
|
|
5465
|
+
var TasksManager = class _TasksManager {
|
|
5466
|
+
context;
|
|
5467
|
+
queue;
|
|
5468
|
+
target;
|
|
5469
|
+
recreateTaskTables;
|
|
5470
|
+
pollMs;
|
|
5471
|
+
maxParallel;
|
|
5472
|
+
scanLimit;
|
|
5473
|
+
allowedTasks;
|
|
5474
|
+
registry;
|
|
5475
|
+
runnerServiceGroup;
|
|
5476
|
+
runnerServiceName;
|
|
5477
|
+
runnerIdentityDir;
|
|
5478
|
+
runnerHeartbeatIntervalMs;
|
|
5479
|
+
runnerHeartbeatStaleMs;
|
|
5480
|
+
runnerGroupMaxInstances;
|
|
5481
|
+
runnerEnforceMaxInstances;
|
|
5482
|
+
runnerMetadata;
|
|
5483
|
+
constructor(context, options = {}) {
|
|
5484
|
+
this.context = context;
|
|
5485
|
+
this.queue = options.queue ?? "tasks";
|
|
5486
|
+
this.target = options.target ?? "localRunner";
|
|
5487
|
+
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5488
|
+
this.pollMs = options.pollMs ?? 1e3;
|
|
5489
|
+
this.maxParallel = options.maxParallel ?? 1;
|
|
5490
|
+
this.scanLimit = options.scanLimit ?? 100;
|
|
5491
|
+
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5492
|
+
this.registry = normalizeRegistry(options.registry);
|
|
5493
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5494
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
5495
|
+
this.runnerIdentityDir = options.runnerIdentityDir;
|
|
5496
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5497
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5498
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5499
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5500
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
5501
|
+
}
|
|
5502
|
+
static init(context, options = {}) {
|
|
5503
|
+
const defs = {
|
|
5504
|
+
table: "string default tasks",
|
|
5505
|
+
target: "string default localRunner",
|
|
5506
|
+
recreateTaskTables: "boolean default false",
|
|
5507
|
+
pollMs: "number default 1000",
|
|
5508
|
+
maxParallel: "number default 1",
|
|
5509
|
+
scanLimit: "number default 100",
|
|
5510
|
+
allowedTasks: "string",
|
|
5511
|
+
runnerServiceGroup: "string",
|
|
5512
|
+
runnerServiceName: "string",
|
|
5513
|
+
runnerIdentityDir: "string default ./data/runner-identities",
|
|
5514
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5515
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
5516
|
+
runnerGroupMaxInstances: "number",
|
|
5517
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
5518
|
+
};
|
|
5519
|
+
const discovered = context.params.getAllForModule(defs);
|
|
5520
|
+
const resolved = {
|
|
5521
|
+
queue: discovered.table,
|
|
5522
|
+
target: discovered.target,
|
|
5523
|
+
recreateTaskTables: discovered.recreateTaskTables,
|
|
5524
|
+
pollMs: discovered.pollMs,
|
|
5525
|
+
maxParallel: discovered.maxParallel,
|
|
5526
|
+
scanLimit: discovered.scanLimit,
|
|
5527
|
+
allowedTasks: discovered.allowedTasks,
|
|
5528
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5529
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
5530
|
+
runnerIdentityDir: discovered.runnerIdentityDir,
|
|
5531
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5532
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5533
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
5534
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
5535
|
+
...options
|
|
5536
|
+
};
|
|
5537
|
+
return new _TasksManager(context, resolved);
|
|
5538
|
+
}
|
|
5539
|
+
async ensureTaskTables(options = {}) {
|
|
5540
|
+
await ensureTaskTables(this.context, {
|
|
5541
|
+
queue: this.queue,
|
|
5542
|
+
recreate: options.recreate ?? this.recreateTaskTables
|
|
5543
|
+
});
|
|
5544
|
+
}
|
|
5545
|
+
async runTasksLoop(options = {}) {
|
|
5546
|
+
await runTasksLoop(this.context, {
|
|
5547
|
+
queue: options.queue ?? this.queue,
|
|
5548
|
+
target: options.target ?? this.target,
|
|
5549
|
+
pollMs: options.pollMs ?? this.pollMs,
|
|
5550
|
+
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5551
|
+
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5552
|
+
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5553
|
+
registry: options.registry ?? this.registry,
|
|
5554
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5555
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5556
|
+
runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
|
|
5557
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5558
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5559
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
5560
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
5561
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
5562
|
+
});
|
|
5563
|
+
}
|
|
5564
|
+
};
|
|
3986
5565
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3987
5566
|
0 && (module.exports = {
|
|
3988
5567
|
Args,
|
|
@@ -4006,8 +5585,18 @@ function setupContext(opts = {}) {
|
|
|
4006
5585
|
ScreenFooter,
|
|
4007
5586
|
ScreenRow,
|
|
4008
5587
|
ScreenTitle,
|
|
5588
|
+
TaskMaster,
|
|
5589
|
+
TaskPing,
|
|
5590
|
+
TaskSampleProcess,
|
|
5591
|
+
TaskShellCommand,
|
|
5592
|
+
TaskStopRunner,
|
|
5593
|
+
TaskSumAB,
|
|
5594
|
+
TaskSystemInfo,
|
|
5595
|
+
TasksManager,
|
|
5596
|
+
TasksRegistry,
|
|
4009
5597
|
Text,
|
|
4010
5598
|
TextBlock,
|
|
5599
|
+
appendTaskIpcLog,
|
|
4011
5600
|
buildBreadcrumb,
|
|
4012
5601
|
buildDetailBreadcrumb,
|
|
4013
5602
|
buildFooter,
|
|
@@ -4015,15 +5604,28 @@ function setupContext(opts = {}) {
|
|
|
4015
5604
|
dbFindAndConnect,
|
|
4016
5605
|
dbInit,
|
|
4017
5606
|
defaultFileSynopsisFunction,
|
|
5607
|
+
defaultTasksRegistry,
|
|
4018
5608
|
defaultVersionSynopsisFunction,
|
|
5609
|
+
enqueueStopTask,
|
|
5610
|
+
enqueueTask,
|
|
5611
|
+
ensureTaskTables,
|
|
4019
5612
|
getArgsInstance,
|
|
4020
5613
|
h,
|
|
4021
5614
|
joiEdateType,
|
|
4022
5615
|
joiStringArrayType,
|
|
5616
|
+
listAliveRunnerHeartbeats,
|
|
5617
|
+
listServicesRegistry,
|
|
4023
5618
|
listSources,
|
|
4024
5619
|
listTables,
|
|
4025
5620
|
load,
|
|
4026
5621
|
organizeFooterMessages,
|
|
5622
|
+
queueToTableNames,
|
|
5623
|
+
registerInServicesRegistry,
|
|
5624
|
+
registerRunnerHeartbeat,
|
|
5625
|
+
runNodeTaskScript,
|
|
5626
|
+
runTasksLoop,
|
|
5627
|
+
runnerHeartbeatsTable,
|
|
5628
|
+
servicesRegistryTable,
|
|
4027
5629
|
setupContext,
|
|
4028
5630
|
showListScreen,
|
|
4029
5631
|
showMenuScreen,
|
|
@@ -4031,11 +5633,18 @@ function setupContext(opts = {}) {
|
|
|
4031
5633
|
showMultiColumnListWithPreviewScreen,
|
|
4032
5634
|
showScreen,
|
|
4033
5635
|
showWordGridScreen,
|
|
5636
|
+
touchRunnerHeartbeat,
|
|
5637
|
+
touchServicesRegistry,
|
|
5638
|
+
unregisterRunnerHeartbeat,
|
|
5639
|
+
unregisterServicesRegistry,
|
|
5640
|
+
updateServicesRegistryMetadata,
|
|
5641
|
+
updateTaskProgress,
|
|
4034
5642
|
useCallback,
|
|
4035
5643
|
useEffect,
|
|
4036
5644
|
useInput,
|
|
4037
5645
|
useMemo,
|
|
4038
5646
|
useRef,
|
|
4039
|
-
useState
|
|
5647
|
+
useState,
|
|
5648
|
+
waitForTaskResult
|
|
4040
5649
|
});
|
|
4041
5650
|
//# sourceMappingURL=index.cjs.map
|