RDP-Verbindungen als Tabs (mstsc/FreeRDP eingebettet); App-Kürzel im Terminal auf Strg+Shift, damit Strg+W/K/T bei nano & Co. ankommen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+65
-1
@@ -8,9 +8,16 @@ const { Store } = require('./store');
|
||||
const { SshManager } = require('./ssh');
|
||||
const rdp = require('./rdp');
|
||||
const { Updater } = require('./updater');
|
||||
const { createEmbed, embedSupported } = require('./rdp-embed');
|
||||
|
||||
let win;
|
||||
const store = new Store();
|
||||
|
||||
// Unter Linux muss MrTerm für eingebettete RDP-Tabs als X11-Client laufen (unter Wayland via XWayland).
|
||||
// Die Wahl wird unverschlüsselt in launch.json gespeichert, weil sie vor dem Start des Fensters gebraucht wird.
|
||||
const launchFile = path.join(app.getPath('userData'), 'launch.json');
|
||||
function readLaunch() { try { return JSON.parse(fs.readFileSync(launchFile, 'utf8')); } catch { return {}; } }
|
||||
if (process.platform === 'linux' && readLaunch().x11 !== false) app.commandLine.appendSwitch('ozone-platform', 'x11');
|
||||
const pendingSecrets = new Map();
|
||||
|
||||
function send(channel, ...args) {
|
||||
@@ -85,7 +92,10 @@ ipcMain.on('win:close', () => win.close());
|
||||
handle('vault:get', () => ({ ...store.get(), encrypted: store.encrypted, platform: process.platform }));
|
||||
handle('vault:upsert', (col, item) => store.upsert(col, item));
|
||||
handle('vault:remove', (col, id) => store.remove(col, id));
|
||||
handle('vault:settings', (s) => store.setSettings(s));
|
||||
handle('vault:settings', (s) => {
|
||||
store.setSettings(s);
|
||||
if ('rdpEmbed' in s) fs.writeFileSync(launchFile, JSON.stringify({ ...readLaunch(), x11: s.rdpEmbed !== false }));
|
||||
});
|
||||
handle('vault:forgetHost', (id) => { delete store.get().knownHosts[id]; store.save(); });
|
||||
handle('vault:export', async () => {
|
||||
const r = await dialog.showSaveDialog(win, { defaultPath: 'mrterm-backup.json', filters: [{ name: 'JSON', extensions: ['json'] }] });
|
||||
@@ -244,6 +254,60 @@ handle('rdp:launch', (hostId) => {
|
||||
return r.client;
|
||||
});
|
||||
|
||||
// Eingebettete RDP-Sitzungen (Anzeige im Tab)
|
||||
const rdpSessions = new Map();
|
||||
handle('rdp:embedSupported', () => {
|
||||
const s = embedSupported();
|
||||
if (s.ok && process.platform === 'linux' && !rdp.linuxClient(null, true)) return { ok: false, reason: 'xfreerdp3 fehlt (sudo pacman -S freerdp)' };
|
||||
return s;
|
||||
});
|
||||
handle('rdp:open', async (id, hostRef, bounds) => {
|
||||
const host = hostWithOverrides(hostRef);
|
||||
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
|
||||
const emb = createEmbed(win.getNativeWindowHandle());
|
||||
const sess = { emb, cancelled: false };
|
||||
rdpSessions.set(id, sess);
|
||||
const size = { width: bounds.width, height: bounds.height };
|
||||
let proc;
|
||||
if (process.platform === 'win32') {
|
||||
const file = rdp.writeRdpFile(host, size);
|
||||
proc = await new Promise((resolve) => rdp.withWinCredentials(host, () => resolve(emb.start({ rdpFile: file }))));
|
||||
} else {
|
||||
proc = emb.start({ client: rdp.linuxClient(null, true), args: rdp.freerdpArgs(host, size), password: host.password });
|
||||
}
|
||||
proc.on('error', (e) => send('rdp:event', id, 'exit', { code: -1, message: e.message }));
|
||||
proc.on('exit', (code) => {
|
||||
const err = (emb.err || '').split('\n').filter((l) => /ERROR|error/.test(l)).slice(-1)[0] || '';
|
||||
if (rdpSessions.get(id) === sess) send('rdp:event', id, 'exit', { code, message: err });
|
||||
rdpSessions.delete(id);
|
||||
});
|
||||
const ok = await emb.attach(bounds, () => sess.cancelled);
|
||||
if (ok) {
|
||||
if (sess.hidden) emb.hide();
|
||||
send('rdp:event', id, 'attached');
|
||||
}
|
||||
return ok;
|
||||
});
|
||||
ipcMain.on('rdp:bounds', (_e, id, b) => { try { rdpSessions.get(id)?.emb.setBounds(b); } catch {} });
|
||||
ipcMain.on('rdp:show', (_e, id) => { const s = rdpSessions.get(id); if (s) { s.hidden = false; try { s.emb.show(); s.emb.focus(); } catch {} } });
|
||||
ipcMain.on('rdp:hide', (_e, id) => { const s = rdpSessions.get(id); if (s) { s.hidden = true; try { s.emb.hide(); } catch {} } });
|
||||
ipcMain.on('rdp:focus', (_e, id) => { try { rdpSessions.get(id)?.emb.focus(); } catch {} });
|
||||
ipcMain.on('rdp:close', (_e, id) => {
|
||||
const s = rdpSessions.get(id);
|
||||
if (!s) return;
|
||||
s.cancelled = true;
|
||||
rdpSessions.delete(id);
|
||||
s.emb.kill();
|
||||
});
|
||||
handle('rdp:detach', (id) => {
|
||||
const s = rdpSessions.get(id);
|
||||
if (!s) return false;
|
||||
if (process.platform !== 'win32') return false;
|
||||
s.emb.detach();
|
||||
rdpSessions.delete(id); // läuft als eigenständiges Fenster weiter
|
||||
return true;
|
||||
});
|
||||
|
||||
// ---------- Import aus anderen Programmen (z. B. Devolutions RDM) ----------
|
||||
handle('import:pickFile', async (title, extensions) => {
|
||||
const r = await dialog.showOpenDialog(win, { title, filters: [{ name: title, extensions }, { name: 'Alle Dateien', extensions: ['*'] }], properties: ['openFile'] });
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Bettet externe RDP-Clients als Kindfenster in das MrTerm-Fenster ein (Anzeige im Tab).
|
||||
// Windows: mstsc.exe wird gestartet, sein Hauptfenster (TscShellContainerClass) per SetParent eingehängt.
|
||||
// Linux: FreeRDP mit /parent-window:<XID>, Position/Größe/Sichtbarkeit über libX11 (MrTerm läuft dafür unter X11/XWayland).
|
||||
// Alle Koordinaten sind physische Pixel relativ zum Client-Bereich des MrTerm-Fensters.
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
let koffi = null;
|
||||
try { koffi = require('koffi'); } catch { /* ohne koffi nur externe Fenster */ }
|
||||
|
||||
// ------------------------------------------------------------------ Windows
|
||||
function winApi() {
|
||||
if (winApi.api) return winApi.api;
|
||||
const u = koffi.load('user32.dll');
|
||||
const EnumProc = koffi.proto('bool __stdcall MrTermEnumProc(intptr hwnd, intptr lParam)');
|
||||
winApi.api = {
|
||||
EnumWindows: u.func('bool __stdcall EnumWindows(MrTermEnumProc *cb, intptr lParam)'),
|
||||
GetWindowThreadProcessId: u.func('uint32 __stdcall GetWindowThreadProcessId(intptr hwnd, _Out_ uint32 *pid)'),
|
||||
GetClassNameW: u.func('int __stdcall GetClassNameW(intptr hwnd, void *buf, int max)'),
|
||||
IsWindowVisible: u.func('bool __stdcall IsWindowVisible(intptr hwnd)'),
|
||||
IsWindow: u.func('bool __stdcall IsWindow(intptr hwnd)'),
|
||||
SetParent: u.func('intptr __stdcall SetParent(intptr child, intptr parent)'),
|
||||
GetWindowLongPtrW: u.func('intptr __stdcall GetWindowLongPtrW(intptr hwnd, int idx)'),
|
||||
SetWindowLongPtrW: u.func('intptr __stdcall SetWindowLongPtrW(intptr hwnd, int idx, intptr value)'),
|
||||
SetWindowPos: u.func('bool __stdcall SetWindowPos(intptr hwnd, intptr after, int x, int y, int cx, int cy, uint32 flags)'),
|
||||
ShowWindow: u.func('bool __stdcall ShowWindow(intptr hwnd, int cmd)'),
|
||||
SetForegroundWindow: u.func('bool __stdcall SetForegroundWindow(intptr hwnd)'),
|
||||
SetFocus: u.func('intptr __stdcall SetFocus(intptr hwnd)'),
|
||||
};
|
||||
return winApi.api;
|
||||
}
|
||||
|
||||
function findWindowsOfPid(pid, cls) {
|
||||
const api = winApi();
|
||||
const out = [];
|
||||
const buf = Buffer.alloc(512);
|
||||
api.EnumWindows((hwnd) => {
|
||||
const p = [0];
|
||||
api.GetWindowThreadProcessId(hwnd, p);
|
||||
if (p[0] === pid && api.IsWindowVisible(hwnd)) {
|
||||
const n = api.GetClassNameW(hwnd, buf, 256);
|
||||
if (!cls || buf.toString('utf16le', 0, n * 2) === cls) out.push(hwnd);
|
||||
}
|
||||
return true;
|
||||
}, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
const GWL_STYLE = -16, GWL_EXSTYLE = -20;
|
||||
const WS_CHILD = 0x40000000, WS_POPUP = 0x80000000, WS_CAPTION = 0x00c00000, WS_THICKFRAME = 0x00040000;
|
||||
const WS_SYSMENU = 0x00080000, WS_MINIMIZEBOX = 0x00020000, WS_MAXIMIZEBOX = 0x00010000, WS_EX_APPWINDOW = 0x00040000;
|
||||
const SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10, SWP_FRAMECHANGED = 0x20, SWP_SHOWWINDOW = 0x40;
|
||||
|
||||
class WinEmbed {
|
||||
constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.cls = 'TscShellContainerClass'; }
|
||||
|
||||
start({ rdpFile }) {
|
||||
this.proc = spawn('mstsc.exe', rdpFile ? [rdpFile] : [], { stdio: 'ignore' });
|
||||
return this.proc;
|
||||
}
|
||||
|
||||
// Wartet, bis mstsc sein Sitzungsfenster geöffnet hat (Anmelde-/Zertifikatsdialoge davor bleiben eigene Fenster)
|
||||
async attach(bounds, isCancelled) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < 120000) {
|
||||
if (isCancelled() || this.proc.exitCode !== null) return false;
|
||||
const [hwnd] = findWindowsOfPid(this.proc.pid, this.cls);
|
||||
if (hwnd) {
|
||||
const api = winApi();
|
||||
let style = Number(api.GetWindowLongPtrW(hwnd, GWL_STYLE)) >>> 0;
|
||||
style = ((style & ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX)) | WS_CHILD) >>> 0;
|
||||
api.SetWindowLongPtrW(hwnd, GWL_STYLE, style);
|
||||
const ex = (Number(api.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_APPWINDOW;
|
||||
api.SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex >>> 0);
|
||||
api.SetParent(hwnd, this.parent);
|
||||
this.hwnd = hwnd;
|
||||
this.setBounds(bounds);
|
||||
return true;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setBounds(b) {
|
||||
if (!this.hwnd) return;
|
||||
this.bounds = b;
|
||||
winApi().SetWindowPos(this.hwnd, 0, b.x, b.y, Math.max(50, b.width), Math.max(50, b.height), SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
|
||||
}
|
||||
show() { if (this.hwnd) { winApi().ShowWindow(this.hwnd, 5); if (this.bounds) this.setBounds(this.bounds); } }
|
||||
hide() { if (this.hwnd) winApi().ShowWindow(this.hwnd, 0); }
|
||||
focus() { if (this.hwnd) { winApi().SetFocus(this.hwnd); } }
|
||||
// Aus MrTerm lösen und als normales Fenster weiterlaufen lassen
|
||||
detach() {
|
||||
if (!this.hwnd) return;
|
||||
const api = winApi();
|
||||
api.SetParent(this.hwnd, 0);
|
||||
const style = ((Number(api.GetWindowLongPtrW(this.hwnd, GWL_STYLE)) >>> 0) & ~WS_CHILD) | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
|
||||
api.SetWindowLongPtrW(this.hwnd, GWL_STYLE, style >>> 0);
|
||||
api.SetWindowPos(this.hwnd, 0, 80, 80, this.bounds?.width || 1280, this.bounds?.height || 800, SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
|
||||
this.hwnd = null;
|
||||
}
|
||||
kill() { try { this.proc?.kill(); } catch {} }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Linux (X11)
|
||||
function x11() {
|
||||
if (x11.api) return x11.api;
|
||||
const l = koffi.load('libX11.so.6');
|
||||
x11.api = {
|
||||
XOpenDisplay: l.func('void *XOpenDisplay(const char *name)'),
|
||||
XQueryTree: l.func('int XQueryTree(void *d, unsigned long w, _Out_ unsigned long *root, _Out_ unsigned long *parent, _Out_ void **children, _Out_ unsigned int *n)'),
|
||||
XFree: l.func('int XFree(void *p)'),
|
||||
XMoveResizeWindow: l.func('int XMoveResizeWindow(void *d, unsigned long w, int x, int y, unsigned int wd, unsigned int ht)'),
|
||||
XMapWindow: l.func('int XMapWindow(void *d, unsigned long w)'),
|
||||
XUnmapWindow: l.func('int XUnmapWindow(void *d, unsigned long w)'),
|
||||
XRaiseWindow: l.func('int XRaiseWindow(void *d, unsigned long w)'),
|
||||
XSetInputFocus: l.func('int XSetInputFocus(void *d, unsigned long w, int revert, unsigned long time)'),
|
||||
XReparentWindow: l.func('int XReparentWindow(void *d, unsigned long w, unsigned long parent, int x, int y)'),
|
||||
XFlush: l.func('int XFlush(void *d)'),
|
||||
XSync: l.func('int XSync(void *d, int discard)'),
|
||||
};
|
||||
x11.api.display = x11.api.XOpenDisplay(null);
|
||||
if (!x11.api.display) throw new Error('Keine Verbindung zum X-Server (MrTerm läuft nicht unter X11/XWayland).');
|
||||
return x11.api;
|
||||
}
|
||||
|
||||
function x11Children(win) {
|
||||
const X = x11();
|
||||
const root = [0], parent = [0], children = [null], n = [0];
|
||||
if (!X.XQueryTree(X.display, win, root, parent, children, n) || !children[0]) return [];
|
||||
const list = koffi.decode(children[0], 'unsigned long', n[0]).map(Number);
|
||||
X.XFree(children[0]);
|
||||
return list;
|
||||
}
|
||||
|
||||
class LinuxEmbed {
|
||||
constructor(parentHandle) {
|
||||
this.parent = Number(parentHandle.readBigUInt64LE(0));
|
||||
x11(); // früh scheitern, falls kein X11
|
||||
}
|
||||
|
||||
start({ client, args, password }) {
|
||||
this.before = new Set(x11Children(this.parent));
|
||||
this.proc = spawn(client, [...args, `/parent-window:${this.parent}`], { stdio: [password ? 'pipe' : 'ignore', 'ignore', 'pipe'] });
|
||||
if (password) { this.proc.stdin.write(password + '\n'); this.proc.stdin.end(); }
|
||||
this.err = '';
|
||||
this.proc.stderr.on('data', (d) => { this.err = (this.err + d).slice(-4000); });
|
||||
return this.proc;
|
||||
}
|
||||
|
||||
async attach(bounds, isCancelled) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < 120000) {
|
||||
if (isCancelled() || this.proc.exitCode !== null) return false;
|
||||
const fresh = x11Children(this.parent).filter((w) => !this.before.has(w));
|
||||
if (fresh.length) {
|
||||
this.xwin = fresh[fresh.length - 1];
|
||||
this.setBounds(bounds);
|
||||
return true;
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setBounds(b) {
|
||||
if (!this.xwin) return;
|
||||
this.bounds = b;
|
||||
const X = x11();
|
||||
X.XMoveResizeWindow(X.display, this.xwin, b.x, b.y, Math.max(50, b.width), Math.max(50, b.height));
|
||||
X.XRaiseWindow(X.display, this.xwin);
|
||||
X.XFlush(X.display);
|
||||
}
|
||||
show() { if (!this.xwin) return; const X = x11(); X.XMapWindow(X.display, this.xwin); if (this.bounds) this.setBounds(this.bounds); X.XFlush(X.display); }
|
||||
hide() { if (!this.xwin) return; const X = x11(); X.XUnmapWindow(X.display, this.xwin); X.XFlush(X.display); }
|
||||
focus() { if (!this.xwin) return; const X = x11(); X.XSetInputFocus(X.display, this.xwin, 2, 0); X.XFlush(X.display); }
|
||||
detach() { /* FreeRDP-Fenster lassen sich nicht sauber herauslösen – Sitzung stattdessen extern neu starten */ }
|
||||
kill() { try { this.proc?.kill(); } catch {} }
|
||||
}
|
||||
|
||||
function embedSupported() {
|
||||
if (!koffi) return { ok: false, reason: 'koffi nicht verfügbar' };
|
||||
if (process.platform === 'win32') return { ok: true };
|
||||
if (process.platform === 'linux') {
|
||||
try { x11(); return { ok: true }; } catch (e) { return { ok: false, reason: e.message }; }
|
||||
}
|
||||
return { ok: false, reason: 'Plattform nicht unterstützt' };
|
||||
}
|
||||
|
||||
function createEmbed(parentHandle) {
|
||||
return process.platform === 'win32' ? new WinEmbed(parentHandle) : new LinuxEmbed(parentHandle);
|
||||
}
|
||||
|
||||
module.exports = { createEmbed, embedSupported };
|
||||
+67
-43
@@ -1,4 +1,5 @@
|
||||
// RDP: Windows nutzt das eingebaute mstsc.exe, Linux (Arch/CachyOS) FreeRDP (xfreerdp3 / xfreerdp / wlfreerdp).
|
||||
// RDP: Windows nutzt das eingebaute mstsc.exe, Linux (Arch/CachyOS) FreeRDP (xfreerdp3 / xfreerdp / sdl-freerdp3 / wlfreerdp3).
|
||||
// Verbindungen laufen entweder eingebettet im MrTerm-Tab (siehe rdp-embed.js) oder im eigenen Fenster.
|
||||
const { spawn, execFile, execFileSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -11,7 +12,9 @@ function which(bin) {
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
function linuxClient(pref) {
|
||||
// Eingebettet geht nur der X11-Client (unterstützt /parent-window)
|
||||
function linuxClient(pref, embedded) {
|
||||
if (embedded) return ['xfreerdp3', 'xfreerdp'].find(which);
|
||||
const candidates = pref && pref !== 'auto'
|
||||
? [pref]
|
||||
: process.env.WAYLAND_DISPLAY
|
||||
@@ -21,53 +24,62 @@ function linuxClient(pref) {
|
||||
}
|
||||
|
||||
function detect(settings) {
|
||||
if (process.platform === 'win32') return { available: true, client: 'mstsc' };
|
||||
if (process.platform === 'win32') return { available: true, client: 'mstsc', embedClient: 'mstsc' };
|
||||
const c = linuxClient(settings.rdpClientLinux);
|
||||
return c ? { available: true, client: c } : { available: false, hint: 'sudo pacman -S freerdp' };
|
||||
const e = linuxClient(null, true);
|
||||
return c ? { available: true, client: c, embedClient: e || null } : { available: false, hint: 'sudo pacman -S freerdp' };
|
||||
}
|
||||
|
||||
function launch(host, settings) {
|
||||
const userOf = (host) => (host.domain ? `${host.domain}\\${host.username || ''}` : host.username || '');
|
||||
|
||||
// ---------- Windows: .rdp-Datei + temporäre Anmeldedaten
|
||||
function writeRdpFile(host, size) {
|
||||
const port = Number(host.port) || 3389;
|
||||
const user = host.domain ? `${host.domain}\\${host.username || ''}` : host.username || '';
|
||||
const target = port === 3389 ? host.address : `${host.address}:${port}`;
|
||||
const user = userOf(host);
|
||||
const w = size?.width || host.rdpWidth;
|
||||
const hgt = size?.height || host.rdpHeight;
|
||||
const lines = [
|
||||
`full address:s:${target}`,
|
||||
user && `username:s:${user}`,
|
||||
`screen mode id:i:${!size && host.rdpFullscreen ? 2 : 1}`,
|
||||
w && `desktopwidth:i:${w}`,
|
||||
hgt && `desktopheight:i:${hgt}`,
|
||||
'dynamic resolution:i:1',
|
||||
// Im Tab keine Skalierung, damit die Auflösung beim Größenändern mitwächst
|
||||
`smart sizing:i:${size ? 0 : 1}`,
|
||||
'displayconnectionbar:i:0',
|
||||
`redirectclipboard:i:${host.rdpClipboard === false ? 0 : 1}`,
|
||||
`redirectdrives:i:${host.rdpDrives ? 1 : 0}`,
|
||||
`audiomode:i:${host.rdpAudio === false ? 2 : 0}`,
|
||||
'prompt for credentials:i:0',
|
||||
'authentication level:i:2',
|
||||
'autoreconnection enabled:i:1',
|
||||
].filter(Boolean);
|
||||
const file = path.join(os.tmpdir(), `mrterm-${host.id || 'quick'}-${Date.now()}.rdp`);
|
||||
fs.writeFileSync(file, lines.join('\r\n'), 'utf16le');
|
||||
setTimeout(() => fs.rm(file, () => {}), 60000);
|
||||
return file;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const target = port === 3389 ? host.address : `${host.address}:${port}`;
|
||||
const lines = [
|
||||
`full address:s:${target}`,
|
||||
user && `username:s:${user}`,
|
||||
`screen mode id:i:${host.rdpFullscreen ? 2 : 1}`,
|
||||
host.rdpWidth && `desktopwidth:i:${host.rdpWidth}`,
|
||||
host.rdpHeight && `desktopheight:i:${host.rdpHeight}`,
|
||||
'dynamic resolution:i:1',
|
||||
'smart sizing:i:1',
|
||||
`redirectclipboard:i:${host.rdpClipboard === false ? 0 : 1}`,
|
||||
`redirectdrives:i:${host.rdpDrives ? 1 : 0}`,
|
||||
`audiomode:i:${host.rdpAudio === false ? 2 : 0}`,
|
||||
'prompt for credentials:i:0',
|
||||
'authentication level:i:2',
|
||||
].filter(Boolean);
|
||||
const file = path.join(os.tmpdir(), `mrterm-${host.id}.rdp`);
|
||||
fs.writeFileSync(file, lines.join('\r\n'), 'utf16le');
|
||||
const run = () => {
|
||||
const p = spawn('mstsc.exe', [file], { detached: true, stdio: 'ignore' });
|
||||
p.unref();
|
||||
};
|
||||
if (host.password && user) {
|
||||
// Anmeldedaten temporär im Windows-Anmeldeinformationsspeicher ablegen, danach wieder entfernen.
|
||||
execFile('cmdkey', [`/generic:TERMSRV/${host.address}`, `/user:${user}`, `/pass:${host.password}`], () => {
|
||||
run();
|
||||
setTimeout(() => execFile('cmdkey', [`/delete:TERMSRV/${host.address}`], () => {}), 20000);
|
||||
});
|
||||
} else run();
|
||||
return { client: 'mstsc' };
|
||||
}
|
||||
// Legt Anmeldedaten kurzzeitig im Windows-Anmeldeinformationsspeicher ab, damit mstsc ohne Rückfrage verbindet.
|
||||
function withWinCredentials(host, run) {
|
||||
const user = userOf(host);
|
||||
if (!(host.password && user)) return run();
|
||||
execFile('cmdkey', [`/generic:TERMSRV/${host.address}`, `/user:${user}`, `/pass:${host.password}`], () => {
|
||||
run();
|
||||
setTimeout(() => execFile('cmdkey', [`/delete:TERMSRV/${host.address}`], () => {}), 20000);
|
||||
});
|
||||
}
|
||||
|
||||
const client = linuxClient(settings.rdpClientLinux);
|
||||
if (!client) throw new Error('Kein FreeRDP gefunden. Installieren mit: sudo pacman -S freerdp');
|
||||
// ---------- Linux: FreeRDP-Argumente
|
||||
function freerdpArgs(host, size) {
|
||||
const port = Number(host.port) || 3389;
|
||||
const args = [`/v:${host.address}:${port}`, '/cert:tofu', '+auto-reconnect'];
|
||||
if (host.username) args.push(`/u:${host.username}`);
|
||||
if (host.domain) args.push(`/d:${host.domain}`);
|
||||
if (host.rdpFullscreen) args.push('/f');
|
||||
if (size) args.push(`/size:${size.width}x${size.height}`, '/dynamic-resolution');
|
||||
else if (host.rdpFullscreen) args.push('/f');
|
||||
else if (host.rdpWidth && host.rdpHeight) args.push(`/size:${host.rdpWidth}x${host.rdpHeight}`);
|
||||
else args.push('/dynamic-resolution');
|
||||
if (host.rdpClipboard !== false) args.push('+clipboard');
|
||||
@@ -76,12 +88,24 @@ function launch(host, settings) {
|
||||
args.push(`/title:${host.label || host.address}`);
|
||||
// Passwort über stdin statt Kommandozeile (nicht in `ps` sichtbar)
|
||||
if (host.password) args.push('/from-stdin');
|
||||
const p = spawn(client, args, { detached: true, stdio: [host.password ? 'pipe' : 'ignore', 'ignore', 'pipe'] });
|
||||
return args;
|
||||
}
|
||||
|
||||
// Externes Fenster
|
||||
function launch(host, settings) {
|
||||
if (process.platform === 'win32') {
|
||||
const file = writeRdpFile(host);
|
||||
withWinCredentials(host, () => spawn('mstsc.exe', [file], { detached: true, stdio: 'ignore' }).unref());
|
||||
return { client: 'mstsc' };
|
||||
}
|
||||
const client = linuxClient(settings.rdpClientLinux);
|
||||
if (!client) throw new Error('Kein FreeRDP gefunden. Installieren mit: sudo pacman -S freerdp');
|
||||
const p = spawn(client, freerdpArgs(host), { detached: true, stdio: [host.password ? 'pipe' : 'ignore', 'ignore', 'pipe'] });
|
||||
if (host.password) { p.stdin.write(host.password + '\n'); p.stdin.end(); }
|
||||
let err = '';
|
||||
p.stderr.on('data', (d) => { err += d; });
|
||||
p.stderr.on('data', (d) => { err = (err + d).slice(-4000); });
|
||||
p.unref();
|
||||
return { client, process: p, getErr: () => err };
|
||||
}
|
||||
|
||||
module.exports = { detect, launch };
|
||||
module.exports = { detect, launch, linuxClient, writeRdpFile, withWinCredentials, freerdpArgs };
|
||||
|
||||
@@ -24,6 +24,7 @@ const DEFAULTS = {
|
||||
copyOnSelect: true,
|
||||
keepAlive: 30,
|
||||
rdpClientLinux: 'auto',
|
||||
rdpEmbed: true,
|
||||
appTheme: 'midnight',
|
||||
accent: '',
|
||||
updateUrl: 'https://git.mrblake.cc/MrBlake/MrTerm',
|
||||
|
||||
Reference in New Issue
Block a user