Files
MrTerm/src/main/rdp-embed.js
T

212 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 */ }
const i18n = require('../i18n');
// ------------------------------------------------------------------ Windows
// Chromium zeichnet per DirectComposition über alle nativen Kindfenster – ein per SetParent eingehängtes
// mstsc bliebe unsichtbar (schwarz). Stattdessen wird das Sitzungsfenster rahmenlos und als "owned window"
// von MrTerm exakt über den Tab gelegt: Es liegt immer über MrTerm, minimiert sich mit und hat keinen Taskleisten-Eintrag.
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)');
const POINT = koffi.struct('MrTermPOINT', { x: 'int', y: 'int' });
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)'),
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)'),
ClientToScreen: u.func('bool __stdcall ClientToScreen(intptr hwnd, _Inout_ MrTermPOINT *pt)'),
POINT,
};
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, GWLP_HWNDPARENT = -8;
const WS_CHILD = 0x40000000, WS_POPUP = 0x80000000, WS_CAPTION = 0x00c00000, WS_THICKFRAME = 0x00040000;
const WS_SYSMENU = 0x00080000, WS_MINIMIZEBOX = 0x00020000, WS_MAXIMIZEBOX = 0x00010000;
const WS_EX_APPWINDOW = 0x00040000, WS_EX_TOOLWINDOW = 0x00000080;
const FRAME = WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
const SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10, SWP_FRAMECHANGED = 0x20, SWP_SHOWWINDOW = 0x40;
const SW_HIDE = 0, SW_SHOWNOACTIVATE = 4, SW_SHOW = 5;
class WinEmbed {
constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.cls = 'TscShellContainerClass'; this.visible = true; }
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();
const style = (((Number(api.GetWindowLongPtrW(hwnd, GWL_STYLE)) >>> 0) & ~(FRAME | WS_CHILD)) | WS_POPUP) >>> 0;
api.SetWindowLongPtrW(hwnd, GWL_STYLE, style);
const ex = ((((Number(api.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW) >>> 0);
api.SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex);
api.SetWindowLongPtrW(hwnd, GWLP_HWNDPARENT, this.parent); // Besitzer = MrTerm
this.hwnd = hwnd;
this.setBounds(bounds);
return true;
}
await new Promise((r) => setTimeout(r, 250));
}
return false;
}
// b: physische Pixel relativ zum Client-Bereich von MrTerm -> Bildschirmkoordinaten
setBounds(b) {
if (b) this.bounds = b;
if (!this.hwnd || !this.bounds) return;
const api = winApi();
const pt = { x: this.bounds.x, y: this.bounds.y };
api.ClientToScreen(this.parent, pt);
api.SetWindowPos(this.hwnd, 0, pt.x, pt.y, Math.max(50, this.bounds.width), Math.max(50, this.bounds.height),
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | (this.visible ? SWP_SHOWWINDOW : 0));
}
reposition() { this.setBounds(); }
show() { this.visible = true; if (this.hwnd) { winApi().ShowWindow(this.hwnd, SW_SHOWNOACTIVATE); this.setBounds(); } }
hide() { this.visible = false; if (this.hwnd) winApi().ShowWindow(this.hwnd, SW_HIDE); }
focus() { if (this.hwnd && this.visible) winApi().SetForegroundWindow(this.hwnd); }
// Aus MrTerm lösen und als normales Fenster weiterlaufen lassen
detach() {
if (!this.hwnd) return;
const api = winApi();
api.SetWindowLongPtrW(this.hwnd, GWLP_HWNDPARENT, 0);
const style = ((((Number(api.GetWindowLongPtrW(this.hwnd, GWL_STYLE)) >>> 0) & ~WS_POPUP) | FRAME) >>> 0);
api.SetWindowLongPtrW(this.hwnd, GWL_STYLE, style);
const ex = ((((Number(api.GetWindowLongPtrW(this.hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW) >>> 0);
api.SetWindowLongPtrW(this.hwnd, GWL_EXSTYLE, ex);
api.ShowWindow(this.hwnd, SW_SHOW);
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(i18n.t('No connection to the X server (MrTerm is not running under 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); }
reposition() {}
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: i18n.t('koffi not available') };
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: i18n.t('Platform not supported') };
}
function createEmbed(parentHandle) {
return process.platform === 'win32' ? new WinEmbed(parentHandle) : new LinuxEmbed(parentHandle);
}
module.exports = { createEmbed, embedSupported };