Internal
Public Access
Fix RDP/VNC clipboard sync randomly stopping
Local copies were de-duplicated against the last synced text with no expiry, so one lost send made re-copying the same text a no-op; every open tab listened to the system clipboard, letting background sessions overwrite it and forwarding one tab's update to all others; and RDP never sent the initial Format List MS-RDPECLIP requires after Monitor Ready, which the server can re-send mid-session (e.g. rdpclip.exe restart), leaving sync dead until reconnect. Announce failures are now logged to the event log instead of silently dropped. Bump version to v2026.9.26. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
34c16cbfa8
commit
0331dc4202
5 files changed
+84
-9
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(OrbitHub VERSION 2026.9.16.7 LANGUAGES CXX)
|
||||
project(OrbitHub VERSION 2026.9.26 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -226,6 +226,22 @@ Delivered:
|
||||
dark mode -- its perceived luminance was well below the four existing
|
||||
state colors -- and was replaced with `#ab47bc`, tuned to roughly
|
||||
match their visibility
|
||||
- Clipboard sync (RDP/VNC) randomly stopping, reported on Windows and
|
||||
Linux: (1) `SessionTab` de-duplicated local copies against the last text
|
||||
it had synced, with no expiry, so once one send was lost (channel not
|
||||
ready, remote clipboard changed to something it never saw) re-copying
|
||||
the same text did nothing indefinitely -- it now suppresses only the
|
||||
echo of its own clipboard write, for one second; (2) every open tab
|
||||
listened to the system clipboard, so a background session re-announcing
|
||||
its stale clipboard overwrote what was just copied and one tab's update
|
||||
was forwarded to every other connected session -- only the visible tab
|
||||
syncs now; (3) RDP answered Monitor Ready with capabilities but never
|
||||
the initial Format List MS-RDPECLIP requires, and the server can send
|
||||
Monitor Ready again mid-session (e.g. rdpclip.exe restarting), leaving
|
||||
sync dead until reconnect -- it now always sends one; announce failures
|
||||
and not-yet-ready-channel copies are logged to the session event log
|
||||
(with a line per copy sent/received) instead of silently dropped. Not
|
||||
reproduced locally; needs confirmation from real use
|
||||
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
||||
used to abort the connection generically; `kAnnouncedEncodings` is now
|
||||
the single source of truth for what `SetEncodings` announces and what
|
||||
|
||||
@@ -2000,6 +2000,10 @@ void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const Q
|
||||
}
|
||||
|
||||
if (cliprdrContext == nullptr || cliprdrContext->ClientFormatList == nullptr) {
|
||||
// Kept in m_pendingLocalClipboardText and announced once the channel
|
||||
// reports MonitorReady (see onCliprdrMonitorReady()).
|
||||
emit eventLogged(QStringLiteral(
|
||||
"RDP clipboard: channel not ready; copied text will be sent once it is."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2012,7 +2016,11 @@ void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const Q
|
||||
formatList.formats = &format;
|
||||
|
||||
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &formatList);
|
||||
Q_UNUSED(rc);
|
||||
if (rc != CHANNEL_RC_OK) {
|
||||
emit eventLogged(
|
||||
QStringLiteral("RDP clipboard: failed to announce copied text to the server (0x%1).")
|
||||
.arg(rc, 0, 16));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2324,6 +2332,29 @@ void RdpSessionBackend::onCliprdrMonitorReady()
|
||||
}
|
||||
|
||||
emit eventLogged(QStringLiteral("RDP clipboard channel ready."));
|
||||
|
||||
QString pendingText;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
|
||||
pendingText = m_pendingLocalClipboardText;
|
||||
}
|
||||
if (!pendingText.isEmpty()) {
|
||||
sendClipboardTextToRemote(nullptr, pendingText);
|
||||
} else if (cliprdrContext->ClientFormatList != nullptr) {
|
||||
// MS-RDPECLIP requires the client to answer Monitor Ready with its
|
||||
// capabilities *and* an initial Format List (empty is fine); the
|
||||
// server doesn't treat the channel as initialized until it has
|
||||
// both. The server can send Monitor Ready again mid-session -- e.g.
|
||||
// when rdpclip.exe restarts -- and without this the clipboard
|
||||
// stayed dead until the session was reconnected.
|
||||
CLIPRDR_FORMAT_LIST emptyList = {};
|
||||
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &emptyList);
|
||||
if (rc != CHANNEL_RC_OK) {
|
||||
emit eventLogged(
|
||||
QStringLiteral("RDP clipboard: initial format list failed (0x%1).")
|
||||
.arg(rc, 0, 16));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+30
-6
@@ -778,17 +778,29 @@ void SessionTab::onBackendUsernameRequested(const QString& prompt)
|
||||
|
||||
void SessionTab::onBackendRemoteClipboardTextChanged(const QString& text)
|
||||
{
|
||||
if (text == m_lastSyncedClipboardText) {
|
||||
// Only the tab in front may touch the system clipboard. Every open
|
||||
// RDP/VNC tab listens to it, and a background session re-announcing its
|
||||
// own (stale) clipboard used to silently overwrite what was just copied
|
||||
// locally, and an update applied by one tab was forwarded on to every
|
||||
// other connected tab's remote as well.
|
||||
if (!isVisible() || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastSyncedClipboardText = text;
|
||||
QApplication::clipboard()->setText(text);
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
if (clipboard->text() == text) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingClipboardEcho = text;
|
||||
m_pendingClipboardEchoMs = QDateTime::currentMSecsSinceEpoch();
|
||||
clipboard->setText(text);
|
||||
appendEvent(QStringLiteral("Clipboard: received %1 characters from remote.").arg(text.size()));
|
||||
}
|
||||
|
||||
void SessionTab::onSystemClipboardChanged()
|
||||
{
|
||||
if (!m_clipboardSyncSupported || m_state != SessionState::Connected) {
|
||||
if (!m_clipboardSyncSupported || m_state != SessionState::Connected || !isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -798,12 +810,24 @@ void SessionTab::onSystemClipboardChanged()
|
||||
}
|
||||
|
||||
const QString text = clipboard->text();
|
||||
if (text == m_lastSyncedClipboardText) {
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Suppress only the change we caused ourselves, and only briefly. The
|
||||
// old "same as last synced text" check had no expiry: once a send was
|
||||
// lost (channel not ready, remote clipboard changed to something we
|
||||
// never saw) copying that same text again did nothing, indefinitely.
|
||||
constexpr qint64 kEchoWindowMs = 1000;
|
||||
const bool isEcho = !m_pendingClipboardEcho.isEmpty() && text == m_pendingClipboardEcho
|
||||
&& QDateTime::currentMSecsSinceEpoch() - m_pendingClipboardEchoMs < kEchoWindowMs;
|
||||
m_pendingClipboardEcho.clear();
|
||||
if (isEcho) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastSyncedClipboardText = text;
|
||||
emit requestSetClipboardText(text);
|
||||
appendEvent(QStringLiteral("Clipboard: sending %1 characters to remote.").arg(text.size()));
|
||||
}
|
||||
|
||||
void SessionTab::setupUi()
|
||||
|
||||
+5
-1
@@ -119,7 +119,11 @@ private:
|
||||
SessionConnectOptions m_lastConnectOptions;
|
||||
QString m_terminalThemeName;
|
||||
int m_terminalFontPointSize;
|
||||
QString m_lastSyncedClipboardText;
|
||||
// Text we just wrote to the system clipboard from a remote update, so
|
||||
// the dataChanged() it triggers isn't echoed straight back out. Only
|
||||
// honored briefly -- see onSystemClipboardChanged().
|
||||
QString m_pendingClipboardEcho;
|
||||
qint64 m_pendingClipboardEchoMs = 0;
|
||||
bool m_clipboardSyncSupported;
|
||||
|
||||
KodoTerm* m_sshTerminal;
|
||||
|
||||
Reference in new issue
Block a user