11package api
22
33import (
4+ "bufio"
45 "context"
56 "encoding/json"
67 "fmt"
8+ "net"
79 "os"
810 "os/exec"
11+ "sync/atomic"
912 "time"
1013
14+ "github.com/google/uuid"
1115 "github.com/onkernel/kernel-images/server/lib/logger"
1216 "github.com/onkernel/kernel-images/server/lib/oapi"
1317)
1418
15- // ExecutePlaywrightCode implements the Playwright code execution endpoint
19+ const (
20+ playwrightDaemonSocket = "/tmp/playwright-daemon.sock"
21+ playwrightDaemonScript = "/usr/local/lib/playwright-daemon.js"
22+ playwrightDaemonStartup = 5 * time .Second
23+ )
24+
25+ type playwrightDaemonRequest struct {
26+ ID string `json:"id"`
27+ Code string `json:"code"`
28+ TimeoutMs int `json:"timeout_ms,omitempty"`
29+ }
30+
31+ type playwrightDaemonResponse struct {
32+ ID string `json:"id"`
33+ Success bool `json:"success"`
34+ Result interface {} `json:"result,omitempty"`
35+ Error string `json:"error,omitempty"`
36+ Stack string `json:"stack,omitempty"`
37+ }
38+
39+ func (s * ApiService ) ensurePlaywrightDaemon (ctx context.Context ) error {
40+ log := logger .FromContext (ctx )
41+
42+ if conn , err := net .DialTimeout ("unix" , playwrightDaemonSocket , 100 * time .Millisecond ); err == nil {
43+ conn .Close ()
44+ return nil
45+ }
46+
47+ if ! atomic .CompareAndSwapInt32 (& s .playwrightDaemonStarting , 0 , 1 ) {
48+ deadline := time .Now ().Add (playwrightDaemonStartup )
49+ for time .Now ().Before (deadline ) {
50+ if conn , err := net .DialTimeout ("unix" , playwrightDaemonSocket , 100 * time .Millisecond ); err == nil {
51+ conn .Close ()
52+ return nil
53+ }
54+ time .Sleep (100 * time .Millisecond )
55+ }
56+ return fmt .Errorf ("timeout waiting for daemon to start" )
57+ }
58+ defer atomic .StoreInt32 (& s .playwrightDaemonStarting , 0 )
59+
60+ log .Info ("starting playwright daemon" )
61+
62+ cmd := exec .Command ("node" , playwrightDaemonScript )
63+ cmd .Stdout = os .Stdout
64+ cmd .Stderr = os .Stderr
65+ cmd .Env = os .Environ ()
66+
67+ if err := cmd .Start (); err != nil {
68+ return fmt .Errorf ("failed to start playwright daemon: %w" , err )
69+ }
70+
71+ s .playwrightDaemonCmd = cmd
72+
73+ deadline := time .Now ().Add (playwrightDaemonStartup )
74+ for time .Now ().Before (deadline ) {
75+ if conn , err := net .DialTimeout ("unix" , playwrightDaemonSocket , 100 * time .Millisecond ); err == nil {
76+ conn .Close ()
77+ log .Info ("playwright daemon started successfully" )
78+ return nil
79+ }
80+ time .Sleep (100 * time .Millisecond )
81+ }
82+
83+ cmd .Process .Kill ()
84+ return fmt .Errorf ("playwright daemon failed to start within %v" , playwrightDaemonStartup )
85+ }
86+
87+ func (s * ApiService ) executeViaUnixSocket (ctx context.Context , code string , timeout time.Duration ) (* playwrightDaemonResponse , error ) {
88+ conn , err := net .DialTimeout ("unix" , playwrightDaemonSocket , 2 * time .Second )
89+ if err != nil {
90+ return nil , fmt .Errorf ("failed to connect to daemon: %w" , err )
91+ }
92+ defer conn .Close ()
93+
94+ if err := conn .SetDeadline (time .Now ().Add (timeout + 5 * time .Second )); err != nil {
95+ return nil , fmt .Errorf ("failed to set deadline: %w" , err )
96+ }
97+
98+ reqID := uuid .New ().String ()
99+ req := playwrightDaemonRequest {
100+ ID : reqID ,
101+ Code : code ,
102+ TimeoutMs : int (timeout .Milliseconds ()),
103+ }
104+
105+ reqBytes , err := json .Marshal (req )
106+ if err != nil {
107+ return nil , fmt .Errorf ("failed to marshal request: %w" , err )
108+ }
109+ reqBytes = append (reqBytes , '\n' )
110+
111+ if _ , err := conn .Write (reqBytes ); err != nil {
112+ return nil , fmt .Errorf ("failed to send request: %w" , err )
113+ }
114+
115+ reader := bufio .NewReader (conn )
116+ respLine , err := reader .ReadBytes ('\n' )
117+ if err != nil {
118+ return nil , fmt .Errorf ("failed to read response: %w" , err )
119+ }
120+
121+ var resp playwrightDaemonResponse
122+ if err := json .Unmarshal (respLine , & resp ); err != nil {
123+ return nil , fmt .Errorf ("failed to parse response: %w" , err )
124+ }
125+
126+ if resp .ID != reqID {
127+ return nil , fmt .Errorf ("response ID mismatch: expected %s, got %s" , reqID , resp .ID )
128+ }
129+
130+ return & resp , nil
131+ }
132+
16133func (s * ApiService ) ExecutePlaywrightCode (ctx context.Context , request oapi.ExecutePlaywrightCodeRequestObject ) (oapi.ExecutePlaywrightCodeResponseObject , error ) {
17- // Serialize Playwright execution - only one execution at a time
18134 s .playwrightMu .Lock ()
19135 defer s .playwrightMu .Unlock ()
20136
21137 log := logger .FromContext (ctx )
22138
23- // Validate request
24139 if request .Body == nil || request .Body .Code == "" {
25140 return oapi.ExecutePlaywrightCode400JSONResponse {
26141 BadRequestErrorJSONResponse : oapi.BadRequestErrorJSONResponse {
@@ -29,107 +144,42 @@ func (s *ApiService) ExecutePlaywrightCode(ctx context.Context, request oapi.Exe
29144 }, nil
30145 }
31146
32- // Determine timeout (default to 60 seconds)
33147 timeout := 60 * time .Second
34148 if request .Body .TimeoutSec != nil && * request .Body .TimeoutSec > 0 {
35149 timeout = time .Duration (* request .Body .TimeoutSec ) * time .Second
36150 }
37151
38- // Create a temporary file for the user code
39- tmpFile , err := os .CreateTemp ("" , "playwright-code-*.ts" )
40- if err != nil {
41- log .Error ("failed to create temp file" , "error" , err )
152+ if err := s .ensurePlaywrightDaemon (ctx ); err != nil {
153+ log .Error ("failed to ensure playwright daemon" , "error" , err )
42154 return oapi.ExecutePlaywrightCode500JSONResponse {
43155 InternalErrorJSONResponse : oapi.InternalErrorJSONResponse {
44- Message : fmt .Sprintf ("failed to create temp file : %v" , err ),
156+ Message : fmt .Sprintf ("failed to start playwright daemon : %v" , err ),
45157 },
46158 }, nil
47159 }
48- tmpFilePath := tmpFile .Name ()
49- defer os .Remove (tmpFilePath ) // Clean up the temp file
50-
51- // Write the user code to the temp file
52- if _ , err := tmpFile .WriteString (request .Body .Code ); err != nil {
53- tmpFile .Close ()
54- log .Error ("failed to write code to temp file" , "error" , err )
55- return oapi.ExecutePlaywrightCode500JSONResponse {
56- InternalErrorJSONResponse : oapi.InternalErrorJSONResponse {
57- Message : fmt .Sprintf ("failed to write code to temp file: %v" , err ),
58- },
59- }, nil
60- }
61- tmpFile .Close ()
62-
63- // Create context with timeout
64- execCtx , cancel := context .WithTimeout (ctx , timeout )
65- defer cancel ()
66-
67- // Execute the Playwright code via the executor script
68- cmd := exec .CommandContext (execCtx , "tsx" , "/usr/local/lib/playwright-executor.ts" , tmpFilePath )
69-
70- output , err := cmd .CombinedOutput ()
71160
161+ resp , err := s .executeViaUnixSocket (ctx , request .Body .Code , timeout )
72162 if err != nil {
73- if execCtx .Err () == context .DeadlineExceeded {
74- log .Error ("playwright execution timed out" , "timeout" , timeout )
75- success := false
76- errorMsg := fmt .Sprintf ("execution timed out after %v" , timeout )
77- return oapi.ExecutePlaywrightCode200JSONResponse {
78- Success : success ,
79- Error : & errorMsg ,
80- }, nil
81- }
82-
83163 log .Error ("playwright execution failed" , "error" , err )
84-
85- // Try to parse the error output as JSON
86- var result struct {
87- Success bool `json:"success"`
88- Result interface {} `json:"result,omitempty"`
89- Error string `json:"error,omitempty"`
90- Stack string `json:"stack,omitempty"`
91- }
92- if jsonErr := json .Unmarshal (output , & result ); jsonErr == nil {
93- success := result .Success
94- errorMsg := result .Error
95- stderr := string (output )
96- return oapi.ExecutePlaywrightCode200JSONResponse {
97- Success : success ,
98- Error : & errorMsg ,
99- Stderr : & stderr ,
100- }, nil
101- }
102-
103- // If we can't parse the output, return a generic error
104- success := false
105164 errorMsg := fmt .Sprintf ("execution failed: %v" , err )
106- stderr := string (output )
107165 return oapi.ExecutePlaywrightCode200JSONResponse {
108- Success : success ,
166+ Success : false ,
109167 Error : & errorMsg ,
110- Stderr : & stderr ,
111168 }, nil
112169 }
113170
114- // Parse successful output
115- var result struct {
116- Success bool `json:"success"`
117- Result interface {} `json:"result,omitempty"`
118- }
119- if err := json .Unmarshal (output , & result ); err != nil {
120- log .Error ("failed to parse playwright output" , "error" , err )
121- success := false
122- errorMsg := fmt .Sprintf ("failed to parse output: %v" , err )
123- stdout := string (output )
171+ if ! resp .Success {
172+ errorMsg := resp .Error
173+ stderr := resp .Stack
124174 return oapi.ExecutePlaywrightCode200JSONResponse {
125- Success : success ,
175+ Success : false ,
126176 Error : & errorMsg ,
127- Stdout : & stdout ,
177+ Stderr : & stderr ,
128178 }, nil
129179 }
130180
131181 return oapi.ExecutePlaywrightCode200JSONResponse {
132- Success : result . Success ,
133- Result : & result .Result ,
182+ Success : true ,
183+ Result : & resp .Result ,
134184 }, nil
135185}
0 commit comments