153 lines
3.3 KiB
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)
|
|
}
|
|
}
|