feat: complete Windows client control and recovery paths
This commit is contained in:
+87
-2
@@ -193,14 +193,99 @@ func stat(ctx context.Context, client rvboxv1.ControlClient, args []string, outp
|
||||
return err
|
||||
}
|
||||
item := response.GetCommand()
|
||||
renderCommandStat(output, item, time.Now().UTC())
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderCommandStat keeps the human CLI useful when a caller does not have a
|
||||
// protobuf-aware inspection tool. Durable lifecycle, expiry, retention, and
|
||||
// Windows identity are rendered independently: a late terminal result must
|
||||
// not erase the fact that the queue deadline was crossed.
|
||||
func renderCommandStat(output io.Writer, item *rvboxv1.CommandRecord, now time.Time) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(output, "command %s client=%s lifecycle=%s revision=%d events=%d\n", item.GetIssueUuid(), item.GetTargetClientId(), item.GetLifecycle(), item.GetCommandRevision(), item.GetLastEventSeq())
|
||||
if item.GetQueueExpiryTime() != nil {
|
||||
fmt.Fprintf(output, "queue_expiry=%s\n", item.GetQueueExpiryTime().AsTime().UTC().Format(time.RFC3339Nano))
|
||||
expiry := item.GetQueueExpiryTime().AsTime().UTC()
|
||||
fmt.Fprintf(output, "queue_expiry=%s\n", expiry.Format(time.RFC3339Nano))
|
||||
if !now.IsZero() && !now.Before(expiry) {
|
||||
fmt.Fprintln(output, "queue_expired=true")
|
||||
}
|
||||
}
|
||||
if item.GetLateAfterExpiry() {
|
||||
fmt.Fprintln(output, "late_after_expiry=true (terminal result arrived after queue expiry)")
|
||||
}
|
||||
if item.GetTerminalTime() != nil {
|
||||
fmt.Fprintf(output, "terminal_time=%s\n", item.GetTerminalTime().AsTime().UTC().Format(time.RFC3339Nano))
|
||||
}
|
||||
return nil
|
||||
if item.GetExitCode() != 0 || item.ExitCode != nil {
|
||||
fmt.Fprintf(output, "exit_code=%d\n", item.GetExitCode())
|
||||
}
|
||||
if item.GetOutputTruncated() {
|
||||
fmt.Fprintln(output, "output_truncated=true")
|
||||
}
|
||||
if item.GetOutputIncomplete() {
|
||||
fmt.Fprintln(output, "output_incomplete=true")
|
||||
}
|
||||
if item.GetRetainedCompressedBytes() > 0 {
|
||||
fmt.Fprintf(output, "retained_compressed_bytes=%d\n", item.GetRetainedCompressedBytes())
|
||||
}
|
||||
if rejection := item.GetRejection(); rejection != nil {
|
||||
fmt.Fprintf(output, "rejection_code=%s rejection=%s\n", rejection.GetCode(), singleLine(rejection.GetMessage()))
|
||||
}
|
||||
if identity := item.GetWindowsExecutionIdentity(); identity != nil {
|
||||
renderWindowsIdentity(output, item.GetSpec().GetElevated(), identity)
|
||||
}
|
||||
}
|
||||
|
||||
func renderWindowsIdentity(output io.Writer, requestedElevated bool, identity *rvboxv1.WindowsExecutionIdentity) {
|
||||
fmt.Fprintf(output, "windows_requested_elevated=%t\n", requestedElevated)
|
||||
if contexts := identity.GetAttemptedContexts(); len(contexts) > 0 {
|
||||
names := make([]string, 0, len(contexts))
|
||||
for _, context := range contexts {
|
||||
names = append(names, windowsContextName(context))
|
||||
}
|
||||
fmt.Fprintf(output, "windows_attempted_contexts=%s\n", strings.Join(names, ","))
|
||||
}
|
||||
if identity.EffectiveContext != nil {
|
||||
fmt.Fprintf(output, "windows_effective_context=%s\n", windowsContextName(identity.GetEffectiveContext()))
|
||||
} else {
|
||||
fmt.Fprintln(output, "windows_effective_context=none")
|
||||
}
|
||||
if identity.GetEffectiveUserSid() != "" {
|
||||
fmt.Fprintf(output, "windows_effective_user_sid=%s\n", singleLine(identity.GetEffectiveUserSid()))
|
||||
}
|
||||
if identity.SessionId != nil {
|
||||
fmt.Fprintf(output, "windows_session_id=%d\n", identity.GetSessionId())
|
||||
}
|
||||
if identity.GetSessionUserSid() != "" {
|
||||
fmt.Fprintf(output, "windows_session_user_sid=%s\n", singleLine(identity.GetSessionUserSid()))
|
||||
}
|
||||
if identity.GetSelectionDetail() != "" {
|
||||
fmt.Fprintf(output, "windows_selection_detail=%s\n", singleLine(identity.GetSelectionDetail()))
|
||||
}
|
||||
}
|
||||
|
||||
func windowsContextName(value rvboxv1.WindowsExecutionContext) string {
|
||||
switch value {
|
||||
case rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SERVICE:
|
||||
return "local-service"
|
||||
case rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SYSTEM:
|
||||
return "local-system"
|
||||
case rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER:
|
||||
return "active-user"
|
||||
case rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED:
|
||||
return "active-user-elevated"
|
||||
case rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM:
|
||||
return "active-system"
|
||||
default:
|
||||
return "unspecified"
|
||||
}
|
||||
}
|
||||
|
||||
func singleLine(value string) string {
|
||||
return strings.NewReplacer("\r", "\\r", "\n", "\\n", "\t", "\\t").Replace(value)
|
||||
}
|
||||
|
||||
func runCommand(ctx context.Context, client rvboxv1.ControlClient, args []string, output, diagnostics io.Writer) error {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
rvboxv1 "github.com/rvbox/rvbox/gen/go/rvbox/v1"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestGlobalSocketAndCLIValueParsing_HP_CTL_11(t *testing.T) {
|
||||
@@ -47,3 +49,32 @@ func TestGlobalRequestIDIsInjectedOnlyForMutations_HP_CTL_12(t *testing.T) {
|
||||
t.Fatal("duplicate global request IDs accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderCommandStatShowsExpiryRetentionAndWindowsIdentity_HP_CTL_13(t *testing.T) {
|
||||
t.Parallel()
|
||||
expiry := time.Date(2026, 9, 6, 12, 0, 0, 0, time.UTC)
|
||||
sessionID := uint32(7)
|
||||
effective := rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM
|
||||
var output bytes.Buffer
|
||||
renderCommandStat(&output, &rvboxv1.CommandRecord{
|
||||
IssueUuid: "019c46f1-1d02-7000-8000-0000000000f2", TargetClientId: "win-a",
|
||||
Lifecycle: rvboxv1.CommandLifecycle_COMMAND_SUCCEEDED, CommandRevision: 3,
|
||||
LastEventSeq: 9, QueueExpiryTime: timestamppb.New(expiry), OutputIncomplete: true,
|
||||
OutputTruncated: true, RetainedCompressedBytes: 1234, LateAfterExpiry: true,
|
||||
WindowsExecutionIdentity: &rvboxv1.WindowsExecutionIdentity{
|
||||
EffectiveContext: &effective, SessionId: &sessionID, EffectiveUserSid: "S-1-5-18",
|
||||
SessionUserSid: "S-1-5-21-user", AttemptedContexts: []rvboxv1.WindowsExecutionContext{
|
||||
rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED, effective,
|
||||
}, SelectionDetail: "fallback\nused",
|
||||
},
|
||||
}, expiry.Add(time.Minute))
|
||||
for _, want := range []string{
|
||||
"queue_expired=true", "late_after_expiry=true", "output_truncated=true", "output_incomplete=true",
|
||||
"retained_compressed_bytes=1234", "windows_attempted_contexts=active-user-elevated,active-system",
|
||||
"windows_effective_context=active-system", "windows_session_id=7", "windows_selection_detail=fallback\\nused",
|
||||
} {
|
||||
if !bytes.Contains(output.Bytes(), []byte(want)) {
|
||||
t.Errorf("stat output missing %q:\n%s", want, output.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user