Initial commit 🎉

fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2
Co-authored-by: Sebastian McKenzie <sebmck@fb.com>
Co-authored-by: John Knox <jknox@fb.com>
Co-authored-by: Emil Sjölander <emilsj@fb.com>
Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
Daniel Büchele
2018-04-13 08:38:06 -07:00
committed by Daniel Buchele
commit fbbf8cf16b
659 changed files with 87130 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {DeviceType, DeviceShell, DeviceLogListener} from './BaseDevice.js';
import {Priority} from 'adbkit-logcat-fb';
import child_process from 'child_process';
// TODO
import BaseDevice from './BaseDevice.js';
type ADBClient = any;
export default class AndroidDevice extends BaseDevice {
constructor(
serial: string,
deviceType: DeviceType,
title: string,
adb: ADBClient,
) {
super(serial, deviceType, title);
this.adb = adb;
if (deviceType == 'physical') {
this.supportedPlugins.push('DeviceCPU');
}
}
supportedPlugins = [
'DeviceLogs',
'DeviceShell',
'DeviceFiles',
'DeviceScreen',
];
icon = 'icons/android.svg';
os = 'Android';
adb: ADBClient;
pidAppMapping: {[key: number]: string} = {};
supportedColumns(): Array<string> {
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
}
addLogListener(callback: DeviceLogListener) {
this.adb.openLogcat(this.serial).then(reader => {
reader.on('entry', async entry => {
let type = 'unknown';
if (entry.priority === Priority.VERBOSE) {
type = 'verbose';
}
if (entry.priority === Priority.DEBUG) {
type = 'debug';
}
if (entry.priority === Priority.INFO) {
type = 'info';
}
if (entry.priority === Priority.WARN) {
type = 'warn';
}
if (entry.priority === Priority.ERROR) {
type = 'error';
}
if (entry.priority === Priority.FATAL) {
type = 'fatal';
}
callback({
tag: entry.tag,
pid: entry.pid,
tid: entry.tid,
message: entry.message,
date: entry.date,
type,
});
});
});
}
reverse(): Promise<void> {
if (this.deviceType === 'physical') {
return this.adb
.reverse(this.serial, 'tcp:8088', 'tcp:8088')
.then(_ => this.adb.reverse(this.serial, 'tcp:8089', 'tcp:8089'));
} else {
return Promise.resolve();
}
}
spawnShell(): DeviceShell {
return child_process.spawn('adb', ['-s', this.serial, 'shell', '-t', '-t']);
}
}

78
src/devices/BaseDevice.js Normal file
View File

@@ -0,0 +1,78 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type stream from 'stream';
import {SonarDevicePlugin} from 'sonar';
export type DeviceLogEntry = {
date: Date,
pid: number,
tid: number,
app?: string,
type: 'unknown' | 'verbose' | 'debug' | 'info' | 'warn' | 'error' | 'fatal',
tag: string,
message: string,
};
export type DeviceShell = {
stdout: stream.Readable,
stderr: stream.Readable,
stdin: stream.Writable,
};
export type DeviceLogListener = (entry: DeviceLogEntry) => void;
export type DeviceType = 'emulator' | 'physical';
export default class BaseDevice {
constructor(serial: string, deviceType: DeviceType, title: string) {
this.serial = serial;
this.title = title;
this.deviceType = deviceType;
}
// operating system of this device
os: string;
// human readable name for this device
title: string;
// type of this device
deviceType: DeviceType;
// serial number for this device
serial: string;
// supported device plugins for this platform
supportedPlugins: Array<string> = [];
// possible src of icon to display next to the device title
icon: ?string;
supportsPlugin(DevicePlugin: Class<SonarDevicePlugin<>>) {
return this.supportedPlugins.includes(DevicePlugin.id);
}
// ensure that we don't serialise devices
toJSON() {
return null;
}
teardown() {}
supportedColumns(): Array<string> {
throw new Error('unimplemented');
}
addLogListener(listener: DeviceLogListener) {
throw new Error('unimplemented');
}
spawnShell(): DeviceShell {
throw new Error('unimplemented');
}
}

162
src/devices/IOSDevice.js Normal file
View File

