1c4ef02aeSEvan Baconimport chalk from 'chalk'; 2c4ef02aeSEvan Baconimport { ChildProcessWithoutNullStreams, spawn } from 'child_process'; 3c4ef02aeSEvan Baconimport { EOL } from 'os'; 4c4ef02aeSEvan Baconimport path from 'path'; 5c4ef02aeSEvan Baconimport wrapAnsi from 'wrap-ansi'; 6c4ef02aeSEvan Bacon 7*8a424bebSJames Ideimport { Device, getContainerPathAsync } from './simctl'; 8c4ef02aeSEvan Baconimport * as Log from '../../../log'; 9c4ef02aeSEvan Baconimport { CommandError } from '../../../utils/errors'; 10c4ef02aeSEvan Baconimport { installExitHooks } from '../../../utils/exit'; 11c4ef02aeSEvan Bacon 12c4ef02aeSEvan Baconexport type SimControlLog = { 13c4ef02aeSEvan Bacon /** 14c4ef02aeSEvan Bacon * 258753568922927108 15c4ef02aeSEvan Bacon */ 16c4ef02aeSEvan Bacon traceID: number; 17c4ef02aeSEvan Bacon /** 18c4ef02aeSEvan Bacon * 19c4ef02aeSEvan Bacon * "Connection 1: done", 20c4ef02aeSEvan Bacon */ 21c4ef02aeSEvan Bacon eventMessage: string; 22c4ef02aeSEvan Bacon /** 23c4ef02aeSEvan Bacon * "logEvent" | "activityCreateEvent", 24c4ef02aeSEvan Bacon */ 25c4ef02aeSEvan Bacon eventType: 'logEvent' | 'activityCreateEvent'; 26c4ef02aeSEvan Bacon source: null | { 27c4ef02aeSEvan Bacon /** 28c4ef02aeSEvan Bacon * 'RCTDefaultLogFunction_block_invoke' | '__TCC_CRASHING_DUE_TO_PRIVACY_VIOLATION__' 29c4ef02aeSEvan Bacon */ 30c4ef02aeSEvan Bacon symbol: string; 31c4ef02aeSEvan Bacon line: number; 32c4ef02aeSEvan Bacon /** 33c4ef02aeSEvan Bacon * 'TCC' | 'Security' | 'CFNetwork' | 'libnetwork.dylib' | 'myapp' 34c4ef02aeSEvan Bacon * 35c4ef02aeSEvan Bacon * TCC is apple sys, it means "Transparency, Consent, and Control" 36c4ef02aeSEvan Bacon */ 37c4ef02aeSEvan Bacon image: string; 38c4ef02aeSEvan Bacon /** 39c4ef02aeSEvan Bacon * 'RCTLog.mm' | '' 40c4ef02aeSEvan Bacon */ 41c4ef02aeSEvan Bacon file: string; 42c4ef02aeSEvan Bacon }; 43c4ef02aeSEvan Bacon /** 44c4ef02aeSEvan Bacon * "Connection %llu: done" 45c4ef02aeSEvan Bacon */ 46c4ef02aeSEvan Bacon formatString: string; 47c4ef02aeSEvan Bacon /** 48c4ef02aeSEvan Bacon * 0 49c4ef02aeSEvan Bacon */ 50c4ef02aeSEvan Bacon activityIdentifier: number; 51c4ef02aeSEvan Bacon subsystem: 52c4ef02aeSEvan Bacon | '' 53c4ef02aeSEvan Bacon | 'com.apple.network' 54c4ef02aeSEvan Bacon | 'com.facebook.react.log' 55c4ef02aeSEvan Bacon | 'com.apple.TCC' 56c4ef02aeSEvan Bacon | 'com.apple.CoreTelephony' 57c4ef02aeSEvan Bacon | 'com.apple.WebKit' 58c4ef02aeSEvan Bacon | 'com.apple.runningboard' 59c4ef02aeSEvan Bacon | string; 60c4ef02aeSEvan Bacon category: '' | 'access' | 'connection' | 'plugin'; 61c4ef02aeSEvan Bacon /** 62c4ef02aeSEvan Bacon * "2021-03-15 15:36:28.004331-0700" 63c4ef02aeSEvan Bacon */ 64c4ef02aeSEvan Bacon timestamp: string; 65c4ef02aeSEvan Bacon /** 66c4ef02aeSEvan Bacon * 706567072091713 67c4ef02aeSEvan Bacon */ 68c4ef02aeSEvan Bacon machTimestamp: number; 69c4ef02aeSEvan Bacon /** 70c4ef02aeSEvan Bacon * "Default" 71c4ef02aeSEvan Bacon */ 72c4ef02aeSEvan Bacon messageType: 'Default' | 'Error'; 73c4ef02aeSEvan Bacon /** 74c4ef02aeSEvan Bacon * 15192 75c4ef02aeSEvan Bacon */ 76c4ef02aeSEvan Bacon processID: number; 77c4ef02aeSEvan Bacon}; 78c4ef02aeSEvan Bacon 79c4ef02aeSEvan Bacontype ProcessResolver = 80c4ef02aeSEvan Bacon | { 81c4ef02aeSEvan Bacon pid: string; 82c4ef02aeSEvan Bacon } 83c4ef02aeSEvan Bacon | { 84c4ef02aeSEvan Bacon appId: string; 85c4ef02aeSEvan Bacon }; 86c4ef02aeSEvan Bacon 87c4ef02aeSEvan Baconexport class SimulatorLogStreamer { 88c4ef02aeSEvan Bacon private childProcess: ChildProcessWithoutNullStreams | null = null; 89c4ef02aeSEvan Bacon 90c4ef02aeSEvan Bacon static cache: SimulatorLogStreamer[] = []; 91c4ef02aeSEvan Bacon 92c4ef02aeSEvan Bacon static getStreamer = (device: Pick<Device, 'udid'>, resolver: ProcessResolver) => { 93c4ef02aeSEvan Bacon return ( 94c4ef02aeSEvan Bacon SimulatorLogStreamer.cache.find((streamer) => streamer.device.udid === device.udid) ?? 95c4ef02aeSEvan Bacon new SimulatorLogStreamer(device, resolver) 96c4ef02aeSEvan Bacon ); 97c4ef02aeSEvan Bacon }; 98c4ef02aeSEvan Bacon 99*8a424bebSJames Ide constructor( 100*8a424bebSJames Ide public device: Pick<Device, 'udid'>, 101*8a424bebSJames Ide public resolver: ProcessResolver 102*8a424bebSJames Ide ) {} 103c4ef02aeSEvan Bacon 104c4ef02aeSEvan Bacon isAttached() { 105c4ef02aeSEvan Bacon return !!this.childProcess; 106c4ef02aeSEvan Bacon } 107c4ef02aeSEvan Bacon 108c4ef02aeSEvan Bacon async resolvePidAsync() { 109c4ef02aeSEvan Bacon if ('pid' in this.resolver) { 110c4ef02aeSEvan Bacon return this.resolver.pid; 111c4ef02aeSEvan Bacon } 112c4ef02aeSEvan Bacon return getImageNameFromBundleIdentifierAsync(this.device.udid, this.resolver.appId); 113c4ef02aeSEvan Bacon } 114c4ef02aeSEvan Bacon 115c4ef02aeSEvan Bacon async attachAsync() { 116c4ef02aeSEvan Bacon await this.detachAsync(); 117c4ef02aeSEvan Bacon 118c4ef02aeSEvan Bacon const pid = await this.resolvePidAsync(); 119c4ef02aeSEvan Bacon 120c4ef02aeSEvan Bacon if (!pid) { 121c4ef02aeSEvan Bacon throw new CommandError(`Could not find pid for ${this.device.udid}`); 122c4ef02aeSEvan Bacon } 123c4ef02aeSEvan Bacon 124c4ef02aeSEvan Bacon // xcrun simctl spawn booted log stream --process --style json 125c4ef02aeSEvan Bacon this.childProcess = spawn('xcrun', [ 126c4ef02aeSEvan Bacon 'simctl', 127c4ef02aeSEvan Bacon 'spawn', 128c4ef02aeSEvan Bacon this.device.udid, 129c4ef02aeSEvan Bacon 'log', 130c4ef02aeSEvan Bacon 'stream', 131c4ef02aeSEvan Bacon '--process', 132c4ef02aeSEvan Bacon pid, 133c4ef02aeSEvan Bacon // ndjson provides a better format than json. 134c4ef02aeSEvan Bacon '--style', 135c4ef02aeSEvan Bacon 'ndjson', 136c4ef02aeSEvan Bacon // Provide the source so we can filter logs better 137c4ef02aeSEvan Bacon '--source', 138c4ef02aeSEvan Bacon // log, activity, trace -- activity was related to layouts, trace didn't work, so that leaves log. 139c4ef02aeSEvan Bacon // Passing nothing combines all three, but we don't use activity. 140c4ef02aeSEvan Bacon '--type', 141c4ef02aeSEvan Bacon 'log', 142c4ef02aeSEvan Bacon // backtrace doesn't seem very useful in basic cases. 143c4ef02aeSEvan Bacon // TODO: Maybe we can format as a stack trace for native errors. 144c4ef02aeSEvan Bacon '--no-backtrace', 145c4ef02aeSEvan Bacon ]); 146c4ef02aeSEvan Bacon 147c4ef02aeSEvan Bacon this.childProcess.stdout.on('data', (data: Buffer) => { 148c4ef02aeSEvan Bacon // Sometimes more than one chunk comes at a time, here we split by system newline, 149c4ef02aeSEvan Bacon // then trim and filter. 150c4ef02aeSEvan Bacon const strings = data 151c4ef02aeSEvan Bacon .toString() 152c4ef02aeSEvan Bacon .split(EOL) 153c4ef02aeSEvan Bacon .map((value) => value.trim()) 154c4ef02aeSEvan Bacon // This filters out the first log which says something like: 155c4ef02aeSEvan Bacon // Filtering the log data using "process BEGINSWITH[cd] "my-app" AND type == 1024" 156c4ef02aeSEvan Bacon .filter((value) => value.startsWith('{')); 157c4ef02aeSEvan Bacon 158c4ef02aeSEvan Bacon strings.forEach((str) => { 159c4ef02aeSEvan Bacon const simLog = parseMessageJson(str); 160c4ef02aeSEvan Bacon if (!simLog) { 161c4ef02aeSEvan Bacon return; 162c4ef02aeSEvan Bacon } 163c4ef02aeSEvan Bacon onMessage(simLog); 164c4ef02aeSEvan Bacon }); 165c4ef02aeSEvan Bacon }); 166c4ef02aeSEvan Bacon 167c4ef02aeSEvan Bacon this.childProcess.on('error', ({ message }) => { 168c4ef02aeSEvan Bacon Log.debug('[simctl error]:', message); 169c4ef02aeSEvan Bacon }); 170c4ef02aeSEvan Bacon 171c4ef02aeSEvan Bacon this.off = installExitHooks(() => { 172c4ef02aeSEvan Bacon this.detachAsync.bind(this); 173c4ef02aeSEvan Bacon }); 174c4ef02aeSEvan Bacon } 175c4ef02aeSEvan Bacon 176c4ef02aeSEvan Bacon private off: (() => void) | null = null; 177c4ef02aeSEvan Bacon 178c4ef02aeSEvan Bacon detachAsync() { 179c4ef02aeSEvan Bacon this.off?.(); 180c4ef02aeSEvan Bacon this.off = null; 181c4ef02aeSEvan Bacon if (this.childProcess) { 182c4ef02aeSEvan Bacon return new Promise<void>((resolve) => { 183c4ef02aeSEvan Bacon this.childProcess?.on('close', resolve); 184c4ef02aeSEvan Bacon this.childProcess?.kill(); 185c4ef02aeSEvan Bacon this.childProcess = null; 186c4ef02aeSEvan Bacon }); 187c4ef02aeSEvan Bacon } 188c4ef02aeSEvan Bacon return Promise.resolve(); 189c4ef02aeSEvan Bacon } 190c4ef02aeSEvan Bacon} 191c4ef02aeSEvan Bacon 192c4ef02aeSEvan Baconfunction parseMessageJson(data: string) { 193c4ef02aeSEvan Bacon const stringData = data.toString(); 194c4ef02aeSEvan Bacon try { 195c4ef02aeSEvan Bacon return JSON.parse(stringData) as SimControlLog; 196c4ef02aeSEvan Bacon } catch { 197c4ef02aeSEvan Bacon Log.debug('Failed to parse simctl JSON message:\n' + stringData); 198c4ef02aeSEvan Bacon } 199c4ef02aeSEvan Bacon return null; 200c4ef02aeSEvan Bacon} 201c4ef02aeSEvan Bacon 202c4ef02aeSEvan Bacon// There are a lot of networking logs in RN that aren't relevant to the user. 203c4ef02aeSEvan Baconfunction isNetworkLog(simLog: SimControlLog): boolean { 204c4ef02aeSEvan Bacon return ( 205c4ef02aeSEvan Bacon simLog.subsystem === 'com.apple.network' || 206c4ef02aeSEvan Bacon simLog.category === 'connection' || 207c4ef02aeSEvan Bacon simLog.source?.image === 'CFNetwork' 208c4ef02aeSEvan Bacon ); 209c4ef02aeSEvan Bacon} 210c4ef02aeSEvan Bacon 211c4ef02aeSEvan Baconfunction isReactLog(simLog: SimControlLog): boolean { 212c4ef02aeSEvan Bacon return simLog.subsystem === 'com.facebook.react.log' && simLog.source?.file === 'RCTLog.mm'; 213c4ef02aeSEvan Bacon} 214c4ef02aeSEvan Bacon 215c4ef02aeSEvan Bacon// It's not clear what these are but they aren't very useful. 216c4ef02aeSEvan Bacon// (The connection to service on pid 0 named com.apple.commcenter.coretelephony.xpc was invalidated) 217c4ef02aeSEvan Bacon// We can add them later if need. 218c4ef02aeSEvan Baconfunction isCoreTelephonyLog(simLog: SimControlLog): boolean { 219c4ef02aeSEvan Bacon // [CoreTelephony] Updating selectors failed with: Error Domain=NSCocoaErrorDomain Code=4099 220c4ef02aeSEvan Bacon // "The connection to service on pid 0 named com.apple.commcenter.coretelephony.xpc was invalidated." UserInfo={NSDebugDescription=The connection to service on pid 0 named com.apple.commcenter.coretelephony.xpc was invalidated.} 221c4ef02aeSEvan Bacon return simLog.subsystem === 'com.apple.CoreTelephony'; 222c4ef02aeSEvan Bacon} 223c4ef02aeSEvan Bacon 224c4ef02aeSEvan Bacon// https://stackoverflow.com/a/65313219/4047926 225c4ef02aeSEvan Baconfunction isWebKitLog(simLog: SimControlLog): boolean { 226c4ef02aeSEvan Bacon // [WebKit] 0x1143ca500 - ProcessAssertion: Failed to acquire RBS Background assertion 'WebProcess Background Assertion' for process with PID 27084, error: Error Domain=RBSAssertionErrorDomain Code=3 "Target is not running or required target 227c4ef02aeSEvan Bacon // entitlement is missing" UserInfo={RBSAssertionAttribute=<RBSDomainAttribute| domain:"com.apple.webkit" name:"Background" sourceEnvironment:"(null)">, NSLocalizedFailureReason=Target is not running or required target entitlement is missing} 228c4ef02aeSEvan Bacon return simLog.subsystem === 'com.apple.WebKit'; 229c4ef02aeSEvan Bacon} 230c4ef02aeSEvan Bacon 231c4ef02aeSEvan Bacon// Similar to WebKit logs 232c4ef02aeSEvan Baconfunction isRunningBoardServicesLog(simLog: SimControlLog): boolean { 233c4ef02aeSEvan Bacon // [RunningBoardServices] Error acquiring assertion: <Error Domain=RBSAssertionErrorDomain Code=3 "Target is not running or required target entitlement is missing" UserInfo={RBSAssertionAttribute=<RBSDomainAttribute| domain:"com.apple.webkit" 234c4ef02aeSEvan Bacon // name:"Background" sourceEnvironment:"(null)">, NSLocalizedFailureReason=Target is not running or required target entitlement is missing}> 235c4ef02aeSEvan Bacon return simLog.subsystem === 'com.apple.runningboard'; 236c4ef02aeSEvan Bacon} 237c4ef02aeSEvan Bacon 238c4ef02aeSEvan Baconfunction formatMessage(simLog: SimControlLog): string { 239c4ef02aeSEvan Bacon // TODO: Maybe change "TCC" to "Consent" or "System". 240c4ef02aeSEvan Bacon const category = chalk.gray(`[${simLog.source?.image ?? simLog.subsystem}]`); 241c4ef02aeSEvan Bacon const message = simLog.eventMessage; 242c4ef02aeSEvan Bacon return wrapAnsi(category + ' ' + message, process.stdout.columns || 80); 243c4ef02aeSEvan Bacon} 244c4ef02aeSEvan Bacon 245c4ef02aeSEvan Baconexport function onMessage(simLog: SimControlLog) { 246c4ef02aeSEvan Bacon let hasLogged = false; 247c4ef02aeSEvan Bacon 248c4ef02aeSEvan Bacon if (simLog.messageType === 'Error') { 249c4ef02aeSEvan Bacon if ( 250c4ef02aeSEvan Bacon // Hide all networking errors which are mostly useless. 251c4ef02aeSEvan Bacon !isNetworkLog(simLog) && 252c4ef02aeSEvan Bacon // Showing React errors will result in duplicate messages. 253c4ef02aeSEvan Bacon !isReactLog(simLog) && 254c4ef02aeSEvan Bacon !isCoreTelephonyLog(simLog) && 255c4ef02aeSEvan Bacon !isWebKitLog(simLog) && 256c4ef02aeSEvan Bacon !isRunningBoardServicesLog(simLog) 257c4ef02aeSEvan Bacon ) { 258c4ef02aeSEvan Bacon hasLogged = true; 259c4ef02aeSEvan Bacon // Sim: This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data. 260c4ef02aeSEvan Bacon Log.error(formatMessage(simLog)); 261c4ef02aeSEvan Bacon } 262c4ef02aeSEvan Bacon } else if (simLog.eventMessage) { 263c4ef02aeSEvan Bacon // If the source has a file (i.e. not a system log). 264c4ef02aeSEvan Bacon if ( 265c4ef02aeSEvan Bacon simLog.source?.file || 266c4ef02aeSEvan Bacon simLog.eventMessage.includes('Terminating app due to uncaught exception') 267c4ef02aeSEvan Bacon ) { 268c4ef02aeSEvan Bacon hasLogged = true; 269c4ef02aeSEvan Bacon Log.log(formatMessage(simLog)); 270c4ef02aeSEvan Bacon } 271c4ef02aeSEvan Bacon } 272c4ef02aeSEvan Bacon 273c4ef02aeSEvan Bacon if (!hasLogged) { 274c4ef02aeSEvan Bacon Log.debug(formatMessage(simLog)); 275c4ef02aeSEvan Bacon } else { 276c4ef02aeSEvan Bacon // console.log('DATA:', JSON.stringify(simLog)); 277c4ef02aeSEvan Bacon } 278c4ef02aeSEvan Bacon} 279c4ef02aeSEvan Bacon 280c4ef02aeSEvan Bacon/** 281c4ef02aeSEvan Bacon * 282c4ef02aeSEvan Bacon * @param udid 283c4ef02aeSEvan Bacon * @param bundleIdentifier 284c4ef02aeSEvan Bacon * @returns Image name like `Exponent` and `null` when the app is not installed on the provided simulator. 285c4ef02aeSEvan Bacon */ 286c4ef02aeSEvan Baconasync function getImageNameFromBundleIdentifierAsync( 287c4ef02aeSEvan Bacon udid: string, 288c4ef02aeSEvan Bacon bundleIdentifier: string 289c4ef02aeSEvan Bacon): Promise<string | null> { 290c4ef02aeSEvan Bacon const containerPath = await getContainerPathAsync({ udid }, { appId: bundleIdentifier }); 291c4ef02aeSEvan Bacon 292c4ef02aeSEvan Bacon if (containerPath) { 293c4ef02aeSEvan Bacon return getImageNameFromContainerPath(containerPath); 294c4ef02aeSEvan Bacon } 295c4ef02aeSEvan Bacon return null; 296c4ef02aeSEvan Bacon} 297c4ef02aeSEvan Bacon 298c4ef02aeSEvan Baconfunction getImageNameFromContainerPath(binaryPath: string): string { 299c4ef02aeSEvan Bacon return path.basename(binaryPath).split('.')[0]; 300c4ef02aeSEvan Bacon} 301