@onivoro/server-process 22.0.2 → 24.0.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 +526 -0
- package/jest.config.ts +11 -0
- package/package.json +8 -40
- package/project.json +23 -0
- package/src/lib/docker.ts +14 -0
- package/src/lib/exec-promise.spec.ts +17 -0
- package/src/lib/exec-promise.ts +14 -0
- package/src/lib/exec-rx-as-json.ts +9 -0
- package/src/lib/exec-rx-as-lines.ts +10 -0
- package/src/lib/exec-rx.spec.ts +22 -0
- package/src/lib/exec-rx.ts +16 -0
- package/src/lib/exit.ts +1 -0
- package/src/lib/listen.ts +12 -0
- package/src/lib/psql.ts +16 -0
- package/src/lib/spawn-promise.spec.ts +17 -0
- package/src/lib/spawn-promise.ts +31 -0
- package/tsconfig.json +16 -0
- package/tsconfig.lib.json +8 -0
- package/tsconfig.spec.json +21 -0
- package/dist/cjs/index.js +0 -21
- package/dist/cjs/lib/docker.d.ts +0 -8
- package/dist/cjs/lib/docker.js +0 -16
- package/dist/cjs/lib/exec-promise.d.ts +0 -3
- package/dist/cjs/lib/exec-promise.js +0 -16
- package/dist/cjs/lib/exec-rx-as-json.d.ts +0 -2
- package/dist/cjs/lib/exec-rx-as-json.js +0 -9
- package/dist/cjs/lib/exec-rx-as-lines.d.ts +0 -2
- package/dist/cjs/lib/exec-rx-as-lines.js +0 -10
- package/dist/cjs/lib/exec-rx.d.ts +0 -4
- package/dist/cjs/lib/exec-rx.js +0 -18
- package/dist/cjs/lib/exit.d.ts +0 -1
- package/dist/cjs/lib/exit.js +0 -5
- package/dist/cjs/lib/listen.d.ts +0 -5
- package/dist/cjs/lib/listen.js +0 -12
- package/dist/cjs/lib/psql.d.ts +0 -5
- package/dist/cjs/lib/psql.js +0 -20
- package/dist/cjs/lib/spawn-promise.d.ts +0 -1
- package/dist/cjs/lib/spawn-promise.js +0 -28
- package/dist/esm/index.d.ts +0 -9
- package/dist/esm/index.js +0 -21
- package/dist/esm/lib/docker.d.ts +0 -8
- package/dist/esm/lib/docker.js +0 -16
- package/dist/esm/lib/exec-promise.d.ts +0 -3
- package/dist/esm/lib/exec-promise.js +0 -16
- package/dist/esm/lib/exec-rx-as-json.d.ts +0 -2
- package/dist/esm/lib/exec-rx-as-json.js +0 -9
- package/dist/esm/lib/exec-rx-as-lines.d.ts +0 -2
- package/dist/esm/lib/exec-rx-as-lines.js +0 -10
- package/dist/esm/lib/exec-rx.d.ts +0 -4
- package/dist/esm/lib/exec-rx.js +0 -18
- package/dist/esm/lib/exit.d.ts +0 -1
- package/dist/esm/lib/exit.js +0 -5
- package/dist/esm/lib/listen.d.ts +0 -5
- package/dist/esm/lib/listen.js +0 -12
- package/dist/esm/lib/psql.d.ts +0 -5
- package/dist/esm/lib/psql.js +0 -20
- package/dist/esm/lib/spawn-promise.d.ts +0 -1
- package/dist/esm/lib/spawn-promise.js +0 -28
- package/dist/types/index.d.ts +0 -9
- package/dist/types/lib/docker.d.ts +0 -8
- package/dist/types/lib/exec-promise.d.ts +0 -3
- package/dist/types/lib/exec-rx-as-json.d.ts +0 -2
- package/dist/types/lib/exec-rx-as-lines.d.ts +0 -2
- package/dist/types/lib/exec-rx.d.ts +0 -4
- package/dist/types/lib/exit.d.ts +0 -1
- package/dist/types/lib/listen.d.ts +0 -5
- package/dist/types/lib/psql.d.ts +0 -5
- package/dist/types/lib/spawn-promise.d.ts +0 -1
- /package/{dist/cjs/index.d.ts → src/index.ts} +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
# @onivoro/server-process
|
|
2
|
+
|
|
3
|
+
A comprehensive process management library for Node.js applications, providing utilities for executing commands, managing Docker containers, PostgreSQL operations, and handling system processes with reactive programming support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @onivoro/server-process
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Command Execution**: Promise-based and reactive command execution
|
|
14
|
+
- **Docker Integration**: Docker container management utilities
|
|
15
|
+
- **PostgreSQL Tools**: PSql command execution and database operations
|
|
16
|
+
- **Process Management**: Spawn and manage child processes
|
|
17
|
+
- **Reactive Streams**: RxJS-based reactive process handling
|
|
18
|
+
- **JSON Processing**: Parse command output as JSON
|
|
19
|
+
- **Line-by-Line Processing**: Stream processing for large outputs
|
|
20
|
+
- **Error Handling**: Comprehensive error handling for process operations
|
|
21
|
+
- **Cross-Platform**: Works on Windows, macOS, and Linux
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Basic Command Execution
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { execPromise, shell } from '@onivoro/server-process';
|
|
29
|
+
|
|
30
|
+
// Simple command execution
|
|
31
|
+
const result = await execPromise('ls -la');
|
|
32
|
+
console.log(result.stdout);
|
|
33
|
+
|
|
34
|
+
// Alternative using shell function (if available in common)
|
|
35
|
+
const output = await shell('pwd');
|
|
36
|
+
console.log(output);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Docker Operations
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { Docker } from '@onivoro/server-process';
|
|
43
|
+
|
|
44
|
+
const docker = new Docker();
|
|
45
|
+
|
|
46
|
+
// List running containers
|
|
47
|
+
const containers = await docker.listContainers();
|
|
48
|
+
|
|
49
|
+
// Run a container
|
|
50
|
+
await docker.run('nginx:latest', {
|
|
51
|
+
ports: ['80:80'],
|
|
52
|
+
detach: true,
|
|
53
|
+
name: 'my-nginx'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Execute command in container
|
|
57
|
+
const result = await docker.exec('my-nginx', 'ls /usr/share/nginx/html');
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### PostgreSQL Operations
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { PSql } from '@onivoro/server-process';
|
|
64
|
+
|
|
65
|
+
const psql = new PSql({
|
|
66
|
+
host: 'localhost',
|
|
67
|
+
port: 5432,
|
|
68
|
+
database: 'mydb',
|
|
69
|
+
username: 'user',
|
|
70
|
+
password: 'password'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Execute SQL query
|
|
74
|
+
const users = await psql.query('SELECT * FROM users');
|
|
75
|
+
|
|
76
|
+
// Execute SQL file
|
|
77
|
+
await psql.executeFile('./migrations/001_create_tables.sql');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Usage Examples
|
|
81
|
+
|
|
82
|
+
### Reactive Command Execution
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { execRx, execRxAsLines, execRxAsJson } from '@onivoro/server-process';
|
|
86
|
+
import { tap, catchError } from 'rxjs/operators';
|
|
87
|
+
import { of } from 'rxjs';
|
|
88
|
+
|
|
89
|
+
// Execute command reactively
|
|
90
|
+
execRx('ping google.com')
|
|
91
|
+
.pipe(
|
|
92
|
+
tap(data => console.log('Output:', data)),
|
|
93
|
+
catchError(error => {
|
|
94
|
+
console.error('Command failed:', error);
|
|
95
|
+
return of(null);
|
|
96
|
+
})
|
|
97
|
+
)
|
|
98
|
+
.subscribe();
|
|
99
|
+
|
|
100
|
+
// Process output line by line
|
|
101
|
+
execRxAsLines('tail -f /var/log/system.log')
|
|
102
|
+
.pipe(
|
|
103
|
+
tap(line => console.log('Log line:', line))
|
|
104
|
+
)
|
|
105
|
+
.subscribe();
|
|
106
|
+
|
|
107
|
+
// Parse JSON output
|
|
108
|
+
execRxAsJson('docker inspect my-container')
|
|
109
|
+
.pipe(
|
|
110
|
+
tap(json => console.log('Container info:', json))
|
|
111
|
+
)
|
|
112
|
+
.subscribe();
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Process Spawning
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { spawnPromise } from '@onivoro/server-process';
|
|
119
|
+
|
|
120
|
+
// Spawn a long-running process
|
|
121
|
+
const result = await spawnPromise('node', ['server.js'], {
|
|
122
|
+
cwd: '/path/to/app',
|
|
123
|
+
env: { ...process.env, NODE_ENV: 'production' }
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log('Process exit code:', result.code);
|
|
127
|
+
console.log('Process output:', result.stdout);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Docker Container Management
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { Docker } from '@onivoro/server-process';
|
|
134
|
+
|
|
135
|
+
class ContainerManager {
|
|
136
|
+
private docker = new Docker();
|
|
137
|
+
|
|
138
|
+
async deployApplication(imageName: string, config: any): Promise<string> {
|
|
139
|
+
// Pull latest image
|
|
140
|
+
await this.docker.pull(imageName);
|
|
141
|
+
|
|
142
|
+
// Stop existing container if running
|
|
143
|
+
try {
|
|
144
|
+
await this.docker.stop(config.containerName);
|
|
145
|
+
await this.docker.remove(config.containerName);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Container might not exist, continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Run new container
|
|
151
|
+
const containerId = await this.docker.run(imageName, {
|
|
152
|
+
name: config.containerName,
|
|
153
|
+
ports: config.ports,
|
|
154
|
+
volumes: config.volumes,
|
|
155
|
+
env: config.environment,
|
|
156
|
+
detach: true,
|
|
157
|
+
restart: 'unless-stopped'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return containerId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async getContainerLogs(containerName: string): Promise<string> {
|
|
164
|
+
return this.docker.logs(containerName, { tail: 100 });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async healthCheck(containerName: string): Promise<boolean> {
|
|
168
|
+
try {
|
|
169
|
+
const result = await this.docker.exec(containerName, 'curl -f http://localhost/health');
|
|
170
|
+
return result.exitCode === 0;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Database Migration with PSql
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { PSql } from '@onivoro/server-process';
|
|
182
|
+
import { readdir } from 'fs/promises';
|
|
183
|
+
import { join } from 'path';
|
|
184
|
+
|
|
185
|
+
class MigrationRunner {
|
|
186
|
+
private psql: PSql;
|
|
187
|
+
|
|
188
|
+
constructor(connectionConfig: any) {
|
|
189
|
+
this.psql = new PSql(connectionConfig);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async runMigrations(migrationsDir: string): Promise<void> {
|
|
193
|
+
// Ensure migrations table exists
|
|
194
|
+
await this.psql.query(`
|
|
195
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
196
|
+
id SERIAL PRIMARY KEY,
|
|
197
|
+
filename VARCHAR(255) NOT NULL,
|
|
198
|
+
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
199
|
+
)
|
|
200
|
+
`);
|
|
201
|
+
|
|
202
|
+
// Get executed migrations
|
|
203
|
+
const executedMigrations = await this.psql.query(
|
|
204
|
+
'SELECT filename FROM migrations ORDER BY id'
|
|
205
|
+
);
|
|
206
|
+
const executedFiles = executedMigrations.map(row => row.filename);
|
|
207
|
+
|
|
208
|
+
// Get migration files
|
|
209
|
+
const files = await readdir(migrationsDir);
|
|
210
|
+
const migrationFiles = files
|
|
211
|
+
.filter(file => file.endsWith('.sql'))
|
|
212
|
+
.sort();
|
|
213
|
+
|
|
214
|
+
// Execute pending migrations
|
|
215
|
+
for (const file of migrationFiles) {
|
|
216
|
+
if (!executedFiles.includes(file)) {
|
|
217
|
+
console.log(`Executing migration: ${file}`);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await this.psql.executeFile(join(migrationsDir, file));
|
|
221
|
+
await this.psql.query(
|
|
222
|
+
'INSERT INTO migrations (filename) VALUES ($1)',
|
|
223
|
+
[file]
|
|
224
|
+
);
|
|
225
|
+
console.log(`Migration ${file} completed successfully`);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error(`Migration ${file} failed:`, error);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async rollback(steps: number = 1): Promise<void> {
|
|
235
|
+
const migrations = await this.psql.query(
|
|
236
|
+
'SELECT filename FROM migrations ORDER BY id DESC LIMIT $1',
|
|
237
|
+
[steps]
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
for (const migration of migrations) {
|
|
241
|
+
// Execute rollback script if exists
|
|
242
|
+
const rollbackFile = migration.filename.replace('.sql', '.rollback.sql');
|
|
243
|
+
try {
|
|
244
|
+
await this.psql.executeFile(rollbackFile);
|
|
245
|
+
await this.psql.query(
|
|
246
|
+
'DELETE FROM migrations WHERE filename = $1',
|
|
247
|
+
[migration.filename]
|
|
248
|
+
);
|
|
249
|
+
console.log(`Rolled back migration: ${migration.filename}`);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(`Rollback failed for ${migration.filename}:`, error);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### System Monitoring
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
import { execPromise, execRxAsLines } from '@onivoro/server-process';
|
|
263
|
+
|
|
264
|
+
class SystemMonitor {
|
|
265
|
+
async getSystemInfo(): Promise<any> {
|
|
266
|
+
const [cpu, memory, disk] = await Promise.all([
|
|
267
|
+
this.getCpuInfo(),
|
|
268
|
+
this.getMemoryInfo(),
|
|
269
|
+
this.getDiskInfo()
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
return { cpu, memory, disk };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async getCpuInfo(): Promise<any> {
|
|
276
|
+
const result = await execPromise('cat /proc/cpuinfo');
|
|
277
|
+
// Parse CPU information
|
|
278
|
+
return this.parseCpuInfo(result.stdout);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async getMemoryInfo(): Promise<any> {
|
|
282
|
+
const result = await execPromise('free -m');
|
|
283
|
+
return this.parseMemoryInfo(result.stdout);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private async getDiskInfo(): Promise<any> {
|
|
287
|
+
const result = await execPromise('df -h');
|
|
288
|
+
return this.parseDiskInfo(result.stdout);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
monitorSystemLogs(): void {
|
|
292
|
+
execRxAsLines('tail -f /var/log/syslog')
|
|
293
|
+
.subscribe(line => {
|
|
294
|
+
if (this.isErrorLog(line)) {
|
|
295
|
+
this.handleSystemError(line);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private isErrorLog(line: string): boolean {
|
|
301
|
+
return line.toLowerCase().includes('error') ||
|
|
302
|
+
line.toLowerCase().includes('fail');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private handleSystemError(errorLine: string): void {
|
|
306
|
+
console.error('System error detected:', errorLine);
|
|
307
|
+
// Implement alerting logic
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Process Exit Handling
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { exit, listen } from '@onivoro/server-process';
|
|
316
|
+
|
|
317
|
+
class GracefulShutdown {
|
|
318
|
+
private cleanup: Array<() => Promise<void>> = [];
|
|
319
|
+
|
|
320
|
+
addCleanupTask(task: () => Promise<void>): void {
|
|
321
|
+
this.cleanup.push(task);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
setupGracefulShutdown(): void {
|
|
325
|
+
// Listen for termination signals
|
|
326
|
+
listen('SIGTERM', async () => {
|
|
327
|
+
console.log('Received SIGTERM, shutting down gracefully...');
|
|
328
|
+
await this.performCleanup();
|
|
329
|
+
exit(0);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
listen('SIGINT', async () => {
|
|
333
|
+
console.log('Received SIGINT, shutting down gracefully...');
|
|
334
|
+
await this.performCleanup();
|
|
335
|
+
exit(0);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Handle uncaught exceptions
|
|
339
|
+
process.on('uncaughtException', async (error) => {
|
|
340
|
+
console.error('Uncaught exception:', error);
|
|
341
|
+
await this.performCleanup();
|
|
342
|
+
exit(1);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async performCleanup(): Promise<void> {
|
|
347
|
+
console.log('Performing cleanup tasks...');
|
|
348
|
+
|
|
349
|
+
await Promise.all(
|
|
350
|
+
this.cleanup.map(async (task, index) => {
|
|
351
|
+
try {
|
|
352
|
+
await task();
|
|
353
|
+
console.log(`Cleanup task ${index + 1} completed`);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`Cleanup task ${index + 1} failed:`, error);
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Usage
|
|
363
|
+
const shutdown = new GracefulShutdown();
|
|
364
|
+
|
|
365
|
+
// Add cleanup tasks
|
|
366
|
+
shutdown.addCleanupTask(async () => {
|
|
367
|
+
// Close database connections
|
|
368
|
+
await database.close();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
shutdown.addCleanupTask(async () => {
|
|
372
|
+
// Stop background jobs
|
|
373
|
+
await jobQueue.stop();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
shutdown.setupGracefulShutdown();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## API Reference
|
|
380
|
+
|
|
381
|
+
### Command Execution
|
|
382
|
+
|
|
383
|
+
#### execPromise(command, options?)
|
|
384
|
+
|
|
385
|
+
Execute a command and return a promise:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
interface ExecResult {
|
|
389
|
+
stdout: string;
|
|
390
|
+
stderr: string;
|
|
391
|
+
code: number;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function execPromise(command: string, options?: ExecOptions): Promise<ExecResult>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
#### spawnPromise(command, args?, options?)
|
|
398
|
+
|
|
399
|
+
Spawn a process and return a promise:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
function spawnPromise(
|
|
403
|
+
command: string,
|
|
404
|
+
args?: string[],
|
|
405
|
+
options?: SpawnOptions
|
|
406
|
+
): Promise<SpawnResult>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Reactive Execution
|
|
410
|
+
|
|
411
|
+
#### execRx(command, options?)
|
|
412
|
+
|
|
413
|
+
Execute command reactively:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
function execRx(command: string, options?: ExecOptions): Observable<string>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
#### execRxAsLines(command, options?)
|
|
420
|
+
|
|
421
|
+
Execute command and emit output line by line:
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
function execRxAsLines(command: string, options?: ExecOptions): Observable<string>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### execRxAsJson(command, options?)
|
|
428
|
+
|
|
429
|
+
Execute command and parse output as JSON:
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
function execRxAsJson<T>(command: string, options?: ExecOptions): Observable<T>
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Docker Class
|
|
436
|
+
|
|
437
|
+
Docker container management:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
class Docker {
|
|
441
|
+
async run(image: string, options?: DockerRunOptions): Promise<string>
|
|
442
|
+
async stop(container: string): Promise<void>
|
|
443
|
+
async remove(container: string): Promise<void>
|
|
444
|
+
async exec(container: string, command: string): Promise<ExecResult>
|
|
445
|
+
async logs(container: string, options?: LogOptions): Promise<string>
|
|
446
|
+
async listContainers(): Promise<ContainerInfo[]>
|
|
447
|
+
async pull(image: string): Promise<void>
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### PSql Class
|
|
452
|
+
|
|
453
|
+
PostgreSQL operations:
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
class PSql {
|
|
457
|
+
constructor(config: PSqlConfig)
|
|
458
|
+
async query(sql: string, params?: any[]): Promise<any[]>
|
|
459
|
+
async executeFile(filePath: string): Promise<void>
|
|
460
|
+
async transaction<T>(callback: (client: any) => Promise<T>): Promise<T>
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Process Management
|
|
465
|
+
|
|
466
|
+
#### listen(signal, callback)
|
|
467
|
+
|
|
468
|
+
Listen for process signals:
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
function listen(signal: string, callback: () => void | Promise<void>): void
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### exit(code?)
|
|
475
|
+
|
|
476
|
+
Exit process with optional code:
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
function exit(code?: number): never
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Best Practices
|
|
483
|
+
|
|
484
|
+
1. **Error Handling**: Always handle errors in process operations
|
|
485
|
+
2. **Resource Cleanup**: Use proper cleanup for long-running processes
|
|
486
|
+
3. **Signal Handling**: Implement graceful shutdown for production applications
|
|
487
|
+
4. **Stream Processing**: Use reactive streams for large outputs
|
|
488
|
+
5. **Security**: Validate input when executing external commands
|
|
489
|
+
6. **Logging**: Log process operations for debugging and monitoring
|
|
490
|
+
7. **Timeouts**: Set appropriate timeouts for long-running operations
|
|
491
|
+
8. **Environment**: Use environment-specific configurations
|
|
492
|
+
|
|
493
|
+
## Security Considerations
|
|
494
|
+
|
|
495
|
+
1. **Command Injection**: Always validate and sanitize command inputs
|
|
496
|
+
2. **Privilege Escalation**: Run processes with minimal required privileges
|
|
497
|
+
3. **Environment Variables**: Be careful with sensitive environment variables
|
|
498
|
+
4. **File Permissions**: Ensure proper file permissions for executed scripts
|
|
499
|
+
5. **Container Security**: Follow Docker security best practices
|
|
500
|
+
|
|
501
|
+
## Testing
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
import { execPromise, Docker } from '@onivoro/server-process';
|
|
505
|
+
|
|
506
|
+
describe('Process Operations', () => {
|
|
507
|
+
it('should execute commands', async () => {
|
|
508
|
+
const result = await execPromise('echo "Hello World"');
|
|
509
|
+
expect(result.stdout.trim()).toBe('Hello World');
|
|
510
|
+
expect(result.code).toBe(0);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('should handle command errors', async () => {
|
|
514
|
+
try {
|
|
515
|
+
await execPromise('nonexistent-command');
|
|
516
|
+
fail('Should have thrown an error');
|
|
517
|
+
} catch (error) {
|
|
518
|
+
expect(error.code).not.toBe(0);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
## License
|
|
525
|
+
|
|
526
|
+
This library is part of the Onivoro monorepo ecosystem.
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
displayName: 'lib-server-process',
|
|
4
|
+
preset: '../../../jest.preset.js',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
|
8
|
+
},
|
|
9
|
+
moduleFileExtensions: ['ts', 'js', 'html'],
|
|
10
|
+
coverageDirectory: '../../../coverage/libs/server/process',
|
|
11
|
+
};
|
package/package.json
CHANGED
|
@@ -1,42 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
"files": [
|
|
11
|
-
"dist/*"
|
|
12
|
-
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"onx": "onx",
|
|
15
|
-
"build": "onx Build",
|
|
16
|
-
"deploy": "onx Publish",
|
|
17
|
-
"test": "onx Test",
|
|
18
|
-
"update": "onx Update"
|
|
19
|
-
},
|
|
20
|
-
"exports": {
|
|
21
|
-
".": {
|
|
22
|
-
"types": "./dist/types/index.d.ts",
|
|
23
|
-
"require": "./dist/cjs/index.js",
|
|
24
|
-
"import": "./dist/esm/index.js",
|
|
25
|
-
"default": "./dist/esm/lib.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"onx": {
|
|
29
|
-
"platform": "server",
|
|
30
|
-
"module": "commonjs"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@onivoro/cli": "^22.0.8",
|
|
34
|
-
"@types/jest": "*",
|
|
35
|
-
"@types/node": "^22.8.1",
|
|
36
|
-
"typescript": "*"
|
|
37
|
-
},
|
|
38
|
-
"engines": {
|
|
39
|
-
"node": "22.10.0",
|
|
40
|
-
"npm": "10.9.0"
|
|
41
|
-
}
|
|
2
|
+
"name": "@onivoro/server-process",
|
|
3
|
+
"version": "24.0.0",
|
|
4
|
+
"type": "commonjs",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"tslib": "^2.3.0"
|
|
9
|
+
}
|
|
42
10
|
}
|
package/project.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lib-server-process",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "libs/server/process/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "@nx/js:tsc",
|
|
9
|
+
"outputs": ["{options.outputPath}"],
|
|
10
|
+
"options": {
|
|
11
|
+
"outputPath": "dist/libs/server/process",
|
|
12
|
+
"main": "libs/server/process/src/index.ts",
|
|
13
|
+
"tsConfig": "libs/server/process/tsconfig.lib.json",
|
|
14
|
+
"assets": [
|
|
15
|
+
"libs/server/process/README.md",
|
|
16
|
+
"libs/server/process/package.json"
|
|
17
|
+
],
|
|
18
|
+
"declaration": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"tags": []
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ExecOptions } from "child_process";
|
|
2
|
+
import { EncodingOption } from "fs";
|
|
3
|
+
import { execRx } from "./exec-rx";
|
|
4
|
+
|
|
5
|
+
export class Docker {
|
|
6
|
+
constructor(
|
|
7
|
+
public readonly containerName: string,
|
|
8
|
+
public readonly binaryName: string,
|
|
9
|
+
) { }
|
|
10
|
+
|
|
11
|
+
execRx(cmd: string, options?: EncodingOption & ExecOptions, emitStdErr=true) {
|
|
12
|
+
return execRx(`docker exec ${this.containerName} ${this.binaryName} ${cmd}`, options, emitStdErr);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parse } from 'path';
|
|
2
|
+
import { execPromise } from './exec-promise';
|
|
3
|
+
|
|
4
|
+
describe('execPromise', () => {
|
|
5
|
+
it('resolves with stdout', async () => {
|
|
6
|
+
const result = await execPromise(`ls ${__dirname}`);
|
|
7
|
+
expect(result).toContain(parse(__filename).base);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('rejects with stderr', async () => {
|
|
11
|
+
try {
|
|
12
|
+
await execPromise(`ls 'no way jose'`);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
expect(e.message.replace(/\n/g, ' ')).toContain('No such file or directory');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { exec, ExecOptions } from "child_process";
|
|
2
|
+
import { EncodingOption } from "fs";
|
|
3
|
+
|
|
4
|
+
export function execPromise(cmd: string, options?: EncodingOption & ExecOptions): Promise<any> {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
exec(cmd, options, (err, stdout) => {
|
|
7
|
+
if (err) {
|
|
8
|
+
reject(err);
|
|
9
|
+
} else {
|
|
10
|
+
resolve(stdout.toString());
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ExecOptions } from 'child_process';
|
|
2
|
+
import { map } from 'rxjs/operators';
|
|
3
|
+
import { execRx } from './exec-rx';
|
|
4
|
+
|
|
5
|
+
export const execRxAsJson = (cmd: string, options?: ExecOptions, emitStdErr = true) => {
|
|
6
|
+
return execRx(cmd, options, emitStdErr).pipe(
|
|
7
|
+
map((s: string) => JSON.parse(s)),
|
|
8
|
+
)
|
|
9
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ExecOptions } from 'child_process';
|
|
2
|
+
import { from } from 'rxjs';
|
|
3
|
+
import { concatMap } from 'rxjs/operators';
|
|
4
|
+
import { execRx } from './exec-rx';
|
|
5
|
+
|
|
6
|
+
export const execRxAsLines = (cmd: string, options?: ExecOptions, emitStdErr = true) => {
|
|
7
|
+
return execRx(cmd, options, emitStdErr).pipe(
|
|
8
|
+
concatMap((s: string) => from(s.split('\n'))),
|
|
9
|
+
)
|
|
10
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execRx } from './exec-rx';
|
|
2
|
+
import { of } from 'rxjs';
|
|
3
|
+
import { catchError } from 'rxjs/operators';
|
|
4
|
+
|
|
5
|
+
describe(execRx.name, () => {
|
|
6
|
+
describe('GIVEN command succeeds', () => {
|
|
7
|
+
it('returns the stdout', (done) => {
|
|
8
|
+
execRx(`cat ${__filename}`).subscribe((d) => {
|
|
9
|
+
expect(d).toEqual(expect.stringContaining('execRx worx!'));
|
|
10
|
+
done();
|
|
11
|
+
}, () => { throw new Error("fail") }
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('GIVEN command fails', () => {
|
|
17
|
+
it('emits error', (done) => {
|
|
18
|
+
execRx(`cat ${__filename + 'blah'}`).pipe(catchError(() => of(done())))
|
|
19
|
+
.subscribe();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|