-
Notifications
You must be signed in to change notification settings - Fork 42
[Extending API] /screen/resolution #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
291dc99
79e4fc0
32c5a24
39a5e26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,12 @@ package api | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
| "strconv" | ||
| "sync" | ||
| "time" | ||
|
|
||
|
|
@@ -28,8 +31,90 @@ type ApiService struct { | |
| procs map[string]*processHandle | ||
| } | ||
|
|
||
| // We're extending the StrictServerInterface to include our new endpoint | ||
| var _ oapi.StrictServerInterface = (*ApiService)(nil) | ||
|
|
||
| // SetScreenResolution endpoint | ||
| // (GET /screen/resolution) | ||
| func (s *ApiService) SetScreenResolutionHandler(w http.ResponseWriter, r *http.Request) { | ||
| // Parse query parameters | ||
| width := 0 | ||
| height := 0 | ||
| var rate *int | ||
|
|
||
| // Parse width | ||
| widthStr := r.URL.Query().Get("width") | ||
| if widthStr == "" { | ||
| http.Error(w, "missing required query parameter: width", http.StatusBadRequest) | ||
| return | ||
| } | ||
| var err error | ||
| width, err = strconv.Atoi(widthStr) | ||
| if err != nil { | ||
| http.Error(w, "invalid width parameter: must be an integer", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| // Parse height | ||
| heightStr := r.URL.Query().Get("height") | ||
| if heightStr == "" { | ||
| http.Error(w, "missing required query parameter: height", http.StatusBadRequest) | ||
| return | ||
| } | ||
| height, err = strconv.Atoi(heightStr) | ||
| if err != nil { | ||
| http.Error(w, "invalid height parameter: must be an integer", http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| // Parse optional rate parameter | ||
| rateStr := r.URL.Query().Get("rate") | ||
| if rateStr != "" { | ||
| rateVal, err := strconv.Atoi(rateStr) | ||
| if err != nil { | ||
| http.Error(w, "invalid rate parameter: must be an integer", http.StatusBadRequest) | ||
| return | ||
| } | ||
| rate = &rateVal | ||
| } | ||
|
|
||
| // Create request object | ||
| reqObj := SetScreenResolutionRequestObject{ | ||
| Width: width, | ||
| Height: height, | ||
| Rate: rate, | ||
| } | ||
|
|
||
| // Call the actual implementation | ||
| resp, err := s.SetScreenResolution(r.Context(), reqObj) | ||
| if err != nil { | ||
| http.Error(w, err.Error(), http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| // Handle different response types | ||
| switch r := resp.(type) { | ||
| case SetScreenResolution200JSONResponse: | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusOK) | ||
| json.NewEncoder(w).Encode(r) | ||
| case SetScreenResolution400JSONResponse: | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusBadRequest) | ||
| json.NewEncoder(w).Encode(r) | ||
| case SetScreenResolution409JSONResponse: | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusConflict) | ||
| json.NewEncoder(w).Encode(r) | ||
| case SetScreenResolution500JSONResponse: | ||
| w.Header().Set("Content-Type", "application/json") | ||
| w.WriteHeader(http.StatusInternalServerError) | ||
| json.NewEncoder(w).Encode(r) | ||
| default: | ||
| http.Error(w, "unexpected response type", http.StatusInternalServerError) | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Manual Implementation Causes JSON Encoding ErrorsThe
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @raiden-staging cursor is correct here. The rough process to add a new endpoint:
|
||
|
|
||
| func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFactory) (*ApiService, error) { | ||
| switch { | ||
| case recordManager == nil: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,9 +2,13 @@ package api | |
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os/exec" | ||
| "strconv" | ||
| "time" | ||
|
|
||
| "github.com/gorilla/websocket" | ||
| "github.com/onkernel/kernel-images/server/lib/logger" | ||
| oapi "github.com/onkernel/kernel-images/server/lib/oapi" | ||
| ) | ||
|
|
@@ -54,6 +58,149 @@ func (s *ApiService) MoveMouse(ctx context.Context, request oapi.MoveMouseReques | |
| return oapi.MoveMouse200Response{}, nil | ||
| } | ||
|
|
||
| // Define interface types for our new endpoint | ||
| // These should match the structure expected by the generated code | ||
|
|
||
| // SetScreenResolutionParams represents query parameters for our endpoint | ||
| type SetScreenResolutionParams struct { | ||
| Width int | ||
| Height int | ||
| Rate *int | ||
| } | ||
|
|
||
| // For testing | ||
| type SetScreenResolutionFunc func(ctx context.Context, req SetScreenResolutionRequestObject) (SetScreenResolutionResponseObject, error) | ||
|
|
||
| // This would be auto-generated by oapi-codegen, but we're defining it manually | ||
| type SetScreenResolutionRequestObject struct { | ||
| Width int // Required query parameter | ||
| Height int // Required query parameter | ||
| Rate *int // Optional query parameter | ||
| } | ||
|
|
||
| // Response types for different status codes | ||
| type SetScreenResolution200JSONResponse struct { | ||
| Ok bool `json:"ok"` | ||
| } | ||
|
|
||
| type SetScreenResolution400JSONResponse struct { | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| type SetScreenResolution409JSONResponse struct { | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| type SetScreenResolution500JSONResponse struct { | ||
| Message string `json:"message"` | ||
| } | ||
|
|
||
| // Union type for all possible responses | ||
| type SetScreenResolutionResponseObject interface { | ||
| SetScreenResolutionResponse() | ||
| } | ||
|
|
||
| // Implement response interface for each response type | ||
| func (SetScreenResolution200JSONResponse) SetScreenResolutionResponse() {} | ||
| func (SetScreenResolution400JSONResponse) SetScreenResolutionResponse() {} | ||
| func (SetScreenResolution409JSONResponse) SetScreenResolutionResponse() {} | ||
| func (SetScreenResolution500JSONResponse) SetScreenResolutionResponse() {} | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Inconsistent API Endpoint RegistrationThe new Additional Locations (1) |
||
| func (s *ApiService) SetScreenResolution(ctx context.Context, request SetScreenResolutionRequestObject) (SetScreenResolutionResponseObject, error) { | ||
| log := logger.FromContext(ctx) | ||
|
|
||
| // Validate parameters | ||
| width := request.Width | ||
| height := request.Height | ||
| rate := request.Rate | ||
|
|
||
| // Parameters were already validated in OpenAPI spec, but we'll do a sanity check here | ||
| if width < 200 || width > 8000 { | ||
| return SetScreenResolution400JSONResponse{ | ||
| Message: fmt.Sprintf("width must be between 200 and 8000, got %d", width), | ||
| }, nil | ||
| } | ||
|
|
||
| if height < 200 || height > 8000 { | ||
| return SetScreenResolution400JSONResponse{ | ||
| Message: fmt.Sprintf("height must be between 200 and 8000, got %d", height), | ||
| }, nil | ||
| } | ||
|
|
||
| if rate != nil && (*rate < 24 || *rate > 240) { | ||
| return SetScreenResolution400JSONResponse{ | ||
| Message: fmt.Sprintf("rate must be between 24 and 240, got %d", *rate), | ||
| }, nil | ||
| } | ||
|
|
||
| // Check if ffmpeg is running (indicating an active recording) | ||
| cmd := exec.Command("pgrep", "ffmpeg") | ||
| if err := cmd.Run(); err == nil { | ||
| // ffmpeg is running | ||
| return SetScreenResolution409JSONResponse{ | ||
| Message: "detected ongoing replay recording process, close the recording first before switching resolution", | ||
| }, nil | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc @Sayan- on ideas for how this should interact with replays
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think instead of a recs := s.recordManager.ListActiveRecorders(ctx)
for _, r := range recs {
if r.IsRecording(ctx) {
return true
}
}
return falseI'd also recommend grabbing a lock and blocking |
||
|
|
||
| // Connect to websocket | ||
| wsURL := "ws://localhost:8080/ws?password=admin&username=kernel" | ||
|
||
| dialer := websocket.Dialer{ | ||
| HandshakeTimeout: 5 * time.Second, | ||
| } | ||
|
|
||
| conn, _, err := dialer.Dial(wsURL, nil) | ||
| if err != nil { | ||
| log.Error("failed to connect to websocket", "err", err) | ||
| return SetScreenResolution500JSONResponse{ | ||
| Message: "failed to connect to websocket server", | ||
| }, nil | ||
| } | ||
| defer conn.Close() | ||
|
|
||
| // Prepare message | ||
| message := map[string]interface{}{ | ||
| "event": "screen/set", | ||
| "width": width, | ||
| "height": height, | ||
| } | ||
|
|
||
| // Add rate if provided | ||
| if rate != nil { | ||
| message["rate"] = *rate | ||
| } | ||
|
|
||
| // Serialize message to JSON | ||
| messageJSON, err := json.Marshal(message) | ||
| if err != nil { | ||
| log.Error("failed to marshal JSON message", "err", err) | ||
| return SetScreenResolution500JSONResponse{ | ||
| Message: "failed to prepare websocket message", | ||
| }, nil | ||
| } | ||
|
|
||
| // Send message | ||
| if err := conn.WriteMessage(websocket.TextMessage, messageJSON); err != nil { | ||
| log.Error("failed to send websocket message", "err", err) | ||
| return SetScreenResolution500JSONResponse{ | ||
| Message: "failed to send command to websocket server", | ||
| }, nil | ||
| } | ||
|
|
||
| // Wait for response (optional, but might be good to ensure it worked) | ||
| conn.SetReadDeadline(time.Now().Add(5 * time.Second)) | ||
| _, response, err := conn.ReadMessage() | ||
| if err != nil { | ||
| log.Warn("did not receive websocket response, but proceeding", "err", err) | ||
| // Continue anyway since we don't know if the server responds | ||
| } else { | ||
| log.Info("received websocket response", "response", string(response)) | ||
| } | ||
|
|
||
| return SetScreenResolution200JSONResponse{ | ||
| Ok: true, | ||
| }, nil | ||
| } | ||
|
|
||
| func (s *ApiService) ClickMouse(ctx context.Context, request oapi.ClickMouseRequestObject) (oapi.ClickMouseResponseObject, error) { | ||
| log := logger.FromContext(ctx) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package api | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestScreenResolutionParameterValidation(t *testing.T) { | ||
| // Test parameter validation in the SetScreenResolution function | ||
| testCases := []struct { | ||
| name string | ||
| width int | ||
| height int | ||
| rate *int | ||
| expectError bool | ||
| errorMsg string | ||
| }{ | ||
| { | ||
| name: "valid parameters", | ||
| width: 1920, | ||
| height: 1080, | ||
| rate: intPtr(60), | ||
| expectError: false, | ||
| }, | ||
| { | ||
| name: "valid without rate", | ||
| width: 1280, | ||
| height: 720, | ||
| rate: nil, | ||
| expectError: false, | ||
| }, | ||
| { | ||
| name: "width too small", | ||
| width: 100, | ||
| height: 1080, | ||
| rate: nil, | ||
| expectError: true, | ||
| errorMsg: "width must be between 200 and 8000", | ||
| }, | ||
| { | ||
| name: "width too large", | ||
| width: 9000, | ||
| height: 1080, | ||
| rate: nil, | ||
| expectError: true, | ||
| errorMsg: "width must be between 200 and 8000", | ||
| }, | ||
| { | ||
| name: "height too small", | ||
| width: 1920, | ||
| height: 100, | ||
| rate: nil, | ||
| expectError: true, | ||
| errorMsg: "height must be between 200 and 8000", | ||
| }, | ||
| { | ||
| name: "height too large", | ||
| width: 1920, | ||
| height: 9000, | ||
| rate: nil, | ||
| expectError: true, | ||
| errorMsg: "height must be between 200 and 8000", | ||
| }, | ||
| { | ||
| name: "rate too small", | ||
| width: 1920, | ||
| height: 1080, | ||
| rate: intPtr(10), | ||
| expectError: true, | ||
| errorMsg: "rate must be between 24 and 240", | ||
| }, | ||
| { | ||
| name: "rate too large", | ||
| width: 1920, | ||
| height: 1080, | ||
| rate: intPtr(300), | ||
| expectError: true, | ||
| errorMsg: "rate must be between 24 and 240", | ||
| }, | ||
| } | ||
|
|
||
| // Create stub request object | ||
| for _, tc := range testCases { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| req := SetScreenResolutionRequestObject{ | ||
| Width: tc.width, | ||
| Height: tc.height, | ||
| Rate: tc.rate, | ||
| } | ||
|
|
||
| // Just test the validation part | ||
| if req.Width < 200 || req.Width > 8000 { | ||
| assert.True(t, tc.expectError, "Expected validation error for width") | ||
| } | ||
|
|
||
| if req.Height < 200 || req.Height > 8000 { | ||
| assert.True(t, tc.expectError, "Expected validation error for height") | ||
| } | ||
|
|
||
| if req.Rate != nil && (*req.Rate < 24 || *req.Rate > 240) { | ||
| assert.True(t, tc.expectError, "Expected validation error for rate") | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Helper function to create int pointer | ||
| func intPtr(i int) *int { | ||
| return &i | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possible temporary note or outdated comment.