package jobs import ( "fmt" "os" "os/signal" "strconv" "strings" "syscall" "time" "xcecli/common/format" "xcecli/common/request" "xcecli/config" "github.com/spf13/cobra" ) // PeekQuerier implements the jobs peek command. type PeekQuerier struct { configManager *config.ConfigManager lines int stderr bool noFollow bool } // NewPeekQuerier creates a new PeekQuerier. func NewPeekQuerier(configManager *config.ConfigManager) *PeekQuerier { return &PeekQuerier{ configManager: configManager, lines: 100, // default show last 100 lines } } // NewPeekJobsCmd creates the xce jobs peek command. func NewPeekJobsCmd(configManager *config.ConfigManager) *cobra.Command { querier := NewPeekQuerier(configManager) cmd := &cobra.Command{ Use: "peek", Short: "Peek at job output", Long: `Peek at job output in real-time. Examples: xce jobs peek jobid # stream stdout output xce jobs peek jobid -n 50 # initial show last 50 lines xce jobs peek jobid --err # view stderr output xce jobs peek jobid --no-follow # one-shot snapshot, no streaming`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { fmt.Println("error: job ID is required") fmt.Println() cmd.Help() return nil } jobID := args[0] if err := querier.Run(jobID); err != nil { fmt.Println(strings.ToLower(err.Error())) return nil } return nil }, } flags := cmd.Flags() flags.IntVarP(&querier.lines, "lines", "n", 100, "number of initial lines to show") flags.BoolVar(&querier.stderr, "err", false, "view stderr output instead of stdout") flags.BoolVar(&querier.noFollow, "no-follow", false, "one-shot snapshot, do not follow new output") return cmd } // Run executes the peek command. func (q *PeekQuerier) Run(jobID string) error { serverInfo, err := q.configManager.GetServerInfo() if err != nil { return err } // Convert job ID to API format apiJobID, err := format.ToAPIJobID(jobID) if err != nil { return fmt.Errorf("invalid job ID %q: %w", jobID, err) } apiClient := request.NewAPIClient(serverInfo.URL) apiClient.SetRootCAs(serverInfo.CACert) // Determine stream type streamType := "stdout" if q.stderr { streamType = "stderr" } // Setup signal handling for graceful exit sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) offset := int64(0) lines := q.lines for { // Check for interrupt select { case <-sigChan: fmt.Println() return nil default: } // Build query params params := map[string]string{ "type": streamType, } if offset == 0 { params["lines"] = strconv.Itoa(lines) } else { params["offset"] = strconv.FormatInt(offset, 10) } // Fetch output resp, err := apiClient.GetJobOutput(serverInfo.Token, serverInfo.CACert, apiJobID, params) if err != nil { return err } // Check for error in response if resp.Code != 200 { return fmt.Errorf("job <%s>: %s", jobID, resp.Msg) } // Print new content if resp.Data.Content != "" { fmt.Print(resp.Data.Content) } // Update offset for next poll offset = resp.Data.Offset // If done, exit if resp.Data.Done { return nil } // If one-shot mode, exit after first fetch if q.noFollow { return nil } // Poll interval time.Sleep(500 * time.Millisecond) } }