@tskmgr/client 2.0.1 → 2.0.2
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/.babelrc +10 -0
- package/.eslintrc.json +21 -0
- package/README.md +7 -7
- package/jest.config.ts +17 -0
- package/package.json +3 -5
- package/project.json +31 -0
- package/src/{index.d.ts → index.ts} +1 -0
- package/src/lib/client-example.ts +134 -0
- package/src/lib/client-factory.ts +17 -0
- package/src/lib/client-options.ts +12 -0
- package/src/lib/client.spec.ts +5 -0
- package/src/lib/client.ts +280 -0
- package/src/lib/run-tasks-result.ts +17 -0
- package/src/lib/task-result.ts +10 -0
- package/src/lib/utils.ts +68 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.spec.json +20 -0
- package/LICENSE +0 -21
- package/src/index.js +0 -22
- package/src/index.js.map +0 -1
- package/src/lib/client-example.d.ts +0 -11
- package/src/lib/client-example.js +0 -120
- package/src/lib/client-example.js.map +0 -1
- package/src/lib/client-factory.d.ts +0 -6
- package/src/lib/client-factory.js +0 -14
- package/src/lib/client-factory.js.map +0 -1
- package/src/lib/client-options.d.ts +0 -12
- package/src/lib/client-options.js +0 -3
- package/src/lib/client-options.js.map +0 -1
- package/src/lib/client.d.ts +0 -24
- package/src/lib/client.js +0 -249
- package/src/lib/client.js.map +0 -1
- package/src/lib/run-tasks-result.d.ts +0 -9
- package/src/lib/run-tasks-result.js +0 -14
- package/src/lib/run-tasks-result.js.map +0 -1
- package/src/lib/task-result.d.ts +0 -10
- package/src/lib/task-result.js +0 -3
- package/src/lib/task-result.js.map +0 -1
- package/src/lib/utils.d.ts +0 -14
- package/src/lib/utils.js +0 -72
- package/src/lib/utils.js.map +0 -1
package/.babelrc
ADDED
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["../../.eslintrc.json"],
|
|
3
|
+
"ignorePatterns": ["!**/*"],
|
|
4
|
+
"overrides": [
|
|
5
|
+
{
|
|
6
|
+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
7
|
+
"rules": {}
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"files": ["*.ts", "*.tsx"],
|
|
11
|
+
"parserOptions": {
|
|
12
|
+
"project": ["libs/client/tsconfig.*?.json"]
|
|
13
|
+
},
|
|
14
|
+
"rules": {}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"files": ["*.js", "*.jsx"],
|
|
18
|
+
"rules": {}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# client
|
|
2
|
-
|
|
3
|
-
This library was generated with [Nx](https://nx.dev).
|
|
4
|
-
|
|
5
|
-
## Running unit tests
|
|
6
|
-
|
|
7
|
-
Run `nx test client` to execute the unit tests via [Jest](https://jestjs.io).
|
|
1
|
+
# client
|
|
2
|
+
|
|
3
|
+
This library was generated with [Nx](https://nx.dev).
|
|
4
|
+
|
|
5
|
+
## Running unit tests
|
|
6
|
+
|
|
7
|
+
Run `nx test client` to execute the unit tests via [Jest](https://jestjs.io).
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: 'client',
|
|
4
|
+
preset: '../../jest.preset.js',
|
|
5
|
+
globals: {},
|
|
6
|
+
testEnvironment: 'node',
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.[tj]sx?$': [
|
|
9
|
+
'ts-jest',
|
|
10
|
+
{
|
|
11
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
16
|
+
coverageDirectory: '../../coverage/libs/client',
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tskmgr/client",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"debug": "^4.3.4",
|
|
7
7
|
"node-fetch": "^2.6.7",
|
|
8
|
-
"@tskmgr/common": "
|
|
8
|
+
"@tskmgr/common": "2.0.2",
|
|
9
9
|
"uuid": "^9.0.1",
|
|
10
10
|
"@nx/devkit": "18.0.7",
|
|
11
11
|
"form-data": "2.3.3"
|
|
12
|
-
}
|
|
13
|
-
"main": "./src/index.js",
|
|
14
|
-
"type": "commonjs"
|
|
12
|
+
}
|
|
15
13
|
}
|
package/project.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "client",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/client/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"lint": {
|
|
8
|
+
"executor": "@nx/eslint:lint",
|
|
9
|
+
"outputs": ["{options.outputFile}"]
|
|
10
|
+
},
|
|
11
|
+
"test": {
|
|
12
|
+
"executor": "@nx/jest:jest",
|
|
13
|
+
"outputs": ["{workspaceRoot}/coverage/libs/client"],
|
|
14
|
+
"options": {
|
|
15
|
+
"jestConfig": "libs/client/jest.config.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"build": {
|
|
19
|
+
"executor": "@nx/js:tsc",
|
|
20
|
+
"outputs": ["{options.outputPath}"],
|
|
21
|
+
"options": {
|
|
22
|
+
"outputPath": "dist/libs/client",
|
|
23
|
+
"tsConfig": "libs/client/tsconfig.lib.json",
|
|
24
|
+
"packageJson": "libs/client/package.json",
|
|
25
|
+
"main": "libs/client/src/index.ts",
|
|
26
|
+
"assets": ["libs/client/*.md", "LICENSE"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"tags": []
|
|
31
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntelliJ debug:
|
|
3
|
+
* Node parameters: --require ts-node/register --require tsconfig-paths/register
|
|
4
|
+
* Environment variables: TS_NODE_PROJECT=libs/client/tsconfig.lib.json
|
|
5
|
+
*
|
|
6
|
+
* Start API:
|
|
7
|
+
* nx serve api
|
|
8
|
+
* Command:
|
|
9
|
+
* DEBUG=tskmgr:* ts-node --project libs/client/tsconfig.lib.json -r tsconfig-paths/register "libs/client/src/lib/client-example.ts"
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { CreateTaskDto, Run, Task, TaskPriority } from '@tskmgr/common';
|
|
14
|
+
import { ClientOptions } from './client-options';
|
|
15
|
+
import { ClientFactory } from './client-factory';
|
|
16
|
+
import { v4 as uuid } from 'uuid';
|
|
17
|
+
import Debug from 'debug';
|
|
18
|
+
import { readJsonFile } from '@nx/devkit';
|
|
19
|
+
import { unlinkSync } from 'fs';
|
|
20
|
+
const debug = Debug('tskmgr:client-example');
|
|
21
|
+
|
|
22
|
+
delete process.env.TS_NODE_PROJECT;
|
|
23
|
+
|
|
24
|
+
const options: ClientOptions = {
|
|
25
|
+
parallel: 1,
|
|
26
|
+
dataCallback,
|
|
27
|
+
errorCallback,
|
|
28
|
+
spawnOptions: { env: { ...process.env } },
|
|
29
|
+
};
|
|
30
|
+
const client = ClientFactory.createNew('http://localhost:3333', 'RUNNER_1', options);
|
|
31
|
+
|
|
32
|
+
let completed = false;
|
|
33
|
+
let run: Run;
|
|
34
|
+
|
|
35
|
+
(async () => {
|
|
36
|
+
try {
|
|
37
|
+
// 1. Create the new run
|
|
38
|
+
run = await client.createRun({
|
|
39
|
+
name: uuid(),
|
|
40
|
+
type: '123',
|
|
41
|
+
prioritization: [TaskPriority.Longest],
|
|
42
|
+
});
|
|
43
|
+
debug(run);
|
|
44
|
+
|
|
45
|
+
// 2. Leader should create some tasks to run
|
|
46
|
+
const election = await client.setLeader(run.id);
|
|
47
|
+
if (election.leader) {
|
|
48
|
+
const tasks = getNxTasks().map<CreateTaskDto>((nxTask) => {
|
|
49
|
+
const command = `npx nx run ${nxTask.target.project}:${nxTask.target.target} --configuration=production`;
|
|
50
|
+
return {
|
|
51
|
+
name: nxTask.target.project,
|
|
52
|
+
type: nxTask.target.target,
|
|
53
|
+
command: command.trim(),
|
|
54
|
+
options: { shell: true },
|
|
55
|
+
priority: TaskPriority.Longest,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const createdTasks = await client.createTasks(run.id, { tasks });
|
|
60
|
+
debug(createdTasks);
|
|
61
|
+
|
|
62
|
+
const closeRun = await client.closeRun(run.id);
|
|
63
|
+
debug(closeRun);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Execute tasks
|
|
67
|
+
const result = await client.runTasks(run.id);
|
|
68
|
+
if (result.completed) {
|
|
69
|
+
// if failFast set to false, runTasks will continue without throwing errors.
|
|
70
|
+
completed = true;
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error(e);
|
|
74
|
+
} finally {
|
|
75
|
+
// 4. See results
|
|
76
|
+
console.log('--------------------------------------------------');
|
|
77
|
+
console.log(` tskmgr run: http://localhost:4200/runs/${run.id}`);
|
|
78
|
+
console.log('--------------------------------------------------');
|
|
79
|
+
console.log(`${completed ? 'COMPLETED!' : 'FAILED!'}`);
|
|
80
|
+
process.exit(completed ? 0 : 1);
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
function getNxTasks(): NxTask[] {
|
|
85
|
+
const graphFileName = uuid() + '.json';
|
|
86
|
+
// In CI environment use npx nx affected --graph=${graphFileName} --target=lint command. run-many is just for demo purpose.
|
|
87
|
+
execSync(`npx nx run-many --graph=lint-${graphFileName} --target=lint`);
|
|
88
|
+
const lintJson = readJsonFile(`lint-${graphFileName}`);
|
|
89
|
+
unlinkSync(`lint-${graphFileName}`);
|
|
90
|
+
const lintTasks: NxTask[] = Object.values(lintJson.tasks.tasks);
|
|
91
|
+
|
|
92
|
+
execSync(`npx nx run-many --graph=test-${graphFileName} --target=test`);
|
|
93
|
+
const testJson = readJsonFile(`test-${graphFileName}`);
|
|
94
|
+
unlinkSync(`test-${graphFileName}`);
|
|
95
|
+
const testTasks: NxTask[] = Object.values(testJson.tasks.tasks);
|
|
96
|
+
|
|
97
|
+
execSync(`npx nx run-many --graph=build-${graphFileName} --target=build`);
|
|
98
|
+
const buildJson = readJsonFile(`build-${graphFileName}`);
|
|
99
|
+
unlinkSync(`build-${graphFileName}`);
|
|
100
|
+
const buildTasks: NxTask[] = Object.values(buildJson.tasks.tasks);
|
|
101
|
+
|
|
102
|
+
execSync(`npx nx run-many --graph=e2e-${graphFileName} --target=e2e`);
|
|
103
|
+
const e2eJson = readJsonFile(`e2e-${graphFileName}`);
|
|
104
|
+
unlinkSync(`e2e-${graphFileName}`);
|
|
105
|
+
const e2eTasks: NxTask[] = Object.values(e2eJson.tasks.tasks);
|
|
106
|
+
|
|
107
|
+
return [...lintTasks, ...testTasks, ...buildTasks, ...e2eTasks];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function dataCallback(task: Task, data: string, cached: () => void): void {
|
|
111
|
+
// > nx run frontend:lint [existing outputs match the cache, left as is]
|
|
112
|
+
// > nx run client:lint [local cache]
|
|
113
|
+
if (
|
|
114
|
+
(data.startsWith(`> nx run ${task.name}:${task.type}`) &&
|
|
115
|
+
data.endsWith('[existing outputs match the cache, left as is]')) ||
|
|
116
|
+
data.endsWith('[local cache]')
|
|
117
|
+
) {
|
|
118
|
+
cached();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`[stdout] ${data}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function errorCallback(task: Task, data: string): void {
|
|
125
|
+
console.log(`[stderr] ${data}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface NxTask {
|
|
129
|
+
id: string;
|
|
130
|
+
overrides: any;
|
|
131
|
+
target: { project: string; target: string };
|
|
132
|
+
command: string;
|
|
133
|
+
outputs: string[];
|
|
134
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Client } from './client';
|
|
2
|
+
import { ClientOptions } from './client-options';
|
|
3
|
+
import { ApiUrl } from '@tskmgr/common';
|
|
4
|
+
|
|
5
|
+
export class ClientFactory {
|
|
6
|
+
public static createNew(
|
|
7
|
+
baseUrl: string, //
|
|
8
|
+
runnerId: string,
|
|
9
|
+
options?: ClientOptions
|
|
10
|
+
): Client {
|
|
11
|
+
return new Client(
|
|
12
|
+
ApiUrl.create(baseUrl), //
|
|
13
|
+
runnerId,
|
|
14
|
+
{ ...Client.DefaultOptions, ...options }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Task } from '@tskmgr/common';
|
|
2
|
+
import { SpawnOptionsWithoutStdio } from 'child_process';
|
|
3
|
+
|
|
4
|
+
export interface ClientOptions {
|
|
5
|
+
parallel?: number;
|
|
6
|
+
dataCallback?: (task: Task, data: string, cached: () => void) => void;
|
|
7
|
+
errorCallback?: (task: Task, data: string) => void;
|
|
8
|
+
pollingDelayMs?: number;
|
|
9
|
+
retryDelayMs?: number;
|
|
10
|
+
retryCount?: number;
|
|
11
|
+
spawnOptions?: SpawnOptionsWithoutStdio;
|
|
12
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { ChildProcess } from 'child_process';
|
|
2
|
+
import {
|
|
3
|
+
ApiUrl,
|
|
4
|
+
Run,
|
|
5
|
+
CompleteTaskDto,
|
|
6
|
+
CreateRunRequestDto,
|
|
7
|
+
CreateTasksDto,
|
|
8
|
+
StartTaskDto,
|
|
9
|
+
StartTaskResponseDto,
|
|
10
|
+
Task,
|
|
11
|
+
File as File_,
|
|
12
|
+
SetLeaderRequestDto,
|
|
13
|
+
SetLeaderResponseDto,
|
|
14
|
+
CreateFileRequestDto,
|
|
15
|
+
} from '@tskmgr/common';
|
|
16
|
+
import fetch from 'node-fetch';
|
|
17
|
+
import * as FormData from 'form-data';
|
|
18
|
+
import { checkStatus, delay, getTaskLogFilename, spawnAsync } from './utils';
|
|
19
|
+
import { RunTasksResult } from './run-tasks-result';
|
|
20
|
+
import { TaskResult } from './task-result';
|
|
21
|
+
import { ClientOptions } from './client-options';
|
|
22
|
+
import { createReadStream, createWriteStream, unlinkSync } from 'fs';
|
|
23
|
+
import Debug from 'debug';
|
|
24
|
+
|
|
25
|
+
const debug = Debug('tskmgr:client');
|
|
26
|
+
|
|
27
|
+
export class Client {
|
|
28
|
+
public static readonly DefaultOptions: ClientOptions = {
|
|
29
|
+
parallel: 1,
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
31
|
+
dataCallback: (task, data, cached) => {},
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
33
|
+
errorCallback: (task, data) => {},
|
|
34
|
+
pollingDelayMs: 5000,
|
|
35
|
+
retryDelayMs: 5000,
|
|
36
|
+
retryCount: 2,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly apiUrl: ApiUrl,
|
|
41
|
+
private readonly runnerId: string,
|
|
42
|
+
private readonly options: ClientOptions
|
|
43
|
+
) {}
|
|
44
|
+
|
|
45
|
+
public async createRun(params: CreateRunRequestDto): Promise<Run> {
|
|
46
|
+
const res = await fetch(this.apiUrl.createRunUrl(), {
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: JSON.stringify(params),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return await checkStatus(res).json();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public async closeRun(runId: number): Promise<Run> {
|
|
56
|
+
const res = await fetch(this.apiUrl.closeRunUrl(runId), {
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return await checkStatus(res).json();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async abortRun(runId: number): Promise<Run> {
|
|
65
|
+
const res = await fetch(this.apiUrl.abortRunUrl(runId), {
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
method: 'PUT',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return await checkStatus(res).json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async failRun(runId: number): Promise<Run> {
|
|
74
|
+
const res = await fetch(this.apiUrl.failRunUrl(runId), {
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
method: 'PUT',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return await checkStatus(res).json();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public async setLeader(runId: number): Promise<SetLeaderResponseDto> {
|
|
83
|
+
const params: SetLeaderRequestDto = { runnerId: this.runnerId };
|
|
84
|
+
const res = await fetch(this.apiUrl.setLeaderUrl(runId), {
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
body: JSON.stringify(params),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return await checkStatus(res).json();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async createTasks(runId: number, params: CreateTasksDto): Promise<Task[]> {
|
|
94
|
+
const res = await fetch(this.apiUrl.createTasksUrl(runId), {
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: JSON.stringify(params),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return await checkStatus(res).json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public async startTask(runId: number, params: StartTaskDto): Promise<StartTaskResponseDto> {
|
|
104
|
+
const res = await fetch(this.apiUrl.startTaskUrl(runId), {
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
method: 'PUT',
|
|
107
|
+
body: JSON.stringify(params),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return await checkStatus(res).json();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async runTasks(runId: number): Promise<RunTasksResult> {
|
|
114
|
+
const taskRunners: Promise<TaskResult[]>[] = [];
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < this.options.parallel; i++) {
|
|
117
|
+
taskRunners.push(this.defaultParallelTaskRunner(runId, i));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const taskResults = await Promise.all(taskRunners);
|
|
121
|
+
return new RunTasksResult(taskResults.flatMap((x) => x));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async completeTask(taskId: number, params: CompleteTaskDto): Promise<Task> {
|
|
125
|
+
const res = await fetch(this.apiUrl.completeTaskUrl(taskId), {
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
method: 'PUT',
|
|
128
|
+
body: JSON.stringify(params),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return await checkStatus(res).json();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public async failTask(taskId: number): Promise<Task> {
|
|
135
|
+
const res = await fetch(this.apiUrl.failTaskUrl(taskId), {
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
method: 'PUT',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return await checkStatus(res).json();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public async uploadRunFile(runId: number, path: string, params: CreateFileRequestDto): Promise<File_> {
|
|
144
|
+
return this.uploadFile(this.apiUrl.createFileRunUrl(runId), path, params);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public async uploadTaskFile(taskId: number, path: string, params: CreateFileRequestDto): Promise<File_> {
|
|
148
|
+
return this.uploadFile(this.apiUrl.createFileTaskUrl(taskId), path, params);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async uploadFile(url: string, path: string, params: CreateFileRequestDto): Promise<File_> {
|
|
152
|
+
// https://github.com/node-fetch/node-fetch/tree/2.x#post-with-form-data-detect-multipart
|
|
153
|
+
// https://github.com/form-data/form-data#readme
|
|
154
|
+
|
|
155
|
+
const formData = new FormData();
|
|
156
|
+
formData.append('file', createReadStream(path));
|
|
157
|
+
|
|
158
|
+
if (params.type) {
|
|
159
|
+
formData.append('type', params.type);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (params.description) {
|
|
163
|
+
formData.append('description', params.description);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const res = await fetch(url, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: formData,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return await checkStatus(res).json();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async defaultParallelTaskRunner(runId: number, parallelId: number): Promise<TaskResult[]> {
|
|
175
|
+
const logInfo = `[${this.runnerId}:${parallelId}]`;
|
|
176
|
+
const taskResults: TaskResult[] = [];
|
|
177
|
+
let _continue = true;
|
|
178
|
+
|
|
179
|
+
debug(`${logInfo} parallel task runner started`);
|
|
180
|
+
|
|
181
|
+
while (_continue) {
|
|
182
|
+
const res = await this.startTask(runId, { runnerId: this.runnerId });
|
|
183
|
+
|
|
184
|
+
if (!res.continue) {
|
|
185
|
+
debug(`${logInfo} continue set to false`);
|
|
186
|
+
_continue = false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!res.continue && !res.task) {
|
|
190
|
+
debug(`${logInfo} continue set to false and no task, exiting`);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!res.task) {
|
|
195
|
+
debug(`${logInfo} polling for new tasks in ${this.options.pollingDelayMs} ms`);
|
|
196
|
+
await delay(this.options.pollingDelayMs);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const { run, task } = res;
|
|
201
|
+
debug(`${logInfo} received task: ${task.id}`);
|
|
202
|
+
|
|
203
|
+
let cached = false;
|
|
204
|
+
let completed = false;
|
|
205
|
+
let writable = false;
|
|
206
|
+
let childProcess: ChildProcess;
|
|
207
|
+
let error: Error;
|
|
208
|
+
|
|
209
|
+
const taskLogFilename = getTaskLogFilename(task.id);
|
|
210
|
+
const writeStream = createWriteStream(taskLogFilename);
|
|
211
|
+
writeStream.on('open', () => (writable = true));
|
|
212
|
+
|
|
213
|
+
const dataHandler = (data: string): void => {
|
|
214
|
+
this.options.dataCallback(task, data, () => (cached = true));
|
|
215
|
+
if (writable) {
|
|
216
|
+
writeStream.write(`[${new Date().toISOString()}] ${data}\n`);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const errorHandler = (data: string): void => {
|
|
221
|
+
this.options.errorCallback(task, data);
|
|
222
|
+
if (writable) {
|
|
223
|
+
writeStream.write(`[${new Date().toISOString()}] ${data}\n`);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
debug(`${logInfo} starting task: ${task.id}`);
|
|
228
|
+
try {
|
|
229
|
+
childProcess = await spawnAsync(
|
|
230
|
+
task.command,
|
|
231
|
+
task.arguments,
|
|
232
|
+
{
|
|
233
|
+
...task.options,
|
|
234
|
+
...this.options.spawnOptions,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
dataCallback: dataHandler,
|
|
238
|
+
errorCallback: errorHandler,
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
completed = true;
|
|
242
|
+
debug(`${logInfo} completed task: ${task.id}`);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error(`${logInfo} failed task: ${task.id} with error: ${err}`);
|
|
245
|
+
error = err;
|
|
246
|
+
|
|
247
|
+
if (run.failFast) {
|
|
248
|
+
// TODO: should abort all running tasks by current client
|
|
249
|
+
debug(`${logInfo} fail fast enabled, aborting`);
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
} finally {
|
|
253
|
+
taskResults.push({ run, task, childProcess, completed, error });
|
|
254
|
+
|
|
255
|
+
if (completed) {
|
|
256
|
+
await this.completeTask(task.id, { cached });
|
|
257
|
+
debug(`${logInfo} completed status for task: ${task.id} sent`);
|
|
258
|
+
} else {
|
|
259
|
+
await this.failTask(task.id);
|
|
260
|
+
debug(`${logInfo} failed status for task: ${task.id} sent`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
writeStream.end();
|
|
264
|
+
writeStream.close();
|
|
265
|
+
|
|
266
|
+
await this.uploadTaskFile(task.id, taskLogFilename, {
|
|
267
|
+
type: 'log',
|
|
268
|
+
description: `Log for task ${task.id}`,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
debug(`${logInfo} uploaded file for task: ${task.id} sent`);
|
|
272
|
+
unlinkSync(taskLogFilename);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
debug(`${logInfo} parallel task runner completed`);
|
|
277
|
+
|
|
278
|
+
return taskResults;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TaskResult } from './task-result';
|
|
2
|
+
|
|
3
|
+
export class RunTasksResult {
|
|
4
|
+
public readonly completedTasks: TaskResult[];
|
|
5
|
+
public readonly failedTasks: TaskResult[];
|
|
6
|
+
|
|
7
|
+
public readonly completed: boolean;
|
|
8
|
+
public readonly failed: boolean;
|
|
9
|
+
|
|
10
|
+
constructor(public readonly tasks: TaskResult[]) {
|
|
11
|
+
this.completedTasks = tasks.filter((x) => x.completed);
|
|
12
|
+
this.failedTasks = tasks.filter((x) => !x.completed);
|
|
13
|
+
|
|
14
|
+
this.completed = this.failedTasks.length === 0;
|
|
15
|
+
this.failed = this.failedTasks.length > 0;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ChildProcess, spawn, SpawnOptionsWithoutStdio } from 'child_process';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
export interface SpawnAsyncOptions {
|
|
7
|
+
dataCallback?: (data: string) => void;
|
|
8
|
+
errorCallback?: (data: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function spawnAsync(
|
|
12
|
+
command: string,
|
|
13
|
+
args?: ReadonlyArray<string>,
|
|
14
|
+
options?: SpawnOptionsWithoutStdio,
|
|
15
|
+
logging?: SpawnAsyncOptions
|
|
16
|
+
): Promise<ChildProcess> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const childProcess = spawn(command, args, options);
|
|
19
|
+
|
|
20
|
+
if (logging.dataCallback) {
|
|
21
|
+
const readlineStdout = createInterface({ input: childProcess.stdout });
|
|
22
|
+
readlineStdout.on('line', logging.dataCallback);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (logging.errorCallback) {
|
|
26
|
+
const readlineStderr = createInterface({ input: childProcess.stderr });
|
|
27
|
+
readlineStderr.on('line', logging.errorCallback);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
childProcess.on('close', (code) => {
|
|
31
|
+
if (code === 0) {
|
|
32
|
+
resolve(childProcess);
|
|
33
|
+
} else {
|
|
34
|
+
reject(childProcess);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
childProcess.on('error', (err: Error) => {
|
|
39
|
+
reject(childProcess); // TODO: try to return error message
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function delay(ms: number): Promise<void> {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
setTimeout(resolve, ms);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getTaskLogFilename(taskId: number): string {
|
|
51
|
+
return join(tmpdir(), `task-${taskId}.log`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class HTTPResponseError extends Error {
|
|
55
|
+
constructor(public readonly response) {
|
|
56
|
+
super(`HTTP Error Response: ${response.status} ${response.statusText}`);
|
|
57
|
+
this.response = response;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const checkStatus = (response) => {
|
|
62
|
+
if (response.ok) {
|
|
63
|
+
// response.status >= 200 && response.status < 300
|
|
64
|
+
return response;
|
|
65
|
+
} else {
|
|
66
|
+
throw new HTTPResponseError(response);
|
|
67
|
+
}
|
|
68
|
+
};
|