main
  1// Copyright 2024 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
 15const EXIT_CODES: Record<number, string> = {
 16  2: 'Misuse of shell builtins',
 17  126: 'Invoked command cannot execute',
 18  127: 'Command not found',
 19  128: 'Invalid exit argument',
 20  129: 'Hangup',
 21  130: 'Interrupt',
 22  131: 'Quit and dump core',
 23  132: 'Illegal instruction',
 24  133: 'Trace/breakpoint trap',
 25  134: 'Process aborted',
 26  135: 'Bus error: "access to undefined portion of memory object"',
 27  136: 'Floating point exception: "erroneous arithmetic operation"',
 28  137: 'Kill (terminate immediately)',
 29  138: 'User-defined 1',
 30  139: 'Segmentation violation',
 31  140: 'User-defined 2',
 32  141: 'Write to pipe with no one reading',
 33  142: 'Signal raised by alarm',
 34  143: 'Termination (request to terminate)',
 35  145: 'Child process terminated, stopped (or continued*)',
 36  146: 'Continue if stopped',
 37  147: 'Stop executing temporarily',
 38  148: 'Terminal stop signal',
 39  149: 'Background process attempting to read from tty ("in")',
 40  150: 'Background process attempting to write to tty ("out")',
 41  151: 'Urgent data available on socket',
 42  152: 'CPU time limit exceeded',
 43  153: 'File size limit exceeded',
 44  154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
 45  155: 'Profiling timer expired',
 46  157: 'Pollable event',
 47  159: 'Bad syscall',
 48}
 49
 50const ERRNO_CODES: Record<number, string> = {
 51  0: 'Success',
 52  1: 'Not super-user',
 53  2: 'No such file or directory',
 54  3: 'No such process',
 55  4: 'Interrupted system call',
 56  5: 'I/O error',
 57  6: 'No such device or address',
 58  7: 'Arg list too long',
 59  8: 'Exec format error',
 60  9: 'Bad file number',
 61  10: 'No children',
 62  11: 'No more processes',
 63  12: 'Not enough core',
 64  13: 'Permission denied',
 65  14: 'Bad address',
 66  15: 'Block device required',
 67  16: 'Mount device busy',
 68  17: 'File exists',
 69  18: 'Cross-device link',
 70  19: 'No such device',
 71  20: 'Not a directory',
 72  21: 'Is a directory',
 73  22: 'Invalid argument',
 74  23: 'Too many open files in system',
 75  24: 'Too many open files',
 76  25: 'Not a typewriter',
 77  26: 'Text file busy',
 78  27: 'File too large',
 79  28: 'No space left on device',
 80  29: 'Illegal seek',
 81  30: 'Read only file system',
 82  31: 'Too many links',
 83  32: 'Broken pipe',
 84  33: 'Math arg out of domain of func',
 85  34: 'Math result not representable',
 86  35: 'File locking deadlock error',
 87  36: 'File or path name too long',
 88  37: 'No record locks available',
 89  38: 'Function not implemented',
 90  39: 'Directory not empty',
 91  40: 'Too many symbolic links',
 92  42: 'No message of desired type',
 93  43: 'Identifier removed',
 94  44: 'Channel number out of range',
 95  45: 'Level 2 not synchronized',
 96  46: 'Level 3 halted',
 97  47: 'Level 3 reset',
 98  48: 'Link number out of range',
 99  49: 'Protocol driver not attached',
100  50: 'No CSI structure available',
101  51: 'Level 2 halted',
102  52: 'Invalid exchange',
103  53: 'Invalid request descriptor',
104  54: 'Exchange full',
105  55: 'No anode',
106  56: 'Invalid request code',
107  57: 'Invalid slot',
108  59: 'Bad font file fmt',
109  60: 'Device not a stream',
110  61: 'No data (for no delay io)',
111  62: 'Timer expired',
112  63: 'Out of streams resources',
113  64: 'Machine is not on the network',
114  65: 'Package not installed',
115  66: 'The object is remote',
116  67: 'The link has been severed',
117  68: 'Advertise error',
118  69: 'Srmount error',
119  70: 'Communication error on send',
120  71: 'Protocol error',
121  72: 'Multihop attempted',
122  73: 'Cross mount point (not really error)',
123  74: 'Trying to read unreadable message',
124  75: 'Value too large for defined data type',
125  76: 'Given log. name not unique',
126  77: 'f.d. invalid for this operation',
127  78: 'Remote address changed',
128  79: 'Can   access a needed shared lib',
129  80: 'Accessing a corrupted shared lib',
130  81: '.lib section in a.out corrupted',
131  82: 'Attempting to link in too many libs',
132  83: 'Attempting to exec a shared library',
133  84: 'Illegal byte sequence',
134  86: 'Streams pipe error',
135  87: 'Too many users',
136  88: 'Socket operation on non-socket',
137  89: 'Destination address required',
138  90: 'Message too long',
139  91: 'Protocol wrong type for socket',
140  92: 'Protocol not available',
141  93: 'Unknown protocol',
142  94: 'Socket type not supported',
143  95: 'Not supported',
144  96: 'Protocol family not supported',
145  97: 'Address family not supported by protocol family',
146  98: 'Address already in use',
147  99: 'Address not available',
148  100: 'Network interface is not configured',
149  101: 'Network is unreachable',
150  102: 'Connection reset by network',
151  103: 'Connection aborted',
152  104: 'Connection reset by peer',
153  105: 'No buffer space available',
154  106: 'Socket is already connected',
155  107: 'Socket is not connected',
156  108: "Can't send after socket shutdown",
157  109: 'Too many references',
158  110: 'Connection timed out',
159  111: 'Connection refused',
160  112: 'Host is down',
161  113: 'Host is unreachable',
162  114: 'Socket already connected',
163  115: 'Connection already in progress',
164  116: 'Stale file handle',
165  122: 'Quota exceeded',
166  123: 'No medium (in tape drive)',
167  125: 'Operation canceled',
168  130: 'Previous owner died',
169  131: 'State not recoverable',
170}
171
172const DOCS_URL = 'https://google.github.io/zx'
173
174export class Fail extends Error {
175  static DOCS_URL = DOCS_URL
176  static EXIT_CODES = EXIT_CODES
177  static ERRNO_CODES = ERRNO_CODES
178
179  static formatExitMessage(
180    code: number | null,
181    signal: NodeJS.Signals | null,
182    stderr: string,
183    from: string,
184    details: string = ''
185  ): string {
186    if (code == 0 && signal == null) return `exit code: ${code}`
187
188    const codeInfo = Fail.getExitCodeInfo(code)
189    let message = `${stderr}
190    at ${from}
191    exit code: ${code}${codeInfo ? ' (' + codeInfo + ')' : ''}`
192
193    if (signal != null) message += `\n    signal: ${signal}`
194
195    if (details) message += `\n    details: \n${details}`
196
197    return message
198  }
199
200  static formatErrorMessage(err: NodeJS.ErrnoException, from: string): string {
201    return `${err.message}
202    errno: ${err.errno} (${Fail.getErrnoMessage(err.errno)})
203    code: ${err.code}
204    at ${from}`
205  }
206
207  static formatErrorDetails(lines: string[] = [], lim = 20): string {
208    if (lines.length < lim) return lines.join('\n')
209
210    let errors = lines.filter((l) => /(fail|error|not ok|exception)/i.test(l))
211    if (errors.length === 0) errors = lines
212    return (
213      errors.slice(0, lim).join('\n') + (errors.length > lim ? '\n...' : '')
214    )
215  }
216
217  static getExitCodeInfo(exitCode: number | null): string | undefined {
218    return EXIT_CODES[exitCode as keyof typeof EXIT_CODES]
219  }
220
221  static getCallerLocationFromString(stackString = 'unknown'): string {
222    const lines = stackString
223      .split(/^\s*(at\s)?/m)
224      .filter((s) => s?.includes(':'))
225    const i = lines.findIndex((l) => l.includes('Proxy.set'))
226    const offset = i < 0 ? i : i + 2
227
228    return (
229      lines.find((l) => l.includes('file://')) ||
230      lines[offset] ||
231      stackString
232    ).trim()
233  }
234
235  static getCallerLocation(err: Error = new Error('zx error')): string {
236    return Fail.getCallerLocationFromString(err.stack)
237  }
238
239  static getErrnoMessage(errno?: number): string {
240    return (
241      ERRNO_CODES[-(errno as number) as keyof typeof ERRNO_CODES] ||
242      'Unknown error'
243    )
244  }
245}