1 | import {injectable, inject} from "inversify";
|
2 | import {Spawn} from '../interfaces/spawn';
|
3 | import {ChildProcess, spawn} from 'child_process';
|
4 | import {SpawnOptions2} from "../custom-typings";
|
5 | import {ForceErrorImpl} from "./force-error-impl";
|
6 | import {CommandUtil} from "../interfaces/command-util";
|
7 |
|
8 | const readlineSync = require('readline-sync');
|
9 | const inpathSync = require('inpath').sync;
|
10 | const psTree = require('ps-tree');
|
11 |
|
12 |
|
13 | @injectable()
|
14 | export class SpawnImpl extends ForceErrorImpl implements Spawn {
|
15 | private cachedPassword: string;
|
16 |
|
17 | constructor(@inject('CommandUtil') public commandUtil: CommandUtil) {
|
18 | super();
|
19 | }
|
20 |
|
21 | private validate_spawnShellCommandAsync_args(cmd: string[],
|
22 | options: SpawnOptions2,
|
23 | cbStatus: (err: Error, result: string) => void,
|
24 | cbFinal: (err: Error, result: string) => void,
|
25 | cbDiagnostic: (message: string) => void = null) {
|
26 | const me = this;
|
27 | cmd = cmd || [];
|
28 | options = options || {};
|
29 | options.preSpawnMessage = options.preSpawnMessage || '';
|
30 | options.postSpawnMessage = options.postSpawnMessage || '';
|
31 | options.sudoUser = options.sudoUser || '';
|
32 | options.sudoPassword = options.sudoPassword || '';
|
33 | options.stdio = options.stdio || 'pipe';
|
34 | options.cwd = options.cwd || process.cwd();
|
35 | cbStatus = me.checkCallback(cbStatus);
|
36 | cbFinal = me.checkCallback(cbFinal);
|
37 | cbDiagnostic = cbDiagnostic || (() => {
|
38 | });
|
39 | return {cmd, options, cbStatus, cbFinal, cbDiagnostic};
|
40 | }
|
41 |
|
42 | spawnShellCommandAsync(_cmd: string[],
|
43 | _options: SpawnOptions2,
|
44 | _cbStatus: (err: Error, result: string) => void,
|
45 | _cbFinal: (err: Error, result: string) => void,
|
46 | _cbDiagnostic: (message: string) => void = null) {
|
47 | const me = this;
|
48 | let {cmd, options, cbStatus, cbFinal, cbDiagnostic}
|
49 | = me.validate_spawnShellCommandAsync_args(_cmd, _options, _cbStatus, _cbFinal, _cbDiagnostic);
|
50 | if (me.checkForceError('spawnShellCommandAsync', cbFinal)) {
|
51 | return;
|
52 | }
|
53 | let childProcess: ChildProcess;
|
54 | try {
|
55 | let command = cmd[0];
|
56 | let args = cmd.slice(1);
|
57 | let stdoutText = '';
|
58 | let stderrText = '';
|
59 | if (!options.suppressDiagnostics) {
|
60 | cbDiagnostic(`Running '${cmd}' @ '${options.cwd}'`);
|
61 | options.preSpawnMessage && cbDiagnostic(options.preSpawnMessage);
|
62 | }
|
63 | childProcess = spawn(command, args, options);
|
64 | childProcess.stderr.on('data', (dataChunk: Uint8Array) => {
|
65 | if (options.suppressStdErr && !options.cacheStdErr) {
|
66 | return;
|
67 | }
|
68 | const text = dataChunk.toString();
|
69 | !options.suppressStdErr && cbStatus(new Error(text), text);
|
70 | options.cacheStdErr && (stderrText += text);
|
71 | });
|
72 | childProcess.stdout.on('data', (dataChunk: Uint8Array) => {
|
73 | if (options.suppressStdOut && !options.cacheStdOut) {
|
74 | return;
|
75 | }
|
76 | const text = dataChunk.toString();
|
77 | !options.suppressStdOut && cbStatus(null, text);
|
78 | options.cacheStdOut && (stdoutText += text);
|
79 | });
|
80 | childProcess.on('error', (code: number, signal: string) => {
|
81 | cbFinal = SpawnImpl.childCloseOrExit(code || 10, signal || '', stdoutText, stderrText, options, cbFinal, cbDiagnostic);
|
82 | });
|
83 | childProcess.on('exit', (code: number, signal: string) => {
|
84 | cbFinal = SpawnImpl.childCloseOrExit(code, signal || '', stdoutText, stderrText, options, cbFinal, cbDiagnostic);
|
85 | });
|
86 | childProcess.on('close', (code: number, signal: string) => {
|
87 | cbFinal = SpawnImpl.childCloseOrExit(code, signal || '', stdoutText, stderrText, options, cbFinal, cbDiagnostic);
|
88 | });
|
89 | } catch (err) {
|
90 | cbFinal && cbFinal(err, null);
|
91 | }
|
92 | return childProcess;
|
93 | }
|
94 |
|
95 | private static childCloseOrExit(code: number,
|
96 | signal: string,
|
97 | stdoutText: string,
|
98 | stderrText: string,
|
99 | options: SpawnOptions2,
|
100 | cbFinal: (err: Error, result: string) => void,
|
101 | cbDiagnostic: (message: string) => void): (err: Error, result: string) => void {
|
102 | if (cbFinal) {
|
103 | !options.suppressDiagnostics && options.postSpawnMessage && cbDiagnostic(options.postSpawnMessage);
|
104 | const returnString = JSON.stringify({code, signal, stdoutText, stderrText}, undefined, 2);
|
105 | const error = (code !== null && code !== 0)
|
106 | ? new Error(returnString)
|
107 | : null;
|
108 | cbFinal(options.suppressFinalError ? null : error, options.suppressResult ? '' : returnString);
|
109 | }
|
110 | return null;
|
111 | }
|
112 |
|
113 | sudoSpawnAsync(cmd: string[],
|
114 | options: SpawnOptions2,
|
115 | cbStatus: (err: Error, result: string) => void,
|
116 | cbFinal: (err: Error, result: string) => void,
|
117 | cbDiagnostic: (message: string) => void = null) {
|
118 | let me = this;
|
119 | let prompt = '#node-sudo-passwd#';
|
120 | let prompts = 0;
|
121 | let args = ['-S', '-p', prompt];
|
122 | if (options.sudoUser) {
|
123 | args.unshift(`--user=${options.sudoUser}`);
|
124 | }
|
125 | cmd = cmd || [];
|
126 | cmd = cmd.slice(0);
|
127 | [].push.apply(args, cmd);
|
128 | let path = process.env['PATH'].split(':');
|
129 | let sudoBin = inpathSync('sudo', path);
|
130 | args.unshift(sudoBin);
|
131 |
|
132 | let child: ChildProcess = me.spawnShellCommandAsync(
|
133 | args,
|
134 | options,
|
135 | (err, result) => {
|
136 | if (err && err.message === prompt) {
|
137 | return;
|
138 | }
|
139 | cbStatus(err, result);
|
140 | },
|
141 | cbFinal);
|
142 |
|
143 | if (!child) {
|
144 |
|
145 | return;
|
146 | }
|
147 |
|
148 | function waitForStartup(err, children: any[]) {
|
149 | if (err) {
|
150 | throw new Error(`Error spawning process`);
|
151 | }
|
152 | if (children && children.length) {
|
153 | child.stderr.removeAllListeners();
|
154 | } else {
|
155 | setTimeout(function () {
|
156 | psTree(child.pid, waitForStartup);
|
157 | }, 100);
|
158 | }
|
159 | }
|
160 |
|
161 | psTree(child.pid, waitForStartup);
|
162 |
|
163 | child.stderr.on('data', function (data) {
|
164 | let lines = data.toString().trim().split('\n');
|
165 | lines.forEach(function (line) {
|
166 | if (line === prompt) {
|
167 | if (++prompts > 1) {
|
168 |
|
169 | me.cachedPassword = null;
|
170 | }
|
171 | let username = require('username').sync();
|
172 | let loginMessage = (prompts > 1)
|
173 | ? `Sorry, try again.\n[sudo] password for ${username}: `
|
174 | : `[sudo] password for ${username}: `;
|
175 |
|
176 | if (!me.cachedPassword) {
|
177 | me.cachedPassword = options.sudoPassword
|
178 | ? options.sudoPassword
|
179 | : readlineSync.question(loginMessage, {hideEchoBack: true});
|
180 | }
|
181 | child.stdin.write(me.cachedPassword + '\n');
|
182 | }
|
183 | });
|
184 | });
|
185 | return child;
|
186 | }
|
187 | }
|
188 |
|