mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
a8d3259d43
- Implemented server management service providing resource snapshots, file browser, service control, and audit logging. - Added terminal proxy for WebSocket-backed PTY, allowing browser-based shell access with user authentication and role-based access control. - Created server management view with tabs for overview, terminal, file management, and services, including UI elements for displaying system metrics and managing files/services. Co-authored-by: Copilot <copilot@github.com>
386 lines
9.5 KiB
Go
386 lines
9.5 KiB
Go
//go:build linux
|
|
|
|
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// injectInput injects a keyboard or mouse event on Linux.
|
|
//
|
|
// Strategy:
|
|
// 1. Wayland session → prefer ydotool, because xdotool only reaches XWayland windows.
|
|
// 2. X11 or XWayland fallback → use xdotool.
|
|
func injectInput(evt *InputEvent) error {
|
|
if isWaylandSession() {
|
|
if commandExists("ydotool") {
|
|
if err := injectInputWayland(evt); err == nil {
|
|
return nil
|
|
} else if !hasX11Display() {
|
|
return err
|
|
}
|
|
}
|
|
if hasX11Display() {
|
|
return injectInputX11(evt)
|
|
}
|
|
return fmt.Errorf("Wayland input injection requires ydotool and a running ydotoold daemon")
|
|
}
|
|
if hasX11Display() {
|
|
return injectInputX11(evt)
|
|
}
|
|
if commandExists("ydotool") {
|
|
return injectInputWayland(evt)
|
|
}
|
|
return fmt.Errorf("no supported input backend found (install xdotool for X11 or ydotool for Wayland)")
|
|
}
|
|
|
|
// ── X11 / XWayland path (xdotool) ────────────────────────────────────────
|
|
|
|
func injectInputX11(evt *InputEvent) error {
|
|
switch evt.Type {
|
|
case "mouse_move":
|
|
return xdotool("mousemove", "--sync",
|
|
fmt.Sprintf("%d", evt.X), fmt.Sprintf("%d", evt.Y))
|
|
|
|
case "mouse_click":
|
|
if err := xdotool("mousemove", "--sync",
|
|
fmt.Sprintf("%d", evt.X), fmt.Sprintf("%d", evt.Y)); err != nil {
|
|
return err
|
|
}
|
|
return xdotool("click", fmt.Sprintf("%d", linuxMouseButton(evt.Button)))
|
|
|
|
case "mouse_down":
|
|
if err := xdotool("mousemove", "--sync",
|
|
fmt.Sprintf("%d", evt.X), fmt.Sprintf("%d", evt.Y)); err != nil {
|
|
return err
|
|
}
|
|
return xdotool("mousedown", fmt.Sprintf("%d", linuxMouseButton(evt.Button)))
|
|
|
|
case "mouse_up":
|
|
if err := xdotool("mousemove", "--sync",
|
|
fmt.Sprintf("%d", evt.X), fmt.Sprintf("%d", evt.Y)); err != nil {
|
|
return err
|
|
}
|
|
return xdotool("mouseup", fmt.Sprintf("%d", linuxMouseButton(evt.Button)))
|
|
|
|
case "mouse_scroll":
|
|
stepsY := scrollSteps(evt.DeltaY)
|
|
stepsX := scrollSteps(evt.DeltaX)
|
|
if stepsY < 0 {
|
|
return xdotool("click", "--repeat", fmt.Sprintf("%d", abs(stepsY)), "4")
|
|
} else if stepsY > 0 {
|
|
return xdotool("click", "--repeat", fmt.Sprintf("%d", abs(stepsY)), "5")
|
|
}
|
|
if stepsX < 0 {
|
|
return xdotool("click", "--repeat", fmt.Sprintf("%d", abs(stepsX)), "6")
|
|
} else if stepsX > 0 {
|
|
return xdotool("click", "--repeat", fmt.Sprintf("%d", abs(stepsX)), "7")
|
|
}
|
|
return nil
|
|
|
|
case "key_press":
|
|
return xdotool("keydown", buildXdotoolKeyCombo(evt.Key, evt.Modifiers))
|
|
|
|
case "key_release":
|
|
return xdotool("keyup", buildXdotoolKeyCombo(evt.Key, evt.Modifiers))
|
|
|
|
case "key_tap":
|
|
return xdotool("key", buildXdotoolKeyCombo(evt.Key, evt.Modifiers))
|
|
|
|
case "text":
|
|
if evt.Text == "" {
|
|
return nil
|
|
}
|
|
return xdotool("type", "--clearmodifiers", "--", evt.Text)
|
|
|
|
default:
|
|
return fmt.Errorf("unknown input type: %s", evt.Type)
|
|
}
|
|
}
|
|
|
|
// xdotool runs xdotool with the given arguments.
|
|
func xdotool(args ...string) error {
|
|
path, err := exec.LookPath("xdotool")
|
|
if err != nil {
|
|
return fmt.Errorf("xdotool not found — install it with: sudo apt install xdotool")
|
|
}
|
|
return exec.Command(path, args...).Run()
|
|
}
|
|
|
|
func commandExists(name string) bool {
|
|
_, err := exec.LookPath(name)
|
|
return err == nil
|
|
}
|
|
|
|
// ── Pure-Wayland path (ydotool) ──────────────────────────────────────────
|
|
|
|
// injectInputWayland uses ydotool for input injection on pure-Wayland sessions.
|
|
//
|
|
// Requirements:
|
|
// - ydotool installed: sudo apt install ydotool (or build from source)
|
|
// - ydotoold daemon running: sudo ydotoold &
|
|
//
|
|
// ydotool 1.x command syntax is used here. On Debian/Ubuntu the package may be
|
|
// older (0.x); if commands fail, upgrade or use XWayland instead.
|
|
func injectInputWayland(evt *InputEvent) error {
|
|
switch evt.Type {
|
|
case "mouse_move":
|
|
return ydotool("mousemove",
|
|
"-x", fmt.Sprintf("%d", evt.X),
|
|
"-y", fmt.Sprintf("%d", evt.Y))
|
|
|
|
case "mouse_click":
|
|
if err := ydotool("mousemove",
|
|
"-x", fmt.Sprintf("%d", evt.X),
|
|
"-y", fmt.Sprintf("%d", evt.Y)); err != nil {
|
|
return err
|
|
}
|
|
return ydotoolClick(evt.Button)
|
|
|
|
case "mouse_down":
|
|
if err := ydotool("mousemove",
|
|
"-x", fmt.Sprintf("%d", evt.X),
|
|
"-y", fmt.Sprintf("%d", evt.Y)); err != nil {
|
|
return err
|
|
}
|
|
return ydotoolMouseDown(evt.Button)
|
|
|
|
case "mouse_up":
|
|
if err := ydotool("mousemove",
|
|
"-x", fmt.Sprintf("%d", evt.X),
|
|
"-y", fmt.Sprintf("%d", evt.Y)); err != nil {
|
|
return err
|
|
}
|
|
return ydotoolMouseUp(evt.Button)
|
|
|
|
case "mouse_scroll":
|
|
// ydotool scroll: positive DeltaY = scroll down
|
|
if steps := scrollSteps(evt.DeltaY); steps != 0 {
|
|
// ydotool scroll button clicks: 4=wheel-up, 5=wheel-down
|
|
btn := "5"
|
|
repeat := steps
|
|
if steps < 0 {
|
|
btn = "4"
|
|
repeat = -repeat
|
|
}
|
|
for i := 0; i < repeat; i++ {
|
|
if err := ydotoolClick(parseScrollButton(btn)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
|
|
case "key_press":
|
|
return ydotool("key", "--key-delay", "0",
|
|
buildYdotoolKey(evt.Key, evt.Modifiers))
|
|
|
|
case "key_release":
|
|
// ydotool does not support individual key-up; send a no-op
|
|
return nil
|
|
|
|
case "key_tap":
|
|
return ydotool("key", "--key-delay", "12",
|
|
buildYdotoolKey(evt.Key, evt.Modifiers))
|
|
|
|
case "text":
|
|
if evt.Text == "" {
|
|
return nil
|
|
}
|
|
// wtype is more reliable than ydotool type on many compositors.
|
|
if path, err := exec.LookPath("wtype"); err == nil {
|
|
return exec.Command(path, "-d", "0", evt.Text).Run()
|
|
}
|
|
return ydotool("type", "--key-delay", "12", "--", evt.Text)
|
|
|
|
default:
|
|
return fmt.Errorf("unknown input type: %s", evt.Type)
|
|
}
|
|
}
|
|
|
|
// ydotool runs ydotool with the given arguments.
|
|
func ydotool(args ...string) error {
|
|
path, err := exec.LookPath("ydotool")
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"ydotool not found — install it and start ydotoold: sudo apt install ydotool && sudo ydotoold &",
|
|
)
|
|
}
|
|
return exec.Command(path, args...).Run()
|
|
}
|
|
|
|
func scrollSteps(delta int) int {
|
|
if delta == 0 {
|
|
return 0
|
|
}
|
|
steps := abs(delta)
|
|
if steps > 12 {
|
|
steps = (steps + 79) / 80
|
|
}
|
|
if steps < 1 {
|
|
steps = 1
|
|
}
|
|
if steps > 10 {
|
|
steps = 10
|
|
}
|
|
if delta < 0 {
|
|
return -steps
|
|
}
|
|
return steps
|
|
}
|
|
|
|
// ydotoolClick sends a full button click (down + up) for a CDAP button number.
|
|
// CDAP: 1=left, 2=right, 3=middle. ydotool uses the same numbering as xdotool.
|
|
func ydotoolClick(button int) error {
|
|
return ydotool("click", fmt.Sprintf("%d", linuxMouseButton(button)))
|
|
}
|
|
|
|
// ydotoolMouseDown presses a mouse button.
|
|
func ydotoolMouseDown(button int) error {
|
|
// ydotool does not have separate mousedown/mouseup in all versions;
|
|
// fall back to a click as a best-effort.
|
|
return ydotoolClick(button)
|
|
}
|
|
|
|
// ydotoolMouseUp releases a mouse button (best-effort no-op for ydotool).
|
|
func ydotoolMouseUp(_ int) error {
|
|
return nil
|
|
}
|
|
|
|
// parseScrollButton converts a scroll button string ("4" or "5") to int.
|
|
func parseScrollButton(btn string) int {
|
|
if btn == "4" {
|
|
return 4
|
|
}
|
|
return 5
|
|
}
|
|
|
|
// buildYdotoolKey constructs a ydotool key combo string using XKB key names.
|
|
// ydotool accepts the same modifier+key syntax as xdotool.
|
|
func buildYdotoolKey(key string, modifiers []string) string {
|
|
return buildXdotoolKeyCombo(key, modifiers)
|
|
}
|
|
|
|
// ── Shared helpers ────────────────────────────────────────────────────────
|
|
|
|
// linuxMouseButton maps CDAP button numbers to Linux tool button numbers.
|
|
// CDAP: 1=left, 2=right, 3=middle. xdotool/ydotool: 1=left, 2=middle, 3=right.
|
|
func linuxMouseButton(btn int) int {
|
|
switch btn {
|
|
case 1:
|
|
return 1 // left
|
|
case 2:
|
|
return 3 // right
|
|
case 3:
|
|
return 2 // middle
|
|
default:
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// buildXdotoolKeyCombo constructs an xdotool/ydotool key combination string.
|
|
// Example: key="Return", modifiers=["ctrl"] → "ctrl+Return".
|
|
func buildXdotoolKeyCombo(key string, modifiers []string) string {
|
|
var parts []string
|
|
for _, mod := range modifiers {
|
|
parts = append(parts, normalizeModifier(mod))
|
|
}
|
|
if key != "" {
|
|
parts = append(parts, xdotoolKeyName(key))
|
|
}
|
|
return strings.Join(parts, "+")
|
|
}
|
|
|
|
// normalizeModifier maps CDAP modifier names to xdotool/ydotool names.
|
|
func normalizeModifier(mod string) string {
|
|
switch strings.ToLower(mod) {
|
|
case "ctrl", "control":
|
|
return "ctrl"
|
|
case "alt":
|
|
return "alt"
|
|
case "shift":
|
|
return "shift"
|
|
case "super", "meta", "win", "cmd":
|
|
return "super"
|
|
default:
|
|
return mod
|
|
}
|
|
}
|
|
|
|
// xdotoolKeyName maps CDAP key names to xdotool/ydotool key symbol names.
|
|
func xdotoolKeyName(key string) string {
|
|
switch key {
|
|
case "Return", "Enter":
|
|
return "Return"
|
|
case "Backspace":
|
|
return "BackSpace"
|
|
case "Delete", "Del":
|
|
return "Delete"
|
|
case "Escape", "Esc":
|
|
return "Escape"
|
|
case "Tab":
|
|
return "Tab"
|
|
case "Space", " ":
|
|
return "space"
|
|
case "ArrowUp", "Up":
|
|
return "Up"
|
|
case "ArrowDown", "Down":
|
|
return "Down"
|
|
case "ArrowLeft", "Left":
|
|
return "Left"
|
|
case "ArrowRight", "Right":
|
|
return "Right"
|
|
case "Home":
|
|
return "Home"
|
|
case "End":
|
|
return "End"
|
|
case "PageUp":
|
|
return "Prior"
|
|
case "PageDown":
|
|
return "Next"
|
|
case "F1":
|
|
return "F1"
|
|
case "F2":
|
|
return "F2"
|
|
case "F3":
|
|
return "F3"
|
|
case "F4":
|
|
return "F4"
|
|
case "F5":
|
|
return "F5"
|
|
case "F6":
|
|
return "F6"
|
|
case "F7":
|
|
return "F7"
|
|
case "F8":
|
|
return "F8"
|
|
case "F9":
|
|
return "F9"
|
|
case "F10":
|
|
return "F10"
|
|
case "F11":
|
|
return "F11"
|
|
case "F12":
|
|
return "F12"
|
|
case "PrintScreen":
|
|
return "Print"
|
|
case "Insert":
|
|
return "Insert"
|
|
case "CapsLock":
|
|
return "Caps_Lock"
|
|
case "NumLock":
|
|
return "Num_Lock"
|
|
default:
|
|
return key
|
|
}
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|