Files
kvm/native_display.go
luckfox-eng29 8fbd6bcf0d Add support for Luckfox PicoKVM
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
2025-09-16 11:58:09 +08:00

276 lines
6.8 KiB
Go

package kvm
import (
"encoding/json"
"errors"
"fmt"
"net"
"os"
"os/exec"
"sync"
"time"
)
var (
displayCmd *exec.Cmd
displayCmdLock = &sync.Mutex{}
)
var displaySocketConn net.Conn
var displayOngoingRequests = make(map[int32]chan *CtrlResponse)
var displayLock = &sync.Mutex{}
func CallDisplayCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
displayLock.Lock()
defer displayLock.Unlock()
ctrlAction := CtrlAction{
Action: action,
Seq: seq,
Params: params,
}
responseChan := make(chan *CtrlResponse)
displayOngoingRequests[seq] = responseChan
seq++
jsonData, err := json.Marshal(ctrlAction)
if err != nil {
delete(displayOngoingRequests, ctrlAction.Seq)
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
}
scopedLogger := displayLogger.With().
Str("action", ctrlAction.Action).
Interface("params", ctrlAction.Params).Logger()
scopedLogger.Debug().Msg("sending display ctrl action")
err = WriteDisplayCtrlMessage(jsonData)
if err != nil {
delete(displayOngoingRequests, ctrlAction.Seq)
return nil, ErrorfL(&scopedLogger, "error writing display ctrl message", err)
}
select {
case response := <-responseChan:
delete(displayOngoingRequests, seq)
if response.Error != "" {
return nil, ErrorfL(
&scopedLogger,
"error display response: %s",
errors.New(response.Error),
)
}
return response, nil
case <-time.After(10 * time.Second):
close(responseChan)
delete(displayOngoingRequests, seq)
return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil)
}
}
func WriteDisplayCtrlMessage(message []byte) error {
if displaySocketConn == nil {
return fmt.Errorf("display socket not conn ected")
}
_, err := displaySocketConn.Write(message)
return err
}
var displayCtrlSocketListener net.Listener
var displayCtrlClientConnected = make(chan struct{})
func waitDisplayCtrlClientConnected() {
<-displayCtrlClientConnected
}
func StartDisplaySocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener {
scopedLogger := displayLogger.With().
Str("socket_path", socketPath).
Logger()
// Remove the socket file if it already exists
if _, err := os.Stat(socketPath); err == nil {
if err := os.Remove(socketPath); err != nil {
scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file")
os.Exit(1)
}
}
listener, err := net.Listen("unixpacket", socketPath)
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to start server")
os.Exit(1)
}
scopedLogger.Info().Msg("server listening")
go func() {
for {
conn, err := listener.Accept()
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
continue
}
if isCtrl {
// check if the channel is closed
select {
case <-displayCtrlClientConnected:
scopedLogger.Debug().Msg("display ctrl client reconnected")
default:
close(displayCtrlClientConnected)
scopedLogger.Debug().Msg("first display ctrl socket client connected")
}
}
//conn.Write([]byte("[handleDisplayCtrlClient]display sock test"))
go handleClient(conn)
}
}()
return listener
}
func StartDisplayCtrlSocketServer() {
displayCtrlSocketListener = StartDisplaySocketServer("/var/run/kvm_display.sock", handleDisplayCtrlClient, true)
displayLogger.Debug().Msg("display ctrl sock started")
}
func handleDisplayCtrlClient(conn net.Conn) {
defer conn.Close()
scopedLogger := displayLogger.With().
Str("addr", conn.RemoteAddr().String()).
Str("type", "display_ctrl").
Logger()
scopedLogger.Info().Msg("display socket client connected")
if displaySocketConn != nil {
scopedLogger.Debug().Msg("closing existing display socket connection")
displaySocketConn.Close()
}
displaySocketConn = conn
readBuf := make([]byte, 4096)
for {
n, err := conn.Read(readBuf)
if err != nil {
scopedLogger.Warn().Err(err).Msg("error reading from display sock")
break
}
readMsg := string(readBuf[:n])
displayResp := CtrlResponse{}
err = json.Unmarshal([]byte(readMsg), &displayResp)
if err != nil {
scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing display sock msg")
continue
}
scopedLogger.Trace().Interface("data", displayResp).Msg("display sock msg")
if displayResp.Seq != 0 {
responseChan, ok := displayOngoingRequests[displayResp.Seq]
if ok {
responseChan <- &displayResp
}
}
}
scopedLogger.Debug().Msg("display sock disconnected")
}
func startDisplayBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
displayCmdLock.Lock()
defer displayCmdLock.Unlock()
cmd, err := startDisplayBinary(binaryPath)
if err != nil {
return nil, err
}
displayCmd = cmd
return cmd, nil
}
func restartDisplayBinary(binaryPath string) error {
time.Sleep(10 * time.Second)
// restart the binary
displayLogger.Info().Msg("restarting display_video binary")
cmd, err := startDisplayBinary(binaryPath)
if err != nil {
displayLogger.Warn().Err(err).Msg("failed to restart binary")
}
displayCmd = cmd
return err
}
func superviseDisplayBinary(binaryPath string) error {
displayCmdLock.Lock()
defer displayCmdLock.Unlock()
if displayCmd == nil || displayCmd.Process == nil {
return restartDisplayBinary(binaryPath)
}
err := displayCmd.Wait()
if err == nil {
displayLogger.Info().Err(err).Msg("kvm_display binary exited with no error")
} else if exiterr, ok := err.(*exec.ExitError); ok {
displayLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("kvm_display binary exited with error")
} else {
displayLogger.Warn().Err(err).Msg("kvm_display binary exited with unknown error")
}
return restartDisplayBinary(binaryPath)
}
func ExtractAndRunDisplayBin() error {
binaryPath := "/userdata/picokvm/bin/kvm_display"
// Make the binary executable
if err := os.Chmod(binaryPath, 0755); err != nil {
return fmt.Errorf("failed to make binary executable: %w", err)
}
// Run the binary in the background
cmd, err := startDisplayBinaryWithLock(binaryPath)
if err != nil {
return fmt.Errorf("failed to start binary: %w", err)
}
// check if the binary is still running every 10 seconds
go func() {
for {
select {
case <-appCtx.Done():
displayLogger.Info().Msg("stopping display binary supervisor")
return
default:
err := superviseDisplayBinary(binaryPath)
if err != nil {
displayLogger.Warn().Err(err).Msg("failed to supervise display binary")
time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls
}
}
}
}()
go func() {
<-appCtx.Done()
displayLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")
err := cmd.Process.Kill()
if err != nil {
displayLogger.Warn().Err(err).Msg("failed to kill process")
return
}
}()
displayLogger.Info().Int("pid", cmd.Process.Pid).Msg("kvm_display binary started")
return nil
}