From 2f1d4ae60d8bd231726c2b4b7411de6257fab5e4 Mon Sep 17 00:00:00 2001 From: AlexSSD7 Date: Mon, 28 Aug 2023 11:35:57 +0200 Subject: [PATCH] Some work on supporting Windows --- .gitignore | 1 + cmd/imgbuilder/builder/build.go | 8 +++++- cmd/run.go | 2 ++ cmd/shell.go | 9 ++++++- cmd/utils.go | 20 +++++++++++--- vm/filemanager.go | 3 +-- vm/os_specifics.go | 19 +++++++++++++ vm/os_specifics_windows.go | 19 +++++++++++++ vm/vm.go | 47 +++++++++++++++++++++++++-------- 9 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 vm/os_specifics.go create mode 100644 vm/os_specifics_windows.go diff --git a/.gitignore b/.gitignore index 8eca159..964f30b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ linsk +linsk.exe *.qcow2 \ No newline at end of file diff --git a/cmd/imgbuilder/builder/build.go b/cmd/imgbuilder/builder/build.go index b4f36aa..da6a9c4 100644 --- a/cmd/imgbuilder/builder/build.go +++ b/cmd/imgbuilder/builder/build.go @@ -8,6 +8,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "runtime" "strings" "sync" "syscall" @@ -69,8 +70,13 @@ func NewBuildContext(logger *slog.Logger, baseISOPath string, outPath string, sh func createQEMUImg(outPath string) error { outPath = filepath.Clean(outPath) + baseCmd := "qemu-img" - err := exec.Command("qemu-img", "create", "-f", "qcow2", outPath, "1G").Run() + if runtime.GOOS == "windows" { + baseCmd += ".exe" + } + + err := exec.Command(baseCmd, "create", "-f", "qcow2", outPath, "1G").Run() if err != nil { return errors.Wrap(err, "run qemu-img create cmd") } diff --git a/cmd/run.go b/cmd/run.go index fcf9ec1..fedd9eb 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -45,6 +45,8 @@ var runCmd = &cobra.Command{ return 1 } + // TODO: Use FTP instead of SMB + shareURI := "smb://linsk:" + sharePWD + "@127.0.0.1:" + fmt.Sprint(networkSharePort) fmt.Fprintf(os.Stderr, "================\n[Network File Share Config]\nThe network file share was started. Please use the credentials below to connect to the file server.\n\nType: SMB\nServer Address: smb://127.0.0.1:%v\nUsername: linsk\nPassword: %v\n\nShare URI: %v\n================\n", networkSharePort, sharePWD, shareURI) diff --git a/cmd/shell.go b/cmd/shell.go index 04159b8..d5a8a77 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" "os" + "runtime" "strings" "github.com/AlexSSD7/linsk/vm" @@ -70,7 +71,13 @@ var shellCmd = &cobra.Command{ } }() - termWidth, termHeight, err := term.GetSize(termFD) + termFDGetSize := termFD + if runtime.GOOS == "windows" { + // Another Windows workaround :/ + termFDGetSize = int(os.Stdout.Fd()) + } + + termWidth, termHeight, err := term.GetSize(termFDGetSize) if err != nil { slog.Error("Failed to get terminal size", "error", err) return 1 diff --git a/cmd/utils.go b/cmd/utils.go index 2d5649e..a6c27ea 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "os/user" + "runtime" "sync" "syscall" @@ -24,7 +25,19 @@ func checkIfRoot() (bool, error) { return currentUser.Username == "root", nil } -func doRootCheck() { +func doUSBRootCheck() { + switch runtime.GOOS { + case "darwin": + // Root privileges is not required in macOS. + return + case "windows": + // Administrator privileges are not required in Windows. + return + default: + // As for everything else, we will likely need root privileges + // for the USB passthrough. + } + ok, err := checkIfRoot() if err != nil { slog.Error("Failed to check whether the command is ran by root", "error", err) @@ -32,18 +45,17 @@ func doRootCheck() { } if !ok { - slog.Error("You must run this program as root") + slog.Error("USB passthrough on your OS requires this program to be ran as root") os.Exit(1) } } func runVM(passthroughArg string, fn func(context.Context, *vm.VM, *vm.FileManager) int, forwardPortsRules []vm.PortForwardingRule, unrestrictedNetworking bool) int { - doRootCheck() - var passthroughConfig []vm.USBDevicePassthroughConfig if passthroughArg != "" { passthroughConfig = []vm.USBDevicePassthroughConfig{getDevicePassthroughConfig(passthroughArg)} + doUSBRootCheck() } vmCfg := vm.VMConfig{ diff --git a/vm/filemanager.go b/vm/filemanager.go index 03335ab..b3b87b9 100644 --- a/vm/filemanager.go +++ b/vm/filemanager.go @@ -6,7 +6,6 @@ import ( "fmt" "log/slog" "os" - "path/filepath" "strings" "sync" "syscall" @@ -174,7 +173,7 @@ func (fm *FileManager) Mount(devName string, mo MountOptions) error { return fmt.Errorf("bad device name") } - fullDevPath := filepath.Clean("/dev/" + devName) + fullDevPath := "/dev/" + devName if mo.FSType == "" { return fmt.Errorf("fs type is empty") diff --git a/vm/os_specifics.go b/vm/os_specifics.go new file mode 100644 index 0000000..e23e1a7 --- /dev/null +++ b/vm/os_specifics.go @@ -0,0 +1,19 @@ +//go:build !windows + +package vm + +import ( + "os/exec" + "syscall" +) + +func prepareVMCmd(cmd *exec.Cmd) { + // This is to prevent Ctrl+C propagating to the child process. + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } +} + +func terminateProcess(pid int) error { + return syscall.Kill(-pid, syscall.SIGTERM) +} diff --git a/vm/os_specifics_windows.go b/vm/os_specifics_windows.go new file mode 100644 index 0000000..72ff773 --- /dev/null +++ b/vm/os_specifics_windows.go @@ -0,0 +1,19 @@ +// go:build windows + +package vm + +import ( + "fmt" + "os/exec" + "syscall" +) + +func prepareVMCmd(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, + } +} + +func terminateProcess(pid int) error { + return exec.Command("TASKKILL", "/T", "/F", "/PID", fmt.Sprint(pid)).Run() +} diff --git a/vm/vm.go b/vm/vm.go index c8a8f85..a962d17 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -13,7 +13,6 @@ import ( "runtime" "sync" "sync/atomic" - "syscall" "time" "log/slog" @@ -91,7 +90,16 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { switch runtime.GOARCH { case "amd64": - cmdArgs = append(cmdArgs, "-accel", "kvm") + accel := "kvm" + if runtime.GOOS == "windows" { + // For Windows, we need to install QEMU using an installer and add it to PATH. + // Then, we should enable Windows Hypervisor Platform in "Turn Windows features on or off". + // IMPORTANT: We should also install libusbK drivers for USB devices we want to pass through. + // This can be easily done with a program called Zadiag by Akeo. + accel = "whpx,kernel-irqchip=off" + } + + cmdArgs = append(cmdArgs, "-accel", accel) baseCmd += "-x86_64" case "arm64": // TODO: EFI firmware path is temporary, for dev purposes only. @@ -101,6 +109,10 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { return nil, fmt.Errorf("arch '%v' is not supported", runtime.GOARCH) } + if runtime.GOOS == "windows" { + baseCmd += ".exe" + } + netdevOpts := "user,id=net0,hostfwd=tcp:127.0.0.1:" + fmt.Sprint(sshPort) + "-:22" if !cfg.UnrestrictedNetworking { @@ -130,7 +142,7 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { } if len(cfg.USBDevices) != 0 { - cmdArgs = append(cmdArgs, "-device", "nec-usb-xhci,id=xhci") + cmdArgs = append(cmdArgs, "-device", "nec-usb-xhci") for _, dev := range cfg.USBDevices { cmdArgs = append(cmdArgs, "-device", "usb-host,vendorid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.VendorID))+",productid=0x"+hex.EncodeToString(utils.Uint16ToBytesBE(dev.ProductID))) @@ -171,10 +183,8 @@ func NewVM(logger *slog.Logger, cfg VMConfig) (*VM, error) { stderrBuf := bytes.NewBuffer(nil) cmd.Stderr = stderrBuf - // This is to prevent Ctrl+C propagating to the child process. - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } + // This function is OS-specific. + prepareVMCmd(cmd) userReader := bufio.NewReader(userRead) @@ -310,9 +320,10 @@ func (vm *VM) Cancel() error { sc, err := vm.DialSSH() if err != nil { if !errors.Is(err, ErrSSHUnavailable) { - vm.logger.Warn("Failed to dial VM ssh to do graceful shutdown", "error", err) + vm.logger.Warn("Failed to dial VM SSH to do graceful shutdown", "error", err) } } else { + vm.logger.Warn("Sending poweroff command to the VM") _, err = runSSHCmd(sc, "poweroff") _ = sc.Close() if err != nil { @@ -325,7 +336,11 @@ func (vm *VM) Cancel() error { var interruptErr error if !gracefulOK { - interruptErr = vm.cmd.Process.Signal(os.Interrupt) + if vm.cmd.Process == nil { + interruptErr = fmt.Errorf("process is not started") + } else { + interruptErr = terminateProcess(vm.cmd.Process.Pid) + } } vm.ctxCancel() @@ -355,8 +370,18 @@ func (vm *VM) writeSerial(b []byte) error { vm.serialWriteMu.Lock() defer vm.serialWriteMu.Unlock() - _, err := vm.serialWrite.Write(b) - return err + // What do you see below is a workaround for the way how serial console + // is implemented in QEMU/Windows pair. Apparently they are using polling, + // and this will ensure that we do not write faster than the polling rate. + for i := range b { + _, err := vm.serialWrite.Write([]byte{b[i]}) + time.Sleep(time.Millisecond * 10) + if err != nil { + return errors.Wrapf(err, "write char #%v", i) + } + } + + return nil } func (vm *VM) runVMLoginHandler() error {