diff --git a/src/main/lens-proxy/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts index 99fefd5c45..5be786e28d 100644 --- a/src/main/lens-proxy/lens-proxy.ts +++ b/src/main/lens-proxy/lens-proxy.ts @@ -22,6 +22,10 @@ import type { GetClusterForRequest } from "../cluster/get-cluster-for-request.in export type ServerIncomingMessage = SetRequired; export type ProxyRequestHandler = (args: ProxyApiRequestArgs) => void | Promise; +export interface ListenOptions { + signal: AbortSignal; +} + interface Dependencies { getClusterForRequest: GetClusterForRequest; shellApiRequest: ProxyRequestHandler; @@ -108,10 +112,14 @@ export class LensProxy { * * Resolves with the port number that was picked */ - private attemptToListen(): Promise { + private attemptToListen(options: ListenOptions): Promise { return new Promise((resolve, reject) => { this.proxyServer.listen(0, "127.0.0.1"); + options.signal.addEventListener("abort", () => { + this.close(); + }); + this.proxyServer .once("listening", () => { this.proxyServer.removeAllListeners("error"); // don't reject the promise @@ -140,12 +148,12 @@ export class LensProxy { * @resolves After the server is listening on a good port * @rejects if there is an error before that happens */ - async listen(): Promise { + async listen(options: ListenOptions): Promise { const seenPorts = new Set(); while(true) { this.proxyServer?.close(); - const port = await this.attemptToListen(); + const port = await this.attemptToListen(options); if (!disallowedPorts.has(port)) { // We didn't get a port that would result in an ERR_UNSAFE_PORT error, use it diff --git a/src/main/start-main-application/runnables/lens-proxy/request-app-version.injectable.ts b/src/main/start-main-application/runnables/lens-proxy/request-app-version.injectable.ts index 5de79196fb..70d4f5e553 100644 --- a/src/main/start-main-application/runnables/lens-proxy/request-app-version.injectable.ts +++ b/src/main/start-main-application/runnables/lens-proxy/request-app-version.injectable.ts @@ -4,16 +4,31 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import lensFetchInjectable from "../../../../common/fetch/lens-fetch.injectable"; +import type { AbortSignal as NonStandardAbortSignal } from "abort-controller"; + +export interface RequestOptions { + signal: AbortSignal; +} + +export type RequestAppVersionViaProxy = (options: RequestOptions) => Promise; const requestAppVersionViaProxyInjectable = getInjectable({ id: "request-app-version-via-proxy", - instantiate: (di) => { + instantiate: (di): RequestAppVersionViaProxy => { const lensFetch = di.inject(lensFetchInjectable); - return async () => { - const response = await lensFetch("/version"); + return async (options) => { + const response = await lensFetch("/version", { + signal: options.signal as NonStandardAbortSignal, + }); - return (await response.json() as { version: string }).version; + if (response.status !== 200) { + throw new Error(`Retrieving version failed: ${response.statusText}`); + } + + const body = await response.json() as { version: string }; + + return body.version; }; }, }); diff --git a/src/main/start-main-application/runnables/lens-proxy/setup.injectable.ts b/src/main/start-main-application/runnables/lens-proxy/setup.injectable.ts index 137bdaebda..317d817df4 100644 --- a/src/main/start-main-application/runnables/lens-proxy/setup.injectable.ts +++ b/src/main/start-main-application/runnables/lens-proxy/setup.injectable.ts @@ -3,68 +3,19 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import exitAppInjectable from "../../../electron-app/features/exit-app.injectable"; -import lensProxyInjectable from "../../../lens-proxy/lens-proxy.injectable"; -import loggerInjectable from "../../../../common/logger.injectable"; -import isWindowsInjectable from "../../../../common/vars/is-windows.injectable"; -import showErrorPopupInjectable from "../../../electron-app/features/show-error-popup.injectable"; import { beforeApplicationIsLoadingInjectionToken } from "../../runnable-tokens/before-application-is-loading-injection-token"; -import buildVersionInjectable from "../../../vars/build-version/build-version.injectable"; -import requestAppVersionViaProxyInjectable from "./request-app-version.injectable"; +import setupLensProxyStartableStoppableInjectable from "./startable-stoppable.injectable"; const setupLensProxyInjectable = getInjectable({ id: "setup-lens-proxy", instantiate: (di) => { - const lensProxy = di.inject(lensProxyInjectable); - const exitApp = di.inject(exitAppInjectable); - const logger = di.inject(loggerInjectable); - const requestAppVersionViaProxy = di.inject(requestAppVersionViaProxyInjectable); - const isWindows = di.inject(isWindowsInjectable); - const showErrorPopup = di.inject(showErrorPopupInjectable); - const buildVersion = di.inject(buildVersionInjectable); + const setupLensProxyStartableStoppable = di.inject(setupLensProxyStartableStoppableInjectable); return { id: "setup-lens-proxy", - run: async () => { - try { - logger.info("🔌 Starting LensProxy"); - await lensProxy.listen(); // lensProxy.port available - } catch (error: any) { - showErrorPopup("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); - - return exitApp(); - } - - // test proxy connection - try { - logger.info("🔎 Testing LensProxy connection ..."); - const versionFromProxy = await requestAppVersionViaProxy(); - - if (buildVersion.get() !== versionFromProxy) { - logger.error("Proxy server responded with invalid response"); - - return exitApp(); - } - - logger.info("⚡ LensProxy connection OK"); - } catch (error) { - logger.error(`🛑 LensProxy: failed connection test: ${error}`); - - const hostsPath = isWindows - ? "C:\\windows\\system32\\drivers\\etc\\hosts" - : "/etc/hosts"; - const message = [ - `Failed connection test: ${error}`, - "Check to make sure that no other versions of Lens are running", - `Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`, - "If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.", - ]; - - showErrorPopup("Lens Proxy Error", message.join("\n\n")); - - return exitApp(); - } + run: () => { + setupLensProxyStartableStoppable.start(); }, }; }, diff --git a/src/main/start-main-application/runnables/lens-proxy/startable-stoppable.injectable.ts b/src/main/start-main-application/runnables/lens-proxy/startable-stoppable.injectable.ts new file mode 100644 index 0000000000..51e4f1c7af --- /dev/null +++ b/src/main/start-main-application/runnables/lens-proxy/startable-stoppable.injectable.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import loggerInjectable from "../../../../common/logger.injectable"; +import { getStartableStoppable } from "../../../../common/utils/get-startable-stoppable"; +import isWindowsInjectable from "../../../../common/vars/is-windows.injectable"; +import exitAppInjectable from "../../../electron-app/features/exit-app.injectable"; +import showErrorPopupInjectable from "../../../electron-app/features/show-error-popup.injectable"; +import lensProxyInjectable from "../../../lens-proxy/lens-proxy.injectable"; +import buildVersionInjectable from "../../../vars/build-version/build-version.injectable"; +import requestAppVersionViaProxyInjectable from "./request-app-version.injectable"; + +const setupLensProxyStartableStoppableInjectable = getInjectable({ + id: "setup-lens-proxy-startable-stoppable", + instantiate: (di) => { + const lensProxy = di.inject(lensProxyInjectable); + const exitApp = di.inject(exitAppInjectable); + const logger = di.inject(loggerInjectable); + const requestAppVersionViaProxy = di.inject(requestAppVersionViaProxyInjectable); + const isWindows = di.inject(isWindowsInjectable); + const showErrorPopup = di.inject(showErrorPopupInjectable); + const buildVersion = di.inject(buildVersionInjectable); + + return getStartableStoppable("setup-lens-proxy", () => { + const controller = new AbortController(); + + (async () => { + try { + logger.info("🔌 Starting LensProxy"); + await lensProxy.listen({ signal: controller.signal }); // lensProxy.port available + } catch (error: any) { + showErrorPopup("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); + + return exitApp(); + } + + // test proxy connection + try { + logger.info("🔎 Testing LensProxy connection ..."); + const versionFromProxy = await requestAppVersionViaProxy({ signal: controller.signal }); + + if (buildVersion.get() !== versionFromProxy) { + logger.error("Proxy server responded with invalid response"); + + return exitApp(); + } + + logger.info("⚡ LensProxy connection OK"); + } catch (error) { + logger.error(`🛑 LensProxy: failed connection test: ${error}`); + + const hostsPath = isWindows + ? "C:\\windows\\system32\\drivers\\etc\\hosts" + : "/etc/hosts"; + const message = [ + `Failed connection test: ${error}`, + "Check to make sure that no other versions of Lens are running", + `Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`, + "If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.", + ]; + + showErrorPopup("Lens Proxy Error", message.join("\n\n")); + + return exitApp(); + } + })(); + + return () => controller.abort(); + }); + }, +}); + +export default setupLensProxyStartableStoppableInjectable; diff --git a/src/main/start-main-application/runnables/lens-proxy/teardown.injectable.ts b/src/main/start-main-application/runnables/lens-proxy/teardown.injectable.ts new file mode 100644 index 0000000000..55ef8e35f3 --- /dev/null +++ b/src/main/start-main-application/runnables/lens-proxy/teardown.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../runnable-tokens/before-quit-of-back-end-injection-token"; +import setupLensProxyStartableStoppableInjectable from "./startable-stoppable.injectable"; + +const stopSettingUpLensProxyInjectable = getInjectable({ + id: "stop-setting-up-lens-proxy", + instantiate: (di) => { + const setupLensProxyStartableStoppable = di.inject(setupLensProxyStartableStoppableInjectable); + + return { + id: "stop-setting-up-lens-proxy", + run: () => { + setupLensProxyStartableStoppable.stop(); + }, + }; + }, + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopSettingUpLensProxyInjectable;