diff --git a/server/cmd/api/api/api.go b/server/cmd/api/api/api.go index b4e892ac..6e5b4440 100644 --- a/server/cmd/api/api/api.go +++ b/server/cmd/api/api/api.go @@ -9,7 +9,9 @@ import ( "sync" "time" + "github.com/kernel/kernel-images/server/lib/cdpmonitor" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" + "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/logger" "github.com/kernel/kernel-images/server/lib/nekoclient" oapi "github.com/kernel/kernel-images/server/lib/oapi" @@ -68,11 +70,26 @@ type ApiService struct { // xvfbResizeMu serializes background Xvfb restarts to prevent races // when multiple CDP fast-path resizes fire in quick succession. xvfbResizeMu sync.Mutex + + // CDP event pipeline and cdpMonitor. + captureSession *events.CaptureSession + cdpMonitor *cdpmonitor.Monitor + monitorMu sync.Mutex + lifecycleCtx context.Context + lifecycleCancel context.CancelFunc } var _ oapi.StrictServerInterface = (*ApiService)(nil) -func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFactory, upstreamMgr *devtoolsproxy.UpstreamManager, stz scaletozero.Controller, nekoAuthClient *nekoclient.AuthClient) (*ApiService, error) { +func New( + recordManager recorder.RecordManager, + factory recorder.FFmpegRecorderFactory, + upstreamMgr *devtoolsproxy.UpstreamManager, + stz scaletozero.Controller, + nekoAuthClient *nekoclient.AuthClient, + captureSession *events.CaptureSession, + displayNum int, +) (*ApiService, error) { switch { case recordManager == nil: return nil, fmt.Errorf("recordManager cannot be nil") @@ -82,11 +99,16 @@ func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFa return nil, fmt.Errorf("upstreamMgr cannot be nil") case nekoAuthClient == nil: return nil, fmt.Errorf("nekoAuthClient cannot be nil") + case captureSession == nil: + return nil, fmt.Errorf("captureSession cannot be nil") } + mon := cdpmonitor.New(upstreamMgr, captureSession.Publish, displayNum) + ctx, cancel := context.WithCancel(context.Background()) + return &ApiService{ - recordManager: recordManager, - factory: factory, + recordManager: recordManager, + factory: factory, defaultRecorderID: "default", watches: make(map[string]*fsWatch), procs: make(map[string]*processHandle), @@ -94,6 +116,10 @@ func New(recordManager recorder.RecordManager, factory recorder.FFmpegRecorderFa stz: stz, nekoAuthClient: nekoAuthClient, policy: &policy.Policy{}, + captureSession: captureSession, + cdpMonitor: mon, + lifecycleCtx: ctx, + lifecycleCancel: cancel, }, nil } @@ -313,5 +339,11 @@ func (s *ApiService) ListRecorders(ctx context.Context, _ oapi.ListRecordersRequ } func (s *ApiService) Shutdown(ctx context.Context) error { + s.monitorMu.Lock() + s.lifecycleCancel() + s.cdpMonitor.Stop() + s.captureSession.Stop() + _ = s.captureSession.Close() + s.monitorMu.Unlock() return s.recordManager.StopAll(ctx) } diff --git a/server/cmd/api/api/api_test.go b/server/cmd/api/api/api_test.go index 3b71de46..cb5e9afc 100644 --- a/server/cmd/api/api/api_test.go +++ b/server/cmd/api/api/api_test.go @@ -12,6 +12,7 @@ import ( "log/slog" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" + "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/nekoclient" oapi "github.com/kernel/kernel-images/server/lib/oapi" "github.com/kernel/kernel-images/server/lib/recorder" @@ -25,7 +26,7 @@ func TestApiService_StartRecording(t *testing.T) { t.Run("success", func(t *testing.T) { mgr := recorder.NewFFmpegManager() - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) resp, err := svc.StartRecording(ctx, oapi.StartRecordingRequestObject{}) @@ -39,7 +40,7 @@ func TestApiService_StartRecording(t *testing.T) { t.Run("already recording", func(t *testing.T) { mgr := recorder.NewFFmpegManager() - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) // First start should succeed @@ -54,7 +55,7 @@ func TestApiService_StartRecording(t *testing.T) { t.Run("custom ids don't collide", func(t *testing.T) { mgr := recorder.NewFFmpegManager() - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) for i := 0; i < 5; i++ { @@ -87,7 +88,7 @@ func TestApiService_StopRecording(t *testing.T) { t.Run("no active recording", func(t *testing.T) { mgr := recorder.NewFFmpegManager() - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) resp, err := svc.StopRecording(ctx, oapi.StopRecordingRequestObject{}) @@ -100,7 +101,7 @@ func TestApiService_StopRecording(t *testing.T) { rec := &mockRecorder{id: "default", isRecordingFlag: true} require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder") - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) resp, err := svc.StopRecording(ctx, oapi.StopRecordingRequestObject{}) require.NoError(t, err) @@ -115,7 +116,7 @@ func TestApiService_StopRecording(t *testing.T) { force := true req := oapi.StopRecordingRequestObject{Body: &oapi.StopRecordingJSONRequestBody{ForceStop: &force}} - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) resp, err := svc.StopRecording(ctx, req) require.NoError(t, err) @@ -129,7 +130,7 @@ func TestApiService_DownloadRecording(t *testing.T) { t.Run("not found", func(t *testing.T) { mgr := recorder.NewFFmpegManager() - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) resp, err := svc.DownloadRecording(ctx, oapi.DownloadRecordingRequestObject{}) require.NoError(t, err) @@ -149,7 +150,7 @@ func TestApiService_DownloadRecording(t *testing.T) { rec := &mockRecorder{id: "default", isRecordingFlag: true, recordingData: randomBytes(minRecordingSizeInBytes - 1)} require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder") - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) // will return a 202 when the recording is too small resp, err := svc.DownloadRecording(ctx, oapi.DownloadRecordingRequestObject{}) @@ -179,7 +180,7 @@ func TestApiService_DownloadRecording(t *testing.T) { rec := &mockRecorder{id: "default", recordingData: data} require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder") - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) resp, err := svc.DownloadRecording(ctx, oapi.DownloadRecordingRequestObject{}) require.NoError(t, err) @@ -199,7 +200,7 @@ func TestApiService_Shutdown(t *testing.T) { rec := &mockRecorder{id: "default", isRecordingFlag: true} require.NoError(t, mgr.RegisterRecorder(ctx, rec), "failed to register recorder") - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) require.NoError(t, svc.Shutdown(ctx)) @@ -303,10 +304,23 @@ func newMockNekoClient(t *testing.T) *nekoclient.AuthClient { return client } +func newCaptureSession(t *testing.T) *events.CaptureSession { + t.Helper() + cs, err := events.NewCaptureSession(events.CaptureSessionConfig{ + LogDir: t.TempDir(), + RingCapacity: 64, + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { cs.Close() }) + return cs +} + func TestApiService_PatchChromiumFlags(t *testing.T) { ctx := context.Background() mgr := recorder.NewFFmpegManager() - svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) // Test with valid flags diff --git a/server/cmd/api/api/capture_session.go b/server/cmd/api/api/capture_session.go new file mode 100644 index 00000000..6ffaa251 --- /dev/null +++ b/server/cmd/api/api/capture_session.go @@ -0,0 +1,146 @@ +package api + +import ( + "context" + "fmt" + "sort" + + "github.com/nrednav/cuid2" + oapi "github.com/kernel/kernel-images/server/lib/oapi" + + "github.com/kernel/kernel-images/server/lib/events" + "github.com/kernel/kernel-images/server/lib/logger" +) + +// StartCaptureSession handles POST /events/capture_session. +// Returns 409 if a session is already active. +func (s *ApiService) StartCaptureSession(ctx context.Context, req oapi.StartCaptureSessionRequestObject) (oapi.StartCaptureSessionResponseObject, error) { + s.monitorMu.Lock() + defer s.monitorMu.Unlock() + + if s.captureSession.ID() != "" { + return oapi.StartCaptureSession409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "a capture session is already active"}}, nil + } + + cfg, err := captureConfigFrom(req.Body) + if err != nil { + return oapi.StartCaptureSession400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + + id := cuid2.Generate() + s.captureSession.Start(id, cfg) + + if err := s.cdpMonitor.Start(s.lifecycleCtx); err != nil { + // Roll back: clear the session so a retry can succeed. + s.captureSession.Stop() + logger.FromContext(ctx).Error("failed to start capture monitor", "err", err) + return oapi.StartCaptureSession500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to start capture"}}, nil + } + + return oapi.StartCaptureSession201JSONResponse(s.buildSessionResponse()), nil +} + +// GetCaptureSession handles GET /events/capture_session. +func (s *ApiService) GetCaptureSession(_ context.Context, _ oapi.GetCaptureSessionRequestObject) (oapi.GetCaptureSessionResponseObject, error) { + s.monitorMu.Lock() + defer s.monitorMu.Unlock() + + if s.captureSession.ID() == "" { + return oapi.GetCaptureSession404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "no active capture session"}}, nil + } + return oapi.GetCaptureSession200JSONResponse(s.buildSessionResponse()), nil +} + +// UpdateCaptureSession handles PATCH /events/capture_session. +func (s *ApiService) UpdateCaptureSession(_ context.Context, req oapi.UpdateCaptureSessionRequestObject) (oapi.UpdateCaptureSessionResponseObject, error) { + s.monitorMu.Lock() + defer s.monitorMu.Unlock() + + if s.captureSession.ID() == "" { + return oapi.UpdateCaptureSession404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "no active capture session"}}, nil + } + + if req.Body != nil && req.Body.Config != nil { + cfg, err := captureConfigFromOAPI(req.Body.Config) + if err != nil { + return oapi.UpdateCaptureSession400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}}, nil + } + s.captureSession.UpdateConfig(cfg) + } + + return oapi.UpdateCaptureSession200JSONResponse(s.buildSessionResponse()), nil +} + +// StopCaptureSession handles DELETE /events/capture_session. +// Stops the capture session and clears it so a new one can be started. +func (s *ApiService) StopCaptureSession(_ context.Context, _ oapi.StopCaptureSessionRequestObject) (oapi.StopCaptureSessionResponseObject, error) { + s.monitorMu.Lock() + defer s.monitorMu.Unlock() + + if s.captureSession.ID() == "" { + return oapi.StopCaptureSession404JSONResponse{NotFoundErrorJSONResponse: oapi.NotFoundErrorJSONResponse{Message: "no active capture session"}}, nil + } + + s.cdpMonitor.Stop() + // Snapshot the final state before clearing the session ID so buildSessionResponse + // can still parse it. Force the status to Stopped because cdpMonitor.Stop may + // tear down asynchronously, leaving IsRunning briefly true. + resp := s.buildSessionResponse() + resp.Status = oapi.CaptureSessionStatusStopped + s.captureSession.Stop() + + return oapi.StopCaptureSession200JSONResponse(resp), nil +} + +// buildSessionResponse constructs the CaptureSession response from current state. +func (s *ApiService) buildSessionResponse() oapi.CaptureSession { + cfg := s.captureSession.Config() + + cats := make([]oapi.CaptureConfigCategories, len(cfg.Categories)) + for i, c := range cfg.Categories { + cats[i] = oapi.CaptureConfigCategories(c) + } + sort.Slice(cats, func(i, j int) bool { return cats[i] < cats[j] }) + + status := oapi.CaptureSessionStatusStopped + if s.cdpMonitor.IsRunning() { + status = oapi.CaptureSessionStatusRunning + } + + return oapi.CaptureSession{ + Id: s.captureSession.ID(), + Status: status, + Config: oapi.CaptureConfig{ + Categories: &cats, + }, + Seq: int64(s.captureSession.Seq()), + CreatedAt: s.captureSession.CreatedAt(), + } +} + +// captureConfigFrom converts the optional StartCaptureSessionRequest body +// into an events.CaptureConfig. +func captureConfigFrom(body *oapi.StartCaptureSessionRequest) (events.CaptureConfig, error) { + if body == nil { + return events.CaptureConfig{}, nil + } + return captureConfigFromOAPI(body.Config) +} + +// captureConfigFromOAPI converts an oapi.CaptureConfig to events.CaptureConfig. +func captureConfigFromOAPI(cfg *oapi.CaptureConfig) (events.CaptureConfig, error) { + if cfg == nil || cfg.Categories == nil { + return events.CaptureConfig{}, nil + } + out := events.CaptureConfig{ + Categories: make([]events.EventCategory, 0, len(*cfg.Categories)), + } + for _, c := range *cfg.Categories { + cat := events.EventCategory(c) + if !events.ValidCategory(cat) { + return events.CaptureConfig{}, fmt.Errorf("unknown category: %q", c) + } + out.Categories = append(out.Categories, cat) + } + return out, nil +} diff --git a/server/cmd/api/api/capture_session_test.go b/server/cmd/api/api/capture_session_test.go new file mode 100644 index 00000000..bcd1f168 --- /dev/null +++ b/server/cmd/api/api/capture_session_test.go @@ -0,0 +1,252 @@ +package api + +import ( + "context" + "testing" + + oapi "github.com/kernel/kernel-images/server/lib/oapi" + "github.com/kernel/kernel-images/server/lib/recorder" + "github.com/kernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCaptureConfigFrom(t *testing.T) { + t.Run("nil body returns defaults", func(t *testing.T) { + cfg, err := captureConfigFrom(nil) + require.NoError(t, err) + assert.Empty(t, cfg.Categories) + }) + + t.Run("valid categories", func(t *testing.T) { + cats := []oapi.CaptureConfigCategories{oapi.Console, oapi.Network} + body := &oapi.StartCaptureSessionRequest{ + Config: &oapi.CaptureConfig{Categories: &cats}, + } + cfg, err := captureConfigFrom(body) + require.NoError(t, err) + assert.Len(t, cfg.Categories, 2) + }) + + t.Run("invalid category returns error", func(t *testing.T) { + cats := []oapi.CaptureConfigCategories{"bogus"} + body := &oapi.StartCaptureSessionRequest{ + Config: &oapi.CaptureConfig{Categories: &cats}, + } + _, err := captureConfigFrom(body) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown category") + }) + + t.Run("nil config returns defaults", func(t *testing.T) { + body := &oapi.StartCaptureSessionRequest{} + cfg, err := captureConfigFrom(body) + require.NoError(t, err) + assert.Empty(t, cfg.Categories) + }) +} + +func TestStartCaptureSession(t *testing.T) { + ctx := context.Background() + + t.Run("success with no body", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + resp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + r201, ok := resp.(oapi.StartCaptureSession201JSONResponse) + require.True(t, ok) + assert.NotEmpty(t, r201.Id) + assert.NotZero(t, r201.CreatedAt) + // Status depends on cdpMonitor.IsRunning(); the stub monitor doesn't + // track state, so we only verify the field is populated. + assert.NotEmpty(t, r201.Status) + }) + + t.Run("success with config", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + cats := []oapi.CaptureConfigCategories{oapi.Console} + resp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{ + Body: &oapi.StartCaptureSessionRequest{ + Config: &oapi.CaptureConfig{Categories: &cats}, + }, + }) + require.NoError(t, err) + r201, ok := resp.(oapi.StartCaptureSession201JSONResponse) + require.True(t, ok) + assert.NotEmpty(t, r201.Id) + }) + + t.Run("invalid category returns 400", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + cats := []oapi.CaptureConfigCategories{"badcat"} + resp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{ + Body: &oapi.StartCaptureSessionRequest{ + Config: &oapi.CaptureConfig{Categories: &cats}, + }, + }) + require.NoError(t, err) + assert.IsType(t, oapi.StartCaptureSession400JSONResponse{}, resp) + }) + + t.Run("duplicate returns 409", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + _, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + + resp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + assert.IsType(t, oapi.StartCaptureSession409JSONResponse{}, resp) + }) +} + +func TestGetCaptureSession(t *testing.T) { + ctx := context.Background() + + t.Run("no session returns 404", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + resp, err := svc.GetCaptureSession(ctx, oapi.GetCaptureSessionRequestObject{}) + require.NoError(t, err) + assert.IsType(t, oapi.GetCaptureSession404JSONResponse{}, resp) + }) + + t.Run("active session returns 200", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + startResp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + started := startResp.(oapi.StartCaptureSession201JSONResponse) + + resp, err := svc.GetCaptureSession(ctx, oapi.GetCaptureSessionRequestObject{}) + require.NoError(t, err) + r200, ok := resp.(oapi.GetCaptureSession200JSONResponse) + require.True(t, ok) + assert.Equal(t, started.Id, r200.Id) + assert.Equal(t, started.CreatedAt, r200.CreatedAt) + }) +} + +func TestUpdateCaptureSession(t *testing.T) { + ctx := context.Background() + + t.Run("no session returns 404", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + resp, err := svc.UpdateCaptureSession(ctx, oapi.UpdateCaptureSessionRequestObject{ + Body: &oapi.UpdateCaptureSessionRequest{}, + }) + require.NoError(t, err) + assert.IsType(t, oapi.UpdateCaptureSession404JSONResponse{}, resp) + }) + + t.Run("update config", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + _, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + + cats := []oapi.CaptureConfigCategories{oapi.Console} + resp, err := svc.UpdateCaptureSession(ctx, oapi.UpdateCaptureSessionRequestObject{ + Body: &oapi.UpdateCaptureSessionRequest{ + Config: &oapi.CaptureConfig{Categories: &cats}, + }, + }) + require.NoError(t, err) + r200, ok := resp.(oapi.UpdateCaptureSession200JSONResponse) + require.True(t, ok) + require.NotNil(t, r200.Config.Categories) + assert.Len(t, *r200.Config.Categories, 1) + assert.Equal(t, oapi.Console, (*r200.Config.Categories)[0]) + }) + + t.Run("empty body is no-op", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + startResp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + started := startResp.(oapi.StartCaptureSession201JSONResponse) + + resp, err := svc.UpdateCaptureSession(ctx, oapi.UpdateCaptureSessionRequestObject{ + Body: &oapi.UpdateCaptureSessionRequest{}, + }) + require.NoError(t, err) + r200, ok := resp.(oapi.UpdateCaptureSession200JSONResponse) + require.True(t, ok) + assert.Equal(t, started.Id, r200.Id) + }) + + t.Run("invalid category returns 400", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + _, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + + cats := []oapi.CaptureConfigCategories{"invalid"} + resp, err := svc.UpdateCaptureSession(ctx, oapi.UpdateCaptureSessionRequestObject{ + Body: &oapi.UpdateCaptureSessionRequest{ + Config: &oapi.CaptureConfig{Categories: &cats}, + }, + }) + require.NoError(t, err) + assert.IsType(t, oapi.UpdateCaptureSession400JSONResponse{}, resp) + }) +} + +func TestStopCaptureSession(t *testing.T) { + ctx := context.Background() + + t.Run("no session returns 404", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + resp, err := svc.StopCaptureSession(ctx, oapi.StopCaptureSessionRequestObject{}) + require.NoError(t, err) + assert.IsType(t, oapi.StopCaptureSession404JSONResponse{}, resp) + }) + + t.Run("stop active session", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + startResp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + started := startResp.(oapi.StartCaptureSession201JSONResponse) + + resp, err := svc.StopCaptureSession(ctx, oapi.StopCaptureSessionRequestObject{}) + require.NoError(t, err) + r200, ok := resp.(oapi.StopCaptureSession200JSONResponse) + require.True(t, ok) + assert.Equal(t, started.Id, r200.Id) + assert.Equal(t, oapi.CaptureSessionStatusStopped, r200.Status) + }) + + t.Run("start succeeds after stop", func(t *testing.T) { + svc := newTestService(t, newMockRecordManager()) + startResp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + started := startResp.(oapi.StartCaptureSession201JSONResponse) + + _, err = svc.StopCaptureSession(ctx, oapi.StopCaptureSessionRequestObject{}) + require.NoError(t, err) + + resp, err := svc.StartCaptureSession(ctx, oapi.StartCaptureSessionRequestObject{}) + require.NoError(t, err) + r201, ok := resp.(oapi.StartCaptureSession201JSONResponse) + require.True(t, ok) + assert.NotEqual(t, started.Id, r201.Id) + }) +} + +// newMockRecordManager returns a minimal record manager for tests that don't +// exercise recording. +func newMockRecordManager() *mockRecordManager { + return &mockRecordManager{} +} + +type mockRecordManager struct{} + +func (m *mockRecordManager) RegisterRecorder(_ context.Context, _ recorder.Recorder) error { return nil } +func (m *mockRecordManager) DeregisterRecorder(_ context.Context, _ recorder.Recorder) error { + return nil +} +func (m *mockRecordManager) GetRecorder(_ string) (recorder.Recorder, bool) { return nil, false } +func (m *mockRecordManager) ListActiveRecorders(_ context.Context) []recorder.Recorder { return nil } +func (m *mockRecordManager) StopAll(_ context.Context) error { return nil } + +// newTestService builds an ApiService with minimal dependencies for capture session tests. +func newTestService(t *testing.T, mgr recorder.RecordManager) *ApiService { + t.Helper() + svc, err := New(mgr, newMockFactory(), newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) + require.NoError(t, err) + return svc +} diff --git a/server/cmd/api/api/display_test.go b/server/cmd/api/api/display_test.go index ed28f501..905a8d72 100644 --- a/server/cmd/api/api/display_test.go +++ b/server/cmd/api/api/display_test.go @@ -34,7 +34,7 @@ func testFFmpegFactory(t *testing.T, tempDir string) recorder.FFmpegRecorderFact func newTestServiceWithFactory(t *testing.T, mgr recorder.RecordManager, factory recorder.FFmpegRecorderFactory) *ApiService { t.Helper() - svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t)) + svc, err := New(mgr, factory, newTestUpstreamManager(), scaletozero.NewNoopController(), newMockNekoClient(t), newCaptureSession(t), 0) require.NoError(t, err) return svc } diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index ac451639..d4b47ac1 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -24,6 +24,7 @@ import ( "github.com/kernel/kernel-images/server/cmd/config" "github.com/kernel/kernel-images/server/lib/chromedriverproxy" "github.com/kernel/kernel-images/server/lib/devtoolsproxy" + "github.com/kernel/kernel-images/server/lib/events" "github.com/kernel/kernel-images/server/lib/logger" "github.com/kernel/kernel-images/server/lib/nekoclient" oapi "github.com/kernel/kernel-images/server/lib/oapi" @@ -90,12 +91,24 @@ func main() { os.Exit(1) } + // Construct events pipeline + captureSession, err := events.NewCaptureSession(events.CaptureSessionConfig{ + LogDir: "/var/log/kernel", + RingCapacity: 1024, + }) + if err != nil { + slogger.Error("failed to create capture session", "err", err) + os.Exit(1) + } + apiService, err := api.New( recorder.NewFFmpegManager(), recorder.NewFFmpegRecorderFactory(config.PathToFFmpeg, defaultParams, stz), upstreamMgr, stz, nekoAuthClient, + captureSession, + config.DisplayNum, ) if err != nil { slogger.Error("failed to create api service", "err", err) diff --git a/server/lib/cdpmonitor/monitor.go b/server/lib/cdpmonitor/monitor.go new file mode 100644 index 00000000..e7cf747f --- /dev/null +++ b/server/lib/cdpmonitor/monitor.go @@ -0,0 +1,41 @@ +package cdpmonitor + +import ( + "context" + "sync/atomic" + + "github.com/kernel/kernel-images/server/lib/events" +) + +// UpstreamProvider abstracts *devtoolsproxy.UpstreamManager for testability. +type UpstreamProvider interface { + Current() string + Subscribe() (<-chan string, func()) +} + +// PublishFunc publishes an Event to the pipeline. +type PublishFunc func(ev events.Event) + +// Monitor manages a CDP WebSocket connection with auto-attach session fan-out. +// Single-use per capture session: call Start to begin, Stop to tear down. +type Monitor struct { + running atomic.Bool +} + +// New creates a Monitor. displayNum is the X display for ffmpeg screenshots. +func New(_ UpstreamProvider, _ PublishFunc, _ int) *Monitor { + return &Monitor{} +} + +// IsRunning reports whether the monitor is actively capturing. +func (m *Monitor) IsRunning() bool { + return m.running.Load() +} + +// Start begins CDP capture. Restarts if already running. +func (m *Monitor) Start(_ context.Context) error { + return nil +} + +// Stop tears down the monitor. Safe to call multiple times. +func (m *Monitor) Stop() {} diff --git a/server/lib/events/capturesession.go b/server/lib/events/capturesession.go index a430980a..a761d570 100644 --- a/server/lib/events/capturesession.go +++ b/server/lib/events/capturesession.go @@ -1,38 +1,103 @@ package events import ( + "fmt" "log/slog" "sync" "time" ) -// CaptureSession is a single-use write path that wraps events in envelopes and -// fans them out to a FileWriter (durable) and RingBuffer (in-memory). Publish -// concurrently; Close flushes the FileWriter. +// CaptureConfig holds caller-supplied capture preferences. All fields are +// optional; zero values mean "use server defaults" (all categories). +type CaptureConfig struct { + // Categories limits which event categories are captured + // nil represents all categories. + Categories []EventCategory +} + +// CaptureSession wraps events in envelopes and fans them out to a fileWriter +// Reusable: call Start with a new ID to begin a new session; call Stop to end +// the current session without closing the underlying writers. Close tears down +// file descriptors and should only be called during server shutdown. type CaptureSession struct { mu sync.Mutex - ring *RingBuffer - files *FileWriter + ring *ringBuffer + files *fileWriter seq uint64 captureSessionID string + categories map[EventCategory]struct{} + createdAt time.Time +} + +// CaptureSessionConfig holds the parameters for creating a CaptureSession. +type CaptureSessionConfig struct { + LogDir string + // RingCapacity is the number of envelopes the in-memory ring buffer holds. + RingCapacity int } -func NewCaptureSession(captureSessionID string, ring *RingBuffer, files *FileWriter) *CaptureSession { - return &CaptureSession{ring: ring, files: files, captureSessionID: captureSessionID} +func NewCaptureSession(cfg CaptureSessionConfig) (*CaptureSession, error) { + rb, err := newRingBuffer(cfg.RingCapacity) + if err != nil { + return nil, fmt.Errorf("capture session: %w", err) + } + fw, err := newFileWriter(cfg.LogDir) + if err != nil { + return nil, fmt.Errorf("capture session: %w", err) + } + cats := make(map[EventCategory]struct{}, len(allCategories)) + for _, c := range allCategories { + cats[c] = struct{}{} + } + return &CaptureSession{ + ring: rb, + files: fw, + categories: cats, + }, nil +} + +// Start sets the capture session ID and applies the given config. It resets +// the sequence counter so each session starts at seq 1. +// The fileWriter is intentionally not rotated: events from different sessions +// are interleaved in the same per-category JSONL files and distinguished by +// their envelope's capture_session_id. +func (s *CaptureSession) Start(captureSessionID string, cfg CaptureConfig) { + s.mu.Lock() + defer s.mu.Unlock() + s.captureSessionID = captureSessionID + s.seq = 0 + s.createdAt = time.Now() + s.ring.reset() + cats := cfg.Categories + if len(cats) == 0 { + cats = allCategories + } + s.categories = make(map[EventCategory]struct{}, len(cats)) + for _, c := range cats { + s.categories[c] = struct{}{} + } } // Publish wraps ev in an Envelope, truncates if needed, then writes to -// FileWriter (durable) before RingBuffer (in-memory fan-out). +// fileWriter (durable) before RingBuffer (in-memory fan-out). func (s *CaptureSession) Publish(ev Event) { s.mu.Lock() defer s.mu.Unlock() + // No active session, drop silently. This can happen when events + // arrive between Stop() and producers noticing, or before Start(). + if s.captureSessionID == "" { + return + } + + // Drop events whose category is outside the configured set. + if _, ok := s.categories[ev.Category]; !ok { + return + } + if ev.Ts == 0 { ev.Ts = time.Now().UnixMicro() } - if ev.DetailLevel == "" { - ev.DetailLevel = DetailStandard - } s.seq++ env := Envelope{ @@ -44,15 +109,76 @@ func (s *CaptureSession) Publish(ev Event) { if data == nil { slog.Error("capture_session: marshal failed, skipping file write", "seq", env.Seq, "category", env.Event.Category) - } else if err := s.files.Write(env, data); err != nil { - slog.Error("capture_session: file write failed", "seq", env.Seq, "category", env.Event.Category, "err", err) + } else { + filename := string(env.Event.Category) + ".log" + if err := s.files.Write(filename, data); err != nil { + slog.Error("capture_session: file write failed", "seq", env.Seq, "category", env.Event.Category, "err", err) + } } - s.ring.Publish(env) + s.ring.publish(env) } // NewReader returns a Reader positioned at the start of the ring buffer. func (s *CaptureSession) NewReader(afterSeq uint64) *Reader { - return s.ring.NewReader(afterSeq) + return s.ring.newReader(afterSeq) +} + +// ID returns the current capture session ID, or "" if no session is active. +func (s *CaptureSession) ID() string { + s.mu.Lock() + defer s.mu.Unlock() + return s.captureSessionID +} + +// Seq returns the current sequence number (last published). +func (s *CaptureSession) Seq() uint64 { + s.mu.Lock() + defer s.mu.Unlock() + return s.seq +} + +// Config returns the current capture configuration. +func (s *CaptureSession) Config() CaptureConfig { + s.mu.Lock() + defer s.mu.Unlock() + cats := make([]EventCategory, 0, len(s.categories)) + for c := range s.categories { + cats = append(cats, c) + } + return CaptureConfig{ + Categories: cats, + } +} + +// CreatedAt returns when the current session was started. +func (s *CaptureSession) CreatedAt() time.Time { + s.mu.Lock() + defer s.mu.Unlock() + return s.createdAt +} + +// UpdateConfig applies a new CaptureConfig to the running session without +// resetting the sequence counter or ring buffer. +func (s *CaptureSession) UpdateConfig(cfg CaptureConfig) { + s.mu.Lock() + defer s.mu.Unlock() + cats := cfg.Categories + if len(cats) == 0 { + cats = allCategories + } + s.categories = make(map[EventCategory]struct{}, len(cats)) + for _, c := range cats { + s.categories[c] = struct{}{} + } +} + +// Stop ends the current session by clearing the session ID. The ring buffer +// is intentionally left intact so existing readers can finish draining. +// A new session can be started by calling Start again. +func (s *CaptureSession) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + s.captureSessionID = "" } // Close flushes and releases all open file descriptors. diff --git a/server/lib/events/event.go b/server/lib/events/event.go index 4db821d4..1153168f 100644 --- a/server/lib/events/event.go +++ b/server/lib/events/event.go @@ -21,6 +21,27 @@ const ( CategorySystem EventCategory = "system" ) +// allCategories is the canonical list of all known event categories. +// Package-internal; treat as read-only. +var allCategories = []EventCategory{ + CategoryConsole, CategoryNetwork, CategoryPage, CategoryInteraction, + CategoryLiveview, CategoryCaptcha, CategorySystem, +} + +var validCategories = func() map[EventCategory]struct{} { + m := make(map[EventCategory]struct{}, len(allCategories)) + for _, c := range allCategories { + m[c] = struct{}{} + } + return m +}() + +// ValidCategory reports whether c is a known EventCategory. +func ValidCategory(c EventCategory) bool { + _, ok := validCategories[c] + return ok +} + type SourceKind string const ( @@ -38,26 +59,15 @@ type Source struct { Metadata map[string]string `json:"metadata,omitempty"` } -type DetailLevel string - -const ( - DetailMinimal DetailLevel = "minimal" - DetailStandard DetailLevel = "standard" - DetailVerbose DetailLevel = "verbose" - DetailRaw DetailLevel = "raw" -) - // Event is the portable event schema. It contains only producer-emitted content; // pipeline metadata (seq, capture session) lives on the Envelope. type Event struct { - Ts int64 `json:"ts"` // Unix microseconds (µs since epoch) - Type string `json:"type"` - Category EventCategory `json:"category"` - Source Source `json:"source"` - DetailLevel DetailLevel `json:"detail_level"` - URL string `json:"url,omitempty"` - Data json.RawMessage `json:"data,omitempty"` - Truncated bool `json:"truncated,omitempty"` + Ts int64 `json:"ts"` // Unix microseconds (µs since epoch) + Type string `json:"type"` + Category EventCategory `json:"category"` + Source Source `json:"source"` + Data json.RawMessage `json:"data,omitempty"` + Truncated bool `json:"truncated,omitempty"` } // Envelope wraps an Event with pipeline-assigned metadata. @@ -69,7 +79,7 @@ type Envelope struct { // truncateIfNeeded marshals env and returns the (possibly truncated) envelope. // If the envelope still exceeds maxS2RecordBytes after nulling data (e.g. huge -// url or source.metadata), it is returned as-is — callers must handle nil data. +// source.metadata), it is returned as-is, callers must handle nil data. func truncateIfNeeded(env Envelope) (Envelope, []byte) { data, err := json.Marshal(env) if err != nil { diff --git a/server/lib/events/events_test.go b/server/lib/events/events_test.go index 9325c6ea..5827c9bb 100644 --- a/server/lib/events/events_test.go +++ b/server/lib/events/events_test.go @@ -39,9 +39,7 @@ func TestEventSerialization(t *testing.T) { "parent_frame_id": "parent-frame-1", }, }, - DetailLevel: DetailStandard, - URL: "https://example.com", - Data: json.RawMessage(`{"message":"hello"}`), + Data: json.RawMessage(`{"message":"hello"}`), } b, err := json.Marshal(ev) @@ -52,8 +50,6 @@ func TestEventSerialization(t *testing.T) { assert.Equal(t, "console.log", decoded["type"]) assert.Equal(t, "console", decoded["category"]) - assert.Equal(t, "standard", decoded["detail_level"]) - assert.Equal(t, "https://example.com", decoded["url"]) src, ok := decoded["source"].(map[string]any) require.True(t, ok) @@ -121,7 +117,6 @@ func TestEventOmitEmpty(t *testing.T) { s := string(b) assert.NotContains(t, s, `"event"`) - assert.Contains(t, s, `"detail_level"`) } func mkEnv(seq uint64, ev Event) Envelope { @@ -132,10 +127,17 @@ func cdpEvent(typ string, cat EventCategory) Event { return Event{Type: typ, Category: cat, Source: Source{Kind: KindCDP}} } +func newTestRingBuffer(t *testing.T, capacity int) *ringBuffer { + t.Helper() + rb, err := newRingBuffer(capacity) + require.NoError(t, err) + return rb +} + // TestRingBuffer: publish 3 envelopes; reader reads all 3 in order func TestRingBuffer(t *testing.T) { - rb := NewRingBuffer(10) - reader := rb.NewReader(0) + rb := newTestRingBuffer(t,10) + reader := rb.newReader(0) envelopes := []Envelope{ mkEnv(1, cdpEvent("console.log", CategoryConsole)), @@ -144,7 +146,7 @@ func TestRingBuffer(t *testing.T) { } for _, env := range envelopes { - rb.Publish(env) + rb.publish(env) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) @@ -159,13 +161,13 @@ func TestRingBuffer(t *testing.T) { // TestRingBufferOverflowNoBlock: writer never blocks even with no readers func TestRingBufferOverflowNoBlock(t *testing.T) { - rb := NewRingBuffer(2) + rb := newTestRingBuffer(t,2) done := make(chan struct{}) go func() { - rb.Publish(mkEnv(1, cdpEvent("console.log", CategoryConsole))) - rb.Publish(mkEnv(2, cdpEvent("console.log", CategoryConsole))) - rb.Publish(mkEnv(3, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(1, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(2, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(3, cdpEvent("console.log", CategoryConsole))) close(done) }() @@ -175,7 +177,7 @@ func TestRingBufferOverflowNoBlock(t *testing.T) { t.Fatal("Publish blocked with no readers") } - reader := rb.NewReader(0) + reader := rb.newReader(0) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() @@ -186,12 +188,12 @@ func TestRingBufferOverflowNoBlock(t *testing.T) { } func TestRingBufferOverflowExistingReader(t *testing.T) { - rb := NewRingBuffer(2) - reader := rb.NewReader(0) + rb := newTestRingBuffer(t,2) + reader := rb.newReader(0) - rb.Publish(mkEnv(1, cdpEvent("console.log", CategoryConsole))) - rb.Publish(mkEnv(2, cdpEvent("console.log", CategoryConsole))) - rb.Publish(mkEnv(3, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(1, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(2, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(3, cdpEvent("console.log", CategoryConsole))) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() @@ -211,22 +213,22 @@ func TestRingBufferOverflowExistingReader(t *testing.T) { } func TestNewReaderResume(t *testing.T) { - rb := NewRingBuffer(10) + rb := newTestRingBuffer(t,10) for i := uint64(1); i <= 5; i++ { - rb.Publish(mkEnv(i, cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(i, cdpEvent("console.log", CategoryConsole))) } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() t.Run("resume_mid_stream", func(t *testing.T) { - reader := rb.NewReader(3) + reader := rb.newReader(3) env := readEnvelope(t, reader, ctx) assert.Equal(t, uint64(4), env.Seq) }) t.Run("resume_at_latest", func(t *testing.T) { - reader := rb.NewReader(5) + reader := rb.newReader(5) // Nothing to read — should block until ctx cancels shortCtx, cancel := context.WithTimeout(ctx, 10*time.Millisecond) defer cancel() @@ -235,12 +237,12 @@ func TestNewReaderResume(t *testing.T) { }) t.Run("resume_before_oldest_triggers_drop", func(t *testing.T) { - small := NewRingBuffer(3) + small := newTestRingBuffer(t, 3) for i := uint64(1); i <= 5; i++ { - small.Publish(mkEnv(i, cdpEvent("console.log", CategoryConsole))) + small.publish(mkEnv(i, cdpEvent("console.log", CategoryConsole))) } // oldest in ring is seq 3, requesting resume after seq 1 - reader := small.NewReader(1) + reader := small.newReader(1) res, err := reader.Read(ctx) require.NoError(t, err) assert.Nil(t, res.Envelope) @@ -253,12 +255,12 @@ func TestNewReaderResume(t *testing.T) { func TestConcurrentPublishRead(t *testing.T) { const numEvents = 20 - rb := NewRingBuffer(32) + rb := newTestRingBuffer(t,32) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - reader := rb.NewReader(0) + reader := rb.newReader(0) var wg sync.WaitGroup @@ -277,7 +279,7 @@ func TestConcurrentPublishRead(t *testing.T) { go func() { defer wg.Done() for i := 1; i <= numEvents; i++ { - rb.Publish(mkEnv(uint64(i), cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(uint64(i), cdpEvent("console.log", CategoryConsole))) } }() @@ -285,18 +287,18 @@ func TestConcurrentPublishRead(t *testing.T) { } func TestConcurrentReaders(t *testing.T) { - rb := NewRingBuffer(20) + rb := newTestRingBuffer(t,20) numReaders := 3 numEvents := 5 readers := make([]*Reader, numReaders) for i := range readers { - readers[i] = rb.NewReader(0) + readers[i] = rb.newReader(0) } for i := 0; i < numEvents; i++ { - rb.Publish(mkEnv(uint64(i+1), cdpEvent("console.log", CategoryConsole))) + rb.publish(mkEnv(uint64(i+1), cdpEvent("console.log", CategoryConsole))) } var wg sync.WaitGroup @@ -332,7 +334,8 @@ func TestConcurrentReaders(t *testing.T) { func TestFileWriter(t *testing.T) { t.Run("category_routing", func(t *testing.T) { dir := t.TempDir() - fw := NewFileWriter(dir) + fw, err := newFileWriter(dir) + require.NoError(t, err) defer fw.Close() envsToFile := []struct { @@ -352,7 +355,7 @@ func TestFileWriter(t *testing.T) { for _, e := range envsToFile { data, err := json.Marshal(e.env) require.NoError(t, err) - require.NoError(t, fw.Write(e.env, data)) + require.NoError(t, fw.Write(e.file, data)) } for _, e := range envsToFile { @@ -373,21 +376,21 @@ func TestFileWriter(t *testing.T) { } }) - t.Run("empty_category_rejected", func(t *testing.T) { + t.Run("empty_filename_rejected", func(t *testing.T) { dir := t.TempDir() - fw := NewFileWriter(dir) + fw, err := newFileWriter(dir) + require.NoError(t, err) defer fw.Close() - env := Envelope{Seq: 1, Event: Event{Type: "mystery", Category: "", Source: Source{Kind: KindCDP}, Ts: 1}} - data, _ := json.Marshal(env) - err := fw.Write(env, data) + err = fw.Write("", []byte(`{"seq":1}`)) require.Error(t, err) - assert.Contains(t, err.Error(), "empty category") + assert.Contains(t, err.Error(), "empty filename") }) t.Run("concurrent_writes", func(t *testing.T) { dir := t.TempDir() - fw := NewFileWriter(dir) + fw, err := newFileWriter(dir) + require.NoError(t, err) defer fw.Close() const goroutines = 10 @@ -405,7 +408,7 @@ func TestFileWriter(t *testing.T) { } envData, err := json.Marshal(env) require.NoError(t, err) - require.NoError(t, fw.Write(env, envData)) + require.NoError(t, fw.Write("console.log", envData)) } }(i) } @@ -423,7 +426,8 @@ func TestFileWriter(t *testing.T) { t.Run("lazy_open", func(t *testing.T) { dir := t.TempDir() - fw := NewFileWriter(dir) + fw, err := newFileWriter(dir) + require.NoError(t, err) defer fw.Close() entries, err := os.ReadDir(dir) @@ -433,7 +437,7 @@ func TestFileWriter(t *testing.T) { env := Envelope{Seq: 1, Event: Event{Type: "console.log", Category: CategoryConsole, Source: Source{Kind: KindCDP}, Ts: 1}} envData, err := json.Marshal(env) require.NoError(t, err) - require.NoError(t, fw.Write(env, envData)) + require.NoError(t, fw.Write("console.log", envData)) entries, err = os.ReadDir(dir) require.NoError(t, err) @@ -443,12 +447,12 @@ func TestFileWriter(t *testing.T) { } func TestCaptureSession(t *testing.T) { - newSession := func(t *testing.T) (*CaptureSession, string) { + newCaptureSession := func(t *testing.T) (*CaptureSession, string) { t.Helper() dir := t.TempDir() - rb := NewRingBuffer(100) - fw := NewFileWriter(dir) - p := NewCaptureSession("", rb, fw) + p, err := NewCaptureSession(CaptureSessionConfig{LogDir: dir, RingCapacity: 100}) + require.NoError(t, err) + p.Start("test-session", CaptureConfig{}) t.Cleanup(func() { p.Close() }) return p, dir } @@ -458,9 +462,9 @@ func TestCaptureSession(t *testing.T) { const eventsEach = 50 const total = goroutines * eventsEach - rb := NewRingBuffer(total) - fw := NewFileWriter(t.TempDir()) - p := NewCaptureSession("", rb, fw) + p, err := NewCaptureSession(CaptureSessionConfig{LogDir: t.TempDir(), RingCapacity: total}) + require.NoError(t, err) + p.Start("test-concurrent", CaptureConfig{}) t.Cleanup(func() { p.Close() }) reader := p.NewReader(0) @@ -486,7 +490,7 @@ func TestCaptureSession(t *testing.T) { }) t.Run("publish_increments_seq", func(t *testing.T) { - p, _ := newSession(t) + p, _ := newCaptureSession(t) reader := p.NewReader(0) for i := 0; i < 3; i++ { @@ -503,7 +507,7 @@ func TestCaptureSession(t *testing.T) { }) t.Run("publish_sets_ts", func(t *testing.T) { - p, _ := newSession(t) + p, _ := newCaptureSession(t) reader := p.NewReader(0) before := time.Now().UnixMicro() @@ -519,7 +523,7 @@ func TestCaptureSession(t *testing.T) { }) t.Run("publish_writes_file", func(t *testing.T) { - p, dir := newSession(t) + p, dir := newCaptureSession(t) p.Publish(Event{Type: "console.log", Category: CategoryConsole, Source: Source{Kind: KindCDP}, Ts: 1}) @@ -533,7 +537,7 @@ func TestCaptureSession(t *testing.T) { }) t.Run("publish_writes_ring", func(t *testing.T) { - p, _ := newSession(t) + p, _ := newCaptureSession(t) reader := p.NewReader(0) p.Publish(Event{Type: "page.navigation", Category: CategoryPage, Source: Source{Kind: KindCDP}, Ts: 1}) @@ -546,10 +550,9 @@ func TestCaptureSession(t *testing.T) { assert.Equal(t, CategoryPage, env.Event.Category) }) - t.Run("constructor_sets_capture_session_id", func(t *testing.T) { - dir := t.TempDir() - p := NewCaptureSession("test-uuid", NewRingBuffer(100), NewFileWriter(dir)) - t.Cleanup(func() { p.Close() }) + t.Run("start_sets_capture_session_id", func(t *testing.T) { + p, _ := newCaptureSession(t) + p.Start("test-uuid", CaptureConfig{}) reader := p.NewReader(0) p.Publish(Event{Type: "page.navigation", Category: CategoryPage, Source: Source{Kind: KindCDP}, Ts: 1}) @@ -562,7 +565,7 @@ func TestCaptureSession(t *testing.T) { }) t.Run("truncation_applied", func(t *testing.T) { - p, dir := newSession(t) + p, dir := newCaptureSession(t) reader := p.NewReader(0) largeData := strings.Repeat("x", 1_100_000) @@ -595,20 +598,44 @@ func TestCaptureSession(t *testing.T) { assert.Contains(t, lines[0], `"truncated":true`) }) - t.Run("defaults_detail_level", func(t *testing.T) { - p, _ := newSession(t) - reader := p.NewReader(0) +} - p.Publish(Event{Type: "console.log", Category: CategoryConsole, Source: Source{Kind: KindCDP}, Ts: 1}) +func TestRingBufferResetWithActiveReader(t *testing.T) { + rb := newTestRingBuffer(t,10) + reader := rb.newReader(0) - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() + // Publish some events so the reader advances. + for i := uint64(1); i <= 5; i++ { + rb.publish(mkEnv(i, cdpEvent("console.log", CategoryConsole))) + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + for i := 0; i < 5; i++ { + readEnvelope(t, reader, ctx) + } + // reader.nextSeq is now 6. - env := readEnvelope(t, reader, ctx) - assert.Equal(t, DetailStandard, env.Event.DetailLevel) + // Reset — reader should wake up and block until new publishes arrive. + rb.reset() - p.Publish(Event{Type: "console.log", Category: CategoryConsole, Source: Source{Kind: KindCDP}, Ts: 1, DetailLevel: DetailVerbose}) - env2 := readEnvelope(t, reader, ctx) - assert.Equal(t, DetailVerbose, env2.Event.DetailLevel) - }) + shortCtx, shortCancel := context.WithTimeout(ctx, 50*time.Millisecond) + defer shortCancel() + _, err := reader.Read(shortCtx) + assert.ErrorIs(t, err, context.DeadlineExceeded, "reader should block after reset") + + // Publish new events; reader should resume from seq 1. + rb.publish(mkEnv(1, cdpEvent("page.navigation", CategoryPage))) + env := readEnvelope(t, reader, ctx) + assert.Equal(t, uint64(1), env.Seq) + assert.Equal(t, "page.navigation", env.Event.Type) } + +func TestNewRingBufferRejectsNonPositiveCapacity(t *testing.T) { + for _, cap := range []int{0, -1} { + rb, err := newRingBuffer(cap) + assert.Nil(t, rb) + require.Error(t, err) + assert.Contains(t, err.Error(), "capacity must be > 0") + } +} + diff --git a/server/lib/events/filewriter.go b/server/lib/events/filewriter.go index 6ce5ff5f..d57002f1 100644 --- a/server/lib/events/filewriter.go +++ b/server/lib/events/filewriter.go @@ -7,50 +7,56 @@ import ( "sync" ) -// FileWriter is a per-category JSONL appender. It opens each log file lazily on -// first write (O_APPEND|O_CREATE|O_WRONLY) and serialises all concurrent writes -// with a single mutex -type FileWriter struct { +// fileWriter is a JSONL appender keyed by filename. It opens each file lazily +// on first write (O_APPEND|O_CREATE|O_WRONLY) and serialises all concurrent +// writes with a single mutex. +type fileWriter struct { mu sync.Mutex - files map[EventCategory]*os.File + files map[string]*os.File dir string } -// NewFileWriter returns a FileWriter that writes to dir -func NewFileWriter(dir string) *FileWriter { - return &FileWriter{dir: dir, files: make(map[EventCategory]*os.File)} +// newFileWriter returns a fileWriter that writes to dir, creating it if needed. +func newFileWriter(dir string) (*fileWriter, error) { + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("filewriter: create dir %s: %w", dir, err) + } + return &fileWriter{dir: dir, files: make(map[string]*os.File)}, nil } -// Write appends data as a single JSONL line to the per-category log file. -func (fw *FileWriter) Write(env Envelope, data []byte) error { - cat := env.Event.Category - if cat == "" { - return fmt.Errorf("filewriter: event %q has empty category", env.Event.Type) +// Write appends data as a single JSONL line to the named file under the +// writer's directory. +func (fw *fileWriter) Write(filename string, data []byte) error { + if filename == "" { + return fmt.Errorf("filewriter: empty filename") } fw.mu.Lock() defer fw.mu.Unlock() - f, ok := fw.files[cat] + f, ok := fw.files[filename] if !ok { - path := filepath.Join(fw.dir, string(cat)+".log") + path := filepath.Join(fw.dir, filename) var err error f, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("filewriter: open %s: %w", path, err) } - fw.files[cat] = f + fw.files[filename] = f } - if _, err := f.Write(append(data, '\n')); err != nil { + if _, err := f.Write(data); err != nil { return fmt.Errorf("filewriter: write: %w", err) } + if _, err := f.Write([]byte{'\n'}); err != nil { + return fmt.Errorf("filewriter: write newline: %w", err) + } return nil } // Close closes all open log file descriptors -func (fw *FileWriter) Close() error { +func (fw *fileWriter) Close() error { fw.mu.Lock() defer fw.mu.Unlock() diff --git a/server/lib/events/ringbuffer.go b/server/lib/events/ringbuffer.go index d30a680c..e9733309 100644 --- a/server/lib/events/ringbuffer.go +++ b/server/lib/events/ringbuffer.go @@ -2,12 +2,13 @@ package events import ( "context" + "fmt" "sync" ) -// RingBuffer is a fixed-capacity circular buffer with closed-channel broadcast fan-out. +// ringBuffer is a fixed-capacity circular buffer with closed-channel broadcast fan-out. // Writers never block regardless of reader count or speed. -type RingBuffer struct { +type ringBuffer struct { mu sync.RWMutex buf []Envelope cap uint64 @@ -15,16 +16,34 @@ type RingBuffer struct { readerWake chan struct{} // closed-and-replaced on each Publish to wake blocked readers } -func NewRingBuffer(capacity int) *RingBuffer { - return &RingBuffer{ +func newRingBuffer(capacity int) (*ringBuffer, error) { + if capacity <= 0 { + return nil, fmt.Errorf("events: ring buffer capacity must be > 0, got %d", capacity) + } + return &ringBuffer{ buf: make([]Envelope, capacity), cap: uint64(capacity), readerWake: make(chan struct{}), + }, nil +} + +// reset clears the buffer and wakes any blocked readers so they re-evaluate +// against the new (empty) state. Readers will reposition to seq 1 on the next +// Read call and block until fresh publishes arrive. +func (rb *ringBuffer) reset() { + rb.mu.Lock() + for i := range rb.buf { + rb.buf[i] = Envelope{} } + rb.latestSeq = 0 + old := rb.readerWake + rb.readerWake = make(chan struct{}) + rb.mu.Unlock() + close(old) } -// Publish adds an envelope to the ring, evicting the oldest on overflow. -func (rb *RingBuffer) Publish(env Envelope) { +// publish adds an envelope to the ring, evicting the oldest on overflow. +func (rb *ringBuffer) publish(env Envelope) { rb.mu.Lock() rb.buf[env.Seq%rb.cap] = env rb.latestSeq = env.Seq @@ -34,16 +53,16 @@ func (rb *RingBuffer) Publish(env Envelope) { close(old) } -func (rb *RingBuffer) oldestSeq() uint64 { +func (rb *ringBuffer) oldestSeq() uint64 { if rb.latestSeq <= rb.cap { return 1 } return rb.latestSeq - rb.cap + 1 } -// NewReader returns a Reader. afterSeq == 0 starts from the oldest available +// newReader returns a Reader. afterSeq == 0 starts from the oldest available // envelope; afterSeq > 0 resumes after that seq. -func (rb *RingBuffer) NewReader(afterSeq uint64) *Reader { +func (rb *ringBuffer) newReader(afterSeq uint64) *Reader { return &Reader{rb: rb, nextSeq: afterSeq + 1} } @@ -55,9 +74,9 @@ type ReadResult struct { Dropped uint64 } -// Reader tracks an independent read position in a RingBuffer. +// Reader tracks an independent read position in a ringBuffer. type Reader struct { - rb *RingBuffer + rb *ringBuffer nextSeq uint64 } @@ -70,6 +89,9 @@ func (r *Reader) Read(ctx context.Context) (ReadResult, error) { oldest := r.rb.oldestSeq() if latest == 0 { + // Buffer is empty (or was just reset). Reset reader position + // so it starts from the beginning when new data arrives. + r.nextSeq = 1 r.rb.mu.RUnlock() select { case <-ctx.Done(): diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index c5467ec0..b81f8905 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -26,6 +26,57 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) +// Defines values for CaptureConfigCategories. +const ( + Captcha CaptureConfigCategories = "captcha" + Console CaptureConfigCategories = "console" + Interaction CaptureConfigCategories = "interaction" + Liveview CaptureConfigCategories = "liveview" + Network CaptureConfigCategories = "network" + Page CaptureConfigCategories = "page" + System CaptureConfigCategories = "system" +) + +// Valid indicates whether the value is a known member of the CaptureConfigCategories enum. +func (e CaptureConfigCategories) Valid() bool { + switch e { + case Captcha: + return true + case Console: + return true + case Interaction: + return true + case Liveview: + return true + case Network: + return true + case Page: + return true + case System: + return true + default: + return false + } +} + +// Defines values for CaptureSessionStatus. +const ( + CaptureSessionStatusRunning CaptureSessionStatus = "running" + CaptureSessionStatusStopped CaptureSessionStatus = "stopped" +) + +// Valid indicates whether the value is a known member of the CaptureSessionStatus enum. +func (e CaptureSessionStatus) Valid() bool { + switch e { + case CaptureSessionStatusRunning: + return true + case CaptureSessionStatusStopped: + return true + default: + return false + } +} + // Defines values for ClickMouseRequestButton. const ( ClickMouseRequestButtonBack ClickMouseRequestButton = "back" @@ -205,16 +256,16 @@ func (e ProcessKillRequestSignal) Valid() bool { // Defines values for ProcessStatusState. const ( - Exited ProcessStatusState = "exited" - Running ProcessStatusState = "running" + ProcessStatusStateExited ProcessStatusState = "exited" + ProcessStatusStateRunning ProcessStatusState = "running" ) // Valid indicates whether the value is a known member of the ProcessStatusState enum. func (e ProcessStatusState) Valid() bool { switch e { - case Exited: + case ProcessStatusStateExited: return true - case Running: + case ProcessStatusStateRunning: return true default: return false @@ -302,6 +353,30 @@ type BatchComputerActionRequest struct { Actions []ComputerAction `json:"actions"` } +// CaptureConfig Capture filtering preferences. +type CaptureConfig struct { + // Categories Event categories to capture. When omitted or empty, all categories are captured. + Categories *[]CaptureConfigCategories `json:"categories,omitempty"` +} + +// CaptureConfigCategories defines model for CaptureConfig.Categories. +type CaptureConfigCategories string + +// CaptureSession A capture session resource. +type CaptureSession struct { + // Config Capture filtering preferences. + Config CaptureConfig `json:"config"` + CreatedAt time.Time `json:"created_at"` + Id string `json:"id"` + + // Seq Monotonically increasing sequence number (last published). + Seq int64 `json:"seq"` + Status CaptureSessionStatus `json:"status"` +} + +// CaptureSessionStatus defines model for CaptureSession.Status. +type CaptureSessionStatus string + // ClickMouseRequest defines model for ClickMouseRequest. type ClickMouseRequest struct { // Button Mouse button to interact with @@ -810,6 +885,12 @@ type SleepAction struct { DurationMs int `json:"duration_ms"` } +// StartCaptureSessionRequest Optional capture configuration. All fields default to the server-defined profile when omitted or when no body is sent. +type StartCaptureSessionRequest struct { + // Config Capture filtering preferences. + Config *CaptureConfig `json:"config,omitempty"` +} + // StartFsWatchRequest defines model for StartFsWatchRequest. type StartFsWatchRequest struct { // Path Directory to watch. @@ -864,6 +945,12 @@ type TypeTextRequest struct { TypoChance *float32 `json:"typo_chance,omitempty"` } +// UpdateCaptureSessionRequest Fields to update on the capture session. +type UpdateCaptureSessionRequest struct { + // Config Capture filtering preferences. + Config *CaptureConfig `json:"config,omitempty"` +} + // WriteClipboardRequest defines model for WriteClipboardRequest. type WriteClipboardRequest struct { // Text Text to write to the system clipboard @@ -1043,6 +1130,12 @@ type TypeTextJSONRequestBody = TypeTextRequest // PatchDisplayJSONRequestBody defines body for PatchDisplay for application/json ContentType. type PatchDisplayJSONRequestBody = PatchDisplayRequest +// UpdateCaptureSessionJSONRequestBody defines body for UpdateCaptureSession for application/json ContentType. +type UpdateCaptureSessionJSONRequestBody = UpdateCaptureSessionRequest + +// StartCaptureSessionJSONRequestBody defines body for StartCaptureSession for application/json ContentType. +type StartCaptureSessionJSONRequestBody = StartCaptureSessionRequest + // CreateDirectoryJSONRequestBody defines body for CreateDirectory for application/json ContentType. type CreateDirectoryJSONRequestBody = CreateDirectoryRequest @@ -1244,6 +1337,22 @@ type ClientInterface interface { PatchDisplay(ctx context.Context, body PatchDisplayJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // StopCaptureSession request + StopCaptureSession(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetCaptureSession request + GetCaptureSession(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UpdateCaptureSessionWithBody request with any body + UpdateCaptureSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateCaptureSession(ctx context.Context, body UpdateCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StartCaptureSessionWithBody request with any body + StartCaptureSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + StartCaptureSession(ctx context.Context, body StartCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateDirectoryWithBody request with any body CreateDirectoryWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1716,6 +1825,78 @@ func (c *Client) PatchDisplay(ctx context.Context, body PatchDisplayJSONRequestB return c.Client.Do(req) } +func (c *Client) StopCaptureSession(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStopCaptureSessionRequest(c.Server) + 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) GetCaptureSession(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetCaptureSessionRequest(c.Server) + 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) UpdateCaptureSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateCaptureSessionRequestWithBody(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) UpdateCaptureSession(ctx context.Context, body UpdateCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateCaptureSessionRequest(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) StartCaptureSessionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartCaptureSessionRequestWithBody(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) StartCaptureSession(ctx context.Context, body StartCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartCaptureSessionRequest(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) CreateDirectoryWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateDirectoryRequestWithBody(c.Server, contentType, body) if err != nil { @@ -2871,6 +3052,140 @@ func NewPatchDisplayRequestWithBody(server string, contentType string, body io.R return req, nil } +// NewStopCaptureSessionRequest generates requests for StopCaptureSession +func NewStopCaptureSessionRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/events/capture_session") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetCaptureSessionRequest generates requests for GetCaptureSession +func NewGetCaptureSessionRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/events/capture_session") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewUpdateCaptureSessionRequest calls the generic UpdateCaptureSession builder with application/json body +func NewUpdateCaptureSessionRequest(server string, body UpdateCaptureSessionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateCaptureSessionRequestWithBody(server, "application/json", bodyReader) +} + +// NewUpdateCaptureSessionRequestWithBody generates requests for UpdateCaptureSession with any type of body +func NewUpdateCaptureSessionRequestWithBody(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("/events/capture_session") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewStartCaptureSessionRequest calls the generic StartCaptureSession builder with application/json body +func NewStartCaptureSessionRequest(server string, body StartCaptureSessionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStartCaptureSessionRequestWithBody(server, "application/json", bodyReader) +} + +// NewStartCaptureSessionRequestWithBody generates requests for StartCaptureSession with any type of body +func NewStartCaptureSessionRequestWithBody(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("/events/capture_session") + 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 +} + // NewCreateDirectoryRequest calls the generic CreateDirectory builder with application/json body func NewCreateDirectoryRequest(server string, body CreateDirectoryJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -4305,6 +4620,22 @@ type ClientWithResponsesInterface interface { PatchDisplayWithResponse(ctx context.Context, body PatchDisplayJSONRequestBody, reqEditors ...RequestEditorFn) (*PatchDisplayResponse, error) + // StopCaptureSessionWithResponse request + StopCaptureSessionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*StopCaptureSessionResponse, error) + + // GetCaptureSessionWithResponse request + GetCaptureSessionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetCaptureSessionResponse, error) + + // UpdateCaptureSessionWithBodyWithResponse request with any body + UpdateCaptureSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateCaptureSessionResponse, error) + + UpdateCaptureSessionWithResponse(ctx context.Context, body UpdateCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateCaptureSessionResponse, error) + + // StartCaptureSessionWithBodyWithResponse request with any body + StartCaptureSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartCaptureSessionResponse, error) + + StartCaptureSessionWithResponse(ctx context.Context, body StartCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*StartCaptureSessionResponse, error) + // CreateDirectoryWithBodyWithResponse request with any body CreateDirectoryWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateDirectoryResponse, error) @@ -4800,6 +5131,101 @@ func (r PatchDisplayResponse) StatusCode() int { return 0 } +type StopCaptureSessionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *CaptureSession + JSON404 *NotFoundError +} + +// Status returns HTTPResponse.Status +func (r StopCaptureSessionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StopCaptureSessionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetCaptureSessionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *CaptureSession + JSON404 *NotFoundError +} + +// Status returns HTTPResponse.Status +func (r GetCaptureSessionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetCaptureSessionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type UpdateCaptureSessionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *CaptureSession + JSON400 *BadRequestError + JSON404 *NotFoundError +} + +// Status returns HTTPResponse.Status +func (r UpdateCaptureSessionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateCaptureSessionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type StartCaptureSessionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *CaptureSession + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r StartCaptureSessionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StartCaptureSessionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateDirectoryResponse struct { Body []byte HTTPResponse *http.Response @@ -5797,6 +6223,58 @@ func (c *ClientWithResponses) PatchDisplayWithResponse(ctx context.Context, body return ParsePatchDisplayResponse(rsp) } +// StopCaptureSessionWithResponse request returning *StopCaptureSessionResponse +func (c *ClientWithResponses) StopCaptureSessionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*StopCaptureSessionResponse, error) { + rsp, err := c.StopCaptureSession(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopCaptureSessionResponse(rsp) +} + +// GetCaptureSessionWithResponse request returning *GetCaptureSessionResponse +func (c *ClientWithResponses) GetCaptureSessionWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetCaptureSessionResponse, error) { + rsp, err := c.GetCaptureSession(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetCaptureSessionResponse(rsp) +} + +// UpdateCaptureSessionWithBodyWithResponse request with arbitrary body returning *UpdateCaptureSessionResponse +func (c *ClientWithResponses) UpdateCaptureSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateCaptureSessionResponse, error) { + rsp, err := c.UpdateCaptureSessionWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateCaptureSessionResponse(rsp) +} + +func (c *ClientWithResponses) UpdateCaptureSessionWithResponse(ctx context.Context, body UpdateCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateCaptureSessionResponse, error) { + rsp, err := c.UpdateCaptureSession(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateCaptureSessionResponse(rsp) +} + +// StartCaptureSessionWithBodyWithResponse request with arbitrary body returning *StartCaptureSessionResponse +func (c *ClientWithResponses) StartCaptureSessionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartCaptureSessionResponse, error) { + rsp, err := c.StartCaptureSessionWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartCaptureSessionResponse(rsp) +} + +func (c *ClientWithResponses) StartCaptureSessionWithResponse(ctx context.Context, body StartCaptureSessionJSONRequestBody, reqEditors ...RequestEditorFn) (*StartCaptureSessionResponse, error) { + rsp, err := c.StartCaptureSession(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartCaptureSessionResponse(rsp) +} + // CreateDirectoryWithBodyWithResponse request with arbitrary body returning *CreateDirectoryResponse func (c *ClientWithResponses) CreateDirectoryWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateDirectoryResponse, error) { rsp, err := c.CreateDirectoryWithBody(ctx, contentType, body, reqEditors...) @@ -6678,7 +7156,160 @@ func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { HTTPResponse: rsp, } - switch { + switch { + 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 +} + +// ParsePatchDisplayResponse parses an HTTP response from a PatchDisplayWithResponse call +func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PatchDisplayResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DisplayConfig + 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 == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &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 +} + +// ParseStopCaptureSessionResponse parses an HTTP response from a StopCaptureSessionWithResponse call +func ParseStopCaptureSessionResponse(rsp *http.Response) (*StopCaptureSessionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &StopCaptureSessionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest CaptureSession + 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 == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParseGetCaptureSessionResponse parses an HTTP response from a GetCaptureSessionWithResponse call +func ParseGetCaptureSessionResponse(rsp *http.Response) (*GetCaptureSessionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetCaptureSessionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest CaptureSession + 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 == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParseUpdateCaptureSessionResponse parses an HTTP response from a UpdateCaptureSessionWithResponse call +func ParseUpdateCaptureSessionResponse(rsp *http.Response) (*UpdateCaptureSessionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UpdateCaptureSessionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest CaptureSession + 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 { @@ -6686,38 +7317,38 @@ func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: - var dest InternalError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSON404 = &dest } return response, nil } -// ParsePatchDisplayResponse parses an HTTP response from a PatchDisplayWithResponse call -func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error) { +// ParseStartCaptureSessionResponse parses an HTTP response from a StartCaptureSessionWithResponse call +func ParseStartCaptureSessionResponse(rsp *http.Response) (*StartCaptureSessionResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &PatchDisplayResponse{ + response := &StartCaptureSessionResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DisplayConfig + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest CaptureSession if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON201 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError @@ -8049,6 +8680,18 @@ type ServerInterface interface { // Update display configuration // (PATCH /display) PatchDisplay(w http.ResponseWriter, r *http.Request) + // Stop the capture session + // (DELETE /events/capture_session) + StopCaptureSession(w http.ResponseWriter, r *http.Request) + // Get the capture session + // (GET /events/capture_session) + GetCaptureSession(w http.ResponseWriter, r *http.Request) + // Update the capture session + // (PATCH /events/capture_session) + UpdateCaptureSession(w http.ResponseWriter, r *http.Request) + // Start the capture session + // (POST /events/capture_session) + StartCaptureSession(w http.ResponseWriter, r *http.Request) // Create a new directory // (PUT /fs/create_directory) CreateDirectory(w http.ResponseWriter, r *http.Request) @@ -8244,6 +8887,30 @@ func (_ Unimplemented) PatchDisplay(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// Stop the capture session +// (DELETE /events/capture_session) +func (_ Unimplemented) StopCaptureSession(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get the capture session +// (GET /events/capture_session) +func (_ Unimplemented) GetCaptureSession(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Update the capture session +// (PATCH /events/capture_session) +func (_ Unimplemented) UpdateCaptureSession(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Start the capture session +// (POST /events/capture_session) +func (_ Unimplemented) StartCaptureSession(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Create a new directory // (PUT /fs/create_directory) func (_ Unimplemented) CreateDirectory(w http.ResponseWriter, r *http.Request) { @@ -8663,6 +9330,62 @@ func (siw *ServerInterfaceWrapper) PatchDisplay(w http.ResponseWriter, r *http.R handler.ServeHTTP(w, r) } +// StopCaptureSession operation middleware +func (siw *ServerInterfaceWrapper) StopCaptureSession(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StopCaptureSession(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetCaptureSession operation middleware +func (siw *ServerInterfaceWrapper) GetCaptureSession(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetCaptureSession(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateCaptureSession operation middleware +func (siw *ServerInterfaceWrapper) UpdateCaptureSession(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateCaptureSession(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// StartCaptureSession operation middleware +func (siw *ServerInterfaceWrapper) StartCaptureSession(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StartCaptureSession(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // CreateDirectory operation middleware func (siw *ServerInterfaceWrapper) CreateDirectory(w http.ResponseWriter, r *http.Request) { @@ -9528,6 +10251,18 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Patch(options.BaseURL+"/display", wrapper.PatchDisplay) }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/events/capture_session", wrapper.StopCaptureSession) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/events/capture_session", wrapper.GetCaptureSession) + }) + r.Group(func(r chi.Router) { + r.Patch(options.BaseURL+"/events/capture_session", wrapper.UpdateCaptureSession) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/events/capture_session", wrapper.StartCaptureSession) + }) r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/fs/create_directory", wrapper.CreateDirectory) }) @@ -10181,6 +10916,135 @@ func (response PatchDisplay500JSONResponse) VisitPatchDisplayResponse(w http.Res return json.NewEncoder(w).Encode(response) } +type StopCaptureSessionRequestObject struct { +} + +type StopCaptureSessionResponseObject interface { + VisitStopCaptureSessionResponse(w http.ResponseWriter) error +} + +type StopCaptureSession200JSONResponse CaptureSession + +func (response StopCaptureSession200JSONResponse) VisitStopCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type StopCaptureSession404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response StopCaptureSession404JSONResponse) VisitStopCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetCaptureSessionRequestObject struct { +} + +type GetCaptureSessionResponseObject interface { + VisitGetCaptureSessionResponse(w http.ResponseWriter) error +} + +type GetCaptureSession200JSONResponse CaptureSession + +func (response GetCaptureSession200JSONResponse) VisitGetCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetCaptureSession404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response GetCaptureSession404JSONResponse) VisitGetCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateCaptureSessionRequestObject struct { + Body *UpdateCaptureSessionJSONRequestBody +} + +type UpdateCaptureSessionResponseObject interface { + VisitUpdateCaptureSessionResponse(w http.ResponseWriter) error +} + +type UpdateCaptureSession200JSONResponse CaptureSession + +func (response UpdateCaptureSession200JSONResponse) VisitUpdateCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateCaptureSession400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response UpdateCaptureSession400JSONResponse) VisitUpdateCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateCaptureSession404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response UpdateCaptureSession404JSONResponse) VisitUpdateCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type StartCaptureSessionRequestObject struct { + Body *StartCaptureSessionJSONRequestBody +} + +type StartCaptureSessionResponseObject interface { + VisitStartCaptureSessionResponse(w http.ResponseWriter) error +} + +type StartCaptureSession201JSONResponse CaptureSession + +func (response StartCaptureSession201JSONResponse) VisitStartCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type StartCaptureSession400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response StartCaptureSession400JSONResponse) VisitStartCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type StartCaptureSession409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response StartCaptureSession409JSONResponse) VisitStartCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type StartCaptureSession500JSONResponse struct{ InternalErrorJSONResponse } + +func (response StartCaptureSession500JSONResponse) VisitStartCaptureSessionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type CreateDirectoryRequestObject struct { Body *CreateDirectoryJSONRequestBody } @@ -11683,6 +12547,18 @@ type StrictServerInterface interface { // Update display configuration // (PATCH /display) PatchDisplay(ctx context.Context, request PatchDisplayRequestObject) (PatchDisplayResponseObject, error) + // Stop the capture session + // (DELETE /events/capture_session) + StopCaptureSession(ctx context.Context, request StopCaptureSessionRequestObject) (StopCaptureSessionResponseObject, error) + // Get the capture session + // (GET /events/capture_session) + GetCaptureSession(ctx context.Context, request GetCaptureSessionRequestObject) (GetCaptureSessionResponseObject, error) + // Update the capture session + // (PATCH /events/capture_session) + UpdateCaptureSession(ctx context.Context, request UpdateCaptureSessionRequestObject) (UpdateCaptureSessionResponseObject, error) + // Start the capture session + // (POST /events/capture_session) + StartCaptureSession(ctx context.Context, request StartCaptureSessionRequestObject) (StartCaptureSessionResponseObject, error) // Create a new directory // (PUT /fs/create_directory) CreateDirectory(ctx context.Context, request CreateDirectoryRequestObject) (CreateDirectoryResponseObject, error) @@ -12292,6 +13168,119 @@ func (sh *strictHandler) PatchDisplay(w http.ResponseWriter, r *http.Request) { } } +// StopCaptureSession operation middleware +func (sh *strictHandler) StopCaptureSession(w http.ResponseWriter, r *http.Request) { + var request StopCaptureSessionRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StopCaptureSession(ctx, request.(StopCaptureSessionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StopCaptureSession") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StopCaptureSessionResponseObject); ok { + if err := validResponse.VisitStopCaptureSessionResponse(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)) + } +} + +// GetCaptureSession operation middleware +func (sh *strictHandler) GetCaptureSession(w http.ResponseWriter, r *http.Request) { + var request GetCaptureSessionRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetCaptureSession(ctx, request.(GetCaptureSessionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetCaptureSession") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetCaptureSessionResponseObject); ok { + if err := validResponse.VisitGetCaptureSessionResponse(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)) + } +} + +// UpdateCaptureSession operation middleware +func (sh *strictHandler) UpdateCaptureSession(w http.ResponseWriter, r *http.Request) { + var request UpdateCaptureSessionRequestObject + + var body UpdateCaptureSessionJSONRequestBody + 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.UpdateCaptureSession(ctx, request.(UpdateCaptureSessionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateCaptureSession") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateCaptureSessionResponseObject); ok { + if err := validResponse.VisitUpdateCaptureSessionResponse(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)) + } +} + +// StartCaptureSession operation middleware +func (sh *strictHandler) StartCaptureSession(w http.ResponseWriter, r *http.Request) { + var request StartCaptureSessionRequestObject + + var body StartCaptureSessionJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + if !errors.Is(err, io.EOF) { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + } else { + request.Body = &body + } + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StartCaptureSession(ctx, request.(StartCaptureSessionRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StartCaptureSession") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StartCaptureSessionResponseObject); ok { + if err := validResponse.VisitStartCaptureSessionResponse(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)) + } +} + // CreateDirectory operation middleware func (sh *strictHandler) CreateDirectory(w http.ResponseWriter, r *http.Request) { var request CreateDirectoryRequestObject @@ -13206,159 +14195,169 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9+XMbN9bgv4Lq/aos7fCSj8zGU98Piiwn2sSxynI2Mwm9HLD7kcSnbqAHQFOiXZ6/", - "fQsPQB9sNC9JsZX9qlIxReJ8Fx4e3vEpikWWCw5cq+jlp0iCygVXgH98R5N38K8ClD6XUkjzVSy4Bq7N", - "R5rnKYupZoIP/0sJbr5T8QIyaj79h4RZ9DL6H8Nq/KH9VQ3taJ8/f+5FCahYstwMEr00ExI3Y/S5F50J", - "PktZ/EfN7qczU19wDZLT9A+a2k9HrkAuQRLXsBf9LPRrUfDkD1rHz0ITnC8yv7nmlhR0vDgTWV5okKex", - "ae4RZVaSJMx8RdNLKXKQmhkCmtFUwfoMp2RqhiJiRmI3HKE4niJaELiFuNBAlBmca0bTdDWIelFeG/dT", - "5DqYj83R38oEJCQkZUqbKdojD8g5fmCCE6VFrojgRC+AzJhUmoCBjJmQacjUNjg2AWLwlTF+YXue9CK9", - "yiF6GVEp6QoBKuFfBZOQRC9/L/fwoWwnpv8FlvrOUhZfvxGFgl2B3ITPtNDa0kMTPDgksb8amDBDdjTW", - "5IbpRdSLgBeZWVsKMx31IsnmC/NvxpIkhagXTWl8HfWimZA3VCa1pSstGZ+bpcdm6RP79fr071c5IOJN", - "G4eb2qyJuDF/FnnkhglOsBBpMrmGlQptL2EzBpKYn83+TFuSFKYr4tiOWkNua/QmynoRL7IJ9nLTzWiR", - "akTuGuMU2RSk2ZxmGeDkEnKgujGvG92AfQ7I37ftXfydxELIhHGqEVrlACQXijmYtUdatUf6xyEjrZHp", - "bWSG7iDSfCqoTM5qIml3GtVwq9tLPiukBK7NMu3gxLQjXuq16GFttThocLFNTt1XZinG5ymsS6y6wKKK", - "5FRaoWNF3IC8XwD5p1nKP8mMQZoQBSnEWpGbBYsXY16NkoOcCZn1COWJRZOQ9ihODO3a3gYIlBlptgC/", - "gpxKmoEGqQZjfn5LY52uiODl77ZnZtbjmcAsiGSF0mQKJJdiyRJIBmPekrKWlTMjM7YKwpbAMkeLpPPd", - "ur+SdL7eOxNL2K33G7GE9d65BKWMmNjW+dI0/BFWtb4qliJNt3W8wlb1bqAncSGVPac3dgV9hg3rvVOA", - "fGtH06g6bDqkrMdxef7VKGxQk7d1/DbgbUeeIDPVQVmCpoHbxs79RkKSuxp0yzbNOfEebnUJnnUuNyMH", - "uVwC1fCKSYi1kKvDDs9MJAGovs1td5L40YlpSI5ErGlK7C57BAbzAfnrixfHA/LKHhZ4Fvz1xQvUYqg2", - "el70Mvq/v4/6f/3w6Vnv+ef/iAKwyqletBdxOlUiNdKmWoRpaGaIcetrkwwH/3OryMSZQsB8BSlouKR6", - "cRgct2zBLzzBae5/4e8gxrNvftjqWdJe+0ViVFLUMNxpKv0ktZ2Q0zRfUF5kIFlMhCSLVb4Avo5/2v94", - "2v9t1P+2/+Ev/xHcbHtjTOUpXZl7CpvvuZ8FoDLXeeAmdmxi2xHGSc5uIVVBXUPCTIJaTCTVsH1I15qY", - "1mbgHz6So4yuzPHDizQlbEa40CQBDbGm0xSOg5PesCREUOuzYbON6w+Cdv0EehiF24jNDmW7VLKt1h0S", - "oAmkdNXQQ0frqsor08TsPmNpyhTEgieKTEHfAHC/EKNoo6ahNJXaUa+R/4SmwmkJhrsGuCzOMrPQUQgn", - "SSHx/jnJAur4eyrnoIkWRkD6lq21zYTECQ1rSbAQMmvJDFJvFsCJyoTQi//UsoABeZsxjX1ooUVGNYuN", - "xm32MKUKErzN4YQoX1Lgc7cPemv3cTIajUa1fb0Ibuwutwyzhb0uGWFJuX6X/f22R1Yf6ip9TplUJe70", - "QopivjDKZWoXMWd8PiBvjKrndEdCNUmBKk2eklwwrlXjrru+5BpAMnrrLrZP67fcp+3dbPzR4rJBwwav", - "62T8iwKyKDLK+ym7BvIdfDQAjwu5hIqaEcM3dGU3QhhXGmhiQJUyDlTa620uUiS8AfnVEBPORpSGXE1y", - "kBMFc6Q0yw6QT5DJJpkiVAJhcy4kJINKikyFSIGi+tVo3tjSiz35UoJZ4xLsuloYvLCraHPDVv5s7bN5", - "ix11X2PLJSFt2XXlIImHF+OVmOheIHljl0dOGms92Xrt7DzcS0PYmtIGStE5BNhtbWDfMDi2vctdpnR1", - "g1L4MBOX61W/HVZDktiol62rVlDnNHrwFf49/N90Se1HHKBh0HqP98UEyIIqQuMYFAqFJzmdw5MeeYKX", - "51v9xN4un0yluFEgn5Allcwct+7qmOUpvCTjiN5QponpPJgLLY6eLLTO1cvhEGybQSyyJ8d/IxJ0ITmp", - "NddMp3B0/LdxNOYhpVazDEShJwriBh1+06LDN1Zauz3iFYZlqDw41inVa8IU+WbUkPDPGvJ9O60h8Hek", - "B4UL3pMcTCfDU2tUUO2uRQ/gqbw5FBI/cSRs9KYKPjPKUkhCUJflotv3xCVNC3CYhIRMV874YC42bEYo", - "Xx1bMZKADKznSlOeUJlYsymZSZHZs7y2sdZ6lE5EoTcMJgqdF3rX0Qok+PZwvy5AL0BWG3L8khDXZVak", - "6Sog2Neow08QIpDXLIULPhNtecTUJGFy86rw/GKK0Oo6Fz5oMpFMDP23h/vJHOUZKiT2IQD5ZGBttBnV", - "0csooRr62DsAvfBd12zL3m6nTCtyZC61PTKOEnlzK/vmv3FkLjbjqC9v+rJv/htHx4PQDJyG1v0dVUDM", - "T/4iNTNTChmExM63Yq+ztomEfYTJdKUhQCdX7CMKFvx5QEaoXPplMFCD7TZS3KNbXWOynqeDGg4d0LvI", - "6WqlNGTny/KsXkeMwgYkXlA+BwKmYfuBZBfyo7MZxIYfdqbDQ3FZTnUoUvejkrBZDEGKhrG6Dezs3fnp", - "+/OoF/367gL/fXX+0zl+eHf+8+mb88A9LGSM6nUrLD8xpRFvgT0ardjsrQ0xxi0DG5YGrj0h7vQqVUql", - "wFXjJzHvoK1Tkoo5zrWqRG/tibFNZDWda00qiXl5SBnNY9ClDChNszxwMpmz3kxfreiGKpJLkRSxpaJd", - "xFuH5lefOoQwvLNfugeSd+49vC3hd3258XbRw19sukbY+aWmZSDfz7hxj5d8tBjf8XqfMKUpj6Gh8714", - "6Eu9WfNel/q733SdYK6uteYj5XoNimFZvY08K6uBpzCixUFkuutIe5Hr4WbnBJSebDOfg9Jm8fYFzSoN", - "26zPvUjJeNvAShQyhp3HXFc1/QS92i5CEHp7XZdLe9xFvgeOVum3PxLv6dOW6+J6K9Ve8MQcC6C8Mj3Y", - "rkiL6+BeLqmOF86yfRjGu0zbr7pN2qWgePp8tL+B+1WnYXtALmZEZExrSHqkUGAfaxdsvgClCV1Slpor", - "t+3ipaIEJB93yDrV5JtR79mo9/RF72T0IbxEBO2EJSlsx9fMGb4kzIzsQPcEo6haEZyyJZAlgxujhJRv", - "GkMJuE2jGsaaLSEsaSSgGXkSL6TImFn7p+7ZsSk5c00JnWmQtf17tVYLAlwVEgjThCY0t89oHG6IWXXj", - "9o80gbBcAE1mRdrD2cpv0g7y7HxReNX5klCSzbOno93eFdaflw87ebfY/P2p648tQ1N4jqGhf+0srpOo", - "QfeoZ9tSCUTTPLf61Waz4oaDtHwnzbadqNewIvi27Jy97Im++wEbnv8nZy03o6tVNhUpTo4TDcg5jRfE", - "TEHUQhRpQqZAaK0tUUWeC6mtLeQ2EVqIdMyPFAD5+8kJ7mWVkQRmjCMS1fGAONuZIozHaZEAGUfv0KIy", - "jsyt+WrBZtp+PNMytZ9OU/fV6xfjaDC2FnNrVGXKmvxjXCBNlTCrjEU2dUeWcs/Mdry/aH8Zx79wtr+8", - "p1Mcdg+ArklrhG5QXkthBP75LcT3Zh6lZnsZmuBX3MgRLgoVdPyT86al/fcPbS9OOxKV88KoR2o/qqJq", - "IoVo2snD2yicBdzCA1/1iOlKcsmWLIU5dIgdqiaFgsDtfH1Iqiw5mNZmKF6keHp4Gd92vrN7D1x+EdB4", - "8ghJ1ALStAS5OQsKHryjxTeBsX4V8trwcHVZPaL1y/qxG9FZ3uwkjIc2sF3nAr7sJq9PoSdSh7NPLd/W", - "c75kUnC8eJSmb7NWBbo8ih3oa9CoKL9lvt7PYt2NwG7DtEXnVja8k1Wa1pmuRFi5jzYTbrwPVt61XZfB", - "QfCWAbdMT8LPIG6rxDRBU254BGuknky/eR62UX3zvA/cdE+IbUqmxWxmOavDSL3rYKLQ3YN97sbej6zy", - "INsPfVdsbg5ZpF7Lw2vU20SZwuYNoRa9P3/3Jto8bt1S5pr/ePHTT1Evuvj5fdSLfvjlcruBzM29gYjf", - "oSp66GmCaiwll+//0Z/S+BqSbjDEIg2Q7M9wQzTIjJmdxyItMq62PVf2Iiluto1lmuz57omj9uxCN0Ds", - "Kqc3DQf8NH07i17+vs3XsXV0f+6t27VomgpztZtovdp+Cp661oSSXEGRiH65+6PL9/84XhesVrPHg8g7", - "n+O7tzmROo7LMNIujP5lKHUNcfZCU9+EuSO0Xsv3QGlrJtPs8Gna4uBDC68HyPOLmsGYTo1AokSZ0Tbx", - "Qx7ycnt7VSLr4lVY1LrfJ6HuNoKlT5Xhe0gIq5zmAodsacctCpaEBTE16viE6rCdGO24Fht1MnPd9jAV", - "d7KaprpQe2LDO6Up7GxP2W6plBeTPA7s71xpllFzGTm7/IUUaE/PQcbANZ3XT0GObhtbjtFzf3wSNmvA", - "akHt2WrBtU1H6UUZZF2PadWKJSjEPMkgMzqiXX35ztZxggfNLZcVTnXj8UYWnBv02W1DEj6LuhGbsAOD", - "mF5RTY0ku5HMGkDXSM++YzOeF4G3uYRqupNikdRnGWy1Hpbjfti65zvpi2Y5zmdQmeHaOzQtNPAuIqmc", - "jLABcc0H0a4mFbcVCbR6KN1Hd7o6JzldpYIaMs0lKCOh+LzEoHNAEJKkbAbxKk7dQ6u6KzbLh7WKWMwu", - "gioohN/pfmouqfWiaVgh6D26k2goBakdnCkyxo7jqItlzfoDp4A1hNuf/UsWgiBeFPy6vmDnD1J6mezG", - "xNa9G2TY/WLGOFOL3Y6Nyofb9+o6NLbev+152P5alc7otd8bnoQ7H3LVal2nAxe7Jjzw8K2vMyRErmIJ", - "wNVC6Hcw3yWMajc7/Q/WPl+61M/dpXGDA3qH5fZXtNjuM9COr7h2rCdGfc37KcwMt0gOd3rX3WPM4NOZ", - "h0LPA3Ybyg6xQMsS0VtioZqEEWTZZsTUvq96qaaT282G8B+EZB8Fx3gcnIvQTBRcD4h9zjcXDfxeEfTC", - "6xEOc9r43uAhLOnsCra43/8fs+J4h/kTccMD0xd5ePK7vFyXMVu7G0G3cQXVNoSxFljWnGp/pth7yJ2f", - "k1vRdntKLZYkwLf4F9pn7+pNwXXa+ibq2nUs+zVL4dLcOpVigqvD1j+XosjDhgr8ybluSfJ947a3r49g", - "IAzum+fPj/eLehM3PGQXN2vFn9AS7tf7S8d6d/Enu1kIhXcpD1v7/GVfWvAJMjk0Im2Df189fHM/lfWS", - "Fgrq3r5C4v0eYsP7SWlr3dNYW385xLjNkK227lfdcLIZbWXK+uRBgBgV5rX6ler4XoMMywhQvD5hMHbY", - "M9owLlvCdjtXye1uPFL2TVc7+D50enIgBO4YqjiTNIOwp8K7Srf1jQyKZ7nh2CVIyRJQRNmkIw4Cx3Wc", - "Px1tM5oFTUj+EThg/KkpsIC8d08Bk7hoT9AX/MoScPdDTbWO+kOFd1jbDJ2NAMnoLTryso9wwd98170C", - "9PpUzv34zXc7YmQ9fu1kR0+EKy3yuxKakDGYcbbzy0WWQcKohnSFaVbweVQUmswljWFWpEQtCm20oAF5", - "v2CKZOhPgzYGxvFBWMoi15CQJUtAILDC9uF9InUtB5sFPWCY7nr4+t6a7t2CPI0eqKW4BhUMzAraqcPB", - "Ywd5MPqn1Wod3oOz5slIyYzdmnPL7GQw5vVYeXTeOVLmlKcKXQvRd3WY+BDd4wG5QpfTyvVnzJ2vBtGr", - "3MyFr/GUE+ElUW2+BqTIEX73nyMDF+dgeTwY81qwIGYgMVBb5ZAYsN8ImfQN4ybWmuEe/8udM64l7ZtW", - "dkI15pQnhFNdSCMUuQZpf87Nua5sCJddm03YYdayAXVjHuSEcEoVQ4oIV8wJYa07C4EOJjabSYcPvZgY", - "TSiGzbR4CbIfL6iksTbMtcoFYdxwghGo5rz5G8mY0vQarI6FiU0w7ABhNqXxtcppDBURkNGAvOXpyjqf", - "gwpBgBwplgLX6aoBpzGvmiFtHFtQlcJzNDgJUr03oO+aTuZXyTSUCXAOY/TN2GqYln3Mh5/w0Dw4phlz", - "BjMM1oteRj+C5JCSi4zOQZHTy4uoFy1BKruc0eBkMELVPAdOcxa9jJ4NRoNnLuIBNzL0nn/DWUrnXi2L", - "A3rZG5BzQC8+bGlJAG6ZQvOr4KB6pMgTo6WsDRrwHVwySlSRg1wyJWTSs0yG0YgF1yxFyJWtX8HyvRCp", - "IuMoZUoDZ3w+jjDCIGUcDMWIKR675gI3E9KHxaGm4pxckZgMDq2SkaBmruOFn+U17t+iApT+TiSrvXK2", - "rR23Hpprb0t+SxaGWpAMwerCtH4fR/3+NRPq2jqY9fsJU0Ys9ed5MY4+HB/uE2YXFCarqp3hT+sWWmUS", - "fDoaBa6QuH6L7wRjU8utOWSvB+t97kXP7Ugha1Q543A9ceHnXvRil37NrH+YAq/IMipX5pizdFkuMaUF", - "jxcOCWbxbs3YraLeXKQsri4r3VxhrrZ9n46pmgYwhl0yc101Q61IpSUy7uTDlJY/DwxV9cZ8K7uQ/bll", - "zPdllzOQmHbAQ4FklNO59a68toKH8ZmkSssi1oV0VEzObzVwI4KuQBvZoHpjnktxu+pjXDok5Yh2H+X4", - "ngzxunH26nLo40gEP8bzZ5qK+BqSMcenGA/LrZx96dF4OHOHj4aQRrUL8gfkR++1637iNAM15kfON9Sd", - "pmdCXDNQDo7j6BjhhXG/zuixKEew3w7G/AqA+KhvpGSoVjKYCzFPoSTsoTVGlJ7t/nsLUhczblNIKhaf", - "FnrxdgnyB63zc/QDSTwMggvGO5ZprH7J55ImoMpe7lB9Q2/PBOdW41CXIC8NnUQvnz3tRZciL3J1mqbi", - "BpLXQv4iU4Vmt3ZEe/Th833JNU8rj1a0rZOd2Uu3hCvyVNCkD55lVZ/ypO/bGrEnVEDR+QW7Ya46IUlm", - "JEg5BPnIckJlvGBLw+FwqzFLpV5ARgpubnHDhchgaEXIsJp6OC5Go2exYQX8BL0xV6CJNDIuq89g5Tbj", - "BygapeQc8z9Q0bDwKgWjOuXJOwfjTTIpK1LNcir1cCZk1k+oppt0jgqU3a71VRujfFj0I0zQmYvqRpxc", - "c/hwBPFrkRqcomFXC5KnNAYX+e/RtR/W1+7wp/3faP/jqP/tYNL/8Omk9/TFi7D9+SPLJzOWBpb4W0WQ", - "PpeOwRc1K8ut12HFPuWqjzDNog8LyChnM1Aaj+jj+rvtlHHDidu0+nJ5LhQ7dDPZqMDVsHuYFncS8h0o", - "qcGSAiS9gLSzXFMyBzNHNU2+tNxriaASmzUiP6LKCCR1XBeC5RadNHR36eHU63hhqXfuIx44EWv5nVrp", - "kNEO5jKPnl5ekJim6YCcul/x5LcPZUadqSdMdgmEFiJNHJHCbZwWyhCvUX96RAnCBRFo0kY3JVIKG0Vi", - "yq2NIgW6BEwOsy1jcpm31AOesDJC0D7r+XykmKZkMOZo9LOxDbMiRR0iXjiuSsD6Wpp7YVxGB6EbnQ19", - "NbNdw8omiHXgGnNvYszpyozCQd8IeU2kKHjS15LlxKiOPF7hbIChQDxhS5YUNHXDhCRvIPf1HdTATe/o", - "G7JsH6qM4JAduU++JO+VjLAhH3idptfYbC03rWe2JuKqrLQPhK9A2tsD0WQTBfqkvp6tvyiGrlhWpNa1", - "23JdPW132JDYwpE1Vw2NqO9G0zugyVnNtBWC1n2hq5mxOlQEoEw87abEc6rFN3eGrtm0tSyXPoEtK18X", - "ONE22A3PpnHygUg/bAE9lPzR6un8QDGbbYmFr0Zg/WoNst6YvgO+ylzQYTSVfikPhKF2lumdkXMv89eS", - "FIT4zLrMLJliU5YyvSpvy18Nxn9giQuXFDf1TCxNNDeznIe1PowCR60FnbO8QLXpWHvlI5XR3KjPf2Km", - "ldq+CvXM9Hw9ReucLX0WTKuYpkAVoG5VTy62JX9oSOMps+E+EGm2870fKDfMQF/JcYlLqXLcWDRRxMMa", - "xcxBW4KZlGUYOoXE96Ab+Yge8ngMJz4K8y5Gt9idlpu4Dyh+D9qzWm0K51/nZ9pF+WiWDwgDt8yL9EBk", - "3i5McCft0EHB7OzLkvobn+6ngR1/KpZOaZWkUbtgrFGyYYMcdTlVqnnwGR9lZu29v/SIs3byyjWzlhhi", - "zEPpHgbkNcpfszAJC+D23tzOK9EjCmDMzWLCuSEI1ZUZfc70YCYBElDXWuQDIefDW/O/XAothrcnJ/ZD", - "nlLGh3awBGaDhZXnziNoIbiQqu740U9hCdV+zY3a+XvFDhTo2aecCc1iQSTBFw+XrOSB2KFVauNAbkCE", - "IrV8TdqCPePrtiSkyx0IX5Xe892i6j29hsrL/qE0xlawwGeHo40nDsvoHIa5DW6pZtpu3WwdLNUCCA76", - "RRF6RnN8kaSkQpD3JtuCTlc+JizEbBgEWbpQgXRltLehMLztwxfMd7qm49UkaVNbbNj5Ghl3nBrYiENw", - "Oc05ScUcoxQ0i68VOeJCuxgZa+KsURCZwoIumSFpuiJLKld/I7pAK50r4eAZ2PtMTYVe1LZinxt9WAQG", - "UTjbpXvq7llpbsWbd/nBl56GSfOoHANV4WqCY+v3gVYk6ywEqYvWc6Lwn943zBow+n1Xmetn0u9bp6sR", - "sS8IViG3bwj/DEnIKx+N8EDsV68odKB0dOT1ldiQ7GIqXcGih2qjGe+hzfmUrh3C0TlcPhBe2uWI7mDk", - "sE6EX82phSX50KjRjQVXWaXhwRJwlXBp0x5KeQikCfyDDRrN8juB4+sXZ8HwpWhibOlzuN0Bzc9H327v", - "16yWeo9+AR3bMaQxU0NbeGpSZoNCMilC1vhmca6HMsmHS4Ad+rpZRZLYfX5FrGt3Sij6U1bg93ix1ah2", - "wIstl/XQeGlXEzvY5lOixG4xuRtnPd/er1mE916MRbjyesb1dbx5N4QNKHttXQG+bmxhnOCfAFGIjxJH", - "4oangiaGuyYfGcbDzEGH4q90IbkilPx2cWkDfmreIzZBHqJL+ZtFLaavnuR+Df9u/ldM/sZy9HbxZUAx", - "CdTOVQO9S4vRoP2mMF+i6fevAlAcWKcdH93YpIFe3ZNoW7Tkh70OZwfXO10oDdT9HstAICSsOoAfI106", - "ZNVFCKGe0NyWO+hV6WQHgtVUDj4qTY40lTXXp8wbXtB334x1vJGux3wDYZPflE6ImM1AKqLYnGMdEwzr", - "mFGlQZYTYlornox5AvWvzGcqARPgfWS5uxDTeMFgiUnjQa+PgmwUfvWocZWB0WNhq96ndgrUcrtoHRyQ", - "H9h8AdL+VVZSICqjaQolehWZFppoeg0kFXwOcjDmfYsJpV+Sfxts2yHISY+4oBqDWEjI0b+fjUb9F6MR", - "efPdUB2bji5oqNnxWY9MaUp5bFQp03OIGCBH/z55UetrEdfs+teex6fv8mLU/1+NTq1lnvTw27LH01H/", - "edmjAyM1apngMFEdHVUCRf+pSmXjQBX1ar/ZJeMHFUrMs69UdNx7J7H43vH2/2eiUTe3XYpHI78mPi7K", - "icWmaChLquwqE7ZWrfkaTtj9dMKqrEyboFDLq9WseYRk8z3oRtUdn0Sxhb2SbFKmNOrpqpNuquI/hx0m", - "j5NSql0HSKW6vqU27u8R0gp6wiPmrZNumzawXEzX9c0XOHnAZ+f7uLrhM29l7niEeMIdYEkLjC3YxMwS", - "aFJeuoO8/A5o4q7cu7EyTuZVQjP+18LNItag+1XqvjvpEij6gz6Sj4xY0COzvMqYjiVxKLCCflLLGNTJ", - "3e3ETQ/n4NeRIergyLVaQiTnjvcIEXkFOlBRr4a6ISaTUguWlxi2oSvdj7YYQ+gjXDBSy8ZlCElshFUK", - "7kBwbjASMuFkgPUTHXREdHn14N5CuEqNpCMG65ACWbWMBE6h3a1klheo+0Y6uSinzVWwNseqIxTuLcoJ", - "sVQGOD12URcIfJo5fa3ODt60uTGAk6LhBfnN1o2wsZpMq8q22XINCxVgCzGHtW7eG2vsS/pJPY9YLQq1", - "vDhrsRsf1AML7xD1t4kfDiTs31hekXUNgX8aIqf1YOI1Em3RuzOubCH4fU2jXXwx5tsZY7uJtGERHfM1", - "k2h3KLGzcd4bc3mrSrDQ+JrppTxCtjJD78sxrfmUTyq625wIqcpqnoJVEfDgrLrbzGOS5T45q1sbBgpj", - "6ixDTv0+tulX/Y63ZepakxceDw8iLk4dDP/kImOdXDvExs16sO/aTaCW3vKh7gCBDJq74/bAxES47WDx", - "j184+1cBobSPFVfeOHBszaTXvmviNsl958/4QsRmN1M3UrsgaD6vaWIIreEnD/LPLkUg2ADAdXoTeUVu", - "a0YKNDw4S4OzO5R43GR72G5qCNQ+8IgSef74EXWF+SvNjjCaPmA8WkfS0PqfdpqSbO2K1+rcNvsDcbVu", - "FtJwq+1qg/agbe8BV3i1tUU4Qv7cV+e1EhDVXdj552Lqeprgrj9Ff+9fXZ33XWhu/32wiP8bSBh1mQxn", - "xAyPNSWcu+/RuhA7brzc+Ve6lqgLPMp9foxkioBuQdmFE1qxW1KsucxvdjLCgNddDJ6vasoXbRk//8B3", - "77dVsk+fnLwzLzmppxz95vnzrmViMu+OZW3MZm6Zb5cT/47m2AOtGWW49WM/RtEsZU5O7w9ZuWqlYq6G", - "FWDDT3Ri7moJdcjhNYKwJd43Uq4XNI7Eq9xRwdo24WlmIk3FTdjzoFHQpZZyfB3NgqerKiMem/ny9EwR", - "t7QNjNl9quwzT23v4dmqBhNXEyn6YifaT2K+41FmCOurPr1CJ4NZNCYQNFNbBslTurrBWihDlyJmh9RF", - "csq0pHJFLsverq4cN9yHJfGrUgWImltN6JwyruxNfCrFjQJJXAG3MRecpCKm6UIo/fLbp0+f2pTIOOqC", - "KkJjXzzySU7n8KRHnrhxn9jEUk/ckE+q0r4uAkqWhcu0H7FaHKah0oXkNrFzPYNRyHDiQFDt+8yeDg9x", - "s2vN9YWiHgLrwPJxobjwCrhfY6qhagsY0nOFK7cUESBOxyBWJiF3dF/0a4VVHyx2tl269Y+lg3bB6QAF", - "VJnCpGvzVaSYClaXbyIYa6VuxTDWZ31YFDdK+34ZHNer0IaOQltW9ivDLd2A3E9VwdrPw2vWjM4NIvpH", - "hmGe2+/ltVK4m1TCLXVud78sHITQep3xryoL0NsfH6V/gRElZaF0r7Z2U5zEGuVbac6WMv/zUF2zrPt/", - "093dHZQ6S91vID5V1q8OXn+bVa7/aNp74HPMbip0hLlfHqWXcq3QtN1eN+oTtoNOg63+NFKnUdb7C+lP", - "tSrbAeL7rl71+tFa3KqTz5YB30yHotDbDHEV8EShN1rkvpA8uoNlKVCzfKuNaa0audFx18uR//cDygM8", - "oNSoWhR6zWBWVg0cVo+wYelqI4ergtoPGajdquvXnbepqz7kFwvR/kK5LcrA7lzCkuGd0dcIrJccbGHd", - "BZd1SjEffVZH/MbXs/LRqqxQWHlPDAimVBKZOSqamZIKnwfPvQqU3bseslDohZ+xttU43C4aEWDDLH9+", - "53CCWsVS+/TYEHDlr/3XrlZ//3RjzXwxs+XjmpVHfaH/Afm+oJJyDdZfbgrk3euzZ8+efTvY/ALSWMqV", - "9Uc5aCXOl+XQhZilPB093cTYzEgylqZYCF+KuQSleiTHXLFEy5W1fWJqfNkE9zvQctU/nelQTearYj63", - "saKYsharq9Tqp1aVTeTKMkG1iU3lUx/juVEGnNo0Vwp50RYn3EGipMyeHp3xg+8cY6u75n4t4wE2HSh+", - "Nhvp2XKyb/GrLwojy1XeW4AdTdP6sE2wtaoLBVzvHvrwDRdvDp69J5tY1AmBR5ghCiFQZkis5Jqr4Cl4", - "XdblIMnFKywvgnkD50xprICC6eCMBBm0sSzyTUiulTR+MBwHyibvr145V7gvm4xPi7x5/OBG/l8AAAD/", - "/6IOz88DwgAA", + "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", + "AAD///9uXeRvzwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/openapi.yaml b/server/openapi.yaml index 2ca99287..dec71c42 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -1194,8 +1194,152 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /events/capture_session: + get: + summary: Get the capture session + description: > + Returns the current state of the capture session. Returns 404 if + no session is active. + operationId: getCaptureSession + responses: + "200": + description: Session state + content: + application/json: + schema: + $ref: "#/components/schemas/CaptureSession" + "404": + $ref: "#/components/responses/NotFoundError" + post: + summary: Start the capture session + description: > + Starts the capture session. At most one session may exist at a + time; returns 409 if a session is already active. The caller must + DELETE the existing session before starting a new one. + operationId: startCaptureSession + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/StartCaptureSessionRequest" + responses: + "201": + description: Session started + content: + application/json: + schema: + $ref: "#/components/schemas/CaptureSession" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" + patch: + summary: Update the capture session + description: > + Updates the active capture session. Returns 404 if no session is + active. + operationId: updateCaptureSession + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateCaptureSessionRequest" + responses: + "200": + description: Session updated + content: + application/json: + schema: + $ref: "#/components/schemas/CaptureSession" + "400": + $ref: "#/components/responses/BadRequestError" + "404": + $ref: "#/components/responses/NotFoundError" + delete: + summary: Stop the capture session + description: > + Stops the capture session and its CDP monitor, returning the final + session state. Returns 404 if no session is active. + operationId: stopCaptureSession + responses: + "200": + description: Session stopped + content: + application/json: + schema: + $ref: "#/components/schemas/CaptureSession" + "404": + $ref: "#/components/responses/NotFoundError" components: schemas: + CaptureConfig: + type: object + description: Capture filtering preferences. + properties: + categories: + type: array + description: > + Event categories to capture. When omitted or empty, all + categories are captured. + items: + type: string + enum: + - console + - network + - page + - interaction + - liveview + - captcha + - system + additionalProperties: false + CaptureSession: + type: object + description: A capture session resource. + required: + - id + - status + - config + - seq + - created_at + properties: + id: + type: string + status: + type: string + enum: + - running + - stopped + config: + $ref: "#/components/schemas/CaptureConfig" + seq: + type: integer + format: int64 + description: Monotonically increasing sequence number (last published). + minimum: 0 + created_at: + type: string + format: date-time + additionalProperties: false + StartCaptureSessionRequest: + type: object + description: > + Optional capture configuration. All fields default to the + server-defined profile when omitted or when no body is sent. + properties: + config: + $ref: "#/components/schemas/CaptureConfig" + additionalProperties: false + UpdateCaptureSessionRequest: + type: object + description: Fields to update on the capture session. + properties: + config: + $ref: "#/components/schemas/CaptureConfig" + additionalProperties: false StartRecordingRequest: type: object properties: