diff --git a/images/README.md b/images/README.md new file mode 100644 index 00000000..0a2462ac --- /dev/null +++ b/images/README.md @@ -0,0 +1,15 @@ +## How to test kernel-images changes locally with docker + +- Make relevant changes to kernel-images example adding a new endpoint at `kernel-images/server/cmd/api/api/computer.go`, example I added `SetCursor()` endpoint. +- Run openApi to generate the boilerplate for the new endpoints with make oapi-generate +- Check changes at `kernel-images/server/lib/oapi/oapi.go` +- `cd kernel-images/images/chromium-headful` +- Build and run the docker image with `./build-docker.sh && ENABLE_WEBRTC=true ./run-docker.sh` +- Open http://localhost:8080/ in your browser +- Now new endpoint should be available for tests example curl command: +```sh +curl -X POST localhost:444/computer/cursor \ + -H "Content-Type: application/json" \ + -d '{"hidden": true}' +``` + diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 4ddc280f..90935726 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -223,7 +223,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap set -eux; \ apt-get update; \ apt-get --no-install-recommends -y install \ - wget ca-certificates python2 supervisor xclip xdotool \ + wget ca-certificates python2 supervisor xclip xdotool unclutter \ pulseaudio dbus-x11 xserver-xorg-video-dummy \ libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 \ x11-xserver-utils \ diff --git a/images/chromium-headful/build-unikernel.sh b/images/chromium-headful/build-unikernel.sh index ab860b43..ea35c745 100755 --- a/images/chromium-headful/build-unikernel.sh +++ b/images/chromium-headful/build-unikernel.sh @@ -25,6 +25,8 @@ docker cp cnt-"$app_name":/ ./.rootfs rm -f initrd || true sudo mkfs.erofs --all-root -d2 -E noinline_data -b 4096 initrd ./.rootfs +echo "Image index/name: $UKC_INDEX/$IMAGE" + # Package the unikernel (and the new initrd) to KraftCloud kraft pkg \ --name $UKC_INDEX/$IMAGE \ diff --git a/server/cmd/api/api/computer.go b/server/cmd/api/api/computer.go index 7f84f960..a2d58df5 100644 --- a/server/cmd/api/api/computer.go +++ b/server/cmd/api/api/computer.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "strconv" + "syscall" "time" "github.com/onkernel/kernel-images/server/lib/logger" @@ -362,6 +363,71 @@ func (s *ApiService) TypeText(ctx context.Context, request oapi.TypeTextRequestO return oapi.TypeText200Response{}, nil } +const ( + + // Unclutter configuration for cursor hiding + // Setting idle to 0 hides the cursor immediately + unclutterIdleSeconds = "0" + + // A very large jitter value (9 million pixels) ensures that all mouse + // movements are treated as "noise", keeping the cursor permanently hidden + // when combined with idle=0 + unclutterJitterPixels = "9000000" +) + +func (s *ApiService) SetCursor(ctx context.Context, request oapi.SetCursorRequestObject) (oapi.SetCursorResponseObject, error) { + log := logger.FromContext(ctx) + + // serialize input operations to avoid overlapping commands + s.inputMu.Lock() + defer s.inputMu.Unlock() + + // Validate request body + if request.Body == nil { + return oapi.SetCursor400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{ + Message: "request body is required"}, + }, nil + } + body := *request.Body + + // Kill any existing unclutter processes first + pkillCmd := exec.CommandContext(ctx, "pkill", "unclutter") + pkillCmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: 0, Gid: 0}, + } + + if err := pkillCmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 1 { + log.Error("failed to kill existing unclutter processes", "err", err) + return oapi.SetCursor500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to kill existing unclutter processes"}, + }, nil + } + } + + if body.Hidden { + display := s.resolveDisplayFromEnv() + unclutterCmd := exec.CommandContext(context.Background(), + "unclutter", + "-idle", unclutterIdleSeconds, + "-jitter", unclutterJitterPixels, + ) + unclutterCmd.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=%s", display)) + unclutterCmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: &syscall.Credential{Uid: 0, Gid: 0}, + } + + if err := unclutterCmd.Start(); err != nil { + log.Error("failed to start unclutter", "err", err) + return oapi.SetCursor500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{ + Message: "failed to start unclutter"}, + }, nil + } + } + + return oapi.SetCursor200JSONResponse{Ok: true}, nil +} + func (s *ApiService) PressKey(ctx context.Context, request oapi.PressKeyRequestObject) (oapi.PressKeyResponseObject, error) { log := logger.FromContext(ctx) diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index e9252d0e..4b08cf14 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -190,7 +190,7 @@ type ExecutePlaywrightRequest struct { // Example: "await page.goto('https://example.com'); return await page.title();" Code string `json:"code"` - // TimeoutSec Maximum execution time in seconds. Default is 30. + // TimeoutSec Maximum execution time in seconds. Default is 60. TimeoutSec *int `json:"timeout_sec,omitempty"` } @@ -488,6 +488,12 @@ type ScrollRequest struct { Y int `json:"y"` } +// SetCursorRequest defines model for SetCursorRequest. +type SetCursorRequest struct { + // Hidden Whether the cursor should be hidden + Hidden bool `json:"hidden"` +} + // SetFilePermissionsRequest defines model for SetFilePermissionsRequest. type SetFilePermissionsRequest struct { // Group New group name or GID. @@ -654,6 +660,9 @@ type UploadExtensionsAndRestartMultipartRequestBody UploadExtensionsAndRestartMu // ClickMouseJSONRequestBody defines body for ClickMouse for application/json ContentType. type ClickMouseJSONRequestBody = ClickMouseRequest +// SetCursorJSONRequestBody defines body for SetCursor for application/json ContentType. +type SetCursorJSONRequestBody = SetCursorRequest + // DragMouseJSONRequestBody defines body for DragMouse for application/json ContentType. type DragMouseJSONRequestBody = DragMouseRequest @@ -809,6 +818,11 @@ type ClientInterface interface { ClickMouse(ctx context.Context, body ClickMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // SetCursorWithBody request with any body + SetCursorWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + SetCursor(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DragMouseWithBody request with any body DragMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1017,6 +1031,30 @@ func (c *Client) ClickMouse(ctx context.Context, body ClickMouseJSONRequestBody, return c.Client.Do(req) } +func (c *Client) SetCursorWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSetCursorRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) SetCursor(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewSetCursorRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) DragMouseWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewDragMouseRequestWithBody(c.Server, contentType, body) if err != nil { @@ -1798,6 +1836,46 @@ func NewClickMouseRequestWithBody(server string, contentType string, body io.Rea return req, nil } +// NewSetCursorRequest calls the generic SetCursor builder with application/json body +func NewSetCursorRequest(server string, body SetCursorJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewSetCursorRequestWithBody(server, "application/json", bodyReader) +} + +// NewSetCursorRequestWithBody generates requests for SetCursor with any type of body +func NewSetCursorRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/computer/cursor") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewDragMouseRequest calls the generic DragMouse builder with application/json body func NewDragMouseRequest(server string, body DragMouseJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -3314,6 +3392,11 @@ type ClientWithResponsesInterface interface { ClickMouseWithResponse(ctx context.Context, body ClickMouseJSONRequestBody, reqEditors ...RequestEditorFn) (*ClickMouseResponse, error) + // SetCursorWithBodyWithResponse request with any body + SetCursorWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) + + SetCursorWithResponse(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) + // DragMouseWithBodyWithResponse request with any body DragMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) @@ -3531,6 +3614,30 @@ func (r ClickMouseResponse) StatusCode() int { return 0 } +type SetCursorResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r SetCursorResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r SetCursorResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type DragMouseResponse struct { Body []byte HTTPResponse *http.Response @@ -4412,6 +4519,23 @@ func (c *ClientWithResponses) ClickMouseWithResponse(ctx context.Context, body C return ParseClickMouseResponse(rsp) } +// SetCursorWithBodyWithResponse request with arbitrary body returning *SetCursorResponse +func (c *ClientWithResponses) SetCursorWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) { + rsp, err := c.SetCursorWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSetCursorResponse(rsp) +} + +func (c *ClientWithResponses) SetCursorWithResponse(ctx context.Context, body SetCursorJSONRequestBody, reqEditors ...RequestEditorFn) (*SetCursorResponse, error) { + rsp, err := c.SetCursor(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseSetCursorResponse(rsp) +} + // DragMouseWithBodyWithResponse request with arbitrary body returning *DragMouseResponse func (c *ClientWithResponses) DragMouseWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DragMouseResponse, error) { rsp, err := c.DragMouseWithBody(ctx, contentType, body, reqEditors...) @@ -4994,6 +5118,46 @@ func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { return response, nil } +// ParseSetCursorResponse parses an HTTP response from a SetCursorWithResponse call +func ParseSetCursorResponse(rsp *http.Response) (*SetCursorResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &SetCursorResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest OkResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseDragMouseResponse parses an HTTP response from a DragMouseWithResponse call func ParseDragMouseResponse(rsp *http.Response) (*DragMouseResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -6370,6 +6534,9 @@ type ServerInterface interface { // Simulate a mouse click action on the host computer // (POST /computer/click_mouse) ClickMouse(w http.ResponseWriter, r *http.Request) + // Hide or show the cursor + // (POST /computer/cursor) + SetCursor(w http.ResponseWriter, r *http.Request) // Drag the mouse along a path // (POST /computer/drag_mouse) DragMouse(w http.ResponseWriter, r *http.Request) @@ -6499,6 +6666,12 @@ func (_ Unimplemented) ClickMouse(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Hide or show the cursor +// (POST /computer/cursor) +func (_ Unimplemented) SetCursor(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Drag the mouse along a path // (POST /computer/drag_mouse) func (_ Unimplemented) DragMouse(w http.ResponseWriter, r *http.Request) { @@ -6760,6 +6933,20 @@ func (siw *ServerInterfaceWrapper) ClickMouse(w http.ResponseWriter, r *http.Req handler.ServeHTTP(w, r) } +// SetCursor operation middleware +func (siw *ServerInterfaceWrapper) SetCursor(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.SetCursor(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // DragMouse operation middleware func (siw *ServerInterfaceWrapper) DragMouse(w http.ResponseWriter, r *http.Request) { @@ -7603,6 +7790,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/click_mouse", wrapper.ClickMouse) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/computer/cursor", wrapper.SetCursor) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/computer/drag_mouse", wrapper.DragMouse) }) @@ -7822,6 +8012,41 @@ func (response ClickMouse500JSONResponse) VisitClickMouseResponse(w http.Respons return json.NewEncoder(w).Encode(response) } +type SetCursorRequestObject struct { + Body *SetCursorJSONRequestBody +} + +type SetCursorResponseObject interface { + VisitSetCursorResponse(w http.ResponseWriter) error +} + +type SetCursor200JSONResponse OkResponse + +func (response SetCursor200JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursor400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response SetCursor400JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursor500JSONResponse struct{ InternalErrorJSONResponse } + +func (response SetCursor500JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type DragMouseRequestObject struct { Body *DragMouseJSONRequestBody } @@ -9393,6 +9618,9 @@ type StrictServerInterface interface { // Simulate a mouse click action on the host computer // (POST /computer/click_mouse) ClickMouse(ctx context.Context, request ClickMouseRequestObject) (ClickMouseResponseObject, error) + // Hide or show the cursor + // (POST /computer/cursor) + SetCursor(ctx context.Context, request SetCursorRequestObject) (SetCursorResponseObject, error) // Drag the mouse along a path // (POST /computer/drag_mouse) DragMouse(ctx context.Context, request DragMouseRequestObject) (DragMouseResponseObject, error) @@ -9622,6 +9850,37 @@ func (sh *strictHandler) ClickMouse(w http.ResponseWriter, r *http.Request) { } } +// SetCursor operation middleware +func (sh *strictHandler) SetCursor(w http.ResponseWriter, r *http.Request) { + var request SetCursorRequestObject + + var body SetCursorJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.SetCursor(ctx, request.(SetCursorRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "SetCursor") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(SetCursorResponseObject); ok { + if err := validResponse.VisitSetCursorResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // DragMouse operation middleware func (sh *strictHandler) DragMouse(w http.ResponseWriter, r *http.Request) { var request DragMouseRequestObject @@ -10654,122 +10913,123 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "9uCwKl9WP7SR/wkAAP//KSSqVIyVAAA=", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 62b818ae..9b23c130 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -332,6 +332,27 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /computer/cursor: + post: + summary: Hide or show the cursor + operationId: setCursor + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SetCursorRequest" + responses: + "200": + description: Cursor visibility updated successfully + content: + application/json: + schema: + $ref: "#/components/schemas/OkResponse" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" /computer/type: post: summary: Type text on the host computer @@ -1088,6 +1109,15 @@ components: region: $ref: "#/components/schemas/ScreenshotRegion" additionalProperties: false + SetCursorRequest: + type: object + required: + - hidden + properties: + hidden: + type: boolean + description: Whether the cursor should be hidden + additionalProperties: false TypeTextRequest: type: object required: