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