UNPKG

7.17 kBPlain TextView Raw
1import {injectable, inject} from "inversify";
2import {Spawn} from '../interfaces/spawn';
3import {ChildProcess, spawn} from 'child_process';
4import {SpawnOptions2} from "../custom-typings";
5import {ForceErrorImpl} from "./force-error-impl";
6import {CommandUtil} from "../interfaces/command-util";
7
8const readlineSync = require('readline-sync');
9const inpathSync = require('inpath').sync;
10const psTree = require('ps-tree');
11
12//noinspection JSUnusedGlobalSymbols
13@injectable()
14export 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 //In this case spawnShellCommandAsync should handle the error callbacks
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 // The previous entry must have been incorrect, since sudo asks again.
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