@@ -0,0 +1,162 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {
DeviceType,
DeviceLogEntry,
DeviceLogListener,
} from './BaseDevice.js';
import child_process from 'child_process';
import BaseDevice from './BaseDevice.js';
import JSONStream from 'JSONStream';
import {Transform} from 'stream';
type RawLogEntry = {
activityID: string, // Number in string format
eventMessage: string,
eventType: string,
machTimestamp: number,
processID: number,
processImagePath: string,
processImageUUID: string,
processUniqueID: number,
senderImagePath: string,
senderImageUUID: string,
senderProgramCounter: number,
threadID: number,
timestamp: string, // "2017-09-27 16:21:15.771213-0400"
timezoneName: string,
traceID: string,
};
export default class IOSDevice extends BaseDevice {
supportedPlugins = ['DeviceLogs'];
icon = 'icons/ios.svg';
os = 'iOS';
log: any;
buffer: string;
constructor(serial: string, deviceType: DeviceType, title: string) {
super(serial, deviceType, title);
this.buffer = '';
this.log = null;
}
teardown() {
if (this.log) {
this.log.kill();
}
}
supportedColumns(): Array<string> {
return ['date', 'pid', 'tid', 'tag', 'message', 'type', 'time'];
}
addLogListener(callback: DeviceLogListener) {
if (!this.log) {
this.log = child_process.spawn(
'xcrun',
[
'simctl',
'spawn',
'booted',
'log',
'stream',
'--style',
'json',
'--predicate',
'senderImagePath contains "Containers"',
'--info',
'--debug',
],
{},
);
this.log.on('error', err => {
console.error(err);
});
this.log.stderr.on('data', data => {
console.error(data.toString());
});
this.log.on('exit', () => {
this.log = null;
});
}
this.log.stdout
.pipe(new StripLogPrefix())
.pipe(JSONStream.parse('*'))
.on('data', (data: RawLogEntry) => {
callback(IOSDevice.parseLogEntry(data));
});
}
static parseLogEntry(entry: RawLogEntry): DeviceLogEntry {
let type = 'unknown';
if (entry.eventMessage.indexOf('[debug]') !== -1) {
type = 'debug';
} else if (entry.eventMessage.indexOf('[info]') !== -1) {
type = 'info';
} else if (entry.eventMessage.indexOf('[warn]') !== -1) {
type = 'warn';
} else if (entry.eventMessage.indexOf('[error]') !== -1) {
type = 'error';
}
// remove timestamp in front of message
entry.eventMessage = entry.eventMessage.replace(
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} /,
'',
);
// remove type from mesage
entry.eventMessage = entry.eventMessage.replace(
/^\[(debug|info|warn|error)\]/,
'',
);
const tags = entry.processImagePath.split('/');
const tag = tags[tags.length - 1];
return {
date: new Date(entry.timestamp),
pid: entry.processID,
tid: entry.threadID,
tag,
message: entry.eventMessage,
type,
};
}
}
// 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();
}
}

142
src/devices/OculusDevice.js Normal file
View File

@@ -0,0 +1,142 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {
DeviceType,
DeviceLogEntry,
DeviceLogListener,
} from './BaseDevice.js';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import BaseDevice from './BaseDevice.js';
function getLogsPath(fileName: ?string): string {
const dir = '/AppData/Local/Oculus/';
if (fileName) {
return path.join(os.homedir(), dir, fileName);
}
return path.join(os.homedir(), dir);
}
export default class OculusDevice extends BaseDevice {
supportedPlugins = ['DeviceLogs'];
icon = 'icons/oculus.png';
os = 'Oculus';
watcher: any;
processedFileMap: {};
watchedFile: ?string;
timer: TimeoutID;
constructor(serial: string, deviceType: DeviceType, title: string) {
super(serial, deviceType, title);
this.watcher = null;
this.processedFileMap = {};
}
teardown() {
clearTimeout(this.timer);
const file = this.watchedFile;
if (file) {
fs.unwatchFile(path.join(getLogsPath(), file));
}
}
supportedColumns(): Array<string> {
return ['date', 'tag', 'message', 'type', 'time'];
}
mapLogLevel(type: string): $PropertyType<DeviceLogEntry, 'type'> {
switch (type) {
case 'WARNING':
return 'warn';
case '!ERROR!':
return 'error';
case 'DEBUG':
return 'debug';
case 'INFO':
return 'info';
default:
return 'verbose';
}
}
processText(text: Buffer, callback: DeviceLogListener) {
text
.toString()
.split('\r\n')
.forEach(line => {
const regex = /(.*){(\S+)}\s*\[([\w :.\\]+)\](.*)/;
const match = regex.exec(line);
if (match && match.length === 5) {
callback({
tid: 0,
pid: 0,
date: new Date(Date.parse(match[1])),
type: this.mapLogLevel(match[2]),
tag: match[3],
message: match[4],
});
} else if (line.trim() === '') {
// skip
} else {
callback({
tid: 0,
pid: 0,
date: new Date(),
type: 'verbose',
tag: 'failed-parse',
message: line,
});
}
});
}
addLogListener = (callback: DeviceLogListener) => {
this.setupListener(callback);
};
async setupListener(callback: DeviceLogListener) {
const files = await fs.readdir(getLogsPath());
this.watchedFile = files
.filter(file => file.startsWith('Service_'))
.sort()
.pop();
this.watch(callback);
this.timer = setTimeout(() => this.checkForNewLog(callback), 5000);
}
watch(callback: DeviceLogListener) {
const filePath = getLogsPath(this.watchedFile);
fs.watchFile(filePath, async (current, previous) => {
const readLen = current.size - previous.size;
const buffer = new Buffer(readLen);
const fd = await fs.open(filePath, 'r');
await fs.read(fd, buffer, 0, readLen, previous.size);
this.processText(buffer, callback);
});
}
async checkForNewLog(callback: DeviceLogListener) {
const files = await fs.readdir(getLogsPath());
const latestLog = files
.filter(file => file.startsWith('Service_'))
.sort()
.pop();
if (this.watchedFile !== latestLog) {
const oldFilePath = getLogsPath(this.watchedFile);
fs.unwatchFile(oldFilePath);
this.watchedFile = latestLog;
this.watch(callback);
}
this.timer = setTimeout(() => this.checkForNewLog(callback), 5000);
}
}