RDP tabs on Windows: no visible mstsc windows and resolution follows the tab size. mstsc is started with /v: instead of an unsigned .rdp file (avoids the mandatory "unknown publisher" security warning; a file is still used when the host needs drive redirection, clipboard/audio off or scaling); a WinEvent hook adopts the session window while it is still hidden (borderless, parked off-screen until logged in), keeps the "connecting" progress dialog invisible and shows real prompts (certificate, errors) centered over the tab; servers without dynamic resolution (mstsc clamps the window to the session size) are detected and reconnected with the new size after resizing, re-confirming a certificate the user already accepted for this session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -306,6 +306,7 @@
|
|||||||
'Connecting to {host} … Login or certificate prompts may appear in a separate window.': 'Verbinde mit {host} … Anmelde- oder Zertifikatsabfragen erscheinen ggf. als eigenes Fenster.',
|
'Connecting to {host} … Login or certificate prompts may appear in a separate window.': 'Verbinde mit {host} … Anmelde- oder Zertifikatsabfragen erscheinen ggf. als eigenes Fenster.',
|
||||||
'The RDP window could not be embedded.': 'Das RDP-Fenster konnte nicht eingebettet werden.',
|
'The RDP window could not be embedded.': 'Das RDP-Fenster konnte nicht eingebettet werden.',
|
||||||
'RDP session ended.': 'RDP-Sitzung beendet.',
|
'RDP session ended.': 'RDP-Sitzung beendet.',
|
||||||
|
'Adjusting resolution …': 'Auflösung wird angepasst …',
|
||||||
'RDP session ended (code {code}).': 'RDP-Sitzung beendet (Code {code}).',
|
'RDP session ended (code {code}).': 'RDP-Sitzung beendet (Code {code}).',
|
||||||
'Session continues in a separate window': 'Sitzung läuft als eigenes Fenster weiter',
|
'Session continues in a separate window': 'Sitzung läuft als eigenes Fenster weiter',
|
||||||
|
|
||||||
|
|||||||
+25
-9
@@ -473,30 +473,46 @@ handle('rdp:open', async (id, hostRef, bounds) => {
|
|||||||
const host = hostWithOverrides(hostRef);
|
const host = hostWithOverrides(hostRef);
|
||||||
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
|
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
|
||||||
if (await vpn.ensureForHost(host)) send('vpn:changed');
|
if (await vpn.ensureForHost(host)) send('vpn:changed');
|
||||||
const emb = createEmbed(win.getNativeWindowHandle());
|
const sess = { cancelled: false };
|
||||||
const sess = { emb, cancelled: false };
|
|
||||||
rdpSessions.set(id, sess);
|
rdpSessions.set(id, sess);
|
||||||
|
return startRdpEmbed(id, sess, host, bounds);
|
||||||
|
});
|
||||||
|
async function startRdpEmbed(id, sess, host, bounds, acceptCert = false) {
|
||||||
|
const emb = createEmbed(win.getNativeWindowHandle());
|
||||||
|
emb.acceptCert = acceptCert;
|
||||||
|
sess.emb = emb;
|
||||||
const size = { width: bounds.width, height: bounds.height };
|
const size = { width: bounds.width, height: bounds.height };
|
||||||
let proc;
|
let proc;
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const file = rdp.writeRdpFile(host, size);
|
const args = rdp.mstscArgs(host, size);
|
||||||
proc = await new Promise((resolve) => rdp.withWinCredentials(host, () => resolve(emb.start({ rdpFile: file }))));
|
proc = await new Promise((resolve) => rdp.withWinCredentials(host, () => resolve(emb.start({ args }))));
|
||||||
} else {
|
} else {
|
||||||
proc = emb.start({ client: rdp.linuxClient(null, true), args: rdp.freerdpArgs(host, size), password: host.password });
|
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 }));
|
const current = () => rdpSessions.get(id) === sess && sess.emb === emb;
|
||||||
|
proc.on('error', (e) => { if (current()) send('rdp:event', id, 'exit', { code: -1, message: e.message }); });
|
||||||
proc.on('exit', (code) => {
|
proc.on('exit', (code) => {
|
||||||
|
if (!current()) return; // durch Neuverbindung ersetzt
|
||||||
const err = (emb.err || '').split('\n').filter((l) => /ERROR|error/.test(l)).slice(-1)[0] || '';
|
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 });
|
send('rdp:event', id, 'exit', { code, message: err });
|
||||||
rdpSessions.delete(id);
|
rdpSessions.delete(id);
|
||||||
});
|
});
|
||||||
const ok = await emb.attach(bounds, () => sess.cancelled);
|
// Server ohne dynamische Auflösung: mit neuer Größe neu verbinden (die Sitzung auf dem Server bleibt erhalten)
|
||||||
if (ok) {
|
emb.onResizeNeeded = (b) => {
|
||||||
|
if (!current()) return;
|
||||||
|
send('rdp:event', id, 'resizing');
|
||||||
|
emb.kill();
|
||||||
|
startRdpEmbed(id, sess, host, b, emb.certAccepted).then((ok) => {
|
||||||
|
if (!ok && rdpSessions.get(id) === sess && !sess.cancelled) send('rdp:event', id, 'exit', { code: -1, message: '' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const ok = await emb.attach(bounds, () => sess.cancelled || !current());
|
||||||
|
if (ok && current()) {
|
||||||
if (sess.hidden) emb.hide();
|
if (sess.hidden) emb.hide();
|
||||||
send('rdp:event', id, 'attached');
|
send('rdp:event', id, 'attached');
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
});
|
}
|
||||||
ipcMain.on('rdp:bounds', (_e, id, b) => { try { rdpSessions.get(id)?.emb.setBounds(b); } catch {} });
|
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: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:hide', (_e, id) => { const s = rdpSessions.get(id); if (s) { s.hidden = true; try { s.emb.hide(); } catch {} } });
|
||||||
|
|||||||
+134
-35
@@ -1,5 +1,5 @@
|
|||||||
// Bettet externe RDP-Clients als Kindfenster in das MrTerm-Fenster ein (Anzeige im Tab).
|
// 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.
|
// 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).
|
// 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.
|
// Alle Koordinaten sind physische Pixel relativ zum Client-Bereich des MrTerm-Fensters.
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
@@ -12,13 +12,25 @@ const i18n = require('../i18n');
|
|||||||
// Chromium zeichnet per DirectComposition über alle nativen Kindfenster – ein per SetParent eingehängtes
|
// 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"
|
// 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.
|
// 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() {
|
function winApi() {
|
||||||
if (winApi.api) return winApi.api;
|
if (winApi.api) return winApi.api;
|
||||||
const u = koffi.load('user32.dll');
|
const u = koffi.load('user32.dll');
|
||||||
const EnumProc = koffi.proto('bool __stdcall MrTermEnumProc(intptr hwnd, intptr lParam)');
|
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 POINT = koffi.struct('MrTermPOINT', { x: 'int', y: 'int' });
|
||||||
|
const RECT = koffi.struct('MrTermRECT', { left: 'int', top: 'int', right: 'int', bottom: 'int' });
|
||||||
winApi.api = {
|
winApi.api = {
|
||||||
EnumWindows: u.func('bool __stdcall EnumWindows(MrTermEnumProc *cb, intptr lParam)'),
|
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)'),
|
GetWindowThreadProcessId: u.func('uint32 __stdcall GetWindowThreadProcessId(intptr hwnd, _Out_ uint32 *pid)'),
|
||||||
GetClassNameW: u.func('int __stdcall GetClassNameW(intptr hwnd, void *buf, int max)'),
|
GetClassNameW: u.func('int __stdcall GetClassNameW(intptr hwnd, void *buf, int max)'),
|
||||||
IsWindowVisible: u.func('bool __stdcall IsWindowVisible(intptr hwnd)'),
|
IsWindowVisible: u.func('bool __stdcall IsWindowVisible(intptr hwnd)'),
|
||||||
@@ -34,58 +46,143 @@ function winApi() {
|
|||||||
return winApi.api;
|
return winApi.api;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findWindowsOfPid(pid, cls) {
|
|
||||||
const api = winApi();
|
|
||||||
const out = [];
|
|
||||||
const buf = Buffer.alloc(512);
|
const buf = Buffer.alloc(512);
|
||||||
api.EnumWindows((hwnd) => {
|
const className = (hwnd) => buf.toString('utf16le', 0, winApi().GetClassNameW(hwnd, buf, 256) * 2);
|
||||||
const p = [0];
|
function childClasses(hwnd) {
|
||||||
api.GetWindowThreadProcessId(hwnd, p);
|
const out = new Set();
|
||||||
if (p[0] === pid && api.IsWindowVisible(hwnd)) {
|
winApi().EnumChildWindows(hwnd, (c) => { out.add(className(c)); return true; }, 0);
|
||||||
const n = api.GetClassNameW(hwnd, buf, 256);
|
|
||||||
if (!cls || buf.toString('utf16le', 0, n * 2) === cls) out.push(hwnd);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}, 0);
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GWL_STYLE = -16, GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8;
|
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_CHILD = 0x40000000, WS_POPUP = 0x80000000, WS_CAPTION = 0x00c00000, WS_THICKFRAME = 0x00040000;
|
||||||
const WS_SYSMENU = 0x00080000, WS_MINIMIZEBOX = 0x00020000, WS_MAXIMIZEBOX = 0x00010000;
|
const WS_SYSMENU = 0x00080000, WS_MINIMIZEBOX = 0x00020000, WS_MAXIMIZEBOX = 0x00010000;
|
||||||
const WS_EX_APPWINDOW = 0x00040000, WS_EX_TOOLWINDOW = 0x00000080;
|
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 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 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 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 {
|
class WinEmbed {
|
||||||
constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.cls = 'TscShellContainerClass'; this.visible = true; }
|
constructor(parentHandle) { this.parent = Number(parentHandle.readBigUInt64LE(0)); this.visible = true; }
|
||||||
|
|
||||||
start({ rdpFile }) {
|
// args: mstsc-Kommandozeile. Mit /v:… statt .rdp-Datei zeigt mstsc keine Sicherheitswarnung "Unbekannter Herausgeber".
|
||||||
this.proc = spawn('mstsc.exe', rdpFile ? [rdpFile] : [], { stdio: 'ignore' });
|
start({ args }) {
|
||||||
|
this.proc = spawn('mstsc.exe', args, { stdio: 'ignore' });
|
||||||
|
this.hookWindows();
|
||||||
|
this.proc.on('exit', () => this.unhook());
|
||||||
return this.proc;
|
return this.proc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wartet, bis mstsc sein Sitzungsfenster geöffnet hat (Anmelde-/Zertifikatsdialoge davor bleiben eigene Fenster)
|
// Fängt die Fenster von mstsc ab, sobald sie entstehen (läuft über die Nachrichtenschleife des Electron-Hauptthreads):
|
||||||
async attach(bounds, isCancelled) {
|
// Sitzungsfenster -> noch unsichtbar rahmenlos über den Tab legen, damit nie ein normales mstsc-Fenster aufblitzt
|
||||||
const t0 = Date.now();
|
// "Verbindung wird hergestellt" (Dialog mit Fortschrittsbalken) -> unsichtbar lassen, der Tab zeigt den Status selbst
|
||||||
while (Date.now() - t0 < 120000) {
|
// andere Dialoge (Zertifikat, Fehler) -> brauchen eine Entscheidung, daher mittig über dem Tab einblenden
|
||||||
if (isCancelled() || this.proc.exitCode !== null) return false;
|
hookWindows() {
|
||||||
const [hwnd] = findWindowsOfPid(this.proc.pid, this.cls);
|
|
||||||
if (hwnd) {
|
|
||||||
const api = winApi();
|
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;
|
const style = (((Number(api.GetWindowLongPtrW(hwnd, GWL_STYLE)) >>> 0) & ~(FRAME | WS_CHILD)) | WS_POPUP) >>> 0;
|
||||||
api.SetWindowLongPtrW(hwnd, GWL_STYLE, style);
|
api.SetWindowLongPtrW(hwnd, GWL_STYLE, style);
|
||||||
const ex = ((((Number(api.GetWindowLongPtrW(hwnd, GWL_EXSTYLE)) >>> 0) & ~WS_EX_APPWINDOW) | WS_EX_TOOLWINDOW) >>> 0);
|
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, GWL_EXSTYLE, ex);
|
||||||
api.SetWindowLongPtrW(hwnd, GWLP_HWNDPARENT, this.parent); // Besitzer = MrTerm
|
api.SetWindowLongPtrW(hwnd, GWLP_HWNDPARENT, this.parent);
|
||||||
this.hwnd = hwnd;
|
// Bis zur Anmeldung außerhalb des Bildschirms parken: mstsc zentriert seine Dialoge über diesem Fenster,
|
||||||
this.setBounds(bounds);
|
// so bleibt auch "Verbindung wird hergestellt" unsichtbar, bevor der Hook ihn ausblenden kann.
|
||||||
return true;
|
if (!this.connected) api.SetWindowPos(hwnd, 0, -32000, -32000, this.bounds?.width || 800, this.bounds?.height || 600, SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||||||
}
|
}
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
|
// 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?.();
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
// 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
|
// b: physische Pixel relativ zum Client-Bereich von MrTerm -> Bildschirmkoordinaten
|
||||||
@@ -96,15 +193,17 @@ class WinEmbed {
|
|||||||
const pt = { x: this.bounds.x, y: this.bounds.y };
|
const pt = { x: this.bounds.x, y: this.bounds.y };
|
||||||
api.ClientToScreen(this.parent, pt);
|
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),
|
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));
|
SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED | (this.visible && this.connected ? SWP_SHOWWINDOW : 0));
|
||||||
|
if (this.connected) this.checkFixedSize();
|
||||||
}
|
}
|
||||||
reposition() { this.setBounds(); }
|
reposition() { this.setBounds(); }
|
||||||
show() { this.visible = true; if (this.hwnd) { winApi().ShowWindow(this.hwnd, SW_SHOWNOACTIVATE); this.setBounds(); } }
|
show() { this.visible = true; if (this.hwnd && this.connected) { winApi().ShowWindow(this.hwnd, SW_SHOWNOACTIVATE); this.setBounds(); } }
|
||||||
hide() { this.visible = false; if (this.hwnd) winApi().ShowWindow(this.hwnd, SW_HIDE); }
|
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); }
|
focus() { if (this.hwnd && this.visible) winApi().SetForegroundWindow(this.hwnd); }
|
||||||
// Aus MrTerm lösen und als normales Fenster weiterlaufen lassen
|
// Aus MrTerm lösen und als normales Fenster weiterlaufen lassen
|
||||||
detach() {
|
detach() {
|
||||||
if (!this.hwnd) return;
|
if (!this.hwnd) return;
|
||||||
|
this.unhook();
|
||||||
const api = winApi();
|
const api = winApi();
|
||||||
api.SetWindowLongPtrW(this.hwnd, GWLP_HWNDPARENT, 0);
|
api.SetWindowLongPtrW(this.hwnd, GWLP_HWNDPARENT, 0);
|
||||||
const style = ((((Number(api.GetWindowLongPtrW(this.hwnd, GWL_STYLE)) >>> 0) & ~WS_POPUP) | FRAME) >>> 0);
|
const style = ((((Number(api.GetWindowLongPtrW(this.hwnd, GWL_STYLE)) >>> 0) & ~WS_POPUP) | FRAME) >>> 0);
|
||||||
@@ -115,7 +214,7 @@ class WinEmbed {
|
|||||||
api.SetWindowPos(this.hwnd, 0, 80, 80, this.bounds?.width || 1280, this.bounds?.height || 800, SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
|
api.SetWindowPos(this.hwnd, 0, 80, 80, this.bounds?.width || 1280, this.bounds?.height || 800, SWP_NOZORDER | SWP_FRAMECHANGED | SWP_SHOWWINDOW);
|
||||||
this.hwnd = null;
|
this.hwnd = null;
|
||||||
}
|
}
|
||||||
kill() { try { this.proc?.kill(); } catch {} }
|
kill() { clearTimeout(this.resizeTimer); this.unhook(); try { this.proc?.kill(); } catch {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ Linux (X11)
|
// ------------------------------------------------------------------ Linux (X11)
|
||||||
|
|||||||
+11
-1
@@ -63,6 +63,16 @@ function writeRdpFile(host, size) {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mstsc-Kommandozeile für eingebettete Sitzungen. Seit 2026 zeigt mstsc bei jeder unsignierten .rdp-Datei eine
|
||||||
|
// Sicherheitswarnung – mit /v:… startet es ohne. Die übrigen Optionen kommen dann aus Default.rdp (Zwischenablage an,
|
||||||
|
// Audio lokal, keine Laufwerke, dynamische Auflösung); weicht der Host davon ab, geht es nur über eine .rdp-Datei.
|
||||||
|
function mstscArgs(host, size) {
|
||||||
|
const port = Number(host.port) || 3389;
|
||||||
|
const needsFile = host.rdpDrives || host.rdpClipboard === false || host.rdpAudio === false || host.rdpResize === 'scale';
|
||||||
|
if (needsFile) return [writeRdpFile(host, size)];
|
||||||
|
return [`/v:${port === 3389 ? host.address : `${host.address}:${port}`}`, `/w:${size.width}`, `/h:${size.height}`];
|
||||||
|
}
|
||||||
|
|
||||||
// Legt Anmeldedaten kurzzeitig im Windows-Anmeldeinformationsspeicher ab, damit mstsc ohne Rückfrage verbindet.
|
// Legt Anmeldedaten kurzzeitig im Windows-Anmeldeinformationsspeicher ab, damit mstsc ohne Rückfrage verbindet.
|
||||||
function withWinCredentials(host, run) {
|
function withWinCredentials(host, run) {
|
||||||
const user = userOf(host);
|
const user = userOf(host);
|
||||||
@@ -110,4 +120,4 @@ function launch(host, settings) {
|
|||||||
return { client, process: p, getErr: () => err };
|
return { client, process: p, getErr: () => err };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { detect, launch, linuxClient, writeRdpFile, withWinCredentials, freerdpArgs };
|
module.exports = { detect, launch, linuxClient, writeRdpFile, mstscArgs, withWinCredentials, freerdpArgs };
|
||||||
|
|||||||
@@ -1994,6 +1994,10 @@ class RdpSession {
|
|||||||
this.overlay?.remove(); this.overlay = null;
|
this.overlay?.remove(); this.overlay = null;
|
||||||
setTabState(this, 'on');
|
setTabState(this, 'on');
|
||||||
if (this.isVisible()) { this.pushBounds(); api.rdp.show(this.id); } else api.rdp.hide(this.id);
|
if (this.isVisible()) { this.pushBounds(); api.rdp.show(this.id); } else api.rdp.hide(this.id);
|
||||||
|
} else if (type === 'resizing') {
|
||||||
|
this.attached = false;
|
||||||
|
setTabState(this, 'wait');
|
||||||
|
this.showOverlay(T('Adjusting resolution …'));
|
||||||
} else if (type === 'exit') {
|
} else if (type === 'exit') {
|
||||||
this.attached = false; this.exited = true;
|
this.attached = false; this.exited = true;
|
||||||
setTabState(this, payload?.code ? 'err' : '');
|
setTabState(this, payload?.code ? 'err' : '');
|
||||||
|
|||||||
Reference in New Issue
Block a user