RF-Swift/go/rfswift/rfutils/hostcli.go

414 lines
12 KiB
Go

/* This code is part of RF Switch by @Penthertz
* Author(s): Sébastien Dudek (@FlUxIuS)
*/
package rfutils
import (
"bufio"
"fmt"
"net"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/lawl/pulseaudio"
common "penthertz/rfswift/common"
)
// USBDevice represents a USB device information
type USBDevice struct {
BusID string
DeviceID string
VendorID string
ProductID string
Description string
}
func ListUSBDevices() ([]USBDevice, error) {
/*
* ListUSBDevices executes the usbipd.exe command and lists USB devices
* out(1): USBDevice array
out(2): Errors
*/
// Execute the usbipd.exe command
cmd := exec.Command("usbipd.exe", "list")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute usbipd.exe: %w", err)
}
// Parse the output
var devices []USBDevice
scanner := bufio.NewScanner(strings.NewReader(string(output)))
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "BusID") {
continue
}
fields := strings.Fields(line)
if len(fields) >= 5 {
device := USBDevice{
BusID: fields[0],
DeviceID: fields[1],
VendorID: fields[2],
ProductID: fields[3],
Description: strings.Join(fields[4:], " "),
}
devices = append(devices, device)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading command output: %w", err)
}
return devices, nil
}
func AttachUSBDevice(busID string) error {
/*
* AttachUSBDevice attaches a USB device using its BusID
* in(1): bus ID string to attach
* out: error
*/
cmd := exec.Command("usbipd.exe", "attach", "--wsl", "--busid", busID)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to attach device %s: %w", busID, err)
}
return nil
}
func BindUSBDevice(busID string) error {
/*
* BindUSBDevice binds a USB device using its BusID
* in(1): bus ID string to bind
* out: error
*/
cmd := exec.Command("usbipd.exe", "bind", "--busid", busID) // autoattach
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to bind device %s: %w", busID, err)
}
return nil
}
func BindAndAttachDevice(busID string) {
/*
* BindAndAttachAllDevices binds and attaches all listed USB devices
* in(1): array of bus ID to attach
* out: error
*/
if err := BindUSBDevice(busID); err != nil {
fmt.Println("Error binding devices:", err)
}
if err := AttachUSBDevice(busID); err != nil {
fmt.Println("Error attaching devices:", err)
}
}
func UnbindAndDetachDevice(busID string) {
/*
* Unbind and detach a specific USB device
* in(1): array of bus ID string to unbind and detach
* out: error
*/
if err := UnbindUSBDevice(busID); err != nil {
fmt.Println("Error unbinding device:", err)
}
if err := DetachUSBDevice(busID); err != nil {
fmt.Println("Error detaching device:", err)
}
}
// TODO: find a way to blacklist some buses like the keyboard...
func BindAndAttachAllDevices(devices []USBDevice) error {
/*
* BindAndAttachAllDevices binds and attaches all listed USB devices
* in(1): array of bus ID to attach
* out: error
*/
for _, device := range devices {
if err := BindUSBDevice(device.BusID); err != nil {
return err
}
if err := AttachUSBDevice(device.BusID); err != nil {
return err
}
}
return nil
}
func UnbindUSBDevice(busID string) error {
/*
* UnbindUSBDevice unbinds a USB device using its BusID
* in(1): bus ID string to attach
* out: error
*/
cmd := exec.Command("usbipd.exe", "unbind", "--busid", busID)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to unbind device %s: %w", busID, err)
}
return nil
}
func DetachUSBDevice(busID string) error {
/*
* DetachUSBDevice detaches a USB device using its BusID
* in(1): bus ID string to attach
* out: error
*/
cmd := exec.Command("usbipd.exe", "detach", "--busid", busID)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to detach device %s: %w", busID, err)
}
return nil
}
// TODO: find a way to blacklist some buses like the keyboard...
func UnbindAndDetachAllDevices(devices []USBDevice) error {
/*
* Unbind and detach all USB devices
* in(1): array of bus ID string to unbind and detach
* out: error
*/
for _, device := range devices {
if err := UnbindUSBDevice(device.BusID); err != nil {
return err
}
if err := DetachUSBDevice(device.BusID); err != nil {
return err
}
}
return nil
}
func BindAttachUSB_Windows(busID string) {
/*
* Bind a specific USB device from the Windows host
*/
devices, err := ListUSBDevices()
if err != nil {
fmt.Println("Error:", err)
return
}
for _, device := range devices {
fmt.Printf("BusID: %s, DeviceID: %s, VendorID: %s, ProductID: %s, Description: %s\n",
device.BusID, device.DeviceID, device.VendorID, device.ProductID, device.Description)
}
if err := BindAndAttachAllDevices(devices); err != nil {
fmt.Println("Error binding and attaching devices:", err)
}
}
func AutoBindAttachUSB_Windows() {
/*
* Automatically bind all USB devices from the Windows host
*/
devices, err := ListUSBDevices()
if err != nil {
fmt.Println("Error:", err)
return
}
for _, device := range devices {
fmt.Printf("BusID: %s, DeviceID: %s, VendorID: %s, ProductID: %s, Description: %s\n",
device.BusID, device.DeviceID, device.VendorID, device.ProductID, device.Description)
}
if err := BindAndAttachAllDevices(devices); err != nil {
fmt.Println("Error binding and attaching devices:", err)
}
}
func AutoUnbindDetachUSB_Windows() {
/*
* Automatically Unbind and detach all USB devices from the Windows host
*/
devices, err := ListUSBDevices()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("USB Devices:")
for _, device := range devices {
fmt.Printf("BusID: %s, DeviceID: %s, VendorID: %s, ProductID: %s, Description: %s\n",
device.BusID, device.DeviceID, device.VendorID, device.ProductID, device.Description)
}
fmt.Println("\nUnbinding and detaching all devices...")
if err := UnbindAndDetachAllDevices(devices); err != nil {
fmt.Println("Error unbinding and detaching devices:", err)
return
}
fmt.Println("Operation completed successfully.")
}
func checkPulseServer(address string, port string) {
// Combine address and port to create the endpoint
endpoint := net.JoinHostPort(address, port)
// Attempt to establish a connection
conn, err := net.DialTimeout("tcp", endpoint, 5*time.Second)
if err != nil {
// Connection failed, prepare the error message
message := fmt.Sprintf("\033[33mWarning: Unable to connect to Pulse server at %s\033[0m\n", endpoint)
message += retInstallationInstructions()
// Display the notification
DisplayNotification(" Warning", message, "warning")
return
}
// Close the connection if successful
conn.Close()
// Prepare success message
successMessage := fmt.Sprintf("Pulse server found at %s", endpoint)
// Display success notification
DisplayNotification(" Audio", successMessage, "info")
}
func retInstallationInstructions() string {
var retstring strings.Builder
os := runtime.GOOS
switch os {
case "windows":
retstring.WriteString("\nTo install Pulse server on Windows, follow these steps:\n")
retstring.WriteString("1. Download the Pulse server installer from the official website.\n")
retstring.WriteString("2. Run the installer and follow the on-screen instructions.\n")
case "darwin":
retstring.WriteString("To install Pulse server on macOS, follow these steps:\n")
retstring.WriteString("1. Install Homebrew if you haven't already: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"\n")
retstring.WriteString("2. Install Pulse server using Homebrew: brew install pulseaudio\n")
case "linux":
if isArchLinux() {
retstring.WriteString("\nTo install Pulse server on Arch Linux, follow these steps:\n")
retstring.WriteString("1. Update your package database: sudo pacman -Syu\n")
retstring.WriteString("2. Install Pulse server: sudo pacman -S pulseaudio\n")
} else {
retstring.WriteString("To install Pulse server on Linux, follow these steps:\n")
retstring.WriteString("1. Update your package manager: sudo apt update (for Debian-based) or sudo yum update (for Red Hat-based).\n")
retstring.WriteString("2. Install Pulse server: sudo apt install pulseaudio (for Debian-based) or sudo yum install pulseaudio (for Red Hat-based).\n")
}
default:
retstring.WriteString("\nPlease refer to the official Pulse server documentation for installation instructions.\n")
}
retstring.WriteString("\n\nAfter installation, enable the module with the following command as unprivileged user:\n")
retstring.WriteString("\033[33m./rfswift host audio enable\033[0m")
return retstring.String()
}
// isArchLinux checks if the current Linux distribution is Arch Linux
func isArchLinux() bool {
// This function checks if /etc/arch-release exists to determine if the system is Arch Linux
if _, err := os.Stat("/etc/arch-release"); err == nil {
return true
}
return false
}
// ensurePulseAudioRunning checks if PulseAudio is running and starts it if not.
func ensurePulseAudioRunning() error {
cmd := exec.Command("pulseaudio", "--check")
if err := cmd.Run(); err != nil {
// If PulseAudio is not running, start it
startCmd := exec.Command("pulseaudio", "--start")
if startErr := startCmd.Run(); startErr != nil {
return fmt.Errorf("failed to start PulseAudio: %w", startErr)
}
common.PrintSuccessMessage(fmt.Sprintf("PulseAudio started successfully."))
time.Sleep(2 * time.Second) // Wait for 2 seconds
} else {
common.PrintInfoMessage(fmt.Sprintf("PulseAudio is already running."))
}
return nil
}
func SetPulseCTL(address string) error {
/*
* Use PACTL in command line to accept connection in TCP with defined port
*/
parts := strings.Split(address, ":")
if len(parts) != 3 {
return fmt.Errorf("invalid address format, expected format 'protocol:ip:port'")
}
port := parts[2]
ip := parts[1]
// Ensure PulseAudio is running
if err := ensurePulseAudioRunning(); err != nil {
return fmt.Errorf("failed to ensure PulseAudio is running: %w", err)
}
// Connect to PulseAudio
client, err := pulseaudio.NewClient()
if err != nil {
return fmt.Errorf("failed to connect to PulseAudio: %w", err)
}
defer client.Close()
// Construct the module arguments string
moduleArgs := fmt.Sprintf("port=%s auth-ip-acl=%s", port, ip)
// Load module-native-protocol-tcp with the specified IP and port
moduleIndex, err := client.LoadModule("module-native-protocol-tcp", moduleArgs)
if err != nil {
return fmt.Errorf("failed to load module-native-protocol-tcp: %w", err)
}
common.PrintSuccessMessage(fmt.Sprintf("Successfully loaded module-native-protocol-tcp with index %d", moduleIndex))
return nil
}
func UnloadPulseCTL() error {
/*
* Unload pulseaudio TCP module
*/
cmd := exec.Command("pactl", "list", "modules")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to list PulseAudio modules: %w\nOutput: %s", err, string(output))
}
// Parse the output to find the module-native-protocol-tcp index
lines := strings.Split(string(output), "\n")
var moduleIndex string
for i, line := range lines {
if strings.Contains(line, "Name: module-native-protocol-tcp") {
// Find the "Index:" line above the module name
for j := i; j >= 0; j-- {
if strings.Contains(lines[j], "Module #") {
moduleIndex = strings.TrimSpace(strings.TrimPrefix(lines[j], "Module #"))
break
}
}
break
}
}
if moduleIndex == "" {
return fmt.Errorf("module-native-protocol-tcp not found")
}
// Execute pactl unload-module to unload the module
unloadCmd := exec.Command("pactl", "unload-module", moduleIndex)
unloadOutput, err := unloadCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to unload module-native-protocol-tcp: %w\nOutput: %s", err, string(unloadOutput))
}
fmt.Printf("Command output: %s\n", string(unloadOutput))
common.PrintSuccessMessage(fmt.Sprintf("Successfully unloaded module-native-protocol-tcp with index %s", moduleIndex))
return nil
}