@miso.ai/server-jobs 0.6.5-beta.10
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/cli/config.js +7 -0
- package/cli/index.js +10 -0
- package/cli/run.js +33 -0
- package/package.json +22 -0
- package/src/constants.js +21 -0
- package/src/index.js +1 -0
- package/src/logs.js +95 -0
- package/src/process.js +25 -0
- package/src/tasks.js +75 -0
- package/src/utils.js +25 -0
- package/src/version.js +1 -0
package/cli/config.js
ADDED
package/cli/index.js
ADDED
package/cli/run.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { loadConfig } from './config.js';
|
|
2
|
+
import { runJob } from '../src/index.js';
|
|
3
|
+
|
|
4
|
+
function build(yargs) {
|
|
5
|
+
return yargs
|
|
6
|
+
.positional('name', {
|
|
7
|
+
describe: 'Job name',
|
|
8
|
+
})
|
|
9
|
+
.option('config', {
|
|
10
|
+
alias: ['c'],
|
|
11
|
+
describe: 'Config file',
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function run({ name, config: configFile, ...options } = {}) {
|
|
16
|
+
const config = await loadConfig(configFile);
|
|
17
|
+
const job = findJob(config, name);
|
|
18
|
+
if (!job) {
|
|
19
|
+
throw new Error(`Job not found: ${name}`);
|
|
20
|
+
}
|
|
21
|
+
await runJob(job, options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findJob(config, name) {
|
|
25
|
+
return config.jobs.find(job => job.name === name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
command: 'run [name]',
|
|
30
|
+
desc: 'Run a job',
|
|
31
|
+
builder: build,
|
|
32
|
+
handler: run,
|
|
33
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miso.ai/server-jobs",
|
|
3
|
+
"description": "Miso job tools",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"miso-jobs": "cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"repository": "MisoAI/miso-server-js-sdk",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"contributors": [
|
|
16
|
+
"simonpai <simon.pai@askmiso.com>"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@miso.ai/server-commons": "0.6.5-beta.10"
|
|
20
|
+
},
|
|
21
|
+
"version": "0.6.5-beta.10"
|
|
22
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function logLevel(NAME, VALUE) {
|
|
2
|
+
return Object.freeze({ NAME, VALUE });
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const LOG_LEVELS = [
|
|
6
|
+
logLevel('fatal', 0),
|
|
7
|
+
logLevel('error', 5),
|
|
8
|
+
logLevel('warn', 10),
|
|
9
|
+
logLevel('report', 10),
|
|
10
|
+
logLevel('info', 15),
|
|
11
|
+
logLevel('debug', 20),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const LOG_LEVEL = Object.freeze(LOG_LEVELS.reduce((acc, level) => {
|
|
15
|
+
acc[level.NAME.toUpperCase()] = level;
|
|
16
|
+
return acc;
|
|
17
|
+
}, {}));
|
|
18
|
+
|
|
19
|
+
export function getLogLevelValue(name) {
|
|
20
|
+
return (LOG_LEVEL[name.toUpperCase()] || LOG_LEVEL.INFO).VALUE;
|
|
21
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './tasks.js';
|
package/src/logs.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { LOG_LEVEL, getLogLevelValue } from './constants.js';
|
|
2
|
+
|
|
3
|
+
export function generateJobId() {
|
|
4
|
+
// 4-char random string
|
|
5
|
+
return Math.random().toString(36).slice(2, 6).toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createLogFunction(context = {}, {
|
|
9
|
+
logOutput = defaultLogOutput,
|
|
10
|
+
logFilter = () => true,
|
|
11
|
+
logLevel = LOG_LEVEL.INFO.NAME,
|
|
12
|
+
} = {}) {
|
|
13
|
+
if (typeof logOutput !== 'function') {
|
|
14
|
+
throw new Error('output must be a function');
|
|
15
|
+
}
|
|
16
|
+
const logLevelThreshold = getLogLevelValue(logLevel);
|
|
17
|
+
return (...args) => {
|
|
18
|
+
const event = normalizeEvent(context, args);
|
|
19
|
+
const { level = LOG_LEVEL.INFO.NAME } = event;
|
|
20
|
+
const logLevelValue = getLogLevelValue(level);
|
|
21
|
+
if (logLevelValue > logLevelThreshold) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!logFilter(event)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const message = formatEvent(event);
|
|
28
|
+
logOutput(level, message);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeEvent(context, args) {
|
|
33
|
+
let event, level = LOG_LEVEL.INFO.NAME;
|
|
34
|
+
if (args.length > 1) {
|
|
35
|
+
level = args[0];
|
|
36
|
+
event = args[1];
|
|
37
|
+
} else {
|
|
38
|
+
event = args[0];
|
|
39
|
+
}
|
|
40
|
+
if (typeof event === 'string') {
|
|
41
|
+
event = { message: event };
|
|
42
|
+
}
|
|
43
|
+
event = { ...context, level, ...event };
|
|
44
|
+
return event;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatEvent({ job, task, type, level, message = '', ...event } = {}) {
|
|
48
|
+
let tags = '';
|
|
49
|
+
if (job) {
|
|
50
|
+
tags += formatJobTag(job);
|
|
51
|
+
}
|
|
52
|
+
if (task) {
|
|
53
|
+
tags += formatTaskTag(task);
|
|
54
|
+
}
|
|
55
|
+
if (level) {
|
|
56
|
+
tags += `[${level}]`;
|
|
57
|
+
}
|
|
58
|
+
if (type) {
|
|
59
|
+
tags += `[${type}]`;
|
|
60
|
+
}
|
|
61
|
+
message = message || JSON.stringify(event);
|
|
62
|
+
return `${tags} ${message}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatJobTag(job) {
|
|
66
|
+
if (typeof job === 'string') {
|
|
67
|
+
return `[${job}]`;
|
|
68
|
+
}
|
|
69
|
+
let tags = '';
|
|
70
|
+
if (job.id) {
|
|
71
|
+
tags += `[${job.id}]`;
|
|
72
|
+
}
|
|
73
|
+
if (job.name) {
|
|
74
|
+
tags += `[${job.name}]`;
|
|
75
|
+
}
|
|
76
|
+
return tags;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatTaskTag(task) {
|
|
80
|
+
if (typeof task === 'string') {
|
|
81
|
+
return `[${task}]`;
|
|
82
|
+
}
|
|
83
|
+
return `[${task.name || task.id}]`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function defaultLogOutput(level, message) {
|
|
87
|
+
switch (level) {
|
|
88
|
+
case LOG_LEVEL.FATAL.NAME:
|
|
89
|
+
case LOG_LEVEL.ERROR.NAME:
|
|
90
|
+
console.error(message);
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
console.log(message);
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/process.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { spawn as _spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export async function spawn(command, args, { onStdout, onStderr, ...options } = {}) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const child = _spawn(command, args, options);
|
|
6
|
+
|
|
7
|
+
if (typeof onStdout === 'function') {
|
|
8
|
+
child.stdout.on('data', data => onStdout(data.toString()));
|
|
9
|
+
}
|
|
10
|
+
if (typeof onStderr === 'function') {
|
|
11
|
+
child.stderr.on('data', data => onStderr(data.toString()));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// fatal errors, like ENOENT, EACCES
|
|
15
|
+
child.on('error', reject);
|
|
16
|
+
|
|
17
|
+
child.on('close', (code, signal) => {
|
|
18
|
+
if (code === 0) {
|
|
19
|
+
resolve();
|
|
20
|
+
} else {
|
|
21
|
+
reject(new Error(`Command ${command} failed with code ${code} and signal ${signal}`));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { LOG_LEVEL } from './constants.js';
|
|
2
|
+
import { spawn } from './process.js';
|
|
3
|
+
import { createLogFunction, generateJobId } from './logs.js';
|
|
4
|
+
import { formatDuration } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export async function runJob(job, options = {}) {
|
|
7
|
+
job = normalizeJob(job);
|
|
8
|
+
const log = createLogFunction({ job }, options);
|
|
9
|
+
const { tasks = [] } = job;
|
|
10
|
+
|
|
11
|
+
log(`Starting job with ${tasks.length} tasks`);
|
|
12
|
+
|
|
13
|
+
let index = 0;
|
|
14
|
+
for (let task of tasks) {
|
|
15
|
+
task = normalizeTask(task, index);
|
|
16
|
+
await runTask(job, task, options);
|
|
17
|
+
index++;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const endTime = Date.now();
|
|
21
|
+
const elapsed = endTime - job.timestamp;
|
|
22
|
+
log(`Job done in ${formatDuration(elapsed)}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function runTask(job, task, options = {}) {
|
|
26
|
+
const log = createLogFunction({ job, task }, options);
|
|
27
|
+
const { command, args, shell } = task;
|
|
28
|
+
log(`Starting task: ${formatTaskCommand(task)}`);
|
|
29
|
+
|
|
30
|
+
await spawn(command, args || [], {
|
|
31
|
+
shell,
|
|
32
|
+
onStdout: data => log(parseJsonIfNecessary(data)),
|
|
33
|
+
onStderr: data => log(LOG_LEVEL.ERROR.NAME, parseJsonIfNecessary(data)),
|
|
34
|
+
...options,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const endTime = Date.now();
|
|
38
|
+
const elapsed = endTime - task.timestamp;
|
|
39
|
+
log(`Task done in ${formatDuration(elapsed)}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeJob(job) {
|
|
43
|
+
job = { ...job, timestamp: Date.now() };
|
|
44
|
+
if (!job.id) {
|
|
45
|
+
job.id = generateJobId();
|
|
46
|
+
}
|
|
47
|
+
return job;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeTask(task, index) {
|
|
51
|
+
task = { ...task, index, timestamp: Date.now() };
|
|
52
|
+
if (!task.id) {
|
|
53
|
+
task.id = generateJobId();
|
|
54
|
+
}
|
|
55
|
+
return task;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTaskCommand({ command, args = [] }) {
|
|
59
|
+
return [command, ...args].join(' ');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseJsonIfNecessary(data) {
|
|
63
|
+
if (typeof data !== 'string') {
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
data = data.trim();
|
|
67
|
+
if (!data.startsWith('{') || !data.endsWith('}')) {
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(data);
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function formatDuration(value) {
|
|
2
|
+
if (isNaN(value)) {
|
|
3
|
+
throw new Error(`Value must be a number: ${value}`);
|
|
4
|
+
}
|
|
5
|
+
value = Math.floor(value);
|
|
6
|
+
const ms = value % 1000;
|
|
7
|
+
value = Math.floor(value / 1000);
|
|
8
|
+
const sec = value % 60;
|
|
9
|
+
value = Math.floor(value / 60);
|
|
10
|
+
const min = value % 60;
|
|
11
|
+
value = Math.floor(value / 60);
|
|
12
|
+
const hour = value;
|
|
13
|
+
const lessThanOneMinute = hour === 0 && min === 0;
|
|
14
|
+
let str = '';
|
|
15
|
+
if (hour > 0) {
|
|
16
|
+
str += `${hour}h`;
|
|
17
|
+
}
|
|
18
|
+
if (min > 0) {
|
|
19
|
+
str += `${min}m`;
|
|
20
|
+
}
|
|
21
|
+
if (lessThanOneMinute || sec > 0 || ms > 0) {
|
|
22
|
+
str += lessThanOneMinute ? `${sec}.${ms}s` : `${sec}s`;
|
|
23
|
+
}
|
|
24
|
+
return str;
|
|
25
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '0.6.5-beta.10';
|