mirror of
https://github.com/sotashimozono/obsidian-remote-ssh.git
synced 2026-07-22 06:52:07 +00:00
The "Debug: test RPC tunnel" command now ships, restarts, and verifies
the daemon end-to-end without any manual scp + nohup steps. Build
pipeline grew an `npm run build:server` that cross-compiles the Go
daemon for Linux/amd64 and stages it next to main.js so the plugin
can find it at runtime.
ServerDeployer (plugin/src/transport/ServerDeployer.ts)
- mkdir -p ~/.obsidian-remote && chmod 700 it
- pkill any prior daemon and remove stale socket / token
- SFTP-upload the local binary, chmod +x
- nohup the daemon with --vault-root / --socket / --token-file
/ --verbose, redirect stdout+stderr to ~/.obsidian-remote/server.log,
detach
- poll the token file for up to 5s; return the token + paths
- stop() helper for the inverse: pkill + cleanup
Built on a tiny `DeployerSshClient` interface so unit tests don't
have to bring up a real SSH session. shellQuote single-quotes every
argument so paths with spaces survive the shell.
SftpClient
- uploadFile(localPath, remotePath) — sftp.fastPut wrapper.
- exec(cmd) — runs a one-shot command, collects stdout/stderr/exit.
Used for mkdir / pkill / chmod / nohup. Long-running streams must
not go through here (they'd never close).
Build pipeline
- plugin/scripts/build-server.mjs cross-compiles
./cmd/obsidian-remote-server with GOOS=linux GOARCH=amd64
CGO_ENABLED=0 and writes the result to plugin/server-bin/
obsidian-remote-server-linux-amd64. Falls back to
~/tools/go-portable/go on the dev box when go isn't on PATH.
REMOTE_SSH_SKIP_SERVER_BUILD=1 short-circuits for hosts without a
Go toolchain.
- dev-install.mjs additionally copies plugin/server-bin/* into
<vault>/.obsidian/plugins/remote-ssh/server-bin/ so the plugin
finds the binary at runtime.
- New scripts: `npm run build:server`, `npm run build:full`
(server + plugin + install in one shot).
- Root .gitignore now excludes plugin/server-bin/.
main.ts
- locateDaemonBinary() resolves
<vault>/.obsidian/plugins/<id>/server-bin/obsidian-remote-server-
linux-amd64; returns null when missing.
- debugTestRpcTunnel():
1. locateDaemonBinary, error early when missing
2. ServerDeployer.deploy(...) ships and starts the daemon
3. openUnixStream + establishRpcConnection
4. RpcRemoteFsClient.list(activeRemoteBasePath) for the smoke
check, log each step, surface a Notice with daemon version.
Tests (vitest)
- ServerDeployer.test.ts (8 cases): order of mkdir → kill → upload
→ chmod → start → wait-for-token, killExisting=false skips the
kill, custom remote paths flow into the start command, paths
with spaces are single-quoted, non-zero exit on a non-tolerant
command surfaces, token-read retries until present, deadline is
enforced, stop() runs pkill + cleanup.
Verification
- cd plugin && npx tsc --noEmit clean
- cd plugin && npm test 123 / 123 pass (8 new)
- cd plugin && npm run build:full Go cross-compile (3.75
MiB) + plugin build +
dev vault install all
green; bundle 382 KB
(+3 KB over 5-D.3).
Dogfood (after merge)
1. cd plugin && npm run build:full
2. Reload the plugin in dev vault.
3. Run "Connect to remote vault" against the Panza profile.
4. Run "Debug: test RPC tunnel against obsidian-remote-server".
Expect: a Notice with daemon version + entry count, and a
per-step trace in console.log. No manual scp / start steps.
Follow-ups
- Phase 5-D.4: switch SftpDataAdapter's client over to
RpcRemoteFsClient automatically when a profile carries an
rpcSocketPath, with a fallback to the SFTP path.
- Cross-arch staging (linux/arm64, darwin/*) for non-amd64 hosts.
- ServerDeployer.stop wired into Plugin.disconnect so daemons don't
outlive the Obsidian session.
119 lines
3.7 KiB
JavaScript
119 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* build-server — cross-compile the obsidian-remote-server Go daemon
|
|
* for linux/amd64 and stage the resulting binary into
|
|
* <plugin>/server-bin/ so dev-install.mjs picks it up alongside
|
|
* main.js / manifest.json / styles.css.
|
|
*
|
|
* Usage:
|
|
* node scripts/build-server.mjs
|
|
* node scripts/build-server.mjs --goos=linux --goarch=arm64
|
|
*
|
|
* Locating Go:
|
|
* 1. `go` on PATH (preferred).
|
|
* 2. `~/tools/go-portable/go/bin/go(.exe)` — useful for the dev box
|
|
* where Go isn't installed system-wide; ignored elsewhere.
|
|
* 3. Otherwise the script exits with a clear error.
|
|
*
|
|
* Skipping:
|
|
* Set REMOTE_SSH_SKIP_SERVER_BUILD=1 to no-op (build:install can
|
|
* then run on machines that don't have Go and don't need a fresh
|
|
* binary).
|
|
*/
|
|
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as os from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const pluginRoot = path.resolve(__dirname, '..');
|
|
const repoRoot = path.resolve(pluginRoot, '..');
|
|
const serverDir = path.join(repoRoot, 'server');
|
|
const stageDir = path.join(pluginRoot, 'server-bin');
|
|
|
|
if (process.env.REMOTE_SSH_SKIP_SERVER_BUILD === '1') {
|
|
console.log('build-server: REMOTE_SSH_SKIP_SERVER_BUILD set, skipping');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Parse flags.
|
|
const args = process.argv.slice(2);
|
|
const flag = (name) => {
|
|
const hit = args.find(a => a.startsWith(`--${name}=`));
|
|
return hit ? hit.slice(name.length + 3) : undefined;
|
|
};
|
|
const goos = flag('goos') ?? 'linux';
|
|
const goarch = flag('goarch') ?? 'amd64';
|
|
const outName = `obsidian-remote-server-${goos}-${goarch}` + (goos === 'windows' ? '.exe' : '');
|
|
|
|
const goExe = locateGo();
|
|
if (!goExe) {
|
|
console.error('build-server: could not locate the Go toolchain.');
|
|
console.error(' Install Go (https://go.dev/dl/) or set REMOTE_SSH_SKIP_SERVER_BUILD=1.');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!fs.existsSync(serverDir)) {
|
|
console.error(`build-server: ${serverDir} does not exist`);
|
|
process.exit(1);
|
|
}
|
|
|
|
fs.mkdirSync(stageDir, { recursive: true });
|
|
const outPath = path.join(stageDir, outName);
|
|
|
|
console.log(`build-server: ${goExe} build -o ${path.relative(repoRoot, outPath)} (GOOS=${goos} GOARCH=${goarch})`);
|
|
|
|
const result = spawnSync(
|
|
goExe,
|
|
['build', '-o', outPath, './cmd/obsidian-remote-server'],
|
|
{
|
|
cwd: serverDir,
|
|
stdio: 'inherit',
|
|
env: { ...process.env, GOOS: goos, GOARCH: goarch, CGO_ENABLED: '0' },
|
|
},
|
|
);
|
|
|
|
if (result.status !== 0) {
|
|
console.error(`build-server: go build exited ${result.status}`);
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
|
|
console.log(`build-server: staged ${path.relative(repoRoot, outPath)} (${prettySize(fs.statSync(outPath).size)})`);
|
|
|
|
function locateGo() {
|
|
const fromPath = which('go') ?? which('go.exe');
|
|
if (fromPath) return fromPath;
|
|
|
|
// Fallback: the portable Go on the dev box.
|
|
const portableCandidates = [
|
|
path.join(os.homedir(), 'tools', 'go-portable', 'go', 'bin', 'go.exe'),
|
|
path.join(os.homedir(), 'tools', 'go-portable', 'go', 'bin', 'go'),
|
|
];
|
|
for (const c of portableCandidates) {
|
|
if (fs.existsSync(c)) return c;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function which(cmd) {
|
|
const sep = process.platform === 'win32' ? ';' : ':';
|
|
const exts = process.platform === 'win32'
|
|
? (process.env.PATHEXT?.split(';') ?? ['.EXE', '.CMD', '.BAT'])
|
|
: [''];
|
|
for (const dir of (process.env.PATH ?? '').split(sep)) {
|
|
if (!dir) continue;
|
|
for (const ext of exts) {
|
|
const p = path.join(dir, cmd + ext);
|
|
if (fs.existsSync(p)) return p;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function prettySize(n) {
|
|
if (n < 1024) return `${n} B`;
|
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
|
|
return `${(n / 1024 / 1024).toFixed(2)} MiB`;
|
|
}
|