status cmd
This commit is contained in:
@@ -28,6 +28,10 @@ Run the local dev server.
|
|||||||
|
|
||||||
Syncs `pb_public`, `pb_migrations`, and `pb_hooks`, then restarts the remote PocketBase service. The command will automatically run `setup` if the PocketBase binary isn’t present on the remote.
|
Syncs `pb_public`, `pb_migrations`, and `pb_hooks`, then restarts the remote PocketBase service. The command will automatically run `setup` if the PocketBase binary isn’t present on the remote.
|
||||||
|
|
||||||
|
### `status`
|
||||||
|
|
||||||
|
Show the remote PocketBase systemd service state along with its configured port, PID, and uptime.
|
||||||
|
|
||||||
### `setup`
|
### `setup`
|
||||||
|
|
||||||
Run everything required to deploy an application to a fresh host. This will:
|
Run everything required to deploy an application to a fresh host. This will:
|
||||||
|
|||||||
150
main.go
150
main.go
@@ -78,6 +78,7 @@ func defaultCommands() []command {
|
|||||||
{name: "dev", description: "run the PocketBase binary locally", action: runDev},
|
{name: "dev", description: "run the PocketBase binary locally", action: runDev},
|
||||||
{name: "setup", description: "provision the remote server and install PocketBase", action: runSetup},
|
{name: "setup", description: "provision the remote server and install PocketBase", action: runSetup},
|
||||||
{name: "deploy", description: "sync migrations/hooks/static assets (runs setup if needed)", action: runDeploy},
|
{name: "deploy", description: "sync migrations/hooks/static assets (runs setup if needed)", action: runDeploy},
|
||||||
|
{name: "status", description: "show status for the remote PocketBase service", action: runStatus},
|
||||||
{name: "logs", description: "show PocketBase logs", action: runLogs},
|
{name: "logs", description: "show PocketBase logs", action: runLogs},
|
||||||
{name: "secrets", description: "manage deployment secrets", action: runSecrets},
|
{name: "secrets", description: "manage deployment secrets", action: runSecrets},
|
||||||
}
|
}
|
||||||
@@ -897,9 +898,12 @@ const (
|
|||||||
totalSetupSteps = 5
|
totalSetupSteps = 5
|
||||||
remoteWindowSize = 10
|
remoteWindowSize = 10
|
||||||
remoteIndent = " "
|
remoteIndent = " "
|
||||||
remoteLineColor = "\033[96m"
|
remoteLineColor = ""
|
||||||
headerColor = "\033[95m"
|
headerColor = "\033[1;37m"
|
||||||
localTimeColor = "\033[92m"
|
localTimeColor = "\033[0;32m"
|
||||||
|
statusActiveColor = "\033[92m"
|
||||||
|
statusWarnColor = "\033[93m"
|
||||||
|
statusFailColor = "\033[91m"
|
||||||
remoteColorReset = "\033[0m"
|
remoteColorReset = "\033[0m"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1112,6 +1116,146 @@ func runDeploy() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runStatus() error {
|
||||||
|
ctx, err := buildDeploymentContext()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer closeSSHControlMaster(ctx.serverIP)
|
||||||
|
|
||||||
|
printInfo("Service", fmt.Sprintf("pb@%s", ctx.serviceName), remoteLineColor)
|
||||||
|
printInfo("Server", ctx.serverIP, remoteLineColor)
|
||||||
|
printInfo("Domain", ctx.domain, remoteLineColor)
|
||||||
|
printInfo("Port", fmt.Sprintf("%d", ctx.port), remoteLineColor)
|
||||||
|
|
||||||
|
props, err := querySystemdProperties(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to query PocketBase service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := strings.TrimSpace(props["ActiveState"])
|
||||||
|
if state == "" {
|
||||||
|
state = "unknown"
|
||||||
|
}
|
||||||
|
statusLine := state
|
||||||
|
if sub := strings.TrimSpace(props["SubState"]); sub != "" {
|
||||||
|
statusLine = fmt.Sprintf("%s (%s)", state, sub)
|
||||||
|
}
|
||||||
|
printInfo("Status", statusLine, statusColorFor(state))
|
||||||
|
|
||||||
|
if pid := strings.TrimSpace(props["ExecMainPID"]); pid != "" && pid != "0" {
|
||||||
|
printInfo("PID", pid, remoteLineColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if started := strings.TrimSpace(props["ActiveEnterTimestamp"]); started != "" {
|
||||||
|
printInfo("Active since", started, localTimeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uptime := computeUptime(ctx.serverIP, props["ActiveEnterTimestampMonotonic"]); uptime > 0 {
|
||||||
|
printInfo("Uptime", formatDuration(uptime), localTimeColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printInfo(label, value, valueColor string) {
|
||||||
|
if valueColor == "" {
|
||||||
|
valueColor = remoteLineColor
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s:%s %s%s%s\n", headerColor, label, remoteColorReset, valueColor, value, remoteColorReset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusColorFor(state string) string {
|
||||||
|
switch strings.ToLower(state) {
|
||||||
|
case "active":
|
||||||
|
return statusActiveColor
|
||||||
|
case "activating", "deactivating":
|
||||||
|
return statusWarnColor
|
||||||
|
case "failed", "inactive":
|
||||||
|
return statusFailColor
|
||||||
|
default:
|
||||||
|
return remoteLineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// querySystemdProperties asks systemd for a few service properties.
|
||||||
|
func querySystemdProperties(ctx *deploymentContext) (map[string]string, error) {
|
||||||
|
script := fmt.Sprintf(`set -euo pipefail
|
||||||
|
systemctl show pb@%s -p ActiveState -p SubState -p ActiveEnterTimestamp -p ActiveEnterTimestampMonotonic -p ExecMainPID
|
||||||
|
`, ctx.serviceName)
|
||||||
|
output, err := runSSHCollect(ctx.serverIP, script)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseKeyValueLines(output), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeUptime(server, startMicro string) time.Duration {
|
||||||
|
startMicro = strings.TrimSpace(startMicro)
|
||||||
|
if startMicro == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(startMicro, 10, 64)
|
||||||
|
if err != nil || start <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
now, err := remoteMonotonicMicro(server)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: unable to compute uptime: %v\n", err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
diff := now - start
|
||||||
|
if diff <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return time.Duration(diff) * time.Microsecond
|
||||||
|
}
|
||||||
|
|
||||||
|
func remoteMonotonicMicro(server string) (int64, error) {
|
||||||
|
output, err := runSSHOutput(server, `awk '{printf "%d", $1*1000000}' /proc/uptime`)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return strconv.ParseInt(strings.TrimSpace(output), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeyValueLines(input string) map[string]string {
|
||||||
|
props := make(map[string]string)
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(input))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if idx := strings.Index(line, "="); idx > 0 {
|
||||||
|
key := strings.TrimSpace(line[:idx])
|
||||||
|
value := strings.TrimSpace(line[idx+1:])
|
||||||
|
props[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
if d < time.Second {
|
||||||
|
return d.Round(time.Millisecond).String()
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, 3)
|
||||||
|
if hours := d / time.Hour; hours > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%dh", hours))
|
||||||
|
d -= hours * time.Hour
|
||||||
|
}
|
||||||
|
if mins := d / time.Minute; mins > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%dm", mins))
|
||||||
|
d -= mins * time.Minute
|
||||||
|
}
|
||||||
|
seconds := int64(d / time.Second)
|
||||||
|
if seconds > 0 || len(parts) == 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%ds", seconds))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
func runLogs() error {
|
func runLogs() error {
|
||||||
ctx, err := buildDeploymentContext()
|
ctx, err := buildDeploymentContext()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user