mirror of https://github.com/jetkvm/kvm.git
378 lines
12 KiB
Go
378 lines
12 KiB
Go
package native
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
|
|
"github.com/rs/zerolog"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
pb "github.com/jetkvm/kvm/internal/native/proto"
|
|
)
|
|
|
|
// grpcServer wraps the Native instance and implements the gRPC service
|
|
type grpcServer struct {
|
|
pb.UnimplementedNativeServiceServer
|
|
native *Native
|
|
logger *zerolog.Logger
|
|
eventChs []chan *pb.Event
|
|
eventM sync.Mutex
|
|
}
|
|
|
|
// NewGRPCServer creates a new gRPC server for the native service
|
|
func NewGRPCServer(n *Native, logger *zerolog.Logger) *grpcServer {
|
|
s := &grpcServer{
|
|
native: n,
|
|
logger: logger,
|
|
eventChs: make([]chan *pb.Event, 0),
|
|
}
|
|
|
|
// Store original callbacks and wrap them to also broadcast events
|
|
originalVideoStateChange := n.onVideoStateChange
|
|
originalIndevEvent := n.onIndevEvent
|
|
originalRpcEvent := n.onRpcEvent
|
|
|
|
// Wrap callbacks to both call original and broadcast events
|
|
n.onVideoStateChange = func(state VideoState) {
|
|
if originalVideoStateChange != nil {
|
|
originalVideoStateChange(state)
|
|
}
|
|
event := &pb.Event{
|
|
Type: "video_state_change",
|
|
Data: &pb.Event_VideoState{
|
|
VideoState: &pb.VideoState{
|
|
Ready: state.Ready,
|
|
Error: state.Error,
|
|
Width: int32(state.Width),
|
|
Height: int32(state.Height),
|
|
FramePerSecond: state.FramePerSecond,
|
|
},
|
|
},
|
|
}
|
|
s.broadcastEvent(event)
|
|
}
|
|
|
|
n.onIndevEvent = func(event string) {
|
|
if originalIndevEvent != nil {
|
|
originalIndevEvent(event)
|
|
}
|
|
s.broadcastEvent(&pb.Event{
|
|
Type: "indev_event",
|
|
Data: &pb.Event_IndevEvent{
|
|
IndevEvent: event,
|
|
},
|
|
})
|
|
}
|
|
|
|
n.onRpcEvent = func(event string) {
|
|
if originalRpcEvent != nil {
|
|
originalRpcEvent(event)
|
|
}
|
|
s.broadcastEvent(&pb.Event{
|
|
Type: "rpc_event",
|
|
Data: &pb.Event_RpcEvent{
|
|
RpcEvent: event,
|
|
},
|
|
})
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *grpcServer) broadcastEvent(event *pb.Event) {
|
|
s.eventM.Lock()
|
|
defer s.eventM.Unlock()
|
|
|
|
for _, ch := range s.eventChs {
|
|
select {
|
|
case ch <- event:
|
|
default:
|
|
// Channel full, skip
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *grpcServer) IsReady(ctx context.Context, req *pb.IsReadyRequest) (*pb.IsReadyResponse, error) {
|
|
return &pb.IsReadyResponse{Ready: true, VideoReady: true}, nil
|
|
}
|
|
|
|
// Video methods
|
|
func (s *grpcServer) VideoSetSleepMode(ctx context.Context, req *pb.VideoSetSleepModeRequest) (*pb.Empty, error) {
|
|
if err := s.native.VideoSetSleepMode(req.Enabled); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoGetSleepMode(ctx context.Context, req *pb.Empty) (*pb.VideoGetSleepModeResponse, error) {
|
|
enabled, err := s.native.VideoGetSleepMode()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.VideoGetSleepModeResponse{Enabled: enabled}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoSleepModeSupported(ctx context.Context, req *pb.Empty) (*pb.VideoSleepModeSupportedResponse, error) {
|
|
return &pb.VideoSleepModeSupportedResponse{Supported: s.native.VideoSleepModeSupported()}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoSetQualityFactor(ctx context.Context, req *pb.VideoSetQualityFactorRequest) (*pb.Empty, error) {
|
|
if err := s.native.VideoSetQualityFactor(req.Factor); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoGetQualityFactor(ctx context.Context, req *pb.Empty) (*pb.VideoGetQualityFactorResponse, error) {
|
|
factor, err := s.native.VideoGetQualityFactor()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.VideoGetQualityFactorResponse{Factor: factor}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoSetEDID(ctx context.Context, req *pb.VideoSetEDIDRequest) (*pb.Empty, error) {
|
|
if err := s.native.VideoSetEDID(req.Edid); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoGetEDID(ctx context.Context, req *pb.Empty) (*pb.VideoGetEDIDResponse, error) {
|
|
edid, err := s.native.VideoGetEDID()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.VideoGetEDIDResponse{Edid: edid}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoLogStatus(ctx context.Context, req *pb.Empty) (*pb.VideoLogStatusResponse, error) {
|
|
logStatus, err := s.native.VideoLogStatus()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.VideoLogStatusResponse{Status: logStatus}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoStop(ctx context.Context, req *pb.Empty) (*pb.Empty, error) {
|
|
procPrefix = "jetkvm: [native]"
|
|
setProcTitle(lastProcTitle)
|
|
|
|
if err := s.native.VideoStop(); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) VideoStart(ctx context.Context, req *pb.Empty) (*pb.Empty, error) {
|
|
procPrefix = "jetkvm: [native+video]"
|
|
setProcTitle(lastProcTitle)
|
|
|
|
if err := s.native.VideoStart(); err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
// UI methods
|
|
func (s *grpcServer) GetLVGLVersion(ctx context.Context, req *pb.Empty) (*pb.GetLVGLVersionResponse, error) {
|
|
version, err := s.native.GetLVGLVersion()
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.GetLVGLVersionResponse{Version: version}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjHide(ctx context.Context, req *pb.UIObjHideRequest) (*pb.UIObjHideResponse, error) {
|
|
success, err := s.native.UIObjHide(req.ObjName)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjHideResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjShow(ctx context.Context, req *pb.UIObjShowRequest) (*pb.UIObjShowResponse, error) {
|
|
success, err := s.native.UIObjShow(req.ObjName)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjShowResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UISetVar(ctx context.Context, req *pb.UISetVarRequest) (*pb.Empty, error) {
|
|
s.native.UISetVar(req.Name, req.Value)
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIGetVar(ctx context.Context, req *pb.UIGetVarRequest) (*pb.UIGetVarResponse, error) {
|
|
value := s.native.UIGetVar(req.Name)
|
|
return &pb.UIGetVarResponse{Value: value}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjAddState(ctx context.Context, req *pb.UIObjAddStateRequest) (*pb.UIObjAddStateResponse, error) {
|
|
success, err := s.native.UIObjAddState(req.ObjName, req.State)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjAddStateResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjClearState(ctx context.Context, req *pb.UIObjClearStateRequest) (*pb.UIObjClearStateResponse, error) {
|
|
success, err := s.native.UIObjClearState(req.ObjName, req.State)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjClearStateResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjAddFlag(ctx context.Context, req *pb.UIObjAddFlagRequest) (*pb.UIObjAddFlagResponse, error) {
|
|
success, err := s.native.UIObjAddFlag(req.ObjName, req.Flag)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjAddFlagResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjClearFlag(ctx context.Context, req *pb.UIObjClearFlagRequest) (*pb.UIObjClearFlagResponse, error) {
|
|
success, err := s.native.UIObjClearFlag(req.ObjName, req.Flag)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjClearFlagResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjSetOpacity(ctx context.Context, req *pb.UIObjSetOpacityRequest) (*pb.UIObjSetOpacityResponse, error) {
|
|
success, err := s.native.UIObjSetOpacity(req.ObjName, int(req.Opacity))
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjSetOpacityResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjFadeIn(ctx context.Context, req *pb.UIObjFadeInRequest) (*pb.UIObjFadeInResponse, error) {
|
|
success, err := s.native.UIObjFadeIn(req.ObjName, req.Duration)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjFadeInResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjFadeOut(ctx context.Context, req *pb.UIObjFadeOutRequest) (*pb.UIObjFadeOutResponse, error) {
|
|
success, err := s.native.UIObjFadeOut(req.ObjName, req.Duration)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjFadeOutResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjSetLabelText(ctx context.Context, req *pb.UIObjSetLabelTextRequest) (*pb.UIObjSetLabelTextResponse, error) {
|
|
success, err := s.native.UIObjSetLabelText(req.ObjName, req.Text)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjSetLabelTextResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UIObjSetImageSrc(ctx context.Context, req *pb.UIObjSetImageSrcRequest) (*pb.UIObjSetImageSrcResponse, error) {
|
|
success, err := s.native.UIObjSetImageSrc(req.ObjName, req.Image)
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.UIObjSetImageSrcResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) DisplaySetRotation(ctx context.Context, req *pb.DisplaySetRotationRequest) (*pb.DisplaySetRotationResponse, error) {
|
|
success, err := s.native.DisplaySetRotation(uint16(req.Rotation))
|
|
if err != nil {
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
}
|
|
return &pb.DisplaySetRotationResponse{Success: success}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UpdateLabelIfChanged(ctx context.Context, req *pb.UpdateLabelIfChangedRequest) (*pb.Empty, error) {
|
|
s.native.UpdateLabelIfChanged(req.ObjName, req.NewText)
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) UpdateLabelAndChangeVisibility(ctx context.Context, req *pb.UpdateLabelAndChangeVisibilityRequest) (*pb.Empty, error) {
|
|
s.native.UpdateLabelAndChangeVisibility(req.ObjName, req.NewText)
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) SwitchToScreenIf(ctx context.Context, req *pb.SwitchToScreenIfRequest) (*pb.Empty, error) {
|
|
s.native.SwitchToScreenIf(req.ScreenName, req.ShouldSwitch)
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) SwitchToScreenIfDifferent(ctx context.Context, req *pb.SwitchToScreenIfDifferentRequest) (*pb.Empty, error) {
|
|
s.native.SwitchToScreenIfDifferent(req.ScreenName)
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
func (s *grpcServer) DoNotUseThisIsForCrashTestingOnly(ctx context.Context, req *pb.Empty) (*pb.Empty, error) {
|
|
s.native.DoNotUseThisIsForCrashTestingOnly()
|
|
return &pb.Empty{}, nil
|
|
}
|
|
|
|
// StreamEvents streams events from the native process
|
|
func (s *grpcServer) StreamEvents(req *pb.Empty, stream pb.NativeService_StreamEventsServer) error {
|
|
setProcTitle("connected")
|
|
defer setProcTitle("waiting")
|
|
|
|
eventCh := make(chan *pb.Event, 100)
|
|
|
|
// Register this channel for events
|
|
s.eventM.Lock()
|
|
s.eventChs = append(s.eventChs, eventCh)
|
|
s.eventM.Unlock()
|
|
|
|
// Unregister on exit
|
|
defer func() {
|
|
s.eventM.Lock()
|
|
defer s.eventM.Unlock()
|
|
for i, ch := range s.eventChs {
|
|
if ch == eventCh {
|
|
s.eventChs = append(s.eventChs[:i], s.eventChs[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
close(eventCh)
|
|
}()
|
|
|
|
// Stream events
|
|
for {
|
|
select {
|
|
case event := <-eventCh:
|
|
if err := stream.Send(event); err != nil {
|
|
return err
|
|
}
|
|
case <-stream.Context().Done():
|
|
return stream.Context().Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// StartGRPCServer starts the gRPC server on a Unix domain socket
|
|
func StartGRPCServer(server *grpcServer, socketPath string, logger *zerolog.Logger) (*grpc.Server, net.Listener, error) {
|
|
lis, err := net.Listen("unix", socketPath)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to listen on socket: %w", err)
|
|
}
|
|
|
|
s := grpc.NewServer()
|
|
pb.RegisterNativeServiceServer(s, server)
|
|
|
|
go func() {
|
|
if err := s.Serve(lis); err != nil {
|
|
logger.Error().Err(err).Msg("gRPC server error")
|
|
}
|
|
}()
|
|
|
|
logger.Info().Str("socket", socketPath).Msg("gRPC server started")
|
|
return s, lis, nil
|
|
}
|