Yarn workspaces
Summary: 1) moved "sonar/desktop/src" to "sonar/desktop/app/src", so "app" is now a separate package containing the core Flipper app code 2) Configured yarn workspaces with the root in "sonar/desktop": app, static, pkg, doctor, headless-tests. Plugins are not included for now, I plan to do this later. Reviewed By: jknoxville Differential Revision: D20535782 fbshipit-source-id: 600b2301960f37c7d72166e0d04eba462bec9fc1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
676d7bbd24
commit
863f89351e
249
desktop/app/src/devices/IOSDevice.tsx
Normal file
249
desktop/app/src/devices/IOSDevice.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {DeviceType, LogLevel, DeviceLogEntry} from './BaseDevice';
|
||||
import child_process, {ChildProcess} from 'child_process';
|
||||
import BaseDevice from './BaseDevice';
|
||||
import JSONStream from 'JSONStream';
|
||||
import {Transform} from 'stream';
|
||||
import electron from 'electron';
|
||||
import fs from 'fs';
|
||||
import {v1 as uuid} from 'uuid';
|
||||
import path from 'path';
|
||||
import {promisify} from 'util';
|
||||
import {exec} from 'child_process';
|
||||
import {default as promiseTimeout} from '../utils/promiseTimeout';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export default class IOSDevice extends BaseDevice {
|
||||
log: any;
|
||||
buffer: string;
|
||||
private recordingProcess?: ChildProcess;
|
||||
private recordingLocation?: string;
|
||||
|
||||
constructor(serial: string, deviceType: DeviceType, title: string) {
|
||||
super(serial, deviceType, title, 'iOS');
|
||||
this.icon = 'icons/ios.svg';
|
||||
this.buffer = '';
|
||||
this.log = this.startLogListener();
|
||||
}
|
||||
|
||||
screenshot(): Promise<Buffer> {
|
||||
const tmpImageName = uuid() + '.png';
|
||||
const tmpDirectory = (electron.app || electron.remote.app).getPath('temp');
|
||||
const tmpFilePath = path.join(tmpDirectory, tmpImageName);
|
||||
const command = `xcrun simctl io booted screenshot ${tmpFilePath}`;
|
||||
return promisify(exec)(command)
|
||||
.then(() => promisify(fs.readFile)(tmpFilePath))
|
||||
.then(buffer => {
|
||||
return promisify(fs.unlink)(tmpFilePath).then(() => buffer);
|
||||
});
|
||||
}
|
||||
|
||||
navigateToLocation(location: string) {
|
||||
const command = `xcrun simctl openurl booted "${location}"`;
|
||||
exec(command);
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if (this.log) {
|
||||
this.log.kill();
|
||||
}
|
||||
}
|
||||
|
||||
supportedColumns(): Array<string> {
|
||||
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
|
||||
}
|
||||
|
||||
startLogListener(retries: number = 3) {
|
||||
if (this.deviceType === 'physical') {
|
||||
return;
|
||||
}
|
||||
if (retries === 0) {
|
||||
console.error('Attaching iOS log listener continuously failed.');
|
||||
return;
|
||||
}
|
||||
if (!this.log) {
|
||||
const deviceSetPath = process.env.DEVICE_SET_PATH
|
||||
? ['--set', process.env.DEVICE_SET_PATH]
|
||||
: [];
|
||||
|
||||
this.log = child_process.spawn(
|
||||
'xcrun',
|
||||
[
|
||||
'simctl',
|
||||
...deviceSetPath,
|
||||
'spawn',
|
||||
'booted',
|
||||
'log',
|
||||
'stream',
|
||||
'--style',
|
||||
'json',
|
||||
'--predicate',
|
||||
'senderImagePath contains "Containers"',
|
||||
'--info',
|
||||
'--debug',
|
||||
],
|
||||
{},
|
||||
);
|
||||
|
||||
this.log.on('error', (err: Error) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
this.log.stderr.on('data', (data: Buffer) => {
|
||||
console.error(data.toString());
|
||||
});
|
||||
|
||||
this.log.on('exit', () => {
|
||||
this.log = null;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
this.log.stdout
|
||||
.pipe(new StripLogPrefix())
|
||||
.pipe(JSONStream.parse('*'))
|
||||
.on('data', (data: RawLogEntry) => {
|
||||
const entry = IOSDevice.parseLogEntry(data);
|
||||
this.addLogEntry(entry);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Could not parse iOS log stream.', e);
|
||||
// restart log stream
|
||||
this.log.kill();
|
||||
this.log = null;
|
||||
this.startLogListener(retries - 1);
|
||||
}
|
||||
}
|
||||
|
||||
static parseLogEntry(entry: RawLogEntry): DeviceLogEntry {
|
||||
const LOG_MAPPING: Map<IOSLogLevel, LogLevel> = new Map([
|
||||
['Default' as IOSLogLevel, 'debug' as LogLevel],
|
||||
['Info' as IOSLogLevel, 'info' as LogLevel],
|
||||
['Debug' as IOSLogLevel, 'debug' as LogLevel],
|
||||
['Error' as IOSLogLevel, 'error' as LogLevel],
|
||||
['Fault' as IOSLogLevel, 'fatal' as LogLevel],
|
||||
]);
|
||||
let type: LogLevel = LOG_MAPPING.get(entry.messageType) || 'unknown';
|
||||
|
||||
// when Apple log levels are not used, log messages can be prefixed with
|
||||
// their loglevel.
|
||||
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';
|
||||
}
|
||||
// remove type from mesage
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async screenCaptureAvailable() {
|
||||
return this.deviceType === 'emulator';
|
||||
}
|
||||
|
||||
async startScreenCapture(destination: string) {
|
||||
this.recordingProcess = exec(
|
||||
`xcrun simctl io booted recordVideo --codec=h264 --force ${destination}`,
|
||||
);
|
||||
this.recordingLocation = destination;
|
||||
}
|
||||
|
||||
async stopScreenCapture(): Promise<string | null> {
|
||||
if (this.recordingProcess && this.recordingLocation) {
|
||||
const prom = new Promise<void>((resolve, _reject) => {
|
||||
this.recordingProcess!.on(
|
||||
'exit',
|
||||
async (_code: number | null, _signal: NodeJS.Signals | null) => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
this.recordingProcess!.kill('SIGINT');
|
||||
});
|
||||
|
||||
const output: string | null = await promiseTimeout<void>(
|
||||
5000,
|
||||
prom,
|
||||
'Timed out to stop a screen capture.',
|
||||
)
|
||||
.then(() => {
|
||||
const {recordingLocation} = this;
|
||||
this.recordingLocation = undefined;
|
||||
return recordingLocation!;
|
||||
})
|
||||
.catch(_e => {
|
||||
this.recordingLocation = undefined;
|
||||
console.error(_e);
|
||||
return null;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Used to strip the initial output of the logging utility where it prints out settings.
|
||||
// We know the log stream is json so it starts with an open brace.
|
||||
class StripLogPrefix extends Transform {
|
||||
passedPrefix = false;
|
||||
|
||||
_transform(
|
||||
data: any,
|
||||
encoding: string,
|
||||
callback: (err?: Error, data?: any) => void,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user