package store import ( "context" "crypto/sha256" "database/sql" "encoding/binary" "errors" "time" rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1" "github.com/rvbox/rvbox/internal/domain" ) var ErrSignalDeliveryUnavailable = errors.New("signal delivery is unavailable for this command state") type SignalInput struct { IssueUUID domain.UUID ClientID string RequestUUID domain.UUID Signal rvboxv1.SignalKind Hash [32]byte OccurredAt time.Time } type SignalResult struct { CommandRevision uint64 Duplicate bool Cancelled bool } // SignalCommand applies the only locally-completable signal operation: a // queued command can be cancelled before dispatch. For dispatched/accepted/ // running work the revisioned remote delivery path is intentionally not // claimed until a live session control queue is available. func (store *Store) SignalCommand(ctx context.Context, input SignalInput) (SignalResult, error) { if input.IssueUUID == (domain.UUID{}) || input.ClientID == "" || input.RequestUUID == (domain.UUID{}) || input.Signal < rvboxv1.SignalKind_SIGNAL_HUP || input.Signal > rvboxv1.SignalKind_SIGNAL_USR2 || input.Hash == [32]byte{} || input.OccurredAt.IsZero() { return SignalResult{}, errors.New("invalid signal request") } store.writeMu.Lock() defer store.writeMu.Unlock() database, err := store.openDatabase() if err != nil { return SignalResult{}, err } method := "signal_command" target := input.IssueUUID.String() if existing, found, lookupErr := lookupControlMutation(ctx, database, input.RequestUUID, method, target, input.Hash); lookupErr != nil { return SignalResult{}, lookupErr } else if found { if len(existing) != 8 { return SignalResult{}, ErrInvalidSegmentRecord } return SignalResult{CommandRevision: binary.BigEndian.Uint64(existing), Duplicate: true}, nil } tx, err := database.BeginTx(ctx, nil) if err != nil { return SignalResult{}, err } defer tx.Rollback() var lifecycle uint32 var revision uint64 err = tx.QueryRowContext(ctx, `SELECT lifecycle, revision FROM commands WHERE issue_uuid = ? AND client_id = ?`, input.IssueUUID[:], input.ClientID).Scan(&lifecycle, &revision) if errors.Is(err, sql.ErrNoRows) { return SignalResult{}, ErrCommandNotFound } if err != nil { return SignalResult{}, err } if lifecycle != uint32(rvboxv1.CommandLifecycle_COMMAND_QUEUED) { return SignalResult{}, ErrSignalDeliveryUnavailable } if revision == ^uint64(0) { return SignalResult{}, errors.New("command revision exhausted") } nextRevision := revision + 1 if _, err := tx.ExecContext(ctx, `UPDATE commands SET lifecycle = ?, terminal_time = ?, revision = ? WHERE issue_uuid = ? AND client_id = ? AND lifecycle = ? AND revision = ?`, rvboxv1.CommandLifecycle_COMMAND_CANCELLED, input.OccurredAt.UTC().UnixNano(), nextRevision, input.IssueUUID[:], input.ClientID, rvboxv1.CommandLifecycle_COMMAND_QUEUED, revision); err != nil { return SignalResult{}, err } result := make([]byte, 8) binary.BigEndian.PutUint64(result, nextRevision) _, err = tx.ExecContext(ctx, `INSERT INTO control_mutations (request_uuid, method, owner_kind, owner_id, target, immutable_sha256, assigned_revision, result, created_at) VALUES (?, ?, 'command', ?, ?, ?, ?, ?, ?)`, input.RequestUUID[:], method, input.IssueUUID.String(), target, input.Hash[:], nextRevision, result, input.OccurredAt.UTC().UnixNano()) if err != nil { return SignalResult{}, err } // Keep a compact audit digest; command text/stdin is never copied to the // audit payload. auditPayload := []byte{byte(input.Signal)} auditHash := sha256.Sum256(auditPayload) _, err = tx.ExecContext(ctx, `INSERT INTO audit_events (occurred_at, source, action, outcome, compression, payload, raw_bytes, stored_bytes, sha256) VALUES (?, 'control', ?, 'success', 1, ?, ?, ?, ?)`, input.OccurredAt.UTC().UnixNano(), method, auditPayload, len(auditPayload), len(auditPayload), auditHash[:]) if err != nil { return SignalResult{}, err } if err := tx.Commit(); err != nil { return SignalResult{}, err } return SignalResult{CommandRevision: nextRevision, Cancelled: true}, nil }