v7
1// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import chalk from 'chalk'
16import { promisify } from 'node:util'
17import psTreeModule from 'ps-tree'
18
19export const psTree = promisify(psTreeModule)
20
21export function noop() {}
22
23export function randomId() {
24 return Math.random().toString(36).slice(2)
25}
26
27export function isString(obj: any) {
28 return typeof obj === 'string'
29}
30
31export function quote(arg: string) {
32 if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
33 return arg
34 }
35 return (
36 `$'` +
37 arg
38 .replace(/\\/g, '\\\\')
39 .replace(/'/g, "\\'")
40 .replace(/\f/g, '\\f')
41 .replace(/\n/g, '\\n')
42 .replace(/\r/g, '\\r')
43 .replace(/\t/g, '\\t')
44 .replace(/\v/g, '\\v')
45 .replace(/\0/g, '\\0') +
46 `'`
47 )
48}
49
50export function quotePowerShell(arg: string) {
51 if (/^[a-z0-9/_.\-]+$/i.test(arg) || arg === '') {
52 return arg
53 }
54 return `'` + arg.replace(/'/g, "''") + `'`
55}
56
57export function exitCodeInfo(exitCode: number | null): string | undefined {
58 return {
59 2: 'Misuse of shell builtins',
60 126: 'Invoked command cannot execute',
61 127: 'Command not found',
62 128: 'Invalid exit argument',
63 129: 'Hangup',
64 130: 'Interrupt',
65 131: 'Quit and dump core',
66 132: 'Illegal instruction',
67 133: 'Trace/breakpoint trap',
68 134: 'Process aborted',
69 135: 'Bus error: "access to undefined portion of memory object"',
70 136: 'Floating point exception: "erroneous arithmetic operation"',
71 137: 'Kill (terminate immediately)',
72 138: 'User-defined 1',
73 139: 'Segmentation violation',
74 140: 'User-defined 2',
75 141: 'Write to pipe with no one reading',
76 142: 'Signal raised by alarm',
77 143: 'Termination (request to terminate)',
78 145: 'Child process terminated, stopped (or continued*)',
79 146: 'Continue if stopped',
80 147: 'Stop executing temporarily',
81 148: 'Terminal stop signal',
82 149: 'Background process attempting to read from tty ("in")',
83 150: 'Background process attempting to write to tty ("out")',
84 151: 'Urgent data available on socket',
85 152: 'CPU time limit exceeded',
86 153: 'File size limit exceeded',
87 154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
88 155: 'Profiling timer expired',
89 157: 'Pollable event',
90 159: 'Bad syscall',
91 }[exitCode || -1]
92}
93
94export function errnoMessage(errno: number | undefined): string {
95 if (errno === undefined) {
96 return 'Unknown error'
97 }
98 return (
99 {
100 0: 'Success',
101 1: 'Not super-user',
102 2: 'No such file or directory',
103 3: 'No such process',
104 4: 'Interrupted system call',
105 5: 'I/O error',
106 6: 'No such device or address',
107 7: 'Arg list too long',
108 8: 'Exec format error',
109 9: 'Bad file number',
110 10: 'No children',
111 11: 'No more processes',
112 12: 'Not enough core',
113 13: 'Permission denied',
114 14: 'Bad address',
115 15: 'Block device required',
116 16: 'Mount device busy',
117 17: 'File exists',
118 18: 'Cross-device link',
119 19: 'No such device',
120 20: 'Not a directory',
121 21: 'Is a directory',
122 22: 'Invalid argument',
123 23: 'Too many open files in system',
124 24: 'Too many open files',
125 25: 'Not a typewriter',
126 26: 'Text file busy',
127 27: 'File too large',
128 28: 'No space left on device',
129 29: 'Illegal seek',
130 30: 'Read only file system',
131 31: 'Too many links',
132 32: 'Broken pipe',
133 33: 'Math arg out of domain of func',
134 34: 'Math result not representable',
135 35: 'File locking deadlock error',
136 36: 'File or path name too long',
137 37: 'No record locks available',
138 38: 'Function not implemented',
139 39: 'Directory not empty',
140 40: 'Too many symbolic links',
141 42: 'No message of desired type',
142 43: 'Identifier removed',
143 44: 'Channel number out of range',
144 45: 'Level 2 not synchronized',
145 46: 'Level 3 halted',
146 47: 'Level 3 reset',
147 48: 'Link number out of range',
148 49: 'Protocol driver not attached',
149 50: 'No CSI structure available',
150 51: 'Level 2 halted',
151 52: 'Invalid exchange',
152 53: 'Invalid request descriptor',
153 54: 'Exchange full',
154 55: 'No anode',
155 56: 'Invalid request code',
156 57: 'Invalid slot',
157 59: 'Bad font file fmt',
158 60: 'Device not a stream',
159 61: 'No data (for no delay io)',
160 62: 'Timer expired',
161 63: 'Out of streams resources',
162 64: 'Machine is not on the network',
163 65: 'Package not installed',
164 66: 'The object is remote',
165 67: 'The link has been severed',
166 68: 'Advertise error',
167 69: 'Srmount error',
168 70: 'Communication error on send',
169 71: 'Protocol error',
170 72: 'Multihop attempted',
171 73: 'Cross mount point (not really error)',
172 74: 'Trying to read unreadable message',
173 75: 'Value too large for defined data type',
174 76: 'Given log. name not unique',
175 77: 'f.d. invalid for this operation',
176 78: 'Remote address changed',
177 79: 'Can access a needed shared lib',
178 80: 'Accessing a corrupted shared lib',
179 81: '.lib section in a.out corrupted',
180 82: 'Attempting to link in too many libs',
181 83: 'Attempting to exec a shared library',
182 84: 'Illegal byte sequence',
183 86: 'Streams pipe error',
184 87: 'Too many users',
185 88: 'Socket operation on non-socket',
186 89: 'Destination address required',
187 90: 'Message too long',
188 91: 'Protocol wrong type for socket',
189 92: 'Protocol not available',
190 93: 'Unknown protocol',
191 94: 'Socket type not supported',
192 95: 'Not supported',
193 96: 'Protocol family not supported',
194 97: 'Address family not supported by protocol family',
195 98: 'Address already in use',
196 99: 'Address not available',
197 100: 'Network interface is not configured',
198 101: 'Network is unreachable',
199 102: 'Connection reset by network',
200 103: 'Connection aborted',
201 104: 'Connection reset by peer',
202 105: 'No buffer space available',
203 106: 'Socket is already connected',
204 107: 'Socket is not connected',
205 108: "Can't send after socket shutdown",
206 109: 'Too many references',
207 110: 'Connection timed out',
208 111: 'Connection refused',
209 112: 'Host is down',
210 113: 'Host is unreachable',
211 114: 'Socket already connected',
212 115: 'Connection already in progress',
213 116: 'Stale file handle',
214 122: 'Quota exceeded',
215 123: 'No medium (in tape drive)',
216 125: 'Operation canceled',
217 130: 'Previous owner died',
218 131: 'State not recoverable',
219 }[-errno] || 'Unknown error'
220 )
221}
222
223export type Duration = number | `${number}s` | `${number}ms`
224
225export function parseDuration(d: Duration) {
226 if (typeof d == 'number') {
227 if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`)
228 return d
229 } else if (/\d+s/.test(d)) {
230 return +d.slice(0, -1) * 1000
231 } else if (/\d+ms/.test(d)) {
232 return +d.slice(0, -2)
233 }
234 throw new Error(`Unknown duration: "${d}".`)
235}
236
237export function formatCmd(cmd?: string): string {
238 if (cmd == undefined) return chalk.grey('undefined')
239 const chars = [...cmd]
240 let out = '$ '
241 let buf = ''
242 let ch: string
243 type State = (() => State) | undefined
244 let state: State = root
245 let wordCount = 0
246 while (state) {
247 ch = chars.shift() || 'EOF'
248 if (ch == '\n') {
249 out += style(state, buf) + '\n> '
250 buf = ''
251 continue
252 }
253 const next: State = ch == 'EOF' ? undefined : state()
254 if (next != state) {
255 out += style(state, buf)
256 buf = ''
257 }
258 state = next == root ? next() : next
259 buf += ch
260 }
261
262 function style(state: State, s: string): string {
263 if (s == '') return ''
264 if (reservedWords.includes(s)) {
265 return chalk.cyanBright(s)
266 }
267 if (state == word && wordCount == 0) {
268 wordCount++
269 return chalk.greenBright(s)
270 }
271 if (state == syntax) {
272 wordCount = 0
273 return chalk.cyanBright(s)
274 }
275 if (state == dollar) return chalk.yellowBright(s)
276 if (state?.name.startsWith('str')) return chalk.yellowBright(s)
277 return s
278 }
279
280 function isSyntax(ch: string) {
281 return '()[]{}<>;:+|&='.includes(ch)
282 }
283
284 function root() {
285 if (/\s/.test(ch)) return space
286 if (isSyntax(ch)) return syntax
287 if (/[$]/.test(ch)) return dollar
288 if (/["]/.test(ch)) return strDouble
289 if (/[']/.test(ch)) return strSingle
290 return word
291 }
292
293 function space() {
294 if (/\s/.test(ch)) return space
295 return root
296 }
297
298 function word() {
299 if (/[0-9a-z/_.]/i.test(ch)) return word
300 return root
301 }
302
303 function syntax() {
304 if (isSyntax(ch)) return syntax
305 return root
306 }
307
308 function dollar() {
309 if (/[']/.test(ch)) return str
310 return root
311 }
312
313 function str() {
314 if (/[']/.test(ch)) return strEnd
315 if (/[\\]/.test(ch)) return strBackslash
316 return str
317 }
318
319 function strBackslash() {
320 return strEscape
321 }
322
323 function strEscape() {
324 return str
325 }
326
327 function strDouble() {
328 if (/["]/.test(ch)) return strEnd
329 return strDouble
330 }
331
332 function strSingle() {
333 if (/[']/.test(ch)) return strEnd
334 return strSingle
335 }
336
337 function strEnd() {
338 return root
339 }
340
341 return out + '\n'
342}
343
344const reservedWords = [
345 'if',
346 'then',
347 'else',
348 'elif',
349 'fi',
350 'case',
351 'esac',
352 'for',
353 'select',
354 'while',
355 'until',
356 'do',
357 'done',
358 'in',
359]