Repository: stateful/vscode-runme
Pull request #904 (2023-10-16 21:22:03)

Optionally allow GRPC using Unix Domain Socket via settings
Ideally this will help not run into firewall issues on systems that are sensitive to making networking connections.


Patch:
diff --git a/package.json b/package.json
index 91f3b05c..e3fa1431 100644
--- a/package.json
+++ b/package.json
@@ -416,6 +416,16 @@
             "default": true,
             "markdownDescription": "Use TLS over GRPC server interface (strongly recommended - only disable if you know what you are doing!!!)"
           },
+          "runme.server.transportType": {
+            "type": "string",
+            "scope": "window",
+            "default": "TCP",
+            "enum": [
+              "TCP",
+              "UDS"
+            ],
+            "markdownDescription": "Connect to GRPC server interface via TCP or Unix Domain Socket (not available on Windows)"
+          },
           "runme.terminal.backgroundTask": {
             "type": "boolean",
             "scope": "machine",
diff --git a/src/extension/server/runmeServer.ts b/src/extension/server/runmeServer.ts
index 07ea4bdd..f8204bde 100644
--- a/src/extension/server/runmeServer.ts
+++ b/src/extension/server/runmeServer.ts
@@ -1,6 +1,7 @@
 import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'
 import fs from 'node:fs/promises'
 import path from 'node:path'
+import crypto from 'node:crypto'
 
 import { ChannelCredentials } from '@grpc/grpc-js'
 import { GrpcTransport } from '@protobuf-ts/grpc-transport'
@@ -10,10 +11,12 @@ import getLogger from '../logger'
 import { HealthCheckRequest, HealthCheckResponse_ServingStatus } from '../grpc/healthTypes'
 import { SERVER_ADDRESS } from '../../constants'
 import {
+  ServerTransportType,
   enableServerLogs,
   getBinaryPath,
   getCustomServerAddress,
   getPortNumber,
+  getServerConfigurationValue,
   getTLSDir,
   getTLSEnabled,
 } from '../../utils/configuration'
@@ -36,6 +39,7 @@ const log = getLogger('RunmeServer')
 
 class RunmeServer implements Disposable {
   #port: number
+  #socketId?: string
   #process: ChildProcessWithoutNullStreams | undefined
   #binaryPath: Uri
   #retryOnFailure: boolean
@@ -106,8 +110,32 @@ class RunmeServer implements Disposable {
     return false
   }
 
-  address() {
-    return getCustomServerAddress() || `${SERVER_ADDRESS}:${this.#port}`
+  address(): string {
+    const customAddress = getCustomServerAddress()
+    if (customAddress) {
+      return customAddress
+    }
+
+    const defaultTransport: ServerTransportType = 'TCP'
+    const transportType = getServerConfigurationValue<ServerTransportType>(
+      'transportType',
+      defaultTransport,
+    )
+
+    if (transportType === defaultTransport) {
+      const host = `${SERVER_ADDRESS}:${this.#port}`
+      return host
+    }
+
+    // only do this once and only if required
+    if (!this.#socketId) {
+      const rndBytes = crypto.randomBytes(4)
+      this.#socketId = rndBytes.toString('hex')
+    }
+
+    const sockPath = path.join('/tmp', `/runme-${this.#socketId}.sock`)
+    const unix = `unix://${sockPath}`
+    return unix
   }
 
   private get externalServer(): boolean {
diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts
index d4db48e3..37517e3c 100644
--- a/src/utils/configuration.ts
+++ b/src/utils/configuration.ts
@@ -51,11 +51,12 @@ const configurationSchema = {
     openViewInEditor: OpenViewInEditorAction.default('split'),
   },
   server: {
-    customAddress: z.string().nonempty().optional(),
+    customAddress: z.string().min(1).optional(),
     binaryPath: z.string().optional(),
     enableLogger: z.boolean().default(false),
     enableTLS: z.boolean().default(true),
     tlsDir: z.string().optional(),
+    transportType: z.enum(['TCP', 'UDS']).default('TCP'),
   },
   codelens: {
     enable: z.boolean().default(true),
@@ -77,6 +78,7 @@ const configurationSchema = {
 
 const notebookTerminalSchemaObject = z.object(notebookTerminalSchema)
 export type TerminalConfiguration = z.infer<typeof notebookTerminalSchemaObject>
+export type ServerTransportType = z.infer<typeof configurationSchema.server.transportType>
 
 const getActionsConfigurationValue = <T>(
   configName: keyof typeof configurationSchema.actions,


Test patch:
diff --git a/tests/extension/server/runmeServer.test.ts b/tests/extension/server/runmeServer.test.ts
index 75c5033f..ea33a622 100644
--- a/tests/extension/server/runmeServer.test.ts
+++ b/tests/extension/server/runmeServer.test.ts
@@ -48,6 +48,12 @@ vi.mock('node:fs/promises', async () => ({
   }
 }))
 
+vi.mock('crypto', async () => ({
+  default: {
+    randomBytes: vi.fn().mockReturnValue('abcdefgh')
+  }
+}))
+
 vi.mock('node:child_process', async () => ({
   spawn: vi.fn(),
 }))
@@ -99,21 +105,53 @@ suite('Runme server spawn process', () => {
       expect(creds._isSecure()).toBe(false)
   })
 
-  test('proper credentials with tls', async () => {
-    configValues.enableTLS = true
+    test('proper credentials with tls', async () => {
+      configValues.enableTLS = true
 
-    const server = new Server(
-      Uri.file('/Users/user/.vscode/extension/stateful.runme'),
-      {
-        retryOnFailure: true,
-        maxNumberOfIntents: 2,
-      },
-      false
-    )
+      const server = new Server(
+        Uri.file('/Users/user/.vscode/extension/stateful.runme'),
+        {
+          retryOnFailure: true,
+          maxNumberOfIntents: 2,
+        },
+        false
+      )
 
-    const creds = await server['channelCredentials']()
-    expect(creds._isSecure()).toBe(true)
-})
+      const creds = await server['channelCredentials']()
+      expect(creds._isSecure()).toBe(true)
+    })
+
+    test('uses UDS instead of TCP socket', async () => {
+      configValues.transportType = 'UDS'
+
+      const server = new Server(
+        Uri.file('/Users/user/.vscode/extension/stateful.runme'),
+        {
+          retryOnFailure: true,
+          maxNumberOfIntents: 2,
+        },
+        false
+      )
+
+      const address = server['address']()
+      expect(address).toStrictEqual('unix:///tmp/runme-abcdefgh.sock')
+    })
+
+    test('uses TCP socket by default', async () => {
+      configValues.transportType = undefined
+
+      const server = new Server(
+        Uri.file('/Users/user/.vscode/extension/stateful.runme'),
+        {
+          retryOnFailure: true,
+          maxNumberOfIntents: 2,
+        },
+        false
+      )
+
+      const address = server['address']()
+      expect(address).toStrictEqual('localhost:7863')
+    })
 
     test('Should try 2 times before failing', async () => {
       configValues.enableTLS = true