Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,15 @@ typings/
# Electron-Forge
out/

bin/
bin/*/thv
bin/*/README.md
bin/*/LICENSE

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/test-videos/


19 changes: 19 additions & 0 deletions bin/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM docker:dind

RUN apk add --no-cache dbus dbus-x11 gnome-keyring libsecret

RUN setcap -r /usr/bin/gnome-keyring-daemon 2>/dev/null || true

ENV XDG_CURRENT_DESKTOP=GNOME \
XDG_SESSION_DESKTOP=gnome \
DESKTOP_SESSION=gnome \
# a writable runtime dir (glib falls back to /tmp if this is unset)
XDG_RUNTIME_DIR=/tmp/xdg-runtime
ENV DOCKER_HOST=unix:///var/run/docker.sock

COPY --chmod=755 linux-x64/thv /usr/local/bin/thv
COPY ./ephemeral/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

ENTRYPOINT ["entrypoint.sh"]
CMD []
21 changes: 21 additions & 0 deletions bin/ephemeral/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env sh
set -eu

export CI=true

/usr/local/bin/dockerd-entrypoint.sh &
until docker info >/dev/null 2>&1; do sleep 0.5; done
echo "🐳 Docker-in-Docker daemon is ready."

mkdir -p /tmp/xdg-runtime/keyring
chmod 700 /tmp/xdg-runtime /tmp/xdg-runtime/keyring

eval "$(dbus-launch --sh-syntax)"
export XDG_RUNTIME_DIR=/tmp/xdg-runtime

echo "default-password" | gnome-keyring-daemon --unlock --components=secrets,ssh &
sleep 2

export GNOME_KEYRING_CONTROL GNOME_KEYRING_PID

exec "$@"
19 changes: 19 additions & 0 deletions bin/ephemeral/thv.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
set -x

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

(
cd "${ROOT_DIR}"
docker run --privileged \
--cap-drop=SETPCAP \
--cap-add=IPC_LOCK \
--tmpfs /run \
--rm -i \
--network host \
-v "${ROOT_DIR}:/workspace" \
-w /workspace \
thv-containerized thv "$@"
)
17 changes: 11 additions & 6 deletions main/src/tests/toolhive-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
vi.mock('node:child_process')
vi.mock('node:fs')
vi.mock('node:net')
vi.mock('../../utils/delay', () => ({
delay: vi.fn(
(ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
),
}))
vi.mock('electron', () => ({
app: {
isPackaged: false,
Expand All @@ -39,7 +44,7 @@
const mockScope = {
addBreadcrumb: vi.fn(),
}
callback(mockScope)
return callback(mockScope)
}),
}))

Expand Down Expand Up @@ -146,7 +151,7 @@
expect(isToolhiveRunning()).toBe(false)
})

it('finds free ports and starts the process successfully', async () => {

Check failure on line 154 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > finds free ports and starts the process successfully

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:154:5
const startPromise = startToolhive()

// Advance timers to complete async port finding
Expand Down Expand Up @@ -178,19 +183,19 @@
expect(getToolhiveMcpPort()).toBeTypeOf('number')
})

it('updates tray status when tray is provided', async () => {

Check failure on line 186 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > updates tray status when tray is provided

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:186:5
const startPromise = startToolhive(mockTray)

await vi.advanceTimersByTimeAsync(50)
await vi.advanceTimersByTimeAsync(4000)
await startPromise

expect(mockUpdateTrayStatus).toHaveBeenCalledWith(mockTray, true)
})

it('logs process PID after spawning', async () => {

Check failure on line 195 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > logs process PID after spawning

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:195:5
const startPromise = startToolhive()

await vi.advanceTimersByTimeAsync(50)
await vi.advanceTimersByTimeAsync(4000)
await startPromise

expect(mockLog.info).toHaveBeenCalledWith(
Expand All @@ -198,10 +203,10 @@
)
})

it('handles process error events', async () => {

Check failure on line 206 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > handles process error events

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:206:5
const startPromise = startToolhive(mockTray)

await vi.advanceTimersByTimeAsync(50)
await vi.advanceTimersByTimeAsync(4000)
await startPromise

const testError = new Error('Test spawn error')
Expand All @@ -218,10 +223,10 @@
expect(mockUpdateTrayStatus).toHaveBeenCalledWith(mockTray, false)
})

it('handles process exit events', async () => {

Check failure on line 226 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > handles process exit events

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:226:5
const startPromise = startToolhive(mockTray)

await vi.advanceTimersByTimeAsync(50)
await vi.advanceTimersByTimeAsync(4000)
await startPromise

mockProcess.emit('exit', 1)
Expand All @@ -233,13 +238,13 @@
expect(isToolhiveRunning()).toBe(false)
})

it('captures Sentry message when process exits unexpectedly', async () => {

Check failure on line 241 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > captures Sentry message when process exits unexpectedly

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:241:5
mockGetQuittingState.mockReturnValue(false)

const startPromise = startToolhive(mockTray)

// Advancing the timer actually allows the promise to resolve
await vi.advanceTimersByTimeAsync(50)
await vi.advanceTimersByTimeAsync(4000)
await startPromise

mockCaptureMessage.mockClear()
Expand All @@ -252,7 +257,7 @@
)
})

it('does not capture Sentry message when app is quitting', async () => {

Check failure on line 260 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > does not capture Sentry message when app is quitting

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:260:5
mockGetQuittingState.mockReturnValue(true)

const startPromise = startToolhive(mockTray)
Expand All @@ -267,7 +272,7 @@
expect(mockCaptureMessage).not.toHaveBeenCalled()
})

it('does not capture Sentry message when process exits during restart', async () => {

Check failure on line 275 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > does not capture Sentry message when process exits during restart

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:275:5
mockGetQuittingState.mockReturnValue(false)

// Start initial process
Expand Down Expand Up @@ -299,7 +304,7 @@
expect(mockCaptureMessage).not.toHaveBeenCalled()
})

it('assigns different ports to main and MCP services', async () => {

Check failure on line 307 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > assigns different ports to main and MCP services

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:307:5
const startPromise = startToolhive()

await vi.advanceTimersByTimeAsync(50)
Expand All @@ -313,7 +318,7 @@
expect(toolhivePort).not.toBe(mcpPort)
})

it('uses range for main port but any port for MCP', async () => {

Check failure on line 321 in main/src/tests/toolhive-manager.test.ts

View workflow job for this annotation

GitHub Actions / Unit Tests / Vitest

main/src/tests/toolhive-manager.test.ts > toolhive-manager > startToolhive > uses range for main port but any port for MCP

Error: Test timed out in 5000ms. If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout". ❯ main/src/tests/toolhive-manager.test.ts:321:5
const startPromise = startToolhive()

await vi.advanceTimersByTimeAsync(50)
Expand Down
27 changes: 11 additions & 16 deletions main/src/toolhive-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,17 @@ import type { Tray } from 'electron'
import { updateTrayStatus } from './system-tray'
import log from './logger'
import * as Sentry from '@sentry/electron/main'
import { delay } from '../../utils/delay'
import { getQuittingState } from './app-state'

const binName = process.platform === 'win32' ? 'thv.exe' : 'thv'
// Use environment variables for binary customization with Windows fallback
const binName =
process.env.BIN_NAME ?? (process.platform === 'win32' ? 'thv.exe' : 'thv')
const binArch = process.env.BIN_ARCH ?? `${process.platform}-${process.arch}`

const binPath = app.isPackaged
? path.join(
process.resourcesPath,
'bin',
`${process.platform}-${process.arch}`,
binName
)
: path.resolve(
__dirname,
'..',
'..',
'bin',
`${process.platform}-${process.arch}`,
binName
)
? path.join(process.resourcesPath, 'bin', binArch, binName)
: path.resolve(__dirname, '..', '..', 'bin', binArch, binName)

let toolhiveProcess: ReturnType<typeof spawn> | undefined
let toolhivePort: number | undefined
Expand Down Expand Up @@ -105,7 +98,7 @@ async function findFreePort(
}

export async function startToolhive(tray?: Tray): Promise<void> {
Sentry.withScope<Promise<void>>(async (scope) => {
return Sentry.withScope<Promise<void>>(async (scope) => {
if (!existsSync(binPath)) {
log.error(`ToolHive binary not found at: ${binPath}`)
return
Expand Down Expand Up @@ -134,6 +127,8 @@ export async function startToolhive(tray?: Tray): Promise<void> {
}
)

await delay(4000)

log.info(`[startToolhive] Process spawned with PID: ${toolhiveProcess.pid}`)

scope.addBreadcrumb({
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"scripts": {
"start": "electron-forge start",
"e2e": "env CI=true sh -c \"tsc -b --clean && tsc -b && electron-forge package && playwright test\"",
"start:ephemeral": "BIN_NAME='thv.sh' BIN_ARCH='ephemeral' electron-forge start",
"e2e": "env CI=true sh -c \"tsc -b --clean && tsc -b && electron-forge package && BIN_NAME='thv.sh' BIN_ARCH='ephemeral' playwright test\"",
"package": "tsc -b --clean && tsc -b && electron-forge package",
"make": "tsc -b --clean && tsc -b && electron-forge make",
"prettier": "prettier . --check",
Expand Down
Loading