From 4c60f546ce2cffa40193282304ff1a18ac3e5502 Mon Sep 17 00:00:00 2001 From: daredoole Date: Tue, 28 Apr 2026 19:52:37 -0400 Subject: [PATCH] Add beta diagnostics summary and issue template --- .github/ISSUE_TEMPLATE/bug_report.yml | 97 +++++++++++++++++++++++++ docs/beta-workflow.md | 13 ++-- scripts/inputflow-diagnostics-bundle.sh | 94 +++++++++++++++++++++++- src/main.cpp | 20 ++++- 4 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..0286ab4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,97 @@ +name: Beta bug report +description: Report an InputFlow Linux beta problem with PowerToys Mouse Without Borders. +title: "[Beta Bug]: " +labels: ["bug", "beta"] +body: + - type: markdown + attributes: + value: | + Attach the diagnostics bundle from `scripts/inputflow-diagnostics-bundle.sh` when possible. Review it before posting publicly; the bundle redacts secrets by best effort. + - type: textarea + id: summary + attributes: + label: Problem summary + description: What failed, and what did you expect instead? + validations: + required: true + - type: textarea + id: diagnostics + attributes: + label: Diagnostics bundle + description: Attach the generated archive or paste the path/name if sharing privately. + placeholder: inputflow-diagnostics-YYYYMMDD-HHMMSS-PID.tar.gz + validations: + required: true + - type: input + id: linux-distro + attributes: + label: Linux distro and version + placeholder: Fedora 40, Ubuntu 24.04, Arch, etc. + validations: + required: true + - type: dropdown + id: session-type + attributes: + label: Linux session type + options: + - X11 + - Wayland + - XWayland/mixed + - Unknown + validations: + required: true + - type: input + id: desktop + attributes: + label: Desktop/compositor + placeholder: GNOME, KDE Plasma, Sway, Hyprland, etc. + - type: input + id: windows-version + attributes: + label: Windows version + placeholder: Windows 11 24H2 + validations: + required: true + - type: input + id: powertoys-version + attributes: + label: PowerToys version + placeholder: v0.xx.x + validations: + required: true + - type: textarea + id: monitor-layout + attributes: + label: Monitor and machine layout + description: Include machine order and display layout, for example AAB, BAA, ABA, stacked, mixed DPI/resolution, wrap on/off. + validations: + required: true + - type: dropdown + id: auth-source + attributes: + label: Linux key source + options: + - key_file + - key_secret_id + - inline key + - not configured + - unknown + - type: dropdown + id: clipboard + attributes: + label: Clipboard mode + options: + - enabled send/receive + - enabled receive-only + - disabled + - unknown + - type: textarea + id: steps + attributes: + label: Reproduction steps + placeholder: | + 1. Start PowerToys Mouse Without Borders on Windows. + 2. Start InputFlow on Linux. + 3. Move cursor from ... + validations: + required: true diff --git a/docs/beta-workflow.md b/docs/beta-workflow.md index d7cd3b9..47fc90c 100644 --- a/docs/beta-workflow.md +++ b/docs/beta-workflow.md @@ -45,19 +45,20 @@ The desktop controller shows the same health check together with the user servic ./mwb-desktop-ui.sh status ``` -Review warnings for missing `/dev/uinput` access, missing `inputflow` group membership, missing packaged files, unavailable clipboard helpers, or invalid authentication configuration. +Review warnings for missing `/dev/uinput` access, Wayland input-gating, missing `inputflow` group membership, missing packaged files, unavailable clipboard helpers, unavailable Secret Service/session bus, or invalid authentication configuration. ## Diagnostics Bundle -Until a one-click diagnostics bundle command is available, collect this minimal bundle for beta reports: +Create a redacted support bundle before filing beta reports: ```bash -./build/mwb_client doctor --config ~/.config/mwb-client/config.ini > inputflow-doctor.txt -systemctl --user status --no-pager mwb-client.service > inputflow-service-status.txt -journalctl --user -u mwb-client.service --since "30 minutes ago" --no-pager > inputflow-service-log.txt +./scripts/inputflow-diagnostics-bundle.sh \ + --config ~/.config/mwb-client/config.ini \ + --state ~/.local/state/mwb-client/state.ini \ + --output . ``` -Also include `~/.config/mwb-client/config.ini` with `key`, `key_file`, `key_secret_id`, Windows IPs, and hostnames redacted as needed. Do not attach exported Windows helper scripts or unredacted Secret Service identifiers to public issues. +The archive includes `summary.json` for machine-readable triage, redacted config/state summaries, session and `/dev/uinput` checks, package/build details, service status, recent user-service journal lines, and `mwb_client doctor` output. Review the archive before posting publicly. Do not attach exported Windows helper scripts or unredacted Secret Service identifiers to public issues. ## Connection Quality diff --git a/scripts/inputflow-diagnostics-bundle.sh b/scripts/inputflow-diagnostics-bundle.sh index 60d64e4..0206407 100755 --- a/scripts/inputflow-diagnostics-bundle.sh +++ b/scripts/inputflow-diagnostics-bundle.sh @@ -146,6 +146,95 @@ redacted_copy_or_note() { } >"$output_file" } +json_escape() { + local value="${1:-}" + value="${value//\\/\\\\}" + value="${value//\"/\\\"}" + value="${value//$'\n'/\\n}" + value="${value//$'\r'/\\r}" + value="${value//$'\t'/\\t}" + printf '%s' "$value" +} + +json_string() { + printf '"%s"' "$(json_escape "${1:-}")" +} + +json_bool() { + if [[ "${1:-}" == "yes" || "${1:-}" == "true" || "${1:-}" == "1" ]]; then + printf 'true' + else + printf 'false' + fi +} + +config_value() { + local lookup="$1" + local line trimmed key value + [[ -r "$CONFIG_PATH" ]] || return 0 + while IFS= read -r line || [[ -n "$line" ]]; do + trimmed="${line#"${line%%[![:space:]]*}"}" + trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" + [[ -z "$trimmed" || "$trimmed" == \#* || "$trimmed" == \;* || "$trimmed" != *"="* ]] && continue + key="${trimmed%%=*}" + value="${trimmed#*=}" + key="${key%"${key##*[![:space:]]}"}" + value="${value#"${value%%[![:space:]]*}"}" + if [[ "$key" == "$lookup" ]]; then + printf '%s\n' "$value" + return 0 + fi + done <"$CONFIG_PATH" +} + +write_json_summary() { + local output_file="$1" + local host machine_name port key_source clipboard_enabled clipboard_send_enabled screen_width screen_height + local config_present=no config_readable=no state_present=no state_readable=no uinput_present=no uinput_writable=no uinput_module=no + local peer_lines=0 + + [[ -e "$CONFIG_PATH" ]] && config_present=yes + [[ -r "$CONFIG_PATH" ]] && config_readable=yes + [[ -e "$STATE_PATH" ]] && state_present=yes + [[ -r "$STATE_PATH" ]] && state_readable=yes + [[ -e /dev/uinput ]] && uinput_present=yes + [[ -w /dev/uinput ]] && uinput_writable=yes + [[ -d /sys/module/uinput ]] && uinput_module=yes + if [[ -r "$STATE_PATH" ]]; then + peer_lines="$(grep -c '^peer=' "$STATE_PATH" 2>/dev/null || true)" + peer_lines="${peer_lines:-0}" + fi + + host="$(config_value host)" + machine_name="$(config_value machine_name)" + port="$(config_value port)" + clipboard_enabled="$(config_value clipboard_enabled)" + clipboard_send_enabled="$(config_value clipboard_send_enabled)" + screen_width="$(config_value screen_width)" + screen_height="$(config_value screen_height)" + if [[ -n "$(config_value key_secret_id)" ]]; then + key_source="secret_service" + elif [[ -n "$(config_value key_file)" ]]; then + key_source="key_file" + elif [[ -n "$(config_value key)" ]]; then + key_source="inline" + else + key_source="missing" + fi + + { + printf '{\n' + printf ' "schema_version": 1,\n' + printf ' "created_at": '; json_string "$(date -Is 2>/dev/null || date)"; printf ',\n' + printf ' "config": {"path": '; json_string "$CONFIG_PATH_DISPLAY"; printf ', "present": '; json_bool "$config_present"; printf ', "readable": '; json_bool "$config_readable"; printf ', "host_configured": '; [[ -n "$host" ]] && printf true || printf false; printf ', "machine_name_configured": '; [[ -n "$machine_name" ]] && printf true || printf false; printf ', "port": '; json_string "$port"; printf ', "key_source": '; json_string "$key_source"; printf ', "clipboard_enabled": '; json_string "$clipboard_enabled"; printf ', "clipboard_send_enabled": '; json_string "$clipboard_send_enabled"; printf ', "screen_override": '; json_string "${screen_width}x${screen_height}"; printf '},\n' + printf ' "state": {"path": '; json_string "$STATE_PATH_DISPLAY"; printf ', "present": '; json_bool "$state_present"; printf ', "readable": '; json_bool "$state_readable"; printf ', "peer_lines": '; printf '%s' "$peer_lines"; printf '},\n' + printf ' "session": {"xdg_session_type": '; json_string "${XDG_SESSION_TYPE:-}"; printf ', "xdg_current_desktop": '; json_string "${XDG_CURRENT_DESKTOP:-}"; printf ', "desktop_session": '; json_string "${DESKTOP_SESSION:-}"; printf ', "wayland_display_set": '; [[ -n "${WAYLAND_DISPLAY:-}" ]] && printf true || printf false; printf ', "display_set": '; [[ -n "${DISPLAY:-}" ]] && printf true || printf false; printf ', "dbus_session_bus_set": '; [[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]] && printf true || printf false; printf '},\n' + printf ' "input": {"uinput_present": '; json_bool "$uinput_present"; printf ', "uinput_writable": '; json_bool "$uinput_writable"; printf ', "uinput_module_loaded": '; json_bool "$uinput_module"; printf '},\n' + printf ' "tools": {"wl_copy": '; have wl-copy && printf true || printf false; printf ', "wl_paste": '; have wl-paste && printf true || printf false; printf ', "xclip": '; have xclip && printf true || printf false; printf ', "xsel": '; have xsel && printf true || printf false; printf ', "secret_tool": '; have secret-tool && printf true || printf false; printf ', "systemctl": '; have systemctl && printf true || printf false; printf ', "journalctl": '; have journalctl && printf true || printf false; printf ', "ip": '; have ip && printf true || printf false; printf ', "ss": '; have ss && printf true || printf false; printf '}\n' + printf '}\n' + } >"$output_file" +} + write_config_summary() { local output_file="$1" { @@ -206,7 +295,9 @@ modified=%y' "$STATE_PATH" 2>/dev/null || true return fi printf 'readable=yes\n' - printf 'peer_lines=%s\n' "$(grep -c '^peer=' "$STATE_PATH" 2>/dev/null || printf '0')" + local peer_lines + peer_lines="$(grep -c '^peer=' "$STATE_PATH" 2>/dev/null || true)" + printf 'peer_lines=%s\n' "${peer_lines:-0}" printf '\n[redacted state]\n' redact_stream <"$STATE_PATH" } >"$output_file" @@ -227,6 +318,7 @@ EOF } write_manifest +write_json_summary "$BUNDLE_DIR/summary.json" write_config_summary "$BUNDLE_DIR/config-summary.txt" write_state_summary "$BUNDLE_DIR/app-state.txt" diff --git a/src/main.cpp b/src/main.cpp index 8566222..7f23a7d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1648,9 +1648,11 @@ int HandleDoctorCommand(const std::vector& args) { PrintDoctorLine("INFO", "drm", DrmSummary()); const std::filesystem::path uinputPath("/dev/uinput"); + const bool uinputExists = std::filesystem::exists(uinputPath); + const bool uinputWritable = uinputExists && access(uinputPath.c_str(), W_OK) == 0; PrintDoctorLine(std::filesystem::exists("/sys/module/uinput") ? "OK" : "WARN", "uinput module", std::filesystem::exists("/sys/module/uinput") ? "loaded" : "not loaded"); - if (!std::filesystem::exists(uinputPath)) { + if (!uinputExists) { PrintDoctorLine("WARN", "uinput", "/dev/uinput missing; load the uinput kernel module"); } else { struct stat uinputStat {}; @@ -1658,7 +1660,7 @@ int HandleDoctorCommand(const std::vector& args) { const std::string detail = haveStat ? "/dev/uinput group=" + GroupName(uinputStat.st_gid) + " mode=" + FormatMode(uinputStat.st_mode) : "/dev/uinput exists"; - PrintDoctorLine(access(uinputPath.c_str(), W_OK) == 0 ? "OK" : "WARN", "uinput", detail); + PrintDoctorLine(uinputWritable ? "OK" : "WARN", "uinput", detail); } if (getgrnam("inputflow") == nullptr) { @@ -1688,6 +1690,12 @@ int HandleDoctorCommand(const std::vector& args) { } else { PrintDoctorLine("WARN", "session", "no Wayland or X11 session variables detected"); } + const bool waylandSession = sessionType != nullptr && std::string(sessionType) == "wayland"; + if (waylandSession && !uinputWritable) { + PrintDoctorLine("WARN", "wayland input", "Wayland session detected but /dev/uinput is not writable; compositor-mediated injection may still require user approval"); + } else if (waylandSession) { + PrintDoctorLine("INFO", "wayland input", "/dev/uinput is writable; compositor policy may still gate input injection"); + } if (const char* desktop = std::getenv("XDG_CURRENT_DESKTOP"); desktop != nullptr && *desktop != '\0') { PrintDoctorLine("INFO", "desktop", desktop); @@ -1697,6 +1705,14 @@ int HandleDoctorCommand(const std::vector& args) { " DISPLAY=" + EnvValueOrUnset("DISPLAY")); PrintDoctorLine("INFO", "runtime env", "XDG_RUNTIME_DIR=" + EnvValueOrUnset("XDG_RUNTIME_DIR") + " DBUS_SESSION_BUS_ADDRESS=" + EnvValueOrUnset("DBUS_SESSION_BUS_ADDRESS")); + if (FindExecutableInPath("secret-tool")) { + PrintDoctorLine("OK", "secret service tool", "secret-tool available"); + } else { + PrintDoctorLine("INFO", "secret service tool", "secret-tool unavailable; key_secret_id depends on libsecret support and an unlocked session"); + } + if (!config.keySecretId.empty() && EnvValueOrUnset("DBUS_SESSION_BUS_ADDRESS") == "") { + PrintDoctorLine("WARN", "secret service", "key_secret_id is configured but the D-Bus session bus is not advertised"); + } if (FindExecutableInPath("wl-copy") && FindExecutableInPath("wl-paste")) { PrintDoctorLine("OK", "clipboard helpers", "wl-clipboard");