11#!/usr/bin/env node
22
33const { spawnSync } = require ( "node:child_process" ) ;
4- const fs = require ( "node:fs/promises" ) ;
4+ const fsp = require ( "node:fs/promises" ) ;
5+ const os = require ( "node:os" ) ;
56const path = require ( "node:path" ) ;
67
78const COMMON_FRAMEWORKS = [
@@ -64,6 +65,191 @@ function getSDKPath(platform) {
6465 return output . stdout . trim ( ) ;
6566}
6667
68+ function sleep ( ms ) {
69+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
70+ }
71+
72+ function findLLDB ( ) {
73+ const xcrun = spawnSync ( "xcrun" , [ "--find" , "lldb" ] , {
74+ stdio : [ "ignore" , "pipe" , "ignore" ] ,
75+ encoding : "utf8" ,
76+ } ) ;
77+
78+ if ( xcrun . status === 0 ) {
79+ const candidate = xcrun . stdout . trim ( ) ;
80+ if ( candidate ) {
81+ return candidate ;
82+ }
83+ }
84+
85+ return "lldb" ;
86+ }
87+
88+ function formatLLDBOutput ( output ) {
89+ return [ output . stdout , output . stderr ] . filter ( Boolean ) . join ( "" ) ;
90+ }
91+
92+ function runLLDBBacktrace ( exec , args ) {
93+ const lldb = findLLDB ( ) ;
94+ const output = spawnSync (
95+ lldb ,
96+ [
97+ "--batch" ,
98+ "--no-lldbinit" ,
99+ "-o" ,
100+ "run" ,
101+ "-k" ,
102+ "bt" ,
103+ "-k" ,
104+ "thread backtrace all" ,
105+ "--" ,
106+ exec ,
107+ ...args ,
108+ ] ,
109+ {
110+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
111+ encoding : "utf8" ,
112+ timeout : 120000 ,
113+ } ,
114+ ) ;
115+
116+ return {
117+ output,
118+ } ;
119+ }
120+
121+ function formatCrashReport ( reportPath , content ) {
122+ try {
123+ const lines = content . split ( / \r ? \n / ) ;
124+ const header = JSON . parse ( lines [ 0 ] ) ;
125+ const body = JSON . parse ( lines . slice ( 1 ) . join ( "\n" ) ) ;
126+ const faultingThreadIndex = body . faultingThread ?? 0 ;
127+ const thread = body . threads ?. [ faultingThreadIndex ] ;
128+ const usedImages = body . usedImages ?? [ ] ;
129+
130+ const imageNames = new Map ( ) ;
131+ for ( const image of usedImages ) {
132+ if ( typeof image . imageIndex === "number" && image . name ) {
133+ imageNames . set ( image . imageIndex , image . name ) ;
134+ }
135+ }
136+
137+ const linesOut = [
138+ `Crash report: ${ reportPath } ` ,
139+ `Process: ${ body . procName ?? header . app_name ?? "unknown" } ` ,
140+ `Timestamp: ${ header . timestamp ?? body . captureTime ?? "unknown" } ` ,
141+ `Exception: ${ body . exception ?. type ?? "unknown" } ${ body . exception ?. signal ?? "" } ` . trim ( ) ,
142+ `Termination: ${ body . termination ?. indicator ?? "unknown" } ` ,
143+ `Faulting thread: ${ faultingThreadIndex } ` ,
144+ ] ;
145+
146+ const frames = thread ?. frames ?? [ ] ;
147+ if ( frames . length > 0 ) {
148+ linesOut . push ( "Faulting thread frames:" ) ;
149+ for ( const [ index , frame ] of frames . entries ( ) ) {
150+ const image = imageNames . get ( frame . imageIndex ) ?? `image#${ frame . imageIndex ?? "?" } ` ;
151+ const symbol = frame . symbol ?? "<unknown>" ;
152+ const offset =
153+ typeof frame . symbolLocation === "number"
154+ ? ` + ${ frame . symbolLocation } `
155+ : typeof frame . imageOffset === "number"
156+ ? ` @ ${ frame . imageOffset } `
157+ : "" ;
158+ linesOut . push ( ` #${ index } ${ image } ${ symbol } ${ offset } ` ) ;
159+ }
160+ }
161+
162+ return linesOut . join ( "\n" ) ;
163+ } catch ( error ) {
164+ return [
165+ `Crash report: ${ reportPath } ` ,
166+ "Unable to parse crash report as JSON; showing raw excerpt instead." ,
167+ content . slice ( 0 , 4000 ) ,
168+ ] . join ( "\n" ) ;
169+ }
170+ }
171+
172+ async function findRecentCrashReport ( exec , startedAtMs ) {
173+ const reportsDir = path . join ( os . homedir ( ) , "Library" , "Logs" , "DiagnosticReports" ) ;
174+ const execName = path . basename ( exec ) ;
175+ const deadline = Date . now ( ) + 10000 ;
176+
177+ while ( Date . now ( ) < deadline ) {
178+ let entries = [ ] ;
179+ try {
180+ entries = await fsp . readdir ( reportsDir , { withFileTypes : true } ) ;
181+ } catch {
182+ return null ;
183+ }
184+
185+ const candidates = [ ] ;
186+ for ( const entry of entries ) {
187+ if ( ! entry . isFile ( ) || ! entry . name . endsWith ( ".ips" ) ) {
188+ continue ;
189+ }
190+ if ( ! entry . name . startsWith ( execName ) ) {
191+ continue ;
192+ }
193+
194+ const reportPath = path . join ( reportsDir , entry . name ) ;
195+ try {
196+ const stat = await fsp . stat ( reportPath ) ;
197+ if ( stat . mtimeMs + 1000 < startedAtMs ) {
198+ continue ;
199+ }
200+ candidates . push ( { reportPath, mtimeMs : stat . mtimeMs } ) ;
201+ } catch {
202+ // Ignore reports that disappear while polling.
203+ }
204+ }
205+
206+ candidates . sort ( ( a , b ) => b . mtimeMs - a . mtimeMs ) ;
207+ if ( candidates . length > 0 ) {
208+ const content = await fsp . readFile ( candidates [ 0 ] . reportPath , "utf8" ) ;
209+ return {
210+ reportPath : candidates [ 0 ] . reportPath ,
211+ content,
212+ } ;
213+ }
214+
215+ await sleep ( 1000 ) ;
216+ }
217+
218+ return null ;
219+ }
220+
221+ async function emitCrashDiagnostics ( exec , args , startedAtMs ) {
222+ console . error ( "Attempting crash diagnostics..." ) ;
223+
224+ const lldbResult = runLLDBBacktrace ( exec , args ) ;
225+ const lldbOutput = formatLLDBOutput ( lldbResult . output ) ;
226+
227+ if ( lldbOutput . trim ( ) ) {
228+ console . error ( "LLDB output:" ) ;
229+ console . error ( lldbOutput . trimEnd ( ) ) ;
230+ } else {
231+ console . error ( "LLDB produced no output." ) ;
232+ }
233+
234+ const attachDenied =
235+ lldbOutput . includes ( "Not allowed to attach to process" ) ||
236+ lldbOutput . includes ( "attach failed" ) ;
237+ if ( lldbResult . output . error ) {
238+ console . error ( `LLDB error: ${ lldbResult . output . error . message } ` ) ;
239+ }
240+ if ( attachDenied ) {
241+ console . error ( "LLDB attach was denied by macOS; falling back to DiagnosticReports." ) ;
242+ }
243+
244+ const crashReport = await findRecentCrashReport ( exec , startedAtMs ) ;
245+ if ( ! crashReport ) {
246+ console . error ( "No recent macOS crash report found in ~/Library/Logs/DiagnosticReports." ) ;
247+ return ;
248+ }
249+
250+ console . error ( formatCrashReport ( crashReport . reportPath , crashReport . content ) ) ;
251+ }
252+
67253const sdks = {
68254 macos : {
69255 path : getSDKPath ( "macosx" ) ,
@@ -126,8 +312,8 @@ async function main() {
126312 }
127313
128314 const typesDir = path . resolve ( __dirname , ".." , "packages" , sdkName , "types" ) ;
129- await fs . rm ( typesDir , { recursive : true , force : true } ) ;
130- await fs . mkdir ( typesDir , { recursive : true } ) ;
315+ await fsp . rm ( typesDir , { recursive : true , force : true } ) ;
316+ await fsp . mkdir ( typesDir , { recursive : true } ) ;
131317
132318 for ( const arch of Object . keys ( sdk . targets ) ) {
133319 // Use the matching arch binary when available, falling back to arm64.
@@ -154,7 +340,7 @@ async function main() {
154340
155341 let exec ;
156342 try {
157- await fs . access ( preferredExec ) ;
343+ await fsp . access ( preferredExec ) ;
158344 exec = preferredExec ;
159345 } catch {
160346 exec = fallbackExec ;
@@ -199,11 +385,15 @@ async function main() {
199385
200386 console . log ( `$ MetadataGenerator ${ args . join ( " " ) } ` ) ;
201387
388+ const startedAtMs = Date . now ( ) ;
202389 const output = spawnSync ( exec , args , {
203390 stdio : "inherit" ,
204391 } ) ;
205392
206393 if ( output . status !== 0 ) {
394+ if ( process . platform === "darwin" && output . signal ) {
395+ await emitCrashDiagnostics ( exec , args , startedAtMs ) ;
396+ }
207397 console . error ( `Failed to generate metadata for ${ sdkName } ${ arch } ` ) ;
208398 console . error ( `Command: ${ exec } ${ args . join ( " " ) } ` ) ;
209399 console . error ( `Exit code: ${ output . status } ` ) ;
0 commit comments