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]