Move flipper-doctor check running to flipper-server-core

Summary: Per title. Two new server API's: get-healthchecks, and run-healtcheck. Types have all been moved to flipper-common, so that they can be used by doctor, server-core and ui-core packages. Since it were quite some, moved them into a FlipperDoctor namespace.

Reviewed By: nikoant

Differential Revision: D32720510

fbshipit-source-id: 37aa35cde6ebd58479cf0dffec5b7b2da6d22198
This commit is contained in:
Michel Weststrate
2021-12-08 04:25:28 -08:00
committed by Facebook GitHub Bot
parent 2a4fe77404
commit 2480ed30c5
19 changed files with 373 additions and 276 deletions

View File

@@ -16,6 +16,7 @@
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.27.1", "eslint-plugin-react": "^7.27.1",
"flipper-common": "0.0.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"ts-jest": "^26.5.6", "ts-jest": "^26.5.6",

View File

@@ -25,7 +25,7 @@ import {getEnvInfo} from './environmentInfo';
category.healthchecks.map(async ({key, label, run}) => ({ category.healthchecks.map(async ({key, label, run}) => ({
key, key,
label, label,
result: await run(environmentInfo), result: await run!(environmentInfo),
})), })),
), ),
}, },

View File

@@ -8,28 +8,7 @@
*/ */
import {run} from 'envinfo'; import {run} from 'envinfo';
import {FlipperDoctor} from 'flipper-common';
export type EnvironmentInfo = {
SDKs: {
'iOS SDK': {
Platforms: string[];
};
'Android SDK':
| {
'API Levels': string[] | 'Not Found';
'Build Tools': string[] | 'Not Found';
'System Images': string[] | 'Not Found';
'Android NDK': string | 'Not Found';
}
| 'Not Found';
};
IDEs: {
Xcode: {
version: string;
path: string;
};
};
};
async function retrieveAndParseEnvInfo(): Promise<any> { async function retrieveAndParseEnvInfo(): Promise<any> {
return JSON.parse( return JSON.parse(
@@ -43,6 +22,6 @@ async function retrieveAndParseEnvInfo(): Promise<any> {
); );
} }
export async function getEnvInfo(): Promise<EnvironmentInfo> { export async function getEnvInfo(): Promise<FlipperDoctor.EnvironmentInfo> {
return await retrieveAndParseEnvInfo(); return await retrieveAndParseEnvInfo();
} }

View File

@@ -9,65 +9,15 @@
import {exec} from 'child_process'; import {exec} from 'child_process';
import {promisify} from 'util'; import {promisify} from 'util';
import {EnvironmentInfo, getEnvInfo} from './environmentInfo'; import {getEnvInfo} from './environmentInfo';
export {EnvironmentInfo, getEnvInfo} from './environmentInfo'; export {getEnvInfo} from './environmentInfo';
import * as watchman from 'fb-watchman'; import * as watchman from 'fb-watchman';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import {FlipperDoctor} from 'flipper-common';
export type HealthcheckCategory = { export function getHealthchecks(): FlipperDoctor.Healthchecks {
label: string;
isSkipped: false;
isRequired: boolean;
healthchecks: Healthcheck[];
};
export type SkippedHealthcheckCategory = {
label: string;
isSkipped: true;
skipReason: string;
};
export type Healthchecks = {
common: HealthcheckCategory | SkippedHealthcheckCategory;
android: HealthcheckCategory | SkippedHealthcheckCategory;
ios: HealthcheckCategory | SkippedHealthcheckCategory;
};
export type Settings = {
idbPath: string;
enablePhysicalIOS: boolean;
};
export type Healthcheck = {
key: string;
label: string;
isRequired?: boolean;
run: (
env: EnvironmentInfo,
settings?: Settings,
) => Promise<HealthcheckRunResult>;
};
export type HealthcheckRunResult = {
hasProblem: boolean;
message: string;
};
export type CategoryResult = [
string,
{
label: string;
results: Array<{
key: string;
label: string;
isRequired: boolean;
result: {hasProblem: boolean};
}>;
},
];
export function getHealthchecks(): Healthchecks {
return { return {
common: { common: {
label: 'Common', label: 'Common',
@@ -77,7 +27,7 @@ export function getHealthchecks(): Healthchecks {
{ {
key: 'common.openssl', key: 'common.openssl',
label: 'OpenSSL Installed', label: 'OpenSSL Installed',
run: async (_: EnvironmentInfo) => { run: async (_: FlipperDoctor.EnvironmentInfo) => {
const result = await tryExecuteCommand('openssl version'); const result = await tryExecuteCommand('openssl version');
const hasProblem = result.hasProblem; const hasProblem = result.hasProblem;
const message = hasProblem const message = hasProblem
@@ -92,7 +42,7 @@ export function getHealthchecks(): Healthchecks {
{ {
key: 'common.watchman', key: 'common.watchman',
label: 'Watchman Installed', label: 'Watchman Installed',
run: async (_: EnvironmentInfo) => { run: async (_: FlipperDoctor.EnvironmentInfo) => {
const isAvailable = await isWatchmanAvailable(); const isAvailable = await isWatchmanAvailable();
return { return {
hasProblem: !isAvailable, hasProblem: !isAvailable,
@@ -113,11 +63,11 @@ export function getHealthchecks(): Healthchecks {
key: 'android.sdk', key: 'android.sdk',
label: 'SDK Installed', label: 'SDK Installed',
isRequired: true, isRequired: true,
run: async (_: EnvironmentInfo) => { run: async (_: FlipperDoctor.EnvironmentInfo) => {
const androidHome = process.env.ANDROID_HOME; const androidHome = process.env.ANDROID_HOME;
const androidSdkRoot = process.env.ANDROID_SDK_ROOT; const androidSdkRoot = process.env.ANDROID_SDK_ROOT;
let androidHomeResult: HealthcheckRunResult; let androidHomeResult: FlipperDoctor.HealthcheckRunResult;
if (!androidHome) { if (!androidHome) {
androidHomeResult = { androidHomeResult = {
hasProblem: true, hasProblem: true,
@@ -145,7 +95,7 @@ export function getHealthchecks(): Healthchecks {
return androidHomeResult; return androidHomeResult;
} }
let androidSdkRootResult: HealthcheckRunResult; let androidSdkRootResult: FlipperDoctor.HealthcheckRunResult;
if (!androidSdkRoot) { if (!androidSdkRoot) {
androidSdkRootResult = { androidSdkRootResult = {
hasProblem: true, hasProblem: true,
@@ -188,7 +138,7 @@ export function getHealthchecks(): Healthchecks {
key: 'ios.sdk', key: 'ios.sdk',
label: 'SDK Installed', label: 'SDK Installed',
isRequired: true, isRequired: true,
run: async (e: EnvironmentInfo) => { run: async (e: FlipperDoctor.EnvironmentInfo) => {
const hasProblem = const hasProblem =
!e.SDKs['iOS SDK'] || !e.SDKs['iOS SDK'] ||
!e.SDKs['iOS SDK'].Platforms || !e.SDKs['iOS SDK'].Platforms ||
@@ -208,7 +158,7 @@ export function getHealthchecks(): Healthchecks {
key: 'ios.xcode', key: 'ios.xcode',
label: 'XCode Installed', label: 'XCode Installed',
isRequired: true, isRequired: true,
run: async (e: EnvironmentInfo) => { run: async (e: FlipperDoctor.EnvironmentInfo) => {
const hasProblem = e.IDEs == null || e.IDEs.Xcode == null; const hasProblem = e.IDEs == null || e.IDEs.Xcode == null;
const message = hasProblem const message = hasProblem
? 'Xcode (https://developer.apple.com/xcode/) is not installed.' ? 'Xcode (https://developer.apple.com/xcode/) is not installed.'
@@ -223,7 +173,7 @@ export function getHealthchecks(): Healthchecks {
key: 'ios.xcode-select', key: 'ios.xcode-select',
label: 'xcode-select set', label: 'xcode-select set',
isRequired: true, isRequired: true,
run: async (_: EnvironmentInfo) => { run: async (_: FlipperDoctor.EnvironmentInfo) => {
const result = await tryExecuteCommand('xcode-select -p'); const result = await tryExecuteCommand('xcode-select -p');
const hasProblem = result.hasProblem; const hasProblem = result.hasProblem;
const message = hasProblem const message = hasProblem
@@ -239,7 +189,7 @@ export function getHealthchecks(): Healthchecks {
key: 'ios.xctrace', key: 'ios.xctrace',
label: 'xctrace exists', label: 'xctrace exists',
isRequired: true, isRequired: true,
run: async (_: EnvironmentInfo) => { run: async (_: FlipperDoctor.EnvironmentInfo) => {
const result = await tryExecuteCommand( const result = await tryExecuteCommand(
'xcrun xctrace version', 'xcrun xctrace version',
); );
@@ -258,7 +208,7 @@ export function getHealthchecks(): Healthchecks {
label: 'IDB installed', label: 'IDB installed',
isRequired: false, isRequired: false,
run: async ( run: async (
_: EnvironmentInfo, _: FlipperDoctor.EnvironmentInfo,
settings?: {enablePhysicalIOS: boolean; idbPath: string}, settings?: {enablePhysicalIOS: boolean; idbPath: string},
) => { ) => {
if (!settings) { if (!settings) {
@@ -299,50 +249,48 @@ export function getHealthchecks(): Healthchecks {
} }
export async function runHealthchecks(): Promise< export async function runHealthchecks(): Promise<
Array<CategoryResult | SkippedHealthcheckCategory> Array<FlipperDoctor.CategoryResult | FlipperDoctor.SkippedHealthcheckCategory>
> { > {
const environmentInfo = await getEnvInfo(); const environmentInfo = await getEnvInfo();
const healthchecks: Healthchecks = getHealthchecks(); const healthchecks: FlipperDoctor.Healthchecks = getHealthchecks();
const results: Array<CategoryResult | SkippedHealthcheckCategory> = const results: Array<
await Promise.all( FlipperDoctor.CategoryResult | FlipperDoctor.SkippedHealthcheckCategory
Object.entries(healthchecks).map(async ([key, category]) => { > = await Promise.all(
if (category.isSkipped) { Object.entries(healthchecks).map(async ([key, category]) => {
return category; if (category.isSkipped) {
} return category;
const categoryResult: CategoryResult = [ }
key, const categoryResult: FlipperDoctor.CategoryResult = [
{ key,
label: category.label, {
results: await Promise.all( label: category.label,
category.healthchecks.map( results: await Promise.all(
async ({key, label, run, isRequired}) => ({ category.healthchecks.map(
key, async ({key, label, run, isRequired}) => ({
label, key,
isRequired: isRequired ?? true, label,
result: await run(environmentInfo).catch((e) => { isRequired: isRequired ?? true,
console.warn( result: await run!(environmentInfo).catch((e) => {
`Health check ${key}/${label} failed with:`, console.warn(`Health check ${key}/${label} failed with:`, e);
e, // TODO Improve result type to be: OK | Problem(message, fix...)
); return {
// TODO Improve result type to be: OK | Problem(message, fix...) hasProblem: true,
return { };
hasProblem: true,
};
}),
}), }),
), }),
), ),
}, ),
]; },
return categoryResult; ];
}), return categoryResult;
); }),
);
return results; return results;
} }
async function tryExecuteCommand( async function tryExecuteCommand(
command: string, command: string,
): Promise<HealthcheckRunResult> { ): Promise<FlipperDoctor.HealthcheckRunResult> {
try { try {
const output = await promisify(exec)(command); const output = await promisify(exec)(command);
return { return {

View File

@@ -3,5 +3,10 @@
"compilerOptions": { "compilerOptions": {
"outDir": "lib", "outDir": "lib",
"rootDir": "src" "rootDir": "src"
} },
"references": [
{
"path": "../flipper-common"
}
]
} }

View File

@@ -0,0 +1,126 @@
/**
* 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
*/
export namespace FlipperDoctor {
export type EnvironmentInfo = {
SDKs: {
'iOS SDK': {
Platforms: string[];
};
'Android SDK':
| {
'API Levels': string[] | 'Not Found';
'Build Tools': string[] | 'Not Found';
'System Images': string[] | 'Not Found';
'Android NDK': string | 'Not Found';
}
| 'Not Found';
};
IDEs: {
Xcode: {
version: string;
path: string;
};
};
};
export type HealthcheckCategory = {
label: string;
isSkipped: false;
isRequired: boolean;
healthchecks: Healthcheck[];
};
export type SkippedHealthcheckCategory = {
label: string;
isSkipped: true;
skipReason: string;
};
export type Healthchecks = {
common: HealthcheckCategory | SkippedHealthcheckCategory;
android: HealthcheckCategory | SkippedHealthcheckCategory;
ios: HealthcheckCategory | SkippedHealthcheckCategory;
};
export type Settings = {
idbPath: string;
enablePhysicalIOS: boolean;
};
export type Healthcheck = {
key: string;
label: string;
isRequired?: boolean;
run?: (
env: EnvironmentInfo,
settings?: Settings,
) => Promise<HealthcheckRunResult>;
};
export type HealthcheckRunResult = {
hasProblem: boolean;
message: string;
};
export type CategoryResult = [
string,
{
label: string;
results: Array<{
key: string;
label: string;
isRequired: boolean;
result: {hasProblem: boolean};
}>;
},
];
export type Dictionary<T> = {[key: string]: T};
export type HealthcheckStatus =
| 'IN_PROGRESS'
| 'SUCCESS'
| 'FAILED'
| 'SKIPPED'
| 'WARNING';
export type HealthcheckResult = {
status: HealthcheckStatus;
isAcknowledged?: boolean;
message?: string;
};
export type HealthcheckReportItem = {
key: string;
label: string;
result: HealthcheckResult;
};
export type HealthcheckReportCategory = {
key: string;
label: string;
result: HealthcheckResult;
checks: Dictionary<HealthcheckReportItem>;
};
export type HealthcheckReport = {
result: HealthcheckResult;
categories: Dictionary<HealthcheckReportCategory>;
};
export type HealthcheckSettings = {
settings: {
enableAndroid: boolean;
enableIOS: boolean;
enablePhysicalIOS: boolean;
idbPath: string;
};
};
}

View File

@@ -47,3 +47,4 @@ export * from './GK';
export * from './clientUtils'; export * from './clientUtils';
export * from './settings'; export * from './settings';
export * from './PluginDetails'; export * from './PluginDetails';
export * from './doctor';

View File

@@ -7,6 +7,7 @@
* @format * @format
*/ */
import {FlipperDoctor} from './doctor';
import { import {
BundledPluginDetails, BundledPluginDetails,
DeviceSpec, DeviceSpec,
@@ -180,6 +181,14 @@ export type FlipperServerCommands = {
path: string, path: string,
) => Promise<InstalledPluginDetails>; ) => Promise<InstalledPluginDetails>;
'plugins-remove-plugins': (names: string[]) => Promise<void>; 'plugins-remove-plugins': (names: string[]) => Promise<void>;
'doctor-get-healthchecks': (
settings: FlipperDoctor.HealthcheckSettings,
) => Promise<FlipperDoctor.Healthchecks>;
'doctor-run-healthcheck': (
settings: FlipperDoctor.HealthcheckSettings,
category: keyof FlipperDoctor.Healthchecks,
name: string,
) => Promise<FlipperDoctor.HealthcheckResult>;
}; };
/** /**

View File

@@ -19,6 +19,7 @@
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"flipper-plugin-lib": "0.0.0", "flipper-plugin-lib": "0.0.0",
"flipper-common": "0.0.0", "flipper-common": "0.0.0",
"flipper-doctor": "0.0.0",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"invariant": "^2.2.4", "invariant": "^2.2.4",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",

View File

@@ -35,6 +35,7 @@ import {saveSettings} from './utils/settings';
import {saveLauncherSettings} from './utils/launcherSettings'; import {saveLauncherSettings} from './utils/launcherSettings';
import {KeytarManager} from './utils/keytar'; import {KeytarManager} from './utils/keytar';
import {PluginManager} from './plugins/PluginManager'; import {PluginManager} from './plugins/PluginManager';
import {runHealthcheck, getHealthChecks} from './utils/runHealthchecks';
/** /**
* FlipperServer takes care of all incoming device & client connections. * FlipperServer takes care of all incoming device & client connections.
@@ -269,6 +270,8 @@ export class FlipperServerImpl implements FlipperServer {
'plugins-install-from-npm': (name) => 'plugins-install-from-npm': (name) =>
this.pluginManager.installPluginFromNpm(name), this.pluginManager.installPluginFromNpm(name),
'plugin-source': (path) => this.pluginManager.loadSource(path), 'plugin-source': (path) => this.pluginManager.loadSource(path),
'doctor-get-healthchecks': getHealthChecks,
'doctor-run-healthcheck': runHealthcheck,
}; };
registerDevice(device: ServerDevice) { registerDevice(device: ServerDevice) {

View File

@@ -0,0 +1,76 @@
/**
* 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 {getHealthchecks, getEnvInfo} from 'flipper-doctor';
import {FlipperDoctor} from 'flipper-common';
import produce from 'immer';
export async function getHealthChecks(
options: FlipperDoctor.HealthcheckSettings,
) {
return produce(getHealthchecks(), (healthchecks) => {
if (!options.settings.enableAndroid) {
healthchecks.android = {
label: healthchecks.android.label,
isSkipped: true,
skipReason:
'Healthcheck is skipped, because "Android Development" option is disabled in the Flipper settings',
};
}
if (!options.settings.enableIOS) {
healthchecks.ios = {
label: healthchecks.ios.label,
isSkipped: true,
skipReason:
'Healthcheck is skipped, because "iOS Development" option is disabled in the Flipper settings',
};
}
Object.keys(healthchecks).forEach((cat) => {
const category = healthchecks[cat as keyof typeof healthchecks];
if ('healthchecks' in category) {
category.healthchecks.forEach((h) => {
delete h.run;
});
}
});
});
}
export async function runHealthcheck(
options: FlipperDoctor.HealthcheckSettings,
categoryName: keyof FlipperDoctor.Healthchecks,
ruleName: string,
): Promise<FlipperDoctor.HealthcheckResult> {
const healthchecks = getHealthchecks();
const category = healthchecks[categoryName];
if (!category) {
throw new Error('Unknown category: ' + categoryName);
}
if (!('healthchecks' in category)) {
throw new Error('Skipped category: ' + categoryName);
}
const check = category.healthchecks.find((h) => h.key === ruleName);
if (!check) {
throw new Error('Unknown healthcheck: ' + ruleName);
}
const environmentInfo = await getEnvInfo();
const checkResult = await check.run!(environmentInfo, options.settings);
return checkResult.hasProblem && check.isRequired
? {
status: 'FAILED',
message: checkResult.message,
}
: checkResult.hasProblem && !check.isRequired
? {
status: 'WARNING',
message: checkResult.message,
}
: {status: 'SUCCESS', message: checkResult.message};
}

View File

@@ -5,6 +5,9 @@
"rootDir": "src" "rootDir": "src"
}, },
"references": [ "references": [
{
"path": "../doctor"
},
{ {
"path": "../flipper-common" "path": "../flipper-common"
}, },

View File

@@ -31,7 +31,6 @@
"expand-tilde": "^2.0.2", "expand-tilde": "^2.0.2",
"flipper-client-sdk": "^0.0.3", "flipper-client-sdk": "^0.0.3",
"flipper-common": "0.0.0", "flipper-common": "0.0.0",
"flipper-doctor": "0.0.0",
"flipper-plugin": "0.0.0", "flipper-plugin": "0.0.0",
"flipper-ui-core": "0.0.0", "flipper-ui-core": "0.0.0",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",

View File

@@ -24,9 +24,6 @@ import React, {Component} from 'react';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {State as Store} from '../reducers'; import {State as Store} from '../reducers';
import { import {
HealthcheckResult,
HealthcheckReportCategory,
HealthcheckReport,
startHealthchecks, startHealthchecks,
finishHealthchecks, finishHealthchecks,
updateHealthcheckResult, updateHealthcheckResult,
@@ -38,10 +35,10 @@ import runHealthchecks, {
HealthcheckEventsHandler, HealthcheckEventsHandler,
} from '../utils/runHealthchecks'; } from '../utils/runHealthchecks';
import {getFlipperLib} from 'flipper-plugin'; import {getFlipperLib} from 'flipper-plugin';
import {reportUsage} from 'flipper-common'; import {reportUsage, FlipperDoctor} from 'flipper-common';
type StateFromProps = { type StateFromProps = {
healthcheckReport: HealthcheckReport; healthcheckReport: FlipperDoctor.HealthcheckReport;
} & HealthcheckSettings; } & HealthcheckSettings;
type DispatchFromProps = { type DispatchFromProps = {
@@ -123,7 +120,9 @@ function CenteredCheckbox(props: {
); );
} }
function HealthcheckIcon(props: {checkResult: HealthcheckResult}) { function HealthcheckIcon(props: {
checkResult: FlipperDoctor.HealthcheckResult;
}) {
const {checkResult: check} = props; const {checkResult: check} = props;
switch (props.checkResult.status) { switch (props.checkResult.status) {
case 'IN_PROGRESS': case 'IN_PROGRESS':
@@ -170,7 +169,7 @@ function HealthcheckIcon(props: {checkResult: HealthcheckResult}) {
function HealthcheckDisplay(props: { function HealthcheckDisplay(props: {
label: string; label: string;
result: HealthcheckResult; result: FlipperDoctor.HealthcheckResult;
selected?: boolean; selected?: boolean;
onClick?: () => void; onClick?: () => void;
}) { }) {
@@ -194,7 +193,7 @@ function SideMessageDisplay(props: {children: React.ReactNode}) {
return <SideContainerText selectable>{props.children}</SideContainerText>; return <SideContainerText selectable>{props.children}</SideContainerText>;
} }
function ResultMessage(props: {result: HealthcheckResult}) { function ResultMessage(props: {result: FlipperDoctor.HealthcheckResult}) {
if (status === 'IN_PROGRESS') { if (status === 'IN_PROGRESS') {
return <p>Doctor is running healthchecks...</p>; return <p>Doctor is running healthchecks...</p>;
} else if (hasProblems(props.result)) { } else if (hasProblems(props.result)) {
@@ -213,12 +212,12 @@ function ResultMessage(props: {result: HealthcheckResult}) {
} }
} }
function hasProblems(result: HealthcheckResult) { function hasProblems(result: FlipperDoctor.HealthcheckResult) {
const {status} = result; const {status} = result;
return status === 'FAILED' || status === 'WARNING'; return status === 'FAILED' || status === 'WARNING';
} }
function hasNewProblems(result: HealthcheckResult) { function hasNewProblems(result: FlipperDoctor.HealthcheckResult) {
return hasProblems(result) && !result.isAcknowledged; return hasProblems(result) && !result.isAcknowledged;
} }
@@ -321,7 +320,7 @@ class DoctorSheet extends Component<Props, State> {
<FlexRow> <FlexRow>
<HealthcheckListContainer> <HealthcheckListContainer>
{Object.values(this.props.healthcheckReport.categories).map( {Object.values(this.props.healthcheckReport.categories).map(
(category: HealthcheckReportCategory) => { (category: FlipperDoctor.HealthcheckReportCategory) => {
return ( return (
<CategoryContainer key={category.key}> <CategoryContainer key={category.key}>
<HealthcheckDisplay <HealthcheckDisplay

View File

@@ -14,9 +14,9 @@ import {
updateHealthcheckResult, updateHealthcheckResult,
acknowledgeProblems, acknowledgeProblems,
} from '../healthchecks'; } from '../healthchecks';
import {Healthchecks, EnvironmentInfo} from 'flipper-doctor'; import type {FlipperDoctor} from 'flipper-common';
const HEALTHCHECKS: Healthchecks = { const HEALTHCHECKS: FlipperDoctor.Healthchecks = {
ios: { ios: {
label: 'iOS', label: 'iOS',
isSkipped: false, isSkipped: false,
@@ -25,7 +25,7 @@ const HEALTHCHECKS: Healthchecks = {
{ {
key: 'ios.sdk', key: 'ios.sdk',
label: 'SDK Installed', label: 'SDK Installed',
run: async (_env: EnvironmentInfo) => { run: async (_env: FlipperDoctor.EnvironmentInfo) => {
return {hasProblem: false, message: ''}; return {hasProblem: false, message: ''};
}, },
}, },
@@ -39,7 +39,7 @@ const HEALTHCHECKS: Healthchecks = {
{ {
key: 'android.sdk', key: 'android.sdk',
label: 'SDK Installed', label: 'SDK Installed',
run: async (_env: EnvironmentInfo) => { run: async (_env: FlipperDoctor.EnvironmentInfo) => {
return {hasProblem: true, message: 'Error'}; return {hasProblem: true, message: 'Error'};
}, },
}, },
@@ -53,7 +53,7 @@ const HEALTHCHECKS: Healthchecks = {
{ {
key: 'common.openssl', key: 'common.openssl',
label: 'OpenSSL Istalled', label: 'OpenSSL Istalled',
run: async (_env: EnvironmentInfo) => { run: async (_env: FlipperDoctor.EnvironmentInfo) => {
return {hasProblem: false, message: ''}; return {hasProblem: false, message: ''};
}, },
}, },

View File

@@ -9,17 +9,17 @@
import {Actions} from './'; import {Actions} from './';
import {produce} from 'immer'; import {produce} from 'immer';
import {Healthchecks} from 'flipper-doctor'; import type {FlipperDoctor} from 'flipper-common';
export type State = { export type State = {
healthcheckReport: HealthcheckReport; healthcheckReport: FlipperDoctor.HealthcheckReport;
acknowledgedProblems: string[]; acknowledgedProblems: string[];
}; };
export type Action = export type Action =
| { | {
type: 'START_HEALTHCHECKS'; type: 'START_HEALTHCHECKS';
payload: Healthchecks; payload: FlipperDoctor.Healthchecks;
} }
| { | {
type: 'FINISH_HEALTHCHECKS'; type: 'FINISH_HEALTHCHECKS';
@@ -29,7 +29,7 @@ export type Action =
payload: { payload: {
categoryKey: string; categoryKey: string;
itemKey: string; itemKey: string;
result: HealthcheckResult; result: FlipperDoctor.HealthcheckResult;
}; };
} }
| { | {
@@ -47,39 +47,6 @@ const INITIAL_STATE: State = {
acknowledgedProblems: [], acknowledgedProblems: [],
}; };
type Dictionary<T> = {[key: string]: T};
export type HealthcheckStatus =
| 'IN_PROGRESS'
| 'SUCCESS'
| 'FAILED'
| 'SKIPPED'
| 'WARNING';
export type HealthcheckResult = {
status: HealthcheckStatus;
isAcknowledged?: boolean;
message?: string;
};
export type HealthcheckReportItem = {
key: string;
label: string;
result: HealthcheckResult;
};
export type HealthcheckReportCategory = {
key: string;
label: string;
result: HealthcheckResult;
checks: Dictionary<HealthcheckReportItem>;
};
export type HealthcheckReport = {
result: HealthcheckResult;
categories: Dictionary<HealthcheckReportCategory>;
};
function recomputeHealthcheckStatus(draft: State): void { function recomputeHealthcheckStatus(draft: State): void {
draft.healthcheckReport.result = computeAggregatedResult( draft.healthcheckReport.result = computeAggregatedResult(
Object.values(draft.healthcheckReport.categories).map((c) => c.result), Object.values(draft.healthcheckReport.categories).map((c) => c.result),
@@ -87,8 +54,8 @@ function recomputeHealthcheckStatus(draft: State): void {
} }
function computeAggregatedResult( function computeAggregatedResult(
results: HealthcheckResult[], results: FlipperDoctor.HealthcheckResult[],
): HealthcheckResult { ): FlipperDoctor.HealthcheckResult {
return results.some((r) => r.status === 'IN_PROGRESS') return results.some((r) => r.status === 'IN_PROGRESS')
? {status: 'IN_PROGRESS'} ? {status: 'IN_PROGRESS'}
: results.every((r) => r.status === 'SUCCESS') : results.every((r) => r.status === 'SUCCESS')
@@ -114,7 +81,7 @@ const updateCheckResult = produce(
}: { }: {
categoryKey: string; categoryKey: string;
itemKey: string; itemKey: string;
result: HealthcheckResult; result: FlipperDoctor.HealthcheckResult;
}, },
) => { ) => {
const category = draft.healthcheckReport.categories[categoryKey]; const category = draft.healthcheckReport.categories[categoryKey];
@@ -124,55 +91,57 @@ const updateCheckResult = produce(
}, },
); );
function createDict<T>(pairs: [string, T][]): Dictionary<T> { function createDict<T>(pairs: [string, T][]): FlipperDoctor.Dictionary<T> {
const obj: Dictionary<T> = {}; const obj: FlipperDoctor.Dictionary<T> = {};
for (const pair of pairs) { for (const pair of pairs) {
obj[pair[0]] = pair[1]; obj[pair[0]] = pair[1];
} }
return obj; return obj;
} }
const start = produce((draft: State, healthchecks: Healthchecks) => { const start = produce(
draft.healthcheckReport = { (draft: State, healthchecks: FlipperDoctor.Healthchecks) => {
result: {status: 'IN_PROGRESS'}, draft.healthcheckReport = {
categories: createDict<HealthcheckReportCategory>( result: {status: 'IN_PROGRESS'},
Object.entries(healthchecks).map(([categoryKey, category]) => { categories: createDict<FlipperDoctor.HealthcheckReportCategory>(
if (category.isSkipped) { Object.entries(healthchecks).map(([categoryKey, category]) => {
if (category.isSkipped) {
return [
categoryKey,
{
key: categoryKey,
result: {
status: 'SKIPPED',
message: category.skipReason,
},
label: category.label,
checks: createDict<FlipperDoctor.HealthcheckReportItem>([]),
},
];
}
return [ return [
categoryKey, categoryKey,
{ {
key: categoryKey, key: categoryKey,
result: { result: {status: 'IN_PROGRESS'},
status: 'SKIPPED',
message: category.skipReason,
},
label: category.label, label: category.label,
checks: createDict<HealthcheckReportItem>([]), checks: createDict<FlipperDoctor.HealthcheckReportItem>(
category.healthchecks.map((check) => [
check.key,
{
key: check.key,
result: {status: 'IN_PROGRESS'},
label: check.label,
},
]),
),
}, },
]; ];
} }),
return [ ),
categoryKey, };
{ },
key: categoryKey, );
result: {status: 'IN_PROGRESS'},
label: category.label,
checks: createDict<HealthcheckReportItem>(
category.healthchecks.map((check) => [
check.key,
{
key: check.key,
result: {status: 'IN_PROGRESS'},
label: check.label,
},
]),
),
},
];
}),
),
};
});
const finish = produce((draft: State) => { const finish = produce((draft: State) => {
Object.values(draft.healthcheckReport.categories) Object.values(draft.healthcheckReport.categories)
@@ -244,7 +213,7 @@ export default function reducer(
export const updateHealthcheckResult = ( export const updateHealthcheckResult = (
categoryKey: string, categoryKey: string,
itemKey: string, itemKey: string,
result: HealthcheckResult, result: FlipperDoctor.HealthcheckResult,
): Action => ({ ): Action => ({
type: 'UPDATE_HEALTHCHECK_RESULT', type: 'UPDATE_HEALTHCHECK_RESULT',
payload: { payload: {
@@ -254,7 +223,9 @@ export const updateHealthcheckResult = (
}, },
}); });
export const startHealthchecks = (healthchecks: Healthchecks): Action => ({ export const startHealthchecks = (
healthchecks: FlipperDoctor.Healthchecks,
): Action => ({
type: 'START_HEALTHCHECKS', type: 'START_HEALTHCHECKS',
payload: healthchecks, payload: healthchecks,
}); });

View File

@@ -18,12 +18,6 @@ import {
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import {Layout} from '../ui'; import {Layout} from '../ui';
import {
HealthcheckReport,
HealthcheckReportItem,
HealthcheckStatus,
HealthcheckResult,
} from '../reducers/healthchecks';
import {theme} from 'flipper-plugin'; import {theme} from 'flipper-plugin';
import { import {
startHealthchecks, startHealthchecks,
@@ -33,13 +27,14 @@ import {
resetAcknowledgedProblems, resetAcknowledgedProblems,
} from '../reducers/healthchecks'; } from '../reducers/healthchecks';
import runHealthchecks from '../utils/runHealthchecks'; import runHealthchecks from '../utils/runHealthchecks';
import {Healthchecks} from 'flipper-doctor'; import type {FlipperDoctor} from 'flipper-common';
type Healthchecks = FlipperDoctor.Healthchecks;
import {reportUsage} from 'flipper-common'; import {reportUsage} from 'flipper-common';
const {Title, Paragraph, Text} = Typography; const {Title, Paragraph, Text} = Typography;
const statusTypeAndMessage: { const statusTypeAndMessage: {
[key in HealthcheckStatus]: { [key in FlipperDoctor.HealthcheckStatus]: {
type: 'success' | 'info' | 'warning' | 'error'; type: 'success' | 'info' | 'warning' | 'error';
message: string; message: string;
}; };
@@ -67,15 +62,15 @@ const statusTypeAndMessage: {
}, },
}; };
function checkHasProblem(result: HealthcheckResult) { function checkHasProblem(result: FlipperDoctor.HealthcheckResult) {
return result.status === 'FAILED' || result.status === 'WARNING'; return result.status === 'FAILED' || result.status === 'WARNING';
} }
export function checkHasNewProblem(result: HealthcheckResult) { export function checkHasNewProblem(result: FlipperDoctor.HealthcheckResult) {
return checkHasProblem(result) && !result.isAcknowledged; return checkHasProblem(result) && !result.isAcknowledged;
} }
function ResultTopDialog(props: {status: HealthcheckStatus}) { function ResultTopDialog(props: {status: FlipperDoctor.HealthcheckStatus}) {
const messages = statusTypeAndMessage[props.status]; const messages = statusTypeAndMessage[props.status];
return ( return (
<Alert <Alert
@@ -92,7 +87,7 @@ function ResultTopDialog(props: {status: HealthcheckStatus}) {
); );
} }
function CheckIcon(props: {status: HealthcheckStatus}) { function CheckIcon(props: {status: FlipperDoctor.HealthcheckStatus}) {
switch (props.status) { switch (props.status) {
case 'SUCCESS': case 'SUCCESS':
return ( return (
@@ -119,7 +114,9 @@ function CheckIcon(props: {status: HealthcheckStatus}) {
} }
} }
function CollapsableCategory(props: {checks: Array<HealthcheckReportItem>}) { function CollapsableCategory(props: {
checks: Array<FlipperDoctor.HealthcheckReportItem>;
}) {
return ( return (
<Collapse ghost> <Collapse ghost>
{props.checks.map((check) => ( {props.checks.map((check) => (
@@ -134,7 +131,7 @@ function CollapsableCategory(props: {checks: Array<HealthcheckReportItem>}) {
); );
} }
function HealthCheckList(props: {report: HealthcheckReport}) { function HealthCheckList(props: {report: FlipperDoctor.HealthcheckReport}) {
useEffect(() => reportUsage('doctor:report:opened'), []); useEffect(() => reportUsage('doctor:report:opened'), []);
return ( return (
<Layout.Container gap> <Layout.Container gap>
@@ -241,7 +238,7 @@ export default function SetupDoctorScreen(props: {
updateHealthcheckResult: ( updateHealthcheckResult: (
categoryKey: string, categoryKey: string,
itemKey: string, itemKey: string,
result: HealthcheckResult, result: FlipperDoctor.HealthcheckResult,
) => dispatch(updateHealthcheckResult(categoryKey, itemKey, result)), ) => dispatch(updateHealthcheckResult(categoryKey, itemKey, result)),
finishHealthchecks: () => dispatch(finishHealthchecks()), finishHealthchecks: () => dispatch(finishHealthchecks()),
}); });

View File

@@ -7,9 +7,12 @@
* @format * @format
*/ */
import {HealthcheckResult} from '../reducers/healthchecks'; import {
import {getHealthchecks, getEnvInfo, Healthchecks} from 'flipper-doctor'; logPlatformSuccessRate,
import {logPlatformSuccessRate, reportPlatformFailures} from 'flipper-common'; reportPlatformFailures,
FlipperDoctor,
} from 'flipper-common';
import {getRenderHostInstance} from '../RenderHost';
let healthcheckIsRunning: boolean; let healthcheckIsRunning: boolean;
let runningHealthcheck: Promise<void>; let runningHealthcheck: Promise<void>;
@@ -18,9 +21,9 @@ export type HealthcheckEventsHandler = {
updateHealthcheckResult: ( updateHealthcheckResult: (
categoryKey: string, categoryKey: string,
itemKey: string, itemKey: string,
result: HealthcheckResult, result: FlipperDoctor.HealthcheckResult,
) => void; ) => void;
startHealthchecks: (healthchecks: Healthchecks) => void; startHealthchecks: (healthchecks: FlipperDoctor.Healthchecks) => void;
finishHealthchecks: () => void; finishHealthchecks: () => void;
}; };
@@ -36,34 +39,25 @@ export type HealthcheckSettings = {
export type HealthcheckOptions = HealthcheckEventsHandler & HealthcheckSettings; export type HealthcheckOptions = HealthcheckEventsHandler & HealthcheckSettings;
async function launchHealthchecks(options: HealthcheckOptions): Promise<void> { async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
const healthchecks = getHealthchecks(); const {flipperServer} = getRenderHostInstance();
if (!options.settings.enableAndroid) { const healthchecks = await flipperServer.exec('doctor-get-healthchecks', {
healthchecks.android = { settings: options.settings,
label: healthchecks.android.label, });
isSkipped: true,
skipReason:
'Healthcheck is skipped, because "Android Development" option is disabled in the Flipper settings',
};
}
if (!options.settings.enableIOS) {
healthchecks.ios = {
label: healthchecks.ios.label,
isSkipped: true,
skipReason:
'Healthcheck is skipped, because "iOS Development" option is disabled in the Flipper settings',
};
}
options.startHealthchecks(healthchecks); options.startHealthchecks(healthchecks);
const environmentInfo = await getEnvInfo();
let hasProblems = false; let hasProblems = false;
for (const [categoryKey, category] of Object.entries(healthchecks)) { for (const [categoryKey, category] of Object.entries(healthchecks)) {
if (category.isSkipped) { if (category.isSkipped) {
continue; continue;
} }
for (const h of category.healthchecks) { for (const h of category.healthchecks) {
const checkResult = await h.run(environmentInfo, options.settings); const checkResult = await flipperServer.exec(
'doctor-run-healthcheck',
{settings: options.settings},
categoryKey as keyof FlipperDoctor.Healthchecks,
h.key,
);
const metricName = `doctor:${h.key.replace('.', ':')}.healthcheck`; // e.g. "doctor:ios:xcode-select.healthcheck" const metricName = `doctor:${h.key.replace('.', ':')}.healthcheck`; // e.g. "doctor:ios:xcode-select.healthcheck"
if (checkResult.hasProblem) { if (checkResult.status !== 'SUCCESS') {
hasProblems = true; hasProblems = true;
logPlatformSuccessRate(metricName, { logPlatformSuccessRate(metricName, {
kind: 'failure', kind: 'failure',
@@ -75,19 +69,7 @@ async function launchHealthchecks(options: HealthcheckOptions): Promise<void> {
kind: 'success', kind: 'success',
}); });
} }
const result: HealthcheckResult = options.updateHealthcheckResult(categoryKey, h.key, checkResult);
checkResult.hasProblem && h.isRequired
? {
status: 'FAILED',
message: checkResult.message,
}
: checkResult.hasProblem && !h.isRequired
? {
status: 'WARNING',
message: checkResult.message,
}
: {status: 'SUCCESS', message: checkResult.message};
options.updateHealthcheckResult(categoryKey, h.key, result);
} }
} }
options.finishHealthchecks(); options.finishHealthchecks();

View File

@@ -7,9 +7,6 @@
"emitDeclarationOnly": true "emitDeclarationOnly": true
}, },
"references": [ "references": [
{
"path": "../doctor"
},
{ {
"path": "../flipper-common" "path": "../flipper-common"
}, },