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

311 lines
16 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 Sitzungsfenster (TscShellContainerClass) rahmenlos über den Tab gelegt.
// 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.
// Mit "dynamic resolution" passt mstsc die Auflösung der Sitzung an, sobald sich die Fenstergröße ändert.
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 WinEventProc = koffi.proto('void __stdcall MrTermWinEventProc(intptr hook, uint32 event, intptr hwnd, long idObject, long idChild, uint32 thread, uint32 time)');
const POINT = koffi.struct('MrTermPOINT', { x: 'int', y: 'int' });
const RECT = koffi.struct('MrTermRECT', { left: 'int', top: 'int', right: 'int', bottom: 'int' });
winApi.api = {
EnumWindows: u.func('bool __stdcall EnumWindows(MrTermEnumProc *cb, intptr lParam)'),
EnumChildWindows: u.func('bool __stdcall EnumChildWindows(intptr hwnd, MrTermEnumProc *cb, intptr lParam)'),
SetWinEventHook: u.func('intptr __stdcall SetWinEventHook(uint32 min, uint32 max, intptr mod, MrTermWinEventProc *cb, uint32 pid, uint32 tid, uint32 flags)'),
UnhookWinEvent: u.func('bool __stdcall UnhookWinEvent(intptr hook)'),
GetDlgItem: u.func('intptr __stdcall GetDlgItem(intptr hwnd, int id)'),
PostMessageW: u.func('bool __stdcall PostMessageW(intptr hwnd, uint32 msg, uintptr w, intptr l)'),
GetAncestor: u.func('intptr __stdcall GetAncestor(intptr hwnd, uint32 flags)'),
GetWindowRect: u.func('bool __stdcall GetWindowRect(intptr hwnd, _Out_ MrTermRECT *rect)'),
SetLayeredWindowAttributes: u.func('bool __stdcall SetLayeredWindowAttributes(intptr hwnd, uint32 key, uint8 alpha, uint32 flags)'),
WinEventProc,
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;
}
const buf = Buffer.alloc(512);
const className = (hwnd) => buf.toString('utf16le', 0, winApi().GetClassNameW(hwnd, buf, 256) * 2);
function childClasses(hwnd) {
const out = new Set();
winApi().EnumChildWindows(hwnd, (c) => { out.add(className(c)); 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, WS_EX_LAYERED = 0x00080000;
const FRAME = WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;
const SWP_NOSIZE = 0x1, SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10, SWP_FRAMECHANGED = 0x20, SWP_SHOWWINDOW = 0x40;
const SW_HIDE = 0, SW_SHOWNOACTIVATE = 4, SW_SHOW = 5;
const EVENT_OBJECT_CREATE = 0x8000, EVENT_OBJECT_SHOW = 0x8002, WINEVENT_OUTOFCONTEXT = 0, GA_ROOT = 2, LWA_ALPHA = 2;
const BM_CLICK = 0xf5, ID_CERT_YES = 14004, ID_CERT_VIEW = 13443; // Zertifikatswarnung von mstsc: "Ja", "Zertifikat anzeigen"
class WinEmbed {
constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.visible = true; }
// args: mstsc-Kommandozeile. Mit /v:… statt .rdp-Datei zeigt mstsc keine Sicherheitswarnung "Unbekannter Herausgeber".
start({ args }) {
this.proc = spawn('mstsc.exe', args, { stdio: 'ignore' });
this.hookWindows();
this.proc.on('exit', () => this.unhook());
return this.proc;
}
// Fängt die Fenster von mstsc ab, sobald sie entstehen (läuft über die Nachrichtenschleife des Electron-Hauptthreads):
// Sitzungsfenster -> noch unsichtbar rahmenlos über den Tab legen, damit nie ein normales mstsc-Fenster aufblitzt
// "Verbindung wird hergestellt" (Dialog mit Fortschrittsbalken) -> unsichtbar lassen, der Tab zeigt den Status selbst
// andere Dialoge (Zertifikat, Fehler) -> brauchen eine Entscheidung, daher mittig über dem Tab einblenden
hookWindows() {
const api = winApi();
this.cb = koffi.register((_hook, event, hwnd, idObject, idChild) => {
try {
if (idObject !== 0 || idChild !== 0 || Number(api.GetAncestor(hwnd, GA_ROOT)) !== Number(hwnd)) return;
const cls = className(hwnd);
if (cls === 'TscShellContainerClass') {
if (event === EVENT_OBJECT_CREATE || !this.hwnd) this.adopt(hwnd);
if (event === EVENT_OBJECT_SHOW) this.onSessionShown();
} else if (cls === '#32770') {
const isCert = () => api.GetDlgItem(hwnd, ID_CERT_YES) && api.GetDlgItem(hwnd, ID_CERT_VIEW);
if (event === EVENT_OBJECT_CREATE) this.setAlpha(hwnd, 0);
else if (childClasses(hwnd).has('msctls_progress32')) api.ShowWindow(hwnd, SW_HIDE);
else if (this.acceptCert && isCert()) api.PostMessageW(api.GetDlgItem(hwnd, ID_CERT_YES), BM_CLICK, 0, 0);
else { if (isCert()) this.certPrompted = true; this.presentDialog(hwnd); }
}
} catch { /* Fenster kann bereits wieder zu sein */ }
}, koffi.pointer(api.WinEventProc));
this.hook = api.SetWinEventHook(EVENT_OBJECT_CREATE, EVENT_OBJECT_SHOW, 0, this.cb, this.proc.pid, 0, WINEVENT_OUTOFCONTEXT);
}
unhook() {
if (this.hook) { winApi().UnhookWinEvent(this.hook); this.hook = null; }
if (this.cb) { koffi.unregister(this.cb); this.cb = null; }
}
setAlpha(hwnd, alpha) {
const api = winApi();
api.SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ((Number(api.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) >>> 0) | WS_EX_LAYERED) >>> 0);
api.SetLayeredWindowAttributes(hwnd, 0, alpha, LWA_ALPHA);
}
presentDialog(hwnd) {
const api = winApi();
api.SetWindowLongPtrW(hwnd, GWLP_HWNDPARENT, this.parent);
const r = {};
api.GetWindowRect(hwnd, r);
const b = this.bounds || { x: 0, y: 0, width: 0, height: 0 };
const pt = { x: b.x + Math.round((b.width - (r.right - r.left)) / 2), y: b.y + Math.round((b.height - (r.bottom - r.top)) / 2) };
api.ClientToScreen(this.parent, pt);
api.SetWindowPos(hwnd, 0, pt.x, pt.y, 0, 0, SWP_NOSIZE | SWP_NOZORDER);
this.setAlpha(hwnd, 255);
api.SetForegroundWindow(hwnd);
}
// Sitzungsfenster rahmenlos und ohne Taskleisten-Eintrag als "owned window" von MrTerm
adopt(hwnd) {
const api = winApi();
this.hwnd = hwnd;
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);
// Bis zur Anmeldung außerhalb des Bildschirms parken: mstsc zentriert seine Dialoge über diesem Fenster,
// so bleibt auch "Verbindung wird hergestellt" unsichtbar, bevor der Hook ihn ausblenden kann.
if (!this.connected) api.SetWindowPos(hwnd, 0, -32000, -32000, this.bounds?.width || 800, this.bounds?.height || 600, SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
// mstsc blendet das Sitzungsfenster erst nach erfolgreicher Anmeldung ein und setzt dabei Rahmen und Größe neu
onSessionShown() {
this.connected = true;
this.adopt(this.hwnd);
if (!this.visible) winApi().ShowWindow(this.hwnd, SW_HIDE);
this.setBounds();
this.detectFixedSize();
this.setBounds();
this.onConnected?.();
}
// Ohne dynamische Auflösung (Server zu alt, xrdp < 0.10 …) begrenzt mstsc das Fenster auf die Sitzungsgröße –
// erkennbar daran, dass es sich nicht vergrößern lässt. Dann hilft nur Neuverbinden mit der neuen Größe.
detectFixedSize() {
const api = winApi();
const r = {};
api.GetWindowRect(this.hwnd, r);
api.SetWindowPos(this.hwnd, 0, r.left, r.top, r.right - r.left + 32, r.bottom - r.top + 32, SWP_NOZORDER | SWP_NOACTIVATE);
const g = {};
api.GetWindowRect(this.hwnd, g);
this.fixedSize = g.right - g.left < r.right - r.left + 32 ? { width: r.right - r.left, height: r.bottom - r.top } : null;
}
// Zertifikat wurde für diese Sitzung bestätigt (Rückfrage gezeigt und danach verbunden) – gilt für Neuverbindungen beim Größenändern
get certAccepted() { return this.acceptCert || (this.certPrompted && this.connected); }
checkFixedSize() {
clearTimeout(this.resizeTimer);
const b = this.bounds;
if (!this.fixedSize || !this.visible || (Math.abs(b.width - this.fixedSize.width) < 8 && Math.abs(b.height - this.fixedSize.height) < 8)) return;
this.resizeTimer = setTimeout(() => this.onResizeNeeded?.(this.bounds), 800);
}
// Wartet, bis die Sitzung steht (Sitzungsfenster sichtbar)
attach(bounds, isCancelled) {
this.bounds = bounds;
return new Promise((resolve) => {
const t0 = Date.now();
const done = (ok) => { clearInterval(iv); this.onConnected = null; resolve(ok); };
this.onConnected = () => done(true);
const iv = setInterval(() => {
if (this.connected) done(true);
else if (isCancelled() || this.proc.exitCode !== null || Date.now() - t0 > 120000) done(false);
}, 250);
});
}
// 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 && this.connected ? SWP_SHOWWINDOW : 0));
if (this.connected) this.checkFixedSize();
}
reposition() { this.setBounds(); }
show() { this.visible = true; if (this.hwnd && this.connected) { winApi().ShowWindow(this.hwnd, SW_SHOWNOACTIVATE); this.setBounds(); } }
hide() { this.visible = false; clearTimeout(this.resizeTimer); 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;
this.unhook();
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() { clearTimeout(this.resizeTimer); this.unhook(); 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 };