@mtakla/cronops 0.1.1-rc6 → 0.1.1
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 +69 -56
- package/config/jobs/example-job.yaml +1 -1
- package/dist/api/openapi.d.ts +217 -41
- package/dist/api/openapi.js +162 -55
- package/dist/api/webapi.js +28 -2
- package/dist/handlers/AbstractHandler.js +3 -3
- package/dist/handlers/ExecHandler.js +0 -2
- package/dist/models/JobRunnerSetup.js +8 -8
- package/dist/server.js +12 -5
- package/dist/tasks/AbstractTask.d.ts +11 -2
- package/dist/tasks/AbstractTask.js +35 -8
- package/dist/tasks/JobRunner.d.ts +2 -3
- package/dist/tasks/JobRunner.js +2 -5
- package/dist/tasks/JobScheduler.d.ts +13 -2
- package/dist/tasks/JobScheduler.js +42 -5
- package/dist/types/Config.types.d.ts +8 -1
- package/dist/types/Config.types.js +1 -1
- package/dist/types/Task.types.d.ts +8 -1
- package/package.json +8 -9
package/dist/api/openapi.js
CHANGED
|
@@ -9,6 +9,84 @@ export const openapi = {
|
|
|
9
9
|
bearerFormat: "hex-256",
|
|
10
10
|
},
|
|
11
11
|
},
|
|
12
|
+
parameters: {
|
|
13
|
+
JobId: {
|
|
14
|
+
name: "jobId",
|
|
15
|
+
in: "path",
|
|
16
|
+
required: true,
|
|
17
|
+
schema: { type: "string" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
schemas: {
|
|
21
|
+
Job: {
|
|
22
|
+
type: "object",
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
properties: {
|
|
25
|
+
id: { type: "string", example: "example-job" },
|
|
26
|
+
cron: { type: "string", minLength: 1, example: "*/5 * * * *" },
|
|
27
|
+
action: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["exec", "call", "copy", "move", "delete", "archive"],
|
|
30
|
+
example: "copy",
|
|
31
|
+
},
|
|
32
|
+
command: { type: "string" },
|
|
33
|
+
shell: {
|
|
34
|
+
anyOf: [{ type: "boolean" }, { type: "string", minLength: 1 }],
|
|
35
|
+
},
|
|
36
|
+
args: {
|
|
37
|
+
type: "array",
|
|
38
|
+
items: { type: "string", minLength: 1 },
|
|
39
|
+
minItems: 1,
|
|
40
|
+
},
|
|
41
|
+
env: {
|
|
42
|
+
type: "object",
|
|
43
|
+
propertyNames: {
|
|
44
|
+
type: "string",
|
|
45
|
+
pattern: "^[A-Z_][A-Z0-9_]*$",
|
|
46
|
+
},
|
|
47
|
+
additionalProperties: { type: "string" },
|
|
48
|
+
},
|
|
49
|
+
source: {
|
|
50
|
+
type: "object",
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
properties: {
|
|
53
|
+
dir: { type: "string", minLength: 1 },
|
|
54
|
+
includes: {
|
|
55
|
+
type: "array",
|
|
56
|
+
items: { type: "string", minLength: 1 },
|
|
57
|
+
minItems: 1,
|
|
58
|
+
},
|
|
59
|
+
excludes: {
|
|
60
|
+
type: "array",
|
|
61
|
+
items: { type: "string", minLength: 1 },
|
|
62
|
+
minItems: 1,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
target: {
|
|
67
|
+
type: "object",
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
properties: {
|
|
70
|
+
dir: { type: "string", minLength: 1 },
|
|
71
|
+
archive_name: { type: "string", minLength: 1 },
|
|
72
|
+
permissions: {
|
|
73
|
+
type: "object",
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
properties: {
|
|
76
|
+
owner: { type: "string", minLength: 1 },
|
|
77
|
+
file_mode: { type: "string", minLength: 1 },
|
|
78
|
+
dir_mode: { type: "string", minLength: 1 },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
retention: { type: "string", minLength: 1 },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
dry_run: { type: "boolean" },
|
|
85
|
+
verbose: { type: "boolean" },
|
|
86
|
+
enabled: { type: "boolean" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
12
90
|
},
|
|
13
91
|
tags: [
|
|
14
92
|
{
|
|
@@ -17,19 +95,11 @@ export const openapi = {
|
|
|
17
95
|
},
|
|
18
96
|
{
|
|
19
97
|
name: "jobs",
|
|
20
|
-
description: "
|
|
98
|
+
description: "job related api",
|
|
21
99
|
},
|
|
22
100
|
{
|
|
23
|
-
name: "
|
|
24
|
-
description: "
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "user",
|
|
28
|
-
description: "Operations about user",
|
|
29
|
-
externalDocs: {
|
|
30
|
-
description: "Find out more about our store",
|
|
31
|
-
url: "http://swagger.io",
|
|
32
|
-
},
|
|
101
|
+
name: "schedule",
|
|
102
|
+
description: "scheduling api",
|
|
33
103
|
},
|
|
34
104
|
],
|
|
35
105
|
paths: {
|
|
@@ -84,18 +154,69 @@ export const openapi = {
|
|
|
84
154
|
},
|
|
85
155
|
},
|
|
86
156
|
},
|
|
87
|
-
"/api/jobs
|
|
157
|
+
"/api/jobs": {
|
|
158
|
+
get: {
|
|
159
|
+
summary: "Get jobs",
|
|
160
|
+
tags: ["jobs"],
|
|
161
|
+
responses: {
|
|
162
|
+
"200": {
|
|
163
|
+
description: "Array of scheduled jobs",
|
|
164
|
+
content: {
|
|
165
|
+
"application/json": {
|
|
166
|
+
schema: {
|
|
167
|
+
type: "array",
|
|
168
|
+
items: { $ref: "#/components/schemas/Job" },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
"/api/jobs/{jobId}": {
|
|
177
|
+
get: {
|
|
178
|
+
summary: "Get a job",
|
|
179
|
+
tags: ["jobs"],
|
|
180
|
+
parameters: [{ $ref: "#/components/parameters/JobId" }],
|
|
181
|
+
responses: {
|
|
182
|
+
"200": {
|
|
183
|
+
description: "Array of scheduled jobs",
|
|
184
|
+
content: {
|
|
185
|
+
"application/json": {
|
|
186
|
+
schema: { $ref: "#/components/schemas/Job" },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
"404": {
|
|
191
|
+
description: "Job not found",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
"/api/status": {
|
|
197
|
+
get: {
|
|
198
|
+
summary: "Get status",
|
|
199
|
+
tags: ["schedule"],
|
|
200
|
+
responses: {
|
|
201
|
+
"200": {
|
|
202
|
+
description: "Array of scheduled jobs",
|
|
203
|
+
content: {
|
|
204
|
+
"application/json": {
|
|
205
|
+
schema: {
|
|
206
|
+
type: "array",
|
|
207
|
+
items: { $ref: "#/components/schemas/Job" },
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
"/api/trigger/{jobId}": {
|
|
88
216
|
post: {
|
|
89
217
|
summary: "Trigger a job",
|
|
90
|
-
tags: ["
|
|
91
|
-
parameters: [
|
|
92
|
-
{
|
|
93
|
-
name: "jobId",
|
|
94
|
-
in: "path",
|
|
95
|
-
required: true,
|
|
96
|
-
schema: { type: "string" },
|
|
97
|
-
},
|
|
98
|
-
],
|
|
218
|
+
tags: ["schedule"],
|
|
219
|
+
parameters: [{ $ref: "#/components/parameters/JobId" }],
|
|
99
220
|
responses: {
|
|
100
221
|
"200": {
|
|
101
222
|
description: "Triggered",
|
|
@@ -118,18 +239,10 @@ export const openapi = {
|
|
|
118
239
|
},
|
|
119
240
|
},
|
|
120
241
|
},
|
|
121
|
-
"/api/
|
|
242
|
+
"/api/pause": {
|
|
122
243
|
post: {
|
|
123
|
-
summary: "Pause
|
|
124
|
-
tags: ["
|
|
125
|
-
parameters: [
|
|
126
|
-
{
|
|
127
|
-
name: "jobId",
|
|
128
|
-
in: "path",
|
|
129
|
-
required: true,
|
|
130
|
-
schema: { type: "string" },
|
|
131
|
-
},
|
|
132
|
-
],
|
|
244
|
+
summary: "Pause jobs",
|
|
245
|
+
tags: ["schedule"],
|
|
133
246
|
responses: {
|
|
134
247
|
"200": {
|
|
135
248
|
description: "Paused",
|
|
@@ -139,7 +252,7 @@ export const openapi = {
|
|
|
139
252
|
type: "object",
|
|
140
253
|
properties: {
|
|
141
254
|
paused: { type: "boolean", example: true },
|
|
142
|
-
|
|
255
|
+
jobs: { type: "number", example: 4 },
|
|
143
256
|
},
|
|
144
257
|
},
|
|
145
258
|
},
|
|
@@ -148,27 +261,20 @@ export const openapi = {
|
|
|
148
261
|
},
|
|
149
262
|
},
|
|
150
263
|
},
|
|
151
|
-
"/api/
|
|
264
|
+
"/api/pause/job/{jobId}": {
|
|
152
265
|
post: {
|
|
153
|
-
summary: "
|
|
154
|
-
tags: ["
|
|
155
|
-
parameters: [
|
|
156
|
-
{
|
|
157
|
-
name: "jobId",
|
|
158
|
-
in: "path",
|
|
159
|
-
required: true,
|
|
160
|
-
schema: { type: "string" },
|
|
161
|
-
},
|
|
162
|
-
],
|
|
266
|
+
summary: "Pause a job",
|
|
267
|
+
tags: ["schedule"],
|
|
268
|
+
parameters: [{ $ref: "#/components/parameters/JobId" }],
|
|
163
269
|
responses: {
|
|
164
270
|
"200": {
|
|
165
|
-
description: "
|
|
271
|
+
description: "Paused",
|
|
166
272
|
content: {
|
|
167
273
|
"application/json": {
|
|
168
274
|
schema: {
|
|
169
275
|
type: "object",
|
|
170
276
|
properties: {
|
|
171
|
-
|
|
277
|
+
paused: { type: "boolean", example: true },
|
|
172
278
|
jobId: { type: "string", example: "job-123" },
|
|
173
279
|
},
|
|
174
280
|
},
|
|
@@ -178,20 +284,21 @@ export const openapi = {
|
|
|
178
284
|
},
|
|
179
285
|
},
|
|
180
286
|
},
|
|
181
|
-
"/api/
|
|
287
|
+
"/api/resume/job/{jobId}": {
|
|
182
288
|
post: {
|
|
183
|
-
summary: "
|
|
184
|
-
tags: ["
|
|
289
|
+
summary: "Resume a paused job",
|
|
290
|
+
tags: ["schedule"],
|
|
291
|
+
parameters: [{ $ref: "#/components/parameters/JobId" }],
|
|
185
292
|
responses: {
|
|
186
293
|
"200": {
|
|
187
|
-
description: "
|
|
294
|
+
description: "Resumed",
|
|
188
295
|
content: {
|
|
189
296
|
"application/json": {
|
|
190
297
|
schema: {
|
|
191
298
|
type: "object",
|
|
192
299
|
properties: {
|
|
193
|
-
|
|
194
|
-
|
|
300
|
+
resumed: { type: "boolean", example: true },
|
|
301
|
+
jobId: { type: "string", example: "job-123" },
|
|
195
302
|
},
|
|
196
303
|
},
|
|
197
304
|
},
|
|
@@ -200,10 +307,10 @@ export const openapi = {
|
|
|
200
307
|
},
|
|
201
308
|
},
|
|
202
309
|
},
|
|
203
|
-
"/api/
|
|
310
|
+
"/api/resume": {
|
|
204
311
|
post: {
|
|
205
|
-
summary: "Resume
|
|
206
|
-
tags: ["
|
|
312
|
+
summary: "Resume paused jobs",
|
|
313
|
+
tags: ["schedule"],
|
|
207
314
|
responses: {
|
|
208
315
|
"200": {
|
|
209
316
|
description: "Resumed",
|
package/dist/api/webapi.js
CHANGED
|
@@ -5,7 +5,7 @@ import { openapi } from "./openapi.js";
|
|
|
5
5
|
const app = Fastify();
|
|
6
6
|
const port = Number(process.env[ENV.PORT] ?? 8118);
|
|
7
7
|
const host = process.env[ENV.HOST] ?? "127.0.0.1";
|
|
8
|
-
const baseUrl = process.env[ENV.BASE_URL] ??
|
|
8
|
+
const baseUrl = process.env[ENV.BASE_URL] ?? `http://127.0.0.1:${port}`;
|
|
9
9
|
const apiKey = process.env[ENV.API_KEY];
|
|
10
10
|
app.addHook("preHandler", async (request, reply) => {
|
|
11
11
|
if (request.method === "OPTIONS" || !request.url.startsWith("/api"))
|
|
@@ -38,7 +38,33 @@ app.get("/health", async (request, reply) => {
|
|
|
38
38
|
const jobs = jobScheduler.getScheduledJobs();
|
|
39
39
|
reply.code(200).send({ status: "ok", active_jobs: jobs.length });
|
|
40
40
|
});
|
|
41
|
-
app.
|
|
41
|
+
app.get("/api/jobs", async (request) => {
|
|
42
|
+
const jobScheduler = request.server.scheduler;
|
|
43
|
+
return jobScheduler.getScheduledJobs();
|
|
44
|
+
});
|
|
45
|
+
app.get("/api/jobs/:jobId", async (request, reply) => {
|
|
46
|
+
const { jobId } = request.params;
|
|
47
|
+
const jobScheduler = request.server.scheduler;
|
|
48
|
+
if (!jobScheduler.isJobScheduled(jobId)) {
|
|
49
|
+
return reply.code(404).send();
|
|
50
|
+
}
|
|
51
|
+
return jobScheduler.getJob(jobId);
|
|
52
|
+
});
|
|
53
|
+
app.get("/api/status", async (request) => {
|
|
54
|
+
const jobScheduler = request.server.scheduler;
|
|
55
|
+
return jobScheduler.getScheduledJobsInfo();
|
|
56
|
+
});
|
|
57
|
+
app.post("/api/pause", async (request) => {
|
|
58
|
+
const jobScheduler = request.server.scheduler;
|
|
59
|
+
await jobScheduler.pauseScheduling();
|
|
60
|
+
return { paused: true };
|
|
61
|
+
});
|
|
62
|
+
app.post("/api/resume", async (request) => {
|
|
63
|
+
const jobScheduler = request.server.scheduler;
|
|
64
|
+
await jobScheduler.resumeScheduling();
|
|
65
|
+
return { resumed: true };
|
|
66
|
+
});
|
|
67
|
+
app.post("/api/trigger/:jobId", async (request, reply) => {
|
|
42
68
|
const { jobId } = request.params;
|
|
43
69
|
const jobScheduler = request.server.scheduler;
|
|
44
70
|
if (!jobScheduler.isJobScheduled(jobId)) {
|
|
@@ -141,12 +141,12 @@ export class AbstractHandler {
|
|
|
141
141
|
await Promise.all(targetScanPromises);
|
|
142
142
|
fileHistory.cleanup();
|
|
143
143
|
}
|
|
144
|
-
async setTargetFilePermissions(
|
|
144
|
+
async setTargetFilePermissions(destFile, perms, isDir = false) {
|
|
145
145
|
const mode = isDir ? perms.dirMode : perms.fileMode;
|
|
146
146
|
if (perms.uid >= 0 && perms.gid >= 0)
|
|
147
|
-
await fsx.chown(
|
|
147
|
+
await fsx.chown(destFile, perms.uid, perms.gid);
|
|
148
148
|
if (mode >= 0)
|
|
149
|
-
await fsx.chmod(
|
|
149
|
+
await fsx.chmod(destFile, mode);
|
|
150
150
|
}
|
|
151
151
|
getFolderPermissionPromises(ctx, dirPath, folderPromises) {
|
|
152
152
|
if (dirPath.length < ctx.targetDir.length || folderPromises.has(dirPath))
|
|
@@ -17,7 +17,6 @@ export class ExecHandler extends AbstractHandler {
|
|
|
17
17
|
await super.cleanup(ctx, fileHistory);
|
|
18
18
|
}
|
|
19
19
|
async exec(ctx, entry) {
|
|
20
|
-
const { targetDir } = ctx;
|
|
21
20
|
const verbose = ctx.job.verbose === true;
|
|
22
21
|
await new Promise((resolve, reject) => {
|
|
23
22
|
const { vars, env } = this.createVars(ctx, entry);
|
|
@@ -42,7 +41,6 @@ export class ExecHandler extends AbstractHandler {
|
|
|
42
41
|
stdio: ["ignore", verbose ? logFd : "ignore", verbose ? logFd : "ignore"],
|
|
43
42
|
shell: ctx.job.shell ?? this.setup.shell,
|
|
44
43
|
env: { ...process.env, ...env },
|
|
45
|
-
cwd: targetDir,
|
|
46
44
|
});
|
|
47
45
|
pid = child.pid;
|
|
48
46
|
ctx.writeLog(`◉ Subprocess started (pid:${pid}) ➜ ${cmd} [${args}]`);
|
|
@@ -27,14 +27,14 @@ export class JobRunnerSetup {
|
|
|
27
27
|
targetRootDirs;
|
|
28
28
|
handlerMap = new Map();
|
|
29
29
|
constructor(options = {}) {
|
|
30
|
-
this.sourceRoot = resolve(options.sourceRoot ?? process.env[ENV.SOURCE_ROOT] ?? "./");
|
|
31
|
-
this.targetRoot = resolve(options.targetRoot ?? process.env[ENV.TARGET_ROOT] ?? "./");
|
|
32
|
-
this.source2Root = resolve(options.source2Root ?? process.env[ENV.SOURCE_2_ROOT] ?? "./");
|
|
33
|
-
this.target2Root = resolve(options.target2Root ?? process.env[ENV.TARGET_2_ROOT] ?? "./");
|
|
34
|
-
this.source3Root = resolve(options.source3Root ?? process.env[ENV.SOURCE_3_ROOT] ?? "./");
|
|
35
|
-
this.target3Root = resolve(options.target3Root ?? process.env[ENV.TARGET_3_ROOT] ?? "./");
|
|
36
|
-
this.configDir = resolve(options.configDir ?? process.env[ENV.CONFIG_DIR] ??
|
|
37
|
-
this.logDir = resolve(options.logDir ?? process.env[ENV.LOG_DIR] ??
|
|
30
|
+
this.sourceRoot = resolve(options.sourceRoot ?? process.env[ENV.SOURCE_ROOT] ?? "./data");
|
|
31
|
+
this.targetRoot = resolve(options.targetRoot ?? process.env[ENV.TARGET_ROOT] ?? "./data");
|
|
32
|
+
this.source2Root = resolve(options.source2Root ?? process.env[ENV.SOURCE_2_ROOT] ?? "./data");
|
|
33
|
+
this.target2Root = resolve(options.target2Root ?? process.env[ENV.TARGET_2_ROOT] ?? "./data");
|
|
34
|
+
this.source3Root = resolve(options.source3Root ?? process.env[ENV.SOURCE_3_ROOT] ?? "./data");
|
|
35
|
+
this.target3Root = resolve(options.target3Root ?? process.env[ENV.TARGET_3_ROOT] ?? "./data");
|
|
36
|
+
this.configDir = resolve(options.configDir ?? process.env[ENV.CONFIG_DIR] ?? "./config");
|
|
37
|
+
this.logDir = resolve(options.logDir ?? process.env[ENV.LOG_DIR] ?? "./logs");
|
|
38
38
|
this.tempDir = resolve(options.tempDir ?? process.env[ENV.TEMP_DIR] ?? join(os.tmpdir(), "cronops"));
|
|
39
39
|
this.uid = options.uid ?? process.env[ENV.PUID] ?? `${process.getuid?.() ?? "0"}`;
|
|
40
40
|
this.gid = options.gid ?? process.env[ENV.PGID] ?? `${process.getgid?.() ?? "0"}`;
|
package/dist/server.js
CHANGED
|
@@ -34,18 +34,25 @@ export async function start() {
|
|
|
34
34
|
console.log(`🔴 Error loading job '${entry}'. ${message}`);
|
|
35
35
|
});
|
|
36
36
|
jobLoader.onJobLoaded((job) => {
|
|
37
|
-
|
|
38
|
-
jobScheduler.scheduleJob(job);
|
|
37
|
+
jobScheduler.scheduleJob(job);
|
|
39
38
|
});
|
|
40
39
|
jobLoader.onJobDeleted((jobId) => {
|
|
41
40
|
jobScheduler.unscheduleJob(jobId);
|
|
42
41
|
});
|
|
43
42
|
jobScheduler.onChanged((isReload) => {
|
|
44
|
-
const jobs = jobScheduler.
|
|
45
|
-
console.log(`\nJob config ${isReload ? "changed" : "loaded"}
|
|
43
|
+
const jobs = jobScheduler.getScheduledJobsInfo();
|
|
44
|
+
console.log(`\nJob config ${isReload ? "changed" : "loaded"}`);
|
|
46
45
|
for (const job of jobs) {
|
|
47
|
-
|
|
46
|
+
if (job.status !== "paused")
|
|
47
|
+
console.log(` 🕔 [${job.id}] scheduled (${chalk.greenBright(job.cron)})${job.dry_run ? " 👋 DRY-RUN mode!" : ""}`);
|
|
48
48
|
}
|
|
49
|
+
for (const job of jobs) {
|
|
50
|
+
if (job.status === "paused")
|
|
51
|
+
console.log(` ⚫ [${job.id}] inactive`);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
jobScheduler.onJobExecute((job) => {
|
|
55
|
+
console.error(`[${job.id}] triggered manually`);
|
|
49
56
|
});
|
|
50
57
|
jobScheduler.onJobError((job, err) => {
|
|
51
58
|
console.error(chalk.red(`[${job.id}] ERROR ${err.message}`));
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { type ScheduledTask } from "node-cron";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import type { Task } from "../types/Task.types.js";
|
|
3
|
+
import type { Task, TaskInfo } from "../types/Task.types.js";
|
|
4
4
|
export declare abstract class AbstractTask<T> implements Task {
|
|
5
5
|
protected cronTask: ScheduledTask;
|
|
6
6
|
protected events: EventEmitter<any>;
|
|
7
7
|
protected errorCount: number;
|
|
8
|
+
private runCount;
|
|
9
|
+
private isScheduled;
|
|
8
10
|
private isRunning;
|
|
11
|
+
private isPaused;
|
|
12
|
+
private lastRun?;
|
|
13
|
+
private lastDuration?;
|
|
9
14
|
constructor(cronStr?: string);
|
|
10
15
|
protected abstract run(): Promise<T>;
|
|
11
16
|
schedule(runImmediately?: boolean): void;
|
|
12
17
|
unschedule(): void;
|
|
13
|
-
|
|
18
|
+
pause(): void;
|
|
19
|
+
resume(): void;
|
|
20
|
+
getInfo(): TaskInfo;
|
|
21
|
+
execute<T>(): Promise<T>;
|
|
14
22
|
onScheduled(cb: () => void): void;
|
|
23
|
+
onExecute(cb: () => void): void;
|
|
15
24
|
onStarted(cb: () => void): void;
|
|
16
25
|
onFinished<T>(cb: (result: T) => void): void;
|
|
17
26
|
onError(cb: (error: Error) => void): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import cron
|
|
1
|
+
import cron from "node-cron";
|
|
2
2
|
import { setTimeout } from "node:timers/promises";
|
|
3
3
|
import { EventEmitter } from "node:events";
|
|
4
4
|
import { ENV } from "../types/Options.types.js";
|
|
@@ -6,20 +6,29 @@ export class AbstractTask {
|
|
|
6
6
|
cronTask;
|
|
7
7
|
events = new EventEmitter();
|
|
8
8
|
errorCount = 0;
|
|
9
|
+
runCount = 0;
|
|
10
|
+
isScheduled = false;
|
|
9
11
|
isRunning = false;
|
|
12
|
+
isPaused = false;
|
|
13
|
+
lastRun;
|
|
14
|
+
lastDuration;
|
|
10
15
|
constructor(cronStr = "* * * * *") {
|
|
11
16
|
const asyncRunner = async () => {
|
|
12
|
-
if (!this.isRunning) {
|
|
17
|
+
if (!this.isRunning && !this.isPaused) {
|
|
18
|
+
this.runCount++;
|
|
13
19
|
this.isRunning = true;
|
|
20
|
+
this.lastRun = Date.now();
|
|
14
21
|
try {
|
|
15
22
|
this.events.emit("started");
|
|
16
23
|
this.events.emit("finished", await this.run());
|
|
17
24
|
}
|
|
18
25
|
catch (err) {
|
|
26
|
+
this.errorCount++;
|
|
19
27
|
this.events.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
20
28
|
}
|
|
21
29
|
finally {
|
|
22
30
|
this.isRunning = false;
|
|
31
|
+
this.lastDuration = Date.now() - this.lastRun;
|
|
23
32
|
}
|
|
24
33
|
}
|
|
25
34
|
};
|
|
@@ -30,26 +39,45 @@ export class AbstractTask {
|
|
|
30
39
|
}
|
|
31
40
|
schedule(runImmediately = false) {
|
|
32
41
|
if (runImmediately)
|
|
33
|
-
this.cronTask.once("task:started", () => this.execute());
|
|
42
|
+
this.cronTask.once("task:started", () => this.cronTask.execute());
|
|
34
43
|
this.cronTask.start();
|
|
44
|
+
this.isScheduled = true;
|
|
35
45
|
}
|
|
36
46
|
unschedule() {
|
|
37
47
|
this.events.removeAllListeners();
|
|
38
48
|
this.cronTask.destroy();
|
|
49
|
+
this.isScheduled = false;
|
|
39
50
|
}
|
|
40
|
-
|
|
51
|
+
pause() {
|
|
52
|
+
this.isPaused = true;
|
|
53
|
+
}
|
|
54
|
+
resume() {
|
|
55
|
+
this.isPaused = false;
|
|
56
|
+
}
|
|
57
|
+
getInfo() {
|
|
58
|
+
return {
|
|
59
|
+
status: this.isRunning ? "running" : this.isPaused ? "paused" : this.isScheduled ? "scheduled" : "unscheduled",
|
|
60
|
+
runCount: this.runCount,
|
|
61
|
+
errorCount: this.errorCount,
|
|
62
|
+
lastRun: this.lastRun,
|
|
63
|
+
lastDuration: this.lastDuration,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
execute() {
|
|
41
67
|
const status = this.cronTask.getStatus();
|
|
42
68
|
if (status === "destroyed")
|
|
43
69
|
throw new Error("Invalid task state (destroyed)");
|
|
44
70
|
if (status === "running" || this.isRunning)
|
|
45
71
|
throw new Error("Invalid task state (running)");
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.cronTask.execute();
|
|
72
|
+
this.events.emit("execute");
|
|
73
|
+
return this.cronTask.execute();
|
|
49
74
|
}
|
|
50
75
|
onScheduled(cb) {
|
|
51
76
|
this.cronTask.on("task:started", cb);
|
|
52
77
|
}
|
|
78
|
+
onExecute(cb) {
|
|
79
|
+
this.events.on("execute", cb);
|
|
80
|
+
}
|
|
53
81
|
onStarted(cb) {
|
|
54
82
|
this.events.on("started", cb);
|
|
55
83
|
}
|
|
@@ -57,7 +85,6 @@ export class AbstractTask {
|
|
|
57
85
|
this.events.on("finished", cb);
|
|
58
86
|
}
|
|
59
87
|
onError(cb) {
|
|
60
|
-
this.errorCount++;
|
|
61
88
|
this.events.on("error", (error) => cb(error));
|
|
62
89
|
}
|
|
63
90
|
async gracefulTerminate(timeout = 500) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { AbstractTask } from "./AbstractTask.js";
|
|
2
|
-
import { JobRunnerResult } from "../models/JobRunnerResult.js";
|
|
3
2
|
import type { JobModel } from "../models/JobModel.js";
|
|
4
3
|
import type { JobRunnerSetup } from "../models/JobRunnerSetup.js";
|
|
5
4
|
import type { RunnerResult, FileHistory } from "../types/Task.types.js";
|
|
@@ -9,8 +8,8 @@ export declare class JobRunner extends AbstractTask<RunnerResult> {
|
|
|
9
8
|
setup: JobRunnerSetup;
|
|
10
9
|
constructor(job: JobModel, setup: JobRunnerSetup);
|
|
11
10
|
onActivity(cb: (action: string, path: string, count: number) => void): void;
|
|
12
|
-
runJob(): Promise<
|
|
13
|
-
protected run(): Promise<
|
|
11
|
+
runJob(): Promise<RunnerResult>;
|
|
12
|
+
protected run(): Promise<RunnerResult>;
|
|
14
13
|
protected loadFileHistory(job: Job): Promise<FileHistory>;
|
|
15
14
|
protected saveFileHistory(job: Job, fileHistory: FileHistory): Promise<void>;
|
|
16
15
|
protected initLog(job: Job): number;
|
package/dist/tasks/JobRunner.js
CHANGED
|
@@ -4,7 +4,6 @@ import { ensureDir, moveSync, readJSON, writeJSON } from "fs-extra/esm";
|
|
|
4
4
|
import { AbstractTask } from "./AbstractTask.js";
|
|
5
5
|
import { JobRunnerContext } from "../models/JobRunnerContext.js";
|
|
6
6
|
import { closeSync, fsyncSync, openSync, writeSync } from "node:fs";
|
|
7
|
-
import { JobRunnerResult } from "../models/JobRunnerResult.js";
|
|
8
7
|
import { FileHistoryModel } from "../models/FileHistoryModel.js";
|
|
9
8
|
export class JobRunner extends AbstractTask {
|
|
10
9
|
job;
|
|
@@ -22,10 +21,8 @@ export class JobRunner extends AbstractTask {
|
|
|
22
21
|
}
|
|
23
22
|
async run() {
|
|
24
23
|
const { setup, job, events } = this;
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
if (this.errorCount >= 25) {
|
|
28
|
-
job.enabled = false;
|
|
24
|
+
if (this.errorCount >= 31) {
|
|
25
|
+
this.pause();
|
|
29
26
|
throw new Error("Too many errors. Job execution disabled!");
|
|
30
27
|
}
|
|
31
28
|
await ensureDir(setup.logDir);
|
|
@@ -2,8 +2,9 @@ import { JobRunner } from "./JobRunner.js";
|
|
|
2
2
|
import { JobRunnerSetup } from "../models/JobRunnerSetup.js";
|
|
3
3
|
import { AbstractTask } from "./AbstractTask.js";
|
|
4
4
|
import type { Job } from "../types/Config.types.js";
|
|
5
|
-
import type { RunnerResult } from "../types/Task.types.js";
|
|
5
|
+
import type { RunnerResult, TaskInfo } from "../types/Task.types.js";
|
|
6
6
|
import type { RunnerOptions } from "../types/Options.types.js";
|
|
7
|
+
type SchedulingInfo = Job & TaskInfo;
|
|
7
8
|
export declare class JobScheduler extends AbstractTask<void> {
|
|
8
9
|
protected runnerSetup: JobRunnerSetup;
|
|
9
10
|
protected runnerMap: Map<string, JobRunner>;
|
|
@@ -13,19 +14,29 @@ export declare class JobScheduler extends AbstractTask<void> {
|
|
|
13
14
|
get scheduledJobs(): number;
|
|
14
15
|
get tempDir(): string;
|
|
15
16
|
protected run(): Promise<void>;
|
|
17
|
+
pauseAll(): void;
|
|
18
|
+
resumeAll(): void;
|
|
16
19
|
unscheduleAll(): void;
|
|
17
20
|
scheduleJobs(jobs: Job[], cb?: (count: number) => void): void;
|
|
18
|
-
scheduleJob(job: Job
|
|
21
|
+
scheduleJob(job: Job): void;
|
|
19
22
|
unscheduleJob(jobId: string): void;
|
|
20
23
|
isJobScheduled(jobId: string): boolean;
|
|
24
|
+
getJob(jobId: string): Job | undefined;
|
|
21
25
|
executeJob(jobId: string): void;
|
|
22
26
|
validateJob(job: Job): void;
|
|
27
|
+
pauseScheduling(): void;
|
|
28
|
+
resumeScheduling(): void;
|
|
29
|
+
pauseJobScheduling(jobId: string): void;
|
|
30
|
+
resumeJobScheduling(jobId: string): void;
|
|
23
31
|
getScheduledJobs(): Job[];
|
|
32
|
+
getScheduledJobsInfo(): SchedulingInfo[];
|
|
24
33
|
gracefulTerminate(timeout?: number): Promise<void>;
|
|
25
34
|
onChanged(cb: (initialConfig: boolean) => void): void;
|
|
26
35
|
onJobScheduled(cb: (job: Job, rescheduled: boolean) => void): void;
|
|
36
|
+
onJobExecute(cb: (job: Job) => void): void;
|
|
27
37
|
onJobStarted(cb: (job: Job) => void): void;
|
|
28
38
|
onJobFinished(cb: (job: Job, stat: RunnerResult) => void): void;
|
|
29
39
|
onJobActivity(cb: (job: Job, activity: string, path: string, count: number) => void): void;
|
|
30
40
|
onJobError(cb: (job: Job, err: Error) => void): void;
|
|
31
41
|
}
|
|
42
|
+
export {};
|