format import { ChildProcessWithoutNullStreams } from 'child_process' ; import { DeviceLogEntry , DeviceLogLevel , DeviceType } from 'flipper-common' ; import { Transform } from 'stream' ; import { DeviceListener } from '../../utils/DeviceListener' ; import { ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB , IOSBridge } from './IOSBridge' ; import JSONStream from 'JSONStream' ; import split2 from 'split2' ; class StripLogPrefix extends Transform { passedPrefix = false ; _transform ( data , _encoding , callback ) { if ( this . passedPrefix ) { this . push ( data ) ; } else { const dataString = data . toString ( ) ; const index = dataString . indexOf ( '[' ) ; if ( index >= 0 ) { this . push ( dataString . substring ( index ) ) ; this . passedPrefix = true ; } } callback ( ) ; } } type IOSLogLevel = 'Default' | 'Info' | 'Debug' | 'Error' | 'Fault' ; type RawLogEntry = { eventMessage : string ; machTimestamp : number ; messageType : IOSLogLevel ; processID : number ; processImagePath : string ; processImageUUID : string ; processUniqueID : number ; senderImagePath : string ; senderImageUUID : string ; senderProgramCounter : number ; threadID : number ; timestamp : string ; timezoneName : string ; traceID : string ; } ; const logRegex = /(^.{15}) ([^ ]+?) ([^\[]+?)\[(\d+?)\] <(\w+?)>: (.*)$/s ; export class iOSLogListener extends DeviceListener { constructor ( isDeviceConnected , private onNewLogEntry , private readonly iOSBridge , private readonly serial , private readonly deviceType ) { super ( isDeviceConnected ) ; } protected async startListener ( ) { let log ; try { log = this . iOSBridge . startLogListener ( this . serial , this . deviceType ) ; } catch ( e ) { if ( e . message === ERR_PHYSICAL_DEVICE_LOGS_WITHOUT_IDB ) { console . warn ( e ) ; return ( ) => { } ; } else { throw e ; } } log . on ( 'error' , ( err ) => { console . error ( 'iOS log tailer error' , err ) ; this . _state . set ( 'fatal' , err ) ; } ) ; log . stderr . on ( 'data' , ( data ) => { console . warn ( 'iOS log tailer stderr: ' , data . toString ( ) ) ; } ) ; if ( this . deviceType === 'physical' ) { log . stdout . pipe ( split2 ( '\0' ) ) . on ( 'data' , ( line ) => { const parsed = iOSLogListener . parseLogLine ( line ) ; if ( parsed ) { this . onNewLogEntry ( parsed ) ; } else { console . warn ( 'Failed to parse iOS log line: ' , line ) ; } } ) ; } else { log . stdout . pipe ( new StripLogPrefix ( ) ) . pipe ( JSONStream . parse ( '*' ) ) . on ( 'data' , ( data ) => { const entry = iOSLogListener . parseJsonLogEntry ( data ) ; this . onNewLogEntry ( entry ) ; } ) ; } return ( ) => { log . kill ( ) ; } ; } static parseJsonLogEntry ( entry ) { let type = iOSLogListener . getLogLevel ( entry . messageType ) ; if ( entry . eventMessage . startsWith ( '[debug]' ) ) { type = 'debug' ; } else if ( entry . eventMessage . startsWith ( '[info]' ) ) { type = 'info' ; } else if ( entry . eventMessage . startsWith ( '[warn]' ) ) { type = 'warn' ; } else if ( entry . eventMessage . startsWith ( '[error]' ) ) { type = 'error' ; } entry . eventMessage = entry . eventMessage . replace ( /^\[(debug|info|warn|error)\]/ , '' ) ; const tag = entry . processImagePath . split ( '/' ) . pop ( ) || '' ; return { date : new Date ( entry . timestamp ) , pid : entry . processID , tid : entry . threadID , tag , message : entry . eventMessage , type , } ; } static getLogLevel ( level ) { switch ( level ) { case 'Default' : return 'debug' ; case 'Info' : return 'info' ; case 'Debug' : return 'debug' ; case 'Error' : return 'error' ; case 'Notice' : return 'verbose' ; case 'Fault' : return 'fatal' ; default : return 'unknown' ; } } static parseLogLine ( line ) { const matches = line . match ( logRegex ) ; if ( matches ) { return { date : new Date ( Date . parse ( matches [ 1 ] ) ) , tag : matches [ 3 ] , tid : 0 , pid : parseInt ( matches [ 4 ] , 10 ) , type : iOSLogListener . getLogLevel ( matches [ 5 ] ) , message : matches [ 6 ] , } ; } return undefined ; } }