From 6646917a9aa738d2df8d0373bdabaaad89b2bea8 Mon Sep 17 00:00:00 2001 From: Tomas Zaluckij Date: Sat, 10 Jan 2026 23:01:24 +0000 Subject: [PATCH 1/3] Fix Windows and MacOS startup issues by using a previous workaround --- .../version-plan-1768086780974.md | 5 +++ apps/desktop/build.ts | 6 +++- apps/desktop/src/main/index.ts | 34 ++++++++----------- tools/eslint/config.ts | 1 - 4 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 .nx/version-plans/version-plan-1768086780974.md diff --git a/.nx/version-plans/version-plan-1768086780974.md b/.nx/version-plans/version-plan-1768086780974.md new file mode 100644 index 000000000..536989d1a --- /dev/null +++ b/.nx/version-plans/version-plan-1768086780974.md @@ -0,0 +1,5 @@ +--- +desktop: patch +--- + +Revert to manual ASAR binary unpacking and execution due to issues on Windows and MacOS diff --git a/apps/desktop/build.ts b/apps/desktop/build.ts index e7c3d8980..c0ae75b8c 100644 --- a/apps/desktop/build.ts +++ b/apps/desktop/build.ts @@ -6,7 +6,11 @@ import { fileURLToPath } from 'node:url'; const config: Configuration = { artifactName: '${productName}-${version}-${platform}-${arch}.${ext}', - asarUnpack: ['resources/**'], + asarUnpack: [ + 'resources/**', + '**/node_modules/@the-dev-tools/server/dist/server', + '**/node_modules/@the-dev-tools/cli/dist/cli', + ], extraMetadata: { name: 'DevTools', }, diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 1e37e2cd9..8f2075cb2 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,10 +1,9 @@ import { Command, FetchHttpClient, Path, Url } from '@effect/platform'; import * as NodeContext from '@effect/platform-node/NodeContext'; import * as NodeRuntime from '@effect/platform-node/NodeRuntime'; -import { Config, Console, Effect, pipe, Runtime } from 'effect'; +import { Config, Console, Effect, pipe, Runtime, String } from 'effect'; import { app, BrowserWindow, dialog, Dialog, globalShortcut, ipcMain, protocol, shell } from 'electron'; import { autoUpdater } from 'electron-updater'; -import child_process from 'node:child_process'; import os from 'node:os'; import { Agent } from 'undici'; import { CustomUpdateProvider, UpdateOptions } from './update'; @@ -99,19 +98,6 @@ const createWindow = Effect.gen(function* () { } }); -// Only 'child_process.execFile' is supported for executing binaries inside ASAR archives -// https://www.electronjs.org/docs/latest/tutorial/asar-archives#executing-binaries-inside-asar-archive -const execFile = (file: string, args?: string[], options?: child_process.ExecFileOptions) => - Effect.async((resume) => { - child_process.execFile(file, args, options, (error, stdout, stderr) => { - Effect.gen(function* () { - if (stdout) console.log(stdout); - if (stderr) console.error(stderr); - if (error) yield* Effect.fail(error); - }).pipe(resume); - }); - }); - const server = pipe( Effect.gen(function* () { const path = yield* Path.Path; @@ -122,16 +108,22 @@ const server = pipe( Effect.flatMap(path.fromFileUrl), ); - yield* execFile(path.join(dist, 'server'), undefined, { - env: { + yield* pipe( + path.join(dist, 'server'), + String.replaceAll('app.asar', 'app.asar.unpacked'), + Command.make, + Command.env({ // TODO: we probably shouldn't encrypt local database DB_ENCRYPTION_KEY: 'secret', DB_MODE: 'local', DB_NAME: 'state', DB_PATH: app.getPath('userData'), HMAC_SECRET: 'secret', - }, - }); + }), + Command.stdout('inherit'), + Command.stderr('inherit'), + Command.exitCode, + ); yield* Effect.interrupt; }), @@ -266,7 +258,9 @@ const cli = pipe( Effect.flatMap(path.fromFileUrl), ); - yield* execFile(path.join(dist, 'cli'), args); + const bin = pipe(path.join(dist, 'cli'), String.replaceAll('app.asar', 'app.asar.unpacked')); + + yield* pipe(Command.make(bin, ...args), Command.stdout('inherit'), Command.stderr('inherit'), Command.exitCode); app.quit(); }), diff --git a/tools/eslint/config.ts b/tools/eslint/config.ts index 1e01a13f7..641d485b9 100644 --- a/tools/eslint/config.ts +++ b/tools/eslint/config.ts @@ -97,7 +97,6 @@ const rules = defineConfig({ rules: { '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreVoidOperator: true }], '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }], - '@typescript-eslint/no-invalid-void-type': 'off', // re-enable once improved https://github.com/typescript-eslint/typescript-eslint/issues/8113 '@typescript-eslint/no-meaningless-void-operator': 'off', '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }], '@typescript-eslint/no-non-null-assertion': 'off', // in protobuf everything is optional, requiring assertions From e72a8a653bf3dc602aad6f52352d34e8903ee95a Mon Sep 17 00:00:00 2001 From: Tomas Zaluckij Date: Sat, 10 Jan 2026 23:20:21 +0000 Subject: [PATCH 2/3] Switch Unix sockets to named pipes for Windows due to library incompatibilities --- .nx/version-plans/version-plan-1768087084699.md | 5 +++++ apps/desktop/src/main/index.ts | 5 ++++- packages/worker-js/src/main.ts | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .nx/version-plans/version-plan-1768087084699.md diff --git a/.nx/version-plans/version-plan-1768087084699.md b/.nx/version-plans/version-plan-1768087084699.md new file mode 100644 index 000000000..65390c53b --- /dev/null +++ b/.nx/version-plans/version-plan-1768087084699.md @@ -0,0 +1,5 @@ +--- +desktop: patch +--- + +Fix Windows RPC issues by switching to named pipes diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 8f2075cb2..d086c0a8e 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -167,11 +167,14 @@ const onReady = Effect.gen(function* () { }); yield* Effect.tryPromise(() => autoUpdater.checkForUpdatesAndNotify()); + let socketPath = path.join(os.tmpdir(), 'the-dev-tools', 'server.socket'); + if (os.platform() === 'win32') socketPath = '\\\\.\\pipe\\the-dev-tools_server.socket'; + // Redirect server IPC into a UDS // https://nodejs.org/api/globals.html#custom-dispatcher // https://undici.nodejs.org/#/docs/api/Client?id=parameter-connectoptions const dispatcher = new Agent({ - socketPath: path.join(os.tmpdir(), 'the-dev-tools', 'server.socket'), + socketPath, // Disable timeout for sync streams bodyTimeout: 0, diff --git a/packages/worker-js/src/main.ts b/packages/worker-js/src/main.ts index bcb835e88..3e08a8b17 100644 --- a/packages/worker-js/src/main.ts +++ b/packages/worker-js/src/main.ts @@ -29,6 +29,13 @@ NodeJsExecutorService(connectRouter); // - WORKER_PORT: port number (tcp mode, defaults to 9090) const WorkerServerUdsLive = Effect.gen(function* () { + if (os.platform() === 'win32') { + return yield* pipe( + NodeHttpServer.layer(createServer, { path: '\\\\.\\pipe\\the-dev-tools_worker-js.socket' }), + Layer.build, + ); + } + const path = yield* Path.Path; const fs = yield* FileSystem.FileSystem; From c0cf3cfe6e9c8f2d44fdb101ed387037d788dd96 Mon Sep 17 00:00:00 2001 From: ElecTwix Date: Sun, 11 Jan 2026 03:30:00 +0300 Subject: [PATCH 3/3] feat(server): support named pipes on Windows for IPC --- packages/server/cmd/server/server.go | 5 +- packages/server/go.mod | 1 + packages/server/go.sum | 2 + packages/server/internal/api/api.go | 93 ++++----------------- packages/server/internal/api/api_unix.go | 67 +++++++++++++++ packages/server/internal/api/api_windows.go | 45 ++++++++++ 6 files changed, 135 insertions(+), 78 deletions(-) create mode 100644 packages/server/internal/api/api_unix.go create mode 100644 packages/server/internal/api/api_windows.go diff --git a/packages/server/cmd/server/server.go b/packages/server/cmd/server/server.go index 3d85d5e61..e82c3f58c 100644 --- a/packages/server/cmd/server/server.go +++ b/packages/server/cmd/server/server.go @@ -400,15 +400,14 @@ func run() error { jsHTTPClient = &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - dialer := net.Dialer{} - return dialer.DialContext(ctx, "unix", workerSocketPath) + return api.DialWorker(ctx, workerSocketPath) }, }, } // NOTE: ConnectRPC requires an address even for Unix sockets. // Use placeholder since actual routing is via socket. jsBaseURL = "http://the-dev-tools:0" - slog.Info("Connecting to worker-js via Unix socket", "path", workerSocketPath) + slog.Info("Connecting to worker-js via socket", "path", workerSocketPath) } jsClient := node_js_executorv1connect.NewNodeJsExecutorServiceClient( diff --git a/packages/server/go.mod b/packages/server/go.mod index c1368b1e3..9283f641e 100644 --- a/packages/server/go.mod +++ b/packages/server/go.mod @@ -4,6 +4,7 @@ go 1.25 require ( connectrpc.com/connect v1.19.1 + github.com/Microsoft/go-winio v0.6.2 github.com/andybalholm/brotli v1.2.0 github.com/expr-lang/expr v1.17.7 github.com/goccy/go-json v0.10.5 diff --git a/packages/server/go.sum b/packages/server/go.sum index 76a72966b..5d0c81d5b 100644 --- a/packages/server/go.sum +++ b/packages/server/go.sum @@ -3,6 +3,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-202512091757 connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= diff --git a/packages/server/internal/api/api.go b/packages/server/internal/api/api.go index 4ab30fa43..8db5460bc 100644 --- a/packages/server/internal/api/api.go +++ b/packages/server/internal/api/api.go @@ -2,13 +2,9 @@ package api import ( - "context" - "log" "log/slog" - "net" "net/http" "os" - "path/filepath" "time" "github.com/rs/cors" @@ -71,14 +67,19 @@ const ( ServerModeTCP = "tcp" ) -// DefaultServerSocketPath returns the default path for the server Unix socket. -func DefaultServerSocketPath() string { - return filepath.Join(os.TempDir(), "the-dev-tools", "server.socket") -} - -// DefaultWorkerSocketPath returns the default path for the worker-js Unix socket. -func DefaultWorkerSocketPath() string { - return filepath.Join(os.TempDir(), "the-dev-tools", "worker-js.socket") +func newH2CServer(mux *http.ServeMux) *http.Server { + return &http.Server{ + // NOTE: ConnectRPC requires an address even for Unix sockets. + // Use a placeholder address since actual routing is via socket. + Addr: "the-dev-tools:0", + ReadHeaderTimeout: 10 * time.Second, + // INFO: Use h2c so we can serve HTTP/2 without TLS. + Handler: h2c.NewHandler(newCORS().Handler(mux), &http2.Server{ + IdleTimeout: 0, + MaxConcurrentStreams: 100000, + MaxHandlers: 0, + }), + } } // ListenServices starts the server listening on either a Unix socket or TCP port. @@ -104,75 +105,17 @@ func ListenServices(services []Service, port string) error { case ServerModeTCP: return listenTCP(mux, port) case ServerModeUDS: - return listenUnix(mux) + return listenIPC(mux) default: slog.Warn("Unknown SERVER_MODE, falling back to uds", "mode", mode) - return listenUnix(mux) + return listenIPC(mux) } } func listenTCP(mux *http.ServeMux, port string) error { - srv := &http.Server{ - Addr: ":" + port, - ReadHeaderTimeout: 10 * time.Second, - // INFO: Use h2c so we can serve HTTP/2 without TLS. - Handler: h2c.NewHandler(newCORS().Handler(mux), &http2.Server{ - IdleTimeout: 0, - MaxConcurrentStreams: 100000, - MaxHandlers: 0, - }), - } + srv := newH2CServer(mux) + srv.Addr = ":" + port slog.Info("Server listening on TCP", "port", port) return srv.ListenAndServe() -} - -func listenUnix(mux *http.ServeMux) error { - socketPath := os.Getenv("SERVER_SOCKET_PATH") - if socketPath == "" { - socketPath = DefaultServerSocketPath() - } - - srv := &http.Server{ - // NOTE: ConnectRPC requires an address even for Unix sockets. - // Use a placeholder address since actual routing is via socket. - Addr: "the-dev-tools:0", - ReadHeaderTimeout: 10 * time.Second, - // INFO: Use h2c so we can serve HTTP/2 without TLS. - Handler: h2c.NewHandler(newCORS().Handler(mux), &http2.Server{ - IdleTimeout: 0, - MaxConcurrentStreams: 100000, - MaxHandlers: 0, - }), - } - - socketDir := filepath.Dir(socketPath) - - // Create socket directory if it doesn't exist - if err := os.MkdirAll(socketDir, 0o750); err != nil { - return err - } - - // Remove stale socket file if present (e.g., from a previous crash) - if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { - slog.Warn("Failed to remove stale socket", "path", socketPath, "error", err) - } - - // Create Unix socket listener - lc := net.ListenConfig{} - socket, err := lc.Listen(context.Background(), "unix", socketPath) - if err != nil { - log.Fatal(err) - } - - slog.Info("Server listening on Unix socket", "path", socketPath) - - // Ensure socket cleanup on server close - srv.RegisterOnShutdown(func() { - if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { - slog.Warn("Failed to remove socket on shutdown", "path", socketPath, "error", err) - } - }) - - return srv.Serve(socket) -} +} \ No newline at end of file diff --git a/packages/server/internal/api/api_unix.go b/packages/server/internal/api/api_unix.go new file mode 100644 index 000000000..e5ba3331f --- /dev/null +++ b/packages/server/internal/api/api_unix.go @@ -0,0 +1,67 @@ +//go:build !windows + +package api + +import ( + "context" + "log" + "log/slog" + "net" + "net/http" + "os" + "path/filepath" +) + +// DefaultServerSocketPath returns the default path for the server Unix socket. +func DefaultServerSocketPath() string { + return filepath.Join(os.TempDir(), "the-dev-tools", "server.socket") +} + +// DefaultWorkerSocketPath returns the default path for the worker-js Unix socket. +func DefaultWorkerSocketPath() string { + return filepath.Join(os.TempDir(), "the-dev-tools", "worker-js.socket") +} + +func listenIPC(mux *http.ServeMux) error { + socketPath := os.Getenv("SERVER_SOCKET_PATH") + if socketPath == "" { + socketPath = DefaultServerSocketPath() + } + + srv := newH2CServer(mux) + + socketDir := filepath.Dir(socketPath) + + // Create socket directory if it doesn't exist + if err := os.MkdirAll(socketDir, 0o750); err != nil { + return err + } + + // Remove stale socket file if present (e.g., from a previous crash) + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + slog.Warn("Failed to remove stale socket", "path", socketPath, "error", err) + } + + // Create Unix socket listener + lc := net.ListenConfig{} + socket, err := lc.Listen(context.Background(), "unix", socketPath) + if err != nil { + log.Fatal(err) + } + + slog.Info("Server listening on Unix socket", "path", socketPath) + + // Ensure socket cleanup on server close + srv.RegisterOnShutdown(func() { + if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { + slog.Warn("Failed to remove socket on shutdown", "path", socketPath, "error", err) + } + }) + + return srv.Serve(socket) +} + +func DialWorker(ctx context.Context, socketPath string) (net.Conn, error) { + dialer := net.Dialer{} + return dialer.DialContext(ctx, "unix", socketPath) +} \ No newline at end of file diff --git a/packages/server/internal/api/api_windows.go b/packages/server/internal/api/api_windows.go new file mode 100644 index 000000000..900418a03 --- /dev/null +++ b/packages/server/internal/api/api_windows.go @@ -0,0 +1,45 @@ +//go:build windows + +package api + +import ( + "context" + "log/slog" + "net" + "net/http" + "os" + + "github.com/Microsoft/go-winio" +) + +// DefaultServerSocketPath returns the default path for the server Unix socket. +func DefaultServerSocketPath() string { + return `\\.\pipe\the-dev-tools_server.socket` +} + +// DefaultWorkerSocketPath returns the default path for the worker-js Unix socket. +func DefaultWorkerSocketPath() string { + return `\\.\pipe\the-dev-tools_worker-js.socket` +} + +func listenIPC(mux *http.ServeMux) error { + socketPath := os.Getenv("SERVER_SOCKET_PATH") + if socketPath == "" { + socketPath = DefaultServerSocketPath() + } + + srv := newH2CServer(mux) + + slog.Info("Server listening on Named Pipe", "path", socketPath) + + listener, err := winio.ListenPipe(socketPath, nil) + if err != nil { + return err + } + + return srv.Serve(listener) +} + +func DialWorker(ctx context.Context, socketPath string) (net.Conn, error) { + return winio.DialPipe(socketPath, nil) +} \ No newline at end of file