Skip to content
Merged
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
21 changes: 9 additions & 12 deletions ts/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { startTray } from './tray.js';
import { resolveTrayBin, startTray } from './tray.js';
import type { TrayHandle, TrayState } from './tray.js';
import { ElgatoServer, ElgatoChildServer } from './elgato.js';
import { WebUIServer } from './web/server';
Expand Down Expand Up @@ -385,23 +385,20 @@ if (!headless && tjs.env.DECKBRIDGE_OPEN) {
if (headless) {
log('info', 'tray', 'skipped (--headless)');
} else {
let trayBin = tjs.env.DECKBRIDGE_TRAY_BIN ?? '';
if (!trayBin) {
// No run.sh anymore: look for a deckbridge-tray sidecar next to the executable.
const exeDir = tjs.exePath.slice(0, tjs.exePath.lastIndexOf('/'));
const candidate = `${exeDir}/deckbridge-tray`;
try {
const st = await tjs.stat(candidate);
if (st.isFile) trayBin = candidate;
} catch {}
}
// $DECKBRIDGE_TRAY_BIN, else a deckbridge-tray sidecar next to the executable
// (no run.sh anymore). Same resolver the /requirements check reports on.
const trayBin = await resolveTrayBin();
if (trayBin) {
tray = startTray(trayBin, () => {
void shutdown().catch(() => tjs.exit(1));
});
log('info', 'tray', tray ? `started: ${trayBin}` : `failed to spawn: ${trayBin}`);
} else {
log('info', 'tray', 'DECKBRIDGE_TRAY_BIN not set — running without tray');
log(
'info',
'tray',
'no tray binary (DECKBRIDGE_TRAY_BIN unset, no deckbridge-tray next to the executable) — running without tray',
);
}
}

Expand Down
23 changes: 23 additions & 0 deletions ts/src/tray.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { warn } from './logger.js';
import { platformName } from './os-utils.ts';

export interface TrayState {
icon: 'full' | 'usb_only' | 'disconnected';
Expand Down Expand Up @@ -117,6 +118,28 @@ export function parentDir(p: string): string {
return i > 0 ? p.slice(0, i) : '.';
}

/**
* Where the tray binary actually comes from, in priority order:
* 1. $DECKBRIDGE_TRAY_BIN (dev via mise, and the Homebrew formula)
* 2. a `deckbridge-tray` sidecar next to the executable (every packaged release)
*
* Both app.ts (which spawns it) and the /requirements check call this, so the
* page can't claim "not set" while the tray is visibly running from the sidecar.
* Returns '' when neither exists.
*/
export async function resolveTrayBin(): Promise<string> {
const fromEnv = tjs.env.DECKBRIDGE_TRAY_BIN ?? '';
if (fromEnv) return fromEnv;

// parentDir handles both separators — tjs.exePath is backslash-separated on Windows.
const candidate = `${parentDir(tjs.exePath)}/deckbridge-tray${platformName() === 'Windows' ? '.exe' : ''}`;
try {
const st = await tjs.stat(candidate);
if (st.isFile) return candidate;
} catch {}
return '';
}

export function startTray(binaryPath: string, onQuit: () => void): TrayHandle | null {
try {
return TrayProcess.create(binaryPath, onQuit);
Expand Down
19 changes: 18 additions & 1 deletion ts/src/web/server/requirements.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import FFI from 'tjs:ffi';
import { getHidapiSystemCandidates } from '../../ffi/hidapi';
import { isNativeMdnsAvailable } from '../../ffi/mdns';
import { resolveTrayBin } from '../../tray.js';

export interface RequirementResult {
name: string;
Expand Down Expand Up @@ -45,6 +46,22 @@ async function checkBinary(
};
}

// Not checkBinary(): the tray is found via $DECKBRIDGE_TRAY_BIN *or* a sidecar
// next to the executable, which is how every packaged release ships it. Checking
// only the env var reported "Not found" while the tray was visibly running.
async function checkTray(): Promise<RequirementResult> {
const path = await resolveTrayBin();
const ok = path !== '' && (await fileExists(path));
return {
name: 'tray',
ok,
message: ok
? `Found: ${path}`
: 'Not found (no deckbridge-tray next to the executable, DECKBRIDGE_TRAY_BIN not set)',
installHint: ok ? undefined : 'Run: mise run tray-rs',
};
}

function checkLibhidapi(): Promise<RequirementResult> {
const bundled = tjs.env.HIDAPI_LIB ?? '';
const candidates = bundled
Expand Down Expand Up @@ -131,7 +148,7 @@ export async function checkRequirements(): Promise<RequirementResult[]> {
'DECKBRIDGE_NATIVE_LIB',
'Run: mise run deckbridge-native',
),
await checkBinary('tray', 'DECKBRIDGE_TRAY_BIN', 'Run: mise run tray-rs'),
await checkTray(),
await checkLibhidapi(),
await checkMdns(),
];
Expand Down
31 changes: 30 additions & 1 deletion ts/test/tray.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'tjs:assert';
import { parentDir, isAbsolutePath } from '../src/tray.js';
import { parentDir, isAbsolutePath, resolveTrayBin } from '../src/tray.js';

let passed = 0;
let failed = 0;
Expand Down Expand Up @@ -99,6 +99,35 @@ await asyncTest('SIGTERM stops a spawned child process', async () => {
assert.ok(exit_status !== 0 || term_signal !== null);
});

// ── resolveTrayBin ────────────────────────────────────────────────────────────
// Regression guard: /requirements used to read $DECKBRIDGE_TRAY_BIN only, so a
// packaged release (which ships the tray as a sidecar next to the executable)
// reported "Not found" while the tray was running.

console.log('\nresolveTrayBin');

await asyncTest('prefers $DECKBRIDGE_TRAY_BIN when set', async () => {
const prev = tjs.env.DECKBRIDGE_TRAY_BIN;
tjs.env.DECKBRIDGE_TRAY_BIN = '/some/explicit/deckbridge-tray';
try {
assert.equal(await resolveTrayBin(), '/some/explicit/deckbridge-tray');
} finally {
if (prev === undefined) delete tjs.env.DECKBRIDGE_TRAY_BIN;
else tjs.env.DECKBRIDGE_TRAY_BIN = prev;
}
});

await asyncTest('returns "" when the env var is unset and no sidecar exists', async () => {
const prev = tjs.env.DECKBRIDGE_TRAY_BIN;
delete tjs.env.DECKBRIDGE_TRAY_BIN;
try {
// tjs.exePath here is the test runner's tjs binary — no deckbridge-tray beside it.
assert.equal(await resolveTrayBin(), '');
} finally {
if (prev !== undefined) tjs.env.DECKBRIDGE_TRAY_BIN = prev;
}
});

// ── Summary ───────────────────────────────────────────────────────────────────

console.log(`\n${passed} passed, ${failed} failed`);
Expand Down