Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { createNotificationsRouter } from "./notifications";
import { createPageContentRouter } from "./page-content";
import { createPermissionsRouter } from "./permissions";
import { createPluginsRouter } from "./plugins";
import { createPortForwardsRouter } from "./port-forwards";
import { createPortsRouter } from "./ports";
import { createProjectsRouter } from "./projects";
import { createResourceMetricsRouter } from "./resource-metrics";
Expand Down Expand Up @@ -50,6 +51,7 @@ export const createAppRouter = (getWindow: () => BrowserWindow | null) => {
permissions: createPermissionsRouter(),
plugins: createPluginsRouter(),
ports: createPortsRouter(),
portForwards: createPortForwardsRouter(),
resourceMetrics: createResourceMetricsRouter(),
menu: createMenuRouter(),
external: createExternalRouter(),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/lib/trpc/routers/port-forwards/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createPortForwardsRouter } from "./port-forwards";
54 changes: 54 additions & 0 deletions apps/desktop/src/lib/trpc/routers/port-forwards/port-forwards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { observable } from "@trpc/server/observable";
import { portForwardManager, setRelayToken } from "main/lib/port-forward";
import type { PortForward } from "shared/types";
import { z } from "zod";
import { publicProcedure, router } from "../..";

export const createPortForwardsRouter = () => {
return router({
setRelayToken: publicProcedure
.input(z.object({ token: z.string().nullable() }))
.mutation(({ input }) => {
setRelayToken(input.token);
}),

list: publicProcedure.query((): PortForward[] => portForwardManager.list()),

subscribe: publicProcedure
.input(z.object({ clientId: z.string() }))
.subscription(({ input }) => {
return observable<PortForward[]>((emit) => {
const onChange = (forwards: PortForward[]) => emit.next(forwards);
portForwardManager.on("change", onChange);
emit.next(portForwardManager.list());
return () => {
portForwardManager.off("change", onChange);
// The subscription is per window; its teardown is how the
// manager learns a window (and its wanted set) is gone.
void portForwardManager.releaseClient(input.clientId);
};
});
}),

sync: publicProcedure
.input(
z.object({
clientId: z.string(),
hostUrl: z.string(),
workspaceId: z.string(),
ports: z.array(z.number().int().positive()),
}),
)
.mutation(
({ input }): Promise<PortForward[]> => portForwardManager.sync(input),
),

retryEphemeral: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) => portForwardManager.retryEphemeral(input.id)),

killLocalOwner: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) => portForwardManager.killLocalOwner(input.id)),
});
};
4 changes: 4 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
shutdownTanstackDbPersistence,
} from "./lib/persistence/persistence";
import { syncInstalledPluginMcpServers } from "./lib/plugin-installs";
import { portForwardManager } from "./lib/port-forward";
import { ensureProjectIconsDir, getProjectIconPath } from "./lib/project-icons";
import { runQuitCleanup } from "./lib/quit-sequence";
import { initSentry } from "./lib/sentry";
Expand Down Expand Up @@ -242,6 +243,9 @@ app.on("before-quit", async (event) => {
}

isQuitting = true;
// Local port-forward listeners hold no state worth draining; drop them so
// nothing keeps 127.0.0.1:<port> bound after the app is gone.
portForwardManager.stopAll();
// Snapshot all open windows (bounds + org) before they close, so relaunch
// restores them. markAppQuitting() stops per-window close handlers from
// shrinking the set as windows close one-by-one.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/lib/host-service-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function normalizePort(port: number): number | null {
return port;
}

function canBindPort(port: number): Promise<boolean> {
export function canBindPort(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();
const finish = (available: boolean) => {
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src/main/lib/port-forward/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { canBindPort } from "../host-service-utils";
import { portManager } from "../terminal/port-manager";
import { PortForwardManager } from "./port-forward-manager";
import { RelayForwardTransport } from "./relay-forward-transport";

export { PortForwardManager } from "./port-forward-manager";
export { RelayForwardTransport } from "./relay-forward-transport";
export type { ForwardTransport } from "./types";

let relayToken: string | null = null;

export function setRelayToken(token: string | null): void {
relayToken = token;
}

export const portForwardManager = new PortForwardManager({
transport: new RelayForwardTransport({ getToken: () => relayToken }),
getLocalPorts: () => portManager.getAllPorts(),
killLocalPort: ({ terminalId, workspaceId, port }) =>
portManager.killPort({ terminalId, workspaceId, port }),
canBindPort,
});
Loading