dsh-remote 0.1.0: master/slave remote control for DSH
- authenticated WebSocket link (dependency-free RFC6455 + stream mux), pairing codes, per-device keys (hashed), revoke/remove - /r/<device>/ proxy of the full DSH UI (HTTP, Remote WebSocket, SSE) with per-device cookie minting; slave->slave relay through the master - Devices sidebar panel (all devices' sessions, new session on device), Remote devices settings page, finished-session toasts - plugin + config sync master->slave: registry bundles via pluginManager, link: plugins as tarballs, cordis.patch.yml into a managed bundle layer (!!js preserved), allow/deny, opt-in per-secret credential sync - deploy helpers + e2e probes, unit tests
This commit is contained in:
commit
8872e73e76
21 files changed
+2914
No files matched your search
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
// E2E UI check with headless Chromium over the DevTools protocol (no playwright dep).
|
||||
// node deploy/e2e-ui.mjs <chrome-binary> <out-dir> [port]
|
||||
// Loads the local DSH (cookie minted from the browser-session secret), opens the Devices
|
||||
// panel, screenshots it, then opens /r/<first remote device>/ and screenshots that.
|
||||
import { spawn } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { cookieName, mintCookie } from '../lib/auth-cookie.mjs'
|
||||
|
||||
const [chrome, outDir, port = '3081'] = process.argv.slice(2)
|
||||
const home = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
||||
const secret = fs.readFileSync(path.join(home, '.credentials.yaml'), 'utf8').match(/browser-session:[\s\S]*?secret:\s*([A-Za-z0-9_-]{43})/)[1]
|
||||
const authority = `127.0.0.1:${port}`
|
||||
const cookieVal = mintCookie(authority, secret, 1).split('=').slice(1).join('=')
|
||||
fs.mkdirSync(outDir, { recursive: true })
|
||||
const prof = fs.mkdtempSync(path.join(os.tmpdir(), 'dshr-chrome-'))
|
||||
const proc = spawn(chrome, ['--headless=new', '--no-sandbox', '--disable-gpu', `--user-data-dir=${prof}`, '--remote-debugging-port=0', '--window-size=1500,950', 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] })
|
||||
const wsUrl = await new Promise((resolve, reject) => {
|
||||
let buf = ''
|
||||
proc.stderr.on('data', (d) => { buf += d; const m = buf.match(/DevTools listening on (ws:\/\/\S+)/); if (m) resolve(m[1]) })
|
||||
setTimeout(() => reject(new Error('chrome did not start')), 15000)
|
||||
})
|
||||
const list = await (await fetch(wsUrl.replace(/^ws/, 'http').replace(/\/devtools\/browser\/.*/, '/json/list'))).json()
|
||||
const page = list.find((t) => t.type === 'page')
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl)
|
||||
await new Promise((r) => ws.addEventListener('open', r))
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
const logs = []
|
||||
ws.addEventListener('message', (ev) => {
|
||||
const m = JSON.parse(ev.data)
|
||||
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id) }
|
||||
if (m.method === 'Runtime.consoleAPICalled') logs.push(`${m.params.type}: ${m.params.args.map((a) => a.value ?? a.description ?? '').join(' ')}`.slice(0, 300))
|
||||
if (m.method === 'Runtime.exceptionThrown') logs.push(`EXC: ${m.params.exceptionDetails?.exception?.description || m.params.exceptionDetails?.text}`.slice(0, Number(process.env.EXC_LEN || 400)))
|
||||
})
|
||||
const send = (method, params = {}) => new Promise((resolve) => { const i = ++id; pending.set(i, resolve); ws.send(JSON.stringify({ id: i, method, params })) })
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
const evalJs = async (expr) => (await send('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true })).result?.result?.value
|
||||
const shot = async (name) => {
|
||||
const r = await send('Page.captureScreenshot', { format: 'png' })
|
||||
fs.writeFileSync(path.join(outDir, name), Buffer.from(r.result.data, 'base64'))
|
||||
console.log('screenshot', path.join(outDir, name))
|
||||
}
|
||||
|
||||
await send('Page.enable'); await send('Runtime.enable')
|
||||
await send('Network.enable')
|
||||
await send('Network.setCookie', { name: cookieName(authority), value: cookieVal, domain: '127.0.0.1', path: '/', httpOnly: true })
|
||||
await send('Page.navigate', { url: `http://${authority}/` })
|
||||
await sleep(9000)
|
||||
// Click the Devices panel entry (its icon carries data-dsh-panel-entry).
|
||||
const clicked = await evalJs(`(() => { const svg = document.querySelector('[data-dsh-panel-entry="dsh-remote-devices"]'); if (!svg) return 'no-entry'; (svg.closest('button,a,[role=button]') || svg).click(); return 'clicked' })()`)
|
||||
console.log('devices entry:', clicked)
|
||||
await sleep(3500)
|
||||
console.log('panel text:', String(await evalJs(`(document.querySelector('[data-dsh-plugin="dsh-remote"]')?.innerText || 'NO PANEL').slice(0, 600)`)).replace(/\n+/g, ' | '))
|
||||
await shot('devices-panel.png')
|
||||
const localErrors = logs.filter((l) => /EXC/.test(l)).length
|
||||
console.log('local page exceptions:', localErrors, logs.filter((l) => /EXC/.test(l)).map((l) => l.slice(0, 90)).join(' || '))
|
||||
logs.length = 0
|
||||
|
||||
const remote = process.env.REMOTE_DEVICE || 'ai-pc'
|
||||
const remoteSession = process.env.REMOTE_SESSION || ''
|
||||
await send('Page.navigate', { url: `http://${authority}/r/${remote}/${remoteSession ? `#/session/${remoteSession}` : ''}` })
|
||||
await sleep(remoteSession ? 14000 : 10000)
|
||||
console.log('remote title/banner:', await evalJs(`document.title + ' :: ' + (document.querySelector('a[title*="another device"]')?.innerText || 'no banner') + ' :: hash=' + location.hash`))
|
||||
if (remoteSession) {
|
||||
console.log('remote session view:', String(await evalJs(`document.body.innerText.slice(0, 500)`)).replace(/\n+/g, ' | '))
|
||||
}
|
||||
await shot('remote-ui.png')
|
||||
console.log('console:', logs.filter((l) => /error|EXC|warn/i.test(l)).slice(0, 12).join('\n '))
|
||||
ws.close(); proc.kill('SIGKILL'); fs.rmSync(prof, { recursive: true, force: true })
|
||||
process.exit(0)
|
||||
Reference in new issue
Block a user