diff --git a/flow-typed/npm/promise-retry_v1.1.x.js b/flow-typed/npm/promise-retry_v1.1.x.js new file mode 100644 index 000000000..5d1a22345 --- /dev/null +++ b/flow-typed/npm/promise-retry_v1.1.x.js @@ -0,0 +1,20 @@ +// flow-typed signature: 83c416de68c62add9a5f5c1178d383bb +// flow-typed version: 15b5072ad2/promise-retry_v1.1.x/flow_>=v0.45.x + +type RetryFn = (err?: Error) => void; +type Options = {| + retries?: number, + factor?: number, + minTimeout?: number, + maxTimeout?: number, + randomize?: boolean, +|}; + +declare module 'promise-retry' { + declare export type RetryOptions = Options; + + declare module.exports: ( + handler: (retry: RetryFn, retryNumber: Number) => Promise, + options?: Options + ) => Promise; +} diff --git a/package.json b/package.json index d4fe472a8..e55807f74 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "lodash.debounce": "^4.0.8", "mkdirp": "^0.5.1", "openssl-wrapper": "^0.3.4", + "promise-retry": "^1.1.1", "prop-types": "^15.6.0", "react": "16", "react-color": "^2.11.7", diff --git a/src/dispatcher/androidDevice.js b/src/dispatcher/androidDevice.js index 614059826..f69d151d1 100644 --- a/src/dispatcher/androidDevice.js +++ b/src/dispatcher/androidDevice.js @@ -7,6 +7,7 @@ import AndroidDevice from '../devices/AndroidDevice'; import child_process from 'child_process'; +import promiseRetry from 'promise-retry'; import type {Store} from '../reducers/index.js'; import type BaseDevice from '../devices/BaseDevice'; import type Logger from '../fb-stubs/Logger.js'; @@ -57,10 +58,28 @@ export default (store: Store, logger: Logger) => { // Using this client before adb server is started up will cause failures. // this gets around this by waiting for listDevices first, which ensures // the server is up and running before allowing any other operations. - const clientPromise = (() => { + + function createClient() { const unsafeClient = adb.createClient(); - return unsafeClient.listDevices().then(() => unsafeClient); - })(); + return promiseRetry( + (retry, number) => { + return unsafeClient + .listDevices() + .then(() => { + return unsafeClient; + }) + .catch(e => { + console.warn(`Failed to start adb client. Retrying. ${e.message}`); + retry(e); + }); + }, + { + minTimeout: 200, + retries: 5, + }, + ); + } + const clientPromise = createClient(); const watchAndroidDevices = () => { // get emulators @@ -79,71 +98,77 @@ export default (store: Store, logger: Logger) => { }, ); - clientPromise.then(client => { - client - .trackDevices() - .then(tracker => { - tracker.on('error', err => { - if (err.message === 'Connection closed') { - // adb server has shutdown, remove all android devices - const {connections} = store.getState(); - const deviceIDsToRemove: Array< - string, - > = connections.devices - .filter((device: BaseDevice) => device instanceof AndroidDevice) - .map((device: BaseDevice) => device.serial); + clientPromise + .then(client => { + client + .trackDevices() + .then(tracker => { + tracker.on('error', err => { + if (err.message === 'Connection closed') { + // adb server has shutdown, remove all android devices + const {connections} = store.getState(); + const deviceIDsToRemove: Array< + string, + > = connections.devices + .filter( + (device: BaseDevice) => device instanceof AndroidDevice, + ) + .map((device: BaseDevice) => device.serial); - store.dispatch({ - type: 'UNREGISTER_DEVICES', - payload: new Set(deviceIDsToRemove), - }); - console.error('adb server was shutdown'); - setTimeout(watchAndroidDevices, 500); - } else { - throw err; - } - }); + store.dispatch({ + type: 'UNREGISTER_DEVICES', + payload: new Set(deviceIDsToRemove), + }); + console.error('adb server was shutdown'); + setTimeout(watchAndroidDevices, 500); + } else { + throw err; + } + }); - tracker.on('add', async device => { - if (device.type !== 'offline') { - const androidDevice = await createDevice(client, device); - store.dispatch({ - type: 'REGISTER_DEVICE', - payload: androidDevice, - }); - } - }); + tracker.on('add', async device => { + if (device.type !== 'offline') { + const androidDevice = await createDevice(client, device); + store.dispatch({ + type: 'REGISTER_DEVICE', + payload: androidDevice, + }); + } + }); - tracker.on('change', async device => { - if (device.type === 'offline') { + tracker.on('change', async device => { + if (device.type === 'offline') { + store.dispatch({ + type: 'UNREGISTER_DEVICES', + payload: new Set([device.id]), + }); + } else { + const androidDevice = await createDevice(client, device); + store.dispatch({ + type: 'REGISTER_DEVICE', + payload: androidDevice, + }); + } + }); + + tracker.on('remove', device => { store.dispatch({ type: 'UNREGISTER_DEVICES', payload: new Set([device.id]), }); + }); + }) + .catch(err => { + if (err.code === 'ECONNREFUSED') { + // adb server isn't running } else { - const androidDevice = await createDevice(client, device); - store.dispatch({ - type: 'REGISTER_DEVICE', - payload: androidDevice, - }); + throw err; } }); - - tracker.on('remove', device => { - store.dispatch({ - type: 'UNREGISTER_DEVICES', - payload: new Set([device.id]), - }); - }); - }) - .catch(err => { - if (err.code === 'ECONNREFUSED') { - // adb server isn't running - } else { - throw err; - } - }); - }); + }) + .catch(e => { + console.error(`Failed to watch for android devices: ${e.message}`); + }); }; watchAndroidDevices(); diff --git a/yarn.lock b/yarn.lock index 9826ce5ba..6259ad0a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2273,6 +2273,11 @@ env-paths@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-1.0.0.tgz#4168133b42bb05c38a35b1ae4397c8298ab369e0" +err-code@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" + integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= + error-ex@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -5137,6 +5142,14 @@ progress@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" +promise-retry@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" + integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= + dependencies: + err-code "^1.0.0" + retry "^0.10.0" + promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" @@ -5627,6 +5640,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= + rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"