blob: 62b2fc495912b3f2cca48ea1cd0401ff3f4594ca [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package ps is ps subcommand to list up active steps of ninja build.
package ps
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"time"
"github.com/maruel/subcommands"
"go.chromium.org/luci/common/cli"
"go.chromium.org/luci/common/system/signals"
"infra/build/siso/build"
"infra/build/siso/ui"
)
func Cmd() *subcommands.Command {
return &subcommands.Command{
UsageLine: "ps [-C dir]",
ShortDesc: "display running steps of ninja build",
LongDesc: `Display running steps of ninja build.
$ siso ps [-C dir]
`,
CommandRun: func() subcommands.CommandRun {
c := &run{}
c.init()
return c
},
}
}
type run struct {
subcommands.CommandRunBase
dir string
n int
interval time.Duration
termui bool
wd string
}
func (c *run) init() {
c.Flags.StringVar(&c.dir, "C", "", "ninja running directory")
c.Flags.IntVar(&c.n, "n", 0, "limit number of steps if it is positive")
c.Flags.DurationVar(&c.interval, "interval", -1, "query interval if it is positive. default 1s on terminal")
}
func (c *run) Run(a subcommands.Application, args []string, env subcommands.Env) int {
ctx := cli.GetContext(a, c, env)
ctx, cancel := context.WithCancel(ctx)
defer signals.HandleInterrupt(func() {
cancel()
})()
u, ok := ui.Default.(*ui.TermUI)
if ok {
c.termui = true
if c.n == 0 {
c.n = u.Height() - 2
}
if c.interval < 0 {
c.interval = 1 * time.Second
}
}
err := os.Chdir(c.dir)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to chdir %s: %v\n", c.dir, err)
return 1
}
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get wd: %v\n", err)
return 1
}
c.wd = wd
ret := 0
for {
buf, err := os.ReadFile(".siso_port")
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
if c.termui {
fmt.Fprintf(os.Stderr, "\033[H\033[JSiso is not running in %s?\n", c.wd)
} else {
fmt.Fprintf(os.Stderr, "Siso is not running in %s?\n", c.wd)
}
} else {
if c.termui {
fmt.Fprintf(os.Stderr, "\033[H\033[JSiso is not running in %s? failed to read .siso_port: %v\n", c.wd, err)
} else {
fmt.Fprintf(os.Stderr, "Siso is not running in %s? failed to read .siso_port: %v\n", c.wd, err)
}
}
ret = 1
} else {
err = c.run(ctx, string(buf))
if err != nil {
fmt.Fprintf(os.Stderr, "failed to get info: %v\n", err)
ret = 1
}
}
if c.interval <= 0 {
break
}
select {
case <-time.After(c.interval):
case <-ctx.Done():
return ret
}
}
return ret
}
func (c *run) run(ctx context.Context, addr string) error {
resp, err := http.Get(fmt.Sprintf("http://%s/api/active_steps", addr))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("/api/active_steps error: %d %s", resp.StatusCode, resp.Status)
}
buf, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("/api/active_steps read error: %w", err)
}
var activeSteps []build.ActiveStepInfo
err = json.Unmarshal(buf, &activeSteps)
if err != nil {
return fmt.Errorf("/api/active_steps unmarshal error: %w", err)
}
var lines []string
if c.termui {
// move to 0,0 and clear to the end of screen.
lines = append(lines, fmt.Sprintf("\033[H\033[JSiso is running in %s", c.wd))
lines = append(lines, fmt.Sprintf("%10s %9s %s", "DURATION", "PHASE", "DESC"))
} else {
lines = append(lines, "\f\n")
lines = append(lines, fmt.Sprintf("%10s %9s %s\n", "DURATION", "PHASE", "DESC"))
}
for _, as := range activeSteps {
if c.termui {
lines = append(lines, fmt.Sprintf("%10s %9s %s", as.Dur, as.Phase, as.Desc))
} else {
lines = append(lines, fmt.Sprintf("%10s %9s %s\n", as.Dur, as.Phase, as.Desc))
}
if c.n > 0 && len(lines) >= c.n {
break
}
}
lines = append(lines, fmt.Sprintf("steps=%d out of %d\n", len(lines)-2, len(activeSteps)))
ui.Default.PrintLines(lines...)
return nil
}