myxce/src/frontend/xcecli/cmd/jobs/peek.go

153 lines
3.3 KiB
Go

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)
}
}