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)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -226,6 +226,22 @@ Delivered:
|
|||||||
dark mode -- its perceived luminance was well below the four existing
|
dark mode -- its perceived luminance was well below the four existing
|
||||||
state colors -- and was replaced with `#ab47bc`, tuned to roughly
|
state colors -- and was replaced with `#ab47bc`, tuned to roughly
|
||||||
match their visibility
|
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
|
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
||||||
used to abort the connection generically; `kAnnouncedEncodings` is now
|
used to abort the connection generically; `kAnnouncedEncodings` is now
|
||||||
the single source of truth for what `SetEncodings` announces and what
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2012,7 +2016,11 @@ void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const Q
|
|||||||
formatList.formats = &format;
|
formatList.formats = &format;
|
||||||
|
|
||||||
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &formatList);
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2324,6 +2332,29 @@ void RdpSessionBackend::onCliprdrMonitorReady()
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit eventLogged(QStringLiteral("RDP clipboard channel ready."));
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-6
@@ -778,17 +778,29 @@ void SessionTab::onBackendUsernameRequested(const QString& prompt)
|
|||||||
|
|
||||||
void SessionTab::onBackendRemoteClipboardTextChanged(const QString& text)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_lastSyncedClipboardText = text;
|
QClipboard* clipboard = QApplication::clipboard();
|
||||||
QApplication::clipboard()->setText(text);
|
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()
|
void SessionTab::onSystemClipboardChanged()
|
||||||
{
|
{
|
||||||
if (!m_clipboardSyncSupported || m_state != SessionState::Connected) {
|
if (!m_clipboardSyncSupported || m_state != SessionState::Connected || !isVisible()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,12 +810,24 @@ void SessionTab::onSystemClipboardChanged()
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QString text = clipboard->text();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_lastSyncedClipboardText = text;
|
|
||||||
emit requestSetClipboardText(text);
|
emit requestSetClipboardText(text);
|
||||||
|
appendEvent(QStringLiteral("Clipboard: sending %1 characters to remote.").arg(text.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionTab::setupUi()
|
void SessionTab::setupUi()
|
||||||
|
|||||||
+5
-1
@@ -119,7 +119,11 @@ private:
|
|||||||
SessionConnectOptions m_lastConnectOptions;
|
SessionConnectOptions m_lastConnectOptions;
|
||||||
QString m_terminalThemeName;
|
QString m_terminalThemeName;
|
||||||
int m_terminalFontPointSize;
|
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;
|
bool m_clipboardSyncSupported;
|
||||||
|
|
||||||
KodoTerm* m_sshTerminal;
|
KodoTerm* m_sshTerminal;
|
||||||
|
|||||||
Reference in new issue
Block a user