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}