212 lines
10 KiB
JavaScript
212 lines
10 KiB
JavaScript
// 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 };
|