Files
flipper/desktop/flipper-server-core/src/devices/ios/IOSDevice.tsx
Lawrence Lomax 3a9c875e03 Avoid double recording in iOSDevice
Summary: This should in theory never happen since the state of the recording is also managed within the UI. However, it's better to ensure that if `startRecording` is called twice, without calling `stopRecording` first it will error rather than leak a recording (the first process will still run but the object representing it is oprhaned)

Reviewed By: passy

Differential Revision: D33913298

fbshipit-source-id: f8fa6b0e4fdbdf4282633fa29a736d3e7dedf3bb
2022-02-01 04:57:16 -08:00

133 lines
3.6 KiB
TypeScript

/**
* Copyright (c) Meta Platforms, Inc. and 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, timeout} from 'flipper-common';
import child_process, {ChildProcess} from 'child_process';
import {IOSBridge} from './IOSBridge';
import {ServerDevice} from '../ServerDevice';
import {FlipperServerImpl} from '../../FlipperServerImpl';
import {iOSCrashWatcher} from './iOSCrashUtils';
import {iOSLogListener} from './iOSLogListener';
export default class IOSDevice extends ServerDevice {
log?: child_process.ChildProcessWithoutNullStreams;
buffer: string;
private recording?: {process: ChildProcess; destination: string};
private iOSBridge: IOSBridge;
readonly logListener: iOSLogListener;
readonly crashWatcher: iOSCrashWatcher;
constructor(
flipperServer: FlipperServerImpl,
iOSBridge: IOSBridge,
serial: string,
deviceType: DeviceType,
title: string,
) {
super(flipperServer, {
serial,
deviceType,
title,
os: 'iOS',
icon: 'mobile',
});
this.buffer = '';
this.iOSBridge = iOSBridge;
this.logListener = new iOSLogListener(
() => this.connected,
(logEntry) => this.addLogEntry(logEntry),
this.iOSBridge,
this.serial,
this.info.deviceType,
);
// It is OK not to await the start of the log listener. We just spawn it and handle errors internally.
this.logListener
.start()
.catch((e) =>
console.error('IOSDevice.logListener.start -> unexpected error', e),
);
this.crashWatcher = new iOSCrashWatcher(this);
// It is OK not to await the start of the crash watcher. We just spawn it and handle errors internally.
this.crashWatcher
.start()
.catch((e) =>
console.error('IOSDevice.crashWatcher.start -> unexpected error', e),
);
}
async screenshot(): Promise<Buffer> {
if (!this.connected) {
return Buffer.from([]);
}
return await this.iOSBridge.screenshot(this.serial);
}
async navigateToLocation(location: string) {
return this.iOSBridge.navigate(this.serial, location).catch((err) => {
console.warn(`Failed to navigate to location ${location}:`, err);
return err;
});
}
async screenCaptureAvailable() {
return this.info.deviceType === 'emulator' && this.connected;
}
async startScreenCapture(destination: string) {
const recording = this.recording;
if (recording) {
throw new Error(
`There is already an active recording at ${recording.destination}`,
);
}
const process = this.iOSBridge.recordVideo(this.serial, destination);
this.recording = {process, destination};
}
async stopScreenCapture(): Promise<string> {
const recording = this.recording;
if (!recording) {
throw new Error('No recording in progress');
}
const prom = new Promise<void>((resolve, _reject) => {
recording.process.on(
'exit',
async (_code: number | null, _signal: NodeJS.Signals | null) => {
resolve();
},
);
recording.process.kill('SIGINT');
});
const output: string = await timeout<void>(
5000,
prom,
'Timed out to stop a screen capture.',
)
.then(() => {
this.recording = undefined;
return recording.destination;
})
.catch((e) => {
this.recording = undefined;
console.warn('Failed to terminate iOS screen recording:', e);
throw e;
});
return output;
}
disconnect() {
if (this.recording) {
this.stopScreenCapture();
}
super.disconnect();
}
}