runtimes.js (17161B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const Actions = require("resource://devtools/client/aboutdebugging/src/actions/index.js"); 8 9 const { 10 getAllRuntimes, 11 getCurrentRuntime, 12 findRuntimeById, 13 } = require("resource://devtools/client/aboutdebugging/src/modules/runtimes-state-helper.js"); 14 15 const { 16 l10n, 17 } = require("resource://devtools/client/aboutdebugging/src/modules/l10n.js"); 18 const { 19 setDefaultPreferencesIfNeeded, 20 DEFAULT_PREFERENCES, 21 } = require("resource://devtools/client/aboutdebugging/src/modules/runtime-default-preferences.js"); 22 const { 23 createClientForRuntime, 24 } = require("resource://devtools/client/aboutdebugging/src/modules/runtime-client-factory.js"); 25 const { 26 isSupportedDebugTargetPane, 27 } = require("resource://devtools/client/aboutdebugging/src/modules/debug-target-support.js"); 28 29 const { 30 remoteClientManager, 31 } = require("resource://devtools/client/shared/remote-debugging/remote-client-manager.js"); 32 33 const { 34 CONNECT_RUNTIME_CANCEL, 35 CONNECT_RUNTIME_FAILURE, 36 CONNECT_RUNTIME_NOT_RESPONDING, 37 CONNECT_RUNTIME_START, 38 CONNECT_RUNTIME_SUCCESS, 39 DEBUG_TARGET_PANE, 40 DISCONNECT_RUNTIME_FAILURE, 41 DISCONNECT_RUNTIME_START, 42 DISCONNECT_RUNTIME_SUCCESS, 43 PAGE_TYPES, 44 REMOTE_RUNTIMES_UPDATED, 45 RUNTIME_PREFERENCE, 46 RUNTIMES, 47 THIS_FIREFOX_RUNTIME_CREATED, 48 UNWATCH_RUNTIME_FAILURE, 49 UNWATCH_RUNTIME_START, 50 UNWATCH_RUNTIME_SUCCESS, 51 UPDATE_CONNECTION_PROMPT_SETTING_FAILURE, 52 UPDATE_CONNECTION_PROMPT_SETTING_START, 53 UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS, 54 WATCH_RUNTIME_FAILURE, 55 WATCH_RUNTIME_START, 56 WATCH_RUNTIME_SUCCESS, 57 } = require("resource://devtools/client/aboutdebugging/src/constants.js"); 58 59 const CONNECTION_TIMING_OUT_DELAY = 3000; 60 const CONNECTION_CANCEL_DELAY = 13000; 61 62 async function getRuntimeIcon(_runtime, _channel) { 63 return "chrome://branding/content/about-logo.svg"; 64 } 65 66 function onRemoteDevToolsClientClosed() { 67 window.AboutDebugging.onNetworkLocationsUpdated(); 68 window.AboutDebugging.onUSBRuntimesUpdated(); 69 } 70 71 function connectRuntime(id) { 72 // Create a random connection id to track the connection attempt in telemetry. 73 const connectionId = (Math.random() * 100000) | 0; 74 75 return async ({ dispatch, getState }) => { 76 dispatch({ type: CONNECT_RUNTIME_START, connectionId, id }); 77 78 // The preferences test-connection-timing-out-delay and test-connection-cancel-delay 79 // don't have a default value but will be overridden during our tests. 80 const connectionTimingOutDelay = Services.prefs.getIntPref( 81 "devtools.aboutdebugging.test-connection-timing-out-delay", 82 CONNECTION_TIMING_OUT_DELAY 83 ); 84 const connectionCancelDelay = Services.prefs.getIntPref( 85 "devtools.aboutdebugging.test-connection-cancel-delay", 86 CONNECTION_CANCEL_DELAY 87 ); 88 89 const connectionNotRespondingTimer = setTimeout(() => { 90 // If connecting to the runtime takes time over CONNECTION_TIMING_OUT_DELAY, 91 // we assume the connection prompt is showing on the runtime, show a dialog 92 // to let user know that. 93 dispatch({ type: CONNECT_RUNTIME_NOT_RESPONDING, connectionId, id }); 94 }, connectionTimingOutDelay); 95 const connectionCancelTimer = setTimeout(() => { 96 // Connect button of the runtime will be disabled during connection, but the status 97 // continues till the connection was either succeed or failed. This may have a 98 // possibility that the disabling continues unless page reloading, user will not be 99 // able to click again. To avoid this, revert the connect button status after 100 // CONNECTION_CANCEL_DELAY ms. 101 dispatch({ type: CONNECT_RUNTIME_CANCEL, connectionId, id }); 102 }, connectionCancelDelay); 103 104 try { 105 const runtime = findRuntimeById(id, getState().runtimes); 106 const clientWrapper = await createClientForRuntime(runtime); 107 108 await setDefaultPreferencesIfNeeded(clientWrapper, DEFAULT_PREFERENCES); 109 110 const deviceDescription = await clientWrapper.getDeviceDescription(); 111 const compatibilityReport = 112 await clientWrapper.checkVersionCompatibility(); 113 const icon = await getRuntimeIcon(runtime, deviceDescription.channel); 114 115 const { 116 CONNECTION_PROMPT, 117 PERMANENT_PRIVATE_BROWSING, 118 SERVICE_WORKERS_ENABLED, 119 } = RUNTIME_PREFERENCE; 120 const connectionPromptEnabled = await clientWrapper.getPreference( 121 CONNECTION_PROMPT, 122 false 123 ); 124 const privateBrowsing = await clientWrapper.getPreference( 125 PERMANENT_PRIVATE_BROWSING, 126 false 127 ); 128 const serviceWorkersEnabled = await clientWrapper.getPreference( 129 SERVICE_WORKERS_ENABLED, 130 true 131 ); 132 const serviceWorkersAvailable = serviceWorkersEnabled && !privateBrowsing; 133 134 // Fenix specific workarounds are needed until we can get proper server side APIs 135 // to detect Fenix and get the proper application names and versions. 136 // See https://github.com/mozilla-mobile/fenix/issues/2016. 137 138 // For Fenix runtimes, the ADB runtime name is more accurate than the one returned 139 // by the Device actor. 140 const runtimeName = runtime.isFenix 141 ? runtime.name 142 : deviceDescription.name; 143 144 // For Fenix runtimes, the version we should display is the application version 145 // retrieved from ADB, and not the Gecko version returned by the Device actor. 146 const version = runtime.isFenix 147 ? runtime.extra.adbPackageVersion 148 : deviceDescription.version; 149 150 const runtimeDetails = { 151 canDebugServiceWorkers: deviceDescription.canDebugServiceWorkers, 152 clientWrapper, 153 compatibilityReport, 154 connectionPromptEnabled, 155 info: { 156 deviceName: deviceDescription.deviceName, 157 icon, 158 isFenix: runtime.isFenix, 159 name: runtimeName, 160 os: deviceDescription.os, 161 type: runtime.type, 162 version, 163 }, 164 serviceWorkersAvailable, 165 }; 166 167 if (runtime.type !== RUNTIMES.THIS_FIREFOX) { 168 // `closed` event will be emitted when disabling remote debugging 169 // on the connected remote runtime. 170 clientWrapper.once("closed", onRemoteDevToolsClientClosed); 171 } 172 173 dispatch({ 174 type: CONNECT_RUNTIME_SUCCESS, 175 connectionId, 176 runtime: { 177 id, 178 runtimeDetails, 179 type: runtime.type, 180 }, 181 }); 182 } catch (e) { 183 dispatch({ type: CONNECT_RUNTIME_FAILURE, connectionId, id, error: e }); 184 } finally { 185 clearTimeout(connectionNotRespondingTimer); 186 clearTimeout(connectionCancelTimer); 187 } 188 }; 189 } 190 191 function createThisFirefoxRuntime() { 192 return ({ dispatch }) => { 193 const thisFirefoxRuntime = { 194 id: RUNTIMES.THIS_FIREFOX, 195 isConnecting: false, 196 isConnectionFailed: false, 197 isConnectionNotResponding: false, 198 isConnectionTimeout: false, 199 isUnavailable: false, 200 isUnplugged: false, 201 name: l10n.getString("about-debugging-this-firefox-runtime-name"), 202 type: RUNTIMES.THIS_FIREFOX, 203 }; 204 dispatch({ 205 type: THIS_FIREFOX_RUNTIME_CREATED, 206 runtime: thisFirefoxRuntime, 207 }); 208 }; 209 } 210 211 function disconnectRuntime(id, shouldRedirect = false) { 212 return async ({ dispatch, getState }) => { 213 dispatch({ type: DISCONNECT_RUNTIME_START }); 214 try { 215 const runtime = findRuntimeById(id, getState().runtimes); 216 const { clientWrapper } = runtime.runtimeDetails; 217 218 if (runtime.type !== RUNTIMES.THIS_FIREFOX) { 219 clientWrapper.off("closed", onRemoteDevToolsClientClosed); 220 } 221 await clientWrapper.close(); 222 if (shouldRedirect) { 223 await dispatch( 224 Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX) 225 ); 226 } 227 228 dispatch({ 229 type: DISCONNECT_RUNTIME_SUCCESS, 230 runtime: { 231 id, 232 type: runtime.type, 233 }, 234 }); 235 } catch (e) { 236 dispatch({ type: DISCONNECT_RUNTIME_FAILURE, error: e }); 237 } 238 }; 239 } 240 241 function updateConnectionPromptSetting(connectionPromptEnabled) { 242 return async ({ dispatch, getState }) => { 243 dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_START }); 244 try { 245 const runtime = getCurrentRuntime(getState().runtimes); 246 const { clientWrapper } = runtime.runtimeDetails; 247 const promptPrefName = RUNTIME_PREFERENCE.CONNECTION_PROMPT; 248 await clientWrapper.setPreference( 249 promptPrefName, 250 connectionPromptEnabled 251 ); 252 // Re-get actual value from the runtime. 253 connectionPromptEnabled = await clientWrapper.getPreference( 254 promptPrefName, 255 connectionPromptEnabled 256 ); 257 258 dispatch({ 259 type: UPDATE_CONNECTION_PROMPT_SETTING_SUCCESS, 260 connectionPromptEnabled, 261 runtime, 262 }); 263 } catch (e) { 264 dispatch({ type: UPDATE_CONNECTION_PROMPT_SETTING_FAILURE, error: e }); 265 } 266 }; 267 } 268 269 function watchRuntime(id) { 270 return async ({ dispatch, getState }) => { 271 dispatch({ type: WATCH_RUNTIME_START }); 272 273 try { 274 if (id === RUNTIMES.THIS_FIREFOX) { 275 // THIS_FIREFOX connects and disconnects on the fly when opening the page. 276 await dispatch(connectRuntime(RUNTIMES.THIS_FIREFOX)); 277 } 278 279 // The selected runtime should already have a connected client assigned. 280 const runtime = findRuntimeById(id, getState().runtimes); 281 await dispatch({ type: WATCH_RUNTIME_SUCCESS, runtime }); 282 283 dispatch(Actions.requestExtensions()); 284 // we have to wait for tabs, otherwise the requests to getTarget may interfer 285 // with listProcesses 286 await dispatch(Actions.requestTabs()); 287 dispatch(Actions.requestWorkers()); 288 289 if ( 290 isSupportedDebugTargetPane( 291 runtime.runtimeDetails.info.type, 292 DEBUG_TARGET_PANE.PROCESSES 293 ) 294 ) { 295 dispatch(Actions.requestProcesses()); 296 } 297 } catch (e) { 298 dispatch({ type: WATCH_RUNTIME_FAILURE, error: e }); 299 } 300 }; 301 } 302 303 function unwatchRuntime(id) { 304 return async ({ dispatch, getState }) => { 305 const runtime = findRuntimeById(id, getState().runtimes); 306 307 dispatch({ type: UNWATCH_RUNTIME_START, runtime }); 308 309 try { 310 if (id === RUNTIMES.THIS_FIREFOX) { 311 // THIS_FIREFOX connects and disconnects on the fly when opening the page. 312 await dispatch(disconnectRuntime(RUNTIMES.THIS_FIREFOX)); 313 } 314 315 dispatch({ type: UNWATCH_RUNTIME_SUCCESS }); 316 } catch (e) { 317 dispatch({ type: UNWATCH_RUNTIME_FAILURE, error: e }); 318 } 319 }; 320 } 321 322 function updateNetworkRuntimes(locations) { 323 const runtimes = locations.map(location => { 324 const [host, port] = location.split(":"); 325 return { 326 id: location, 327 extra: { 328 connectionParameters: { host, port: parseInt(port, 10) }, 329 }, 330 isConnecting: false, 331 isConnectionFailed: false, 332 isConnectionNotResponding: false, 333 isConnectionTimeout: false, 334 isFenix: false, 335 isUnavailable: false, 336 isUnplugged: false, 337 isUnknown: false, 338 name: location, 339 type: RUNTIMES.NETWORK, 340 }; 341 }); 342 return updateRemoteRuntimes(runtimes, RUNTIMES.NETWORK); 343 } 344 345 function updateUSBRuntimes(adbRuntimes) { 346 const runtimes = adbRuntimes.map(adbRuntime => { 347 // Set connectionParameters only for known runtimes. 348 const socketPath = adbRuntime.socketPath; 349 const deviceId = adbRuntime.deviceId; 350 const connectionParameters = socketPath ? { deviceId, socketPath } : null; 351 return { 352 id: adbRuntime.id, 353 extra: { 354 connectionParameters, 355 deviceName: adbRuntime.deviceName, 356 adbPackageVersion: adbRuntime.versionName, 357 }, 358 isConnecting: false, 359 isConnectionFailed: false, 360 isConnectionNotResponding: false, 361 isConnectionTimeout: false, 362 isFenix: adbRuntime.isFenix, 363 isUnavailable: adbRuntime.isUnavailable, 364 isUnplugged: adbRuntime.isUnplugged, 365 name: adbRuntime.shortName, 366 type: RUNTIMES.USB, 367 }; 368 }); 369 return updateRemoteRuntimes(runtimes, RUNTIMES.USB); 370 } 371 372 /** 373 * Check that a given runtime can still be found in the provided array of runtimes, and 374 * that the connection of the associated DevToolsClient is still valid. 375 * Note that this check is only valid for runtimes which match the type of the runtimes 376 * in the array. 377 */ 378 function _isRuntimeValid(runtime, runtimes) { 379 const isRuntimeAvailable = runtimes.some(r => r.id === runtime.id); 380 const isConnectionValid = 381 runtime.runtimeDetails && !runtime.runtimeDetails.clientWrapper.isClosed(); 382 return isRuntimeAvailable && isConnectionValid; 383 } 384 385 function updateRemoteRuntimes(runtimes, type) { 386 return async ({ dispatch, getState }) => { 387 const currentRuntime = getCurrentRuntime(getState().runtimes); 388 389 // Check if the updated remote runtimes should trigger a navigation out of the current 390 // runtime page. 391 if ( 392 currentRuntime && 393 currentRuntime.type === type && 394 !_isRuntimeValid(currentRuntime, runtimes) 395 ) { 396 // Since current remote runtime is invalid, move to this firefox page. 397 // This case is considered as followings and so on: 398 // * Remove ADB addon 399 // * (Physically) Disconnect USB runtime 400 // 401 // The reason we call selectPage before REMOTE_RUNTIMES_UPDATED is fired is below. 402 // Current runtime can not be retrieved after REMOTE_RUNTIMES_UPDATED action, since 403 // that updates runtime state. So, before that we fire selectPage action to execute 404 // `unwatchRuntime` correctly. 405 await dispatch( 406 Actions.selectPage(PAGE_TYPES.RUNTIME, RUNTIMES.THIS_FIREFOX) 407 ); 408 } 409 410 // For existing runtimes, transfer all properties that are not available in the 411 // runtime objects passed to this method: 412 // - runtimeDetails (set by about:debugging after a successful connection) 413 // - isConnecting (set by about:debugging during the connection) 414 // - isConnectionFailed (set by about:debugging if connection was failed) 415 // - isConnectionNotResponding 416 // (set by about:debugging if connection is taking too much time) 417 // - isConnectionTimeout (set by about:debugging if connection was timeout) 418 runtimes.forEach(runtime => { 419 const existingRuntime = findRuntimeById(runtime.id, getState().runtimes); 420 const isConnectionValid = 421 existingRuntime?.runtimeDetails && 422 !existingRuntime.runtimeDetails.clientWrapper.isClosed(); 423 runtime.runtimeDetails = isConnectionValid 424 ? existingRuntime.runtimeDetails 425 : null; 426 runtime.isConnecting = existingRuntime 427 ? existingRuntime.isConnecting 428 : false; 429 runtime.isConnectionFailed = existingRuntime 430 ? existingRuntime.isConnectionFailed 431 : false; 432 runtime.isConnectionNotResponding = existingRuntime 433 ? existingRuntime.isConnectionNotResponding 434 : false; 435 runtime.isConnectionTimeout = existingRuntime 436 ? existingRuntime.isConnectionTimeout 437 : false; 438 }); 439 440 const existingRuntimes = getAllRuntimes(getState().runtimes); 441 for (const runtime of existingRuntimes) { 442 // Runtime was connected before. 443 const isConnected = runtime.runtimeDetails; 444 // Runtime is of the same type as the updated runtimes array, so we should check it. 445 const isSameType = runtime.type === type; 446 if (isConnected && isSameType && !_isRuntimeValid(runtime, runtimes)) { 447 // Disconnect runtimes that were no longer valid. 448 await dispatch(disconnectRuntime(runtime.id)); 449 } 450 } 451 452 dispatch({ type: REMOTE_RUNTIMES_UPDATED, runtimes, runtimeType: type }); 453 454 for (const runtime of getAllRuntimes(getState().runtimes)) { 455 if (runtime.type !== type) { 456 continue; 457 } 458 459 // Reconnect clients already available in the RemoteClientManager. 460 const isConnected = !!runtime.runtimeDetails; 461 const hasConnectedClient = remoteClientManager.hasClient( 462 runtime.id, 463 runtime.type 464 ); 465 if (!isConnected && hasConnectedClient) { 466 await dispatch(connectRuntime(runtime.id)); 467 } 468 } 469 }; 470 } 471 472 /** 473 * Remove all the listeners added on client objects. Since those objects are persisted 474 * regardless of the about:debugging lifecycle, all the added events should be removed 475 * before leaving about:debugging. 476 */ 477 function removeRuntimeListeners() { 478 return ({ getState }) => { 479 const allRuntimes = getAllRuntimes(getState().runtimes); 480 const remoteRuntimes = allRuntimes.filter( 481 r => r.type !== RUNTIMES.THIS_FIREFOX 482 ); 483 for (const runtime of remoteRuntimes) { 484 if (runtime.runtimeDetails) { 485 const { clientWrapper } = runtime.runtimeDetails; 486 clientWrapper.off("closed", onRemoteDevToolsClientClosed); 487 } 488 } 489 }; 490 } 491 492 module.exports = { 493 connectRuntime, 494 createThisFirefoxRuntime, 495 disconnectRuntime, 496 removeRuntimeListeners, 497 unwatchRuntime, 498 updateConnectionPromptSetting, 499 updateNetworkRuntimes, 500 updateUSBRuntimes, 501 watchRuntime, 502 };