bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package busdiscover
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/larksuite/cli/internal/lockfile"
|
|
"github.com/larksuite/cli/internal/vfs"
|
|
)
|
|
|
|
const (
|
|
pidFileName = "bus.pid"
|
|
aliveLockFileName = "bus.alive.lock"
|
|
)
|
|
|
|
// Handle keeps the lifetime lock fd alive; OS releases on process exit.
|
|
type Handle struct {
|
|
lock *lockfile.LockFile
|
|
}
|
|
|
|
// Release is for tests only; production lets process exit release the lock.
|
|
func (h *Handle) Release() error {
|
|
if h == nil || h.lock == nil {
|
|
return nil
|
|
}
|
|
return h.lock.Unlock()
|
|
}
|
|
|
|
// WritePIDFile takes the alive lock and atomically writes pid + RFC3339 start time.
|
|
// Returns lockfile.ErrHeld if another bus holds the lock.
|
|
func WritePIDFile(eventsDir string, pid int) (*Handle, error) {
|
|
if err := vfs.MkdirAll(eventsDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("busdiscover: mkdir %s: %w", eventsDir, err)
|
|
}
|
|
lock := lockfile.New(filepath.Join(eventsDir, aliveLockFileName))
|
|
if err := lock.TryLock(); err != nil {
|
|
return nil, err
|
|
}
|
|
pidPath := filepath.Join(eventsDir, pidFileName)
|
|
tmpPath := pidPath + ".tmp"
|
|
payload := fmt.Sprintf("%d\n%s\n", pid, time.Now().UTC().Format(time.RFC3339))
|
|
if err := vfs.WriteFile(tmpPath, []byte(payload), 0600); err != nil {
|
|
_ = lock.Unlock()
|
|
return nil, fmt.Errorf("busdiscover: write pid tmp: %w", err)
|
|
}
|
|
if err := vfs.Rename(tmpPath, pidPath); err != nil {
|
|
_ = vfs.Remove(tmpPath)
|
|
_ = lock.Unlock()
|
|
return nil, fmt.Errorf("busdiscover: rename pid file: %w", err)
|
|
}
|
|
return &Handle{lock: lock}, nil
|
|
}
|
|
|
|
func readPIDFile(eventsDir string) (int, time.Time, error) {
|
|
pidPath := filepath.Join(eventsDir, pidFileName)
|
|
data, err := vfs.ReadFile(pidPath)
|
|
if err != nil {
|
|
return 0, time.Time{}, err
|
|
}
|
|
lines := strings.SplitN(strings.TrimSpace(string(data)), "\n", 2)
|
|
if len(lines) < 2 {
|
|
return 0, time.Time{}, fmt.Errorf("busdiscover: malformed pid file %s", pidPath)
|
|
}
|
|
pid, err := strconv.Atoi(strings.TrimSpace(lines[0]))
|
|
if err != nil {
|
|
return 0, time.Time{}, fmt.Errorf("busdiscover: malformed pid in %s: %w", pidPath, err)
|
|
}
|
|
startTime, err := time.Parse(time.RFC3339, strings.TrimSpace(lines[1]))
|
|
if err != nil {
|
|
return 0, time.Time{}, fmt.Errorf("busdiscover: malformed timestamp in %s: %w", pidPath, err)
|
|
}
|
|
return pid, startTime, nil
|
|
}
|
|
|
|
// isBusAlive: try-lock the alive file. ErrHeld = live holder; success = stale (release immediately).
|
|
func isBusAlive(appDir string) bool {
|
|
lockPath := filepath.Join(appDir, aliveLockFileName)
|
|
if _, err := vfs.Stat(lockPath); err != nil {
|
|
return false
|
|
}
|
|
probe := lockfile.New(lockPath)
|
|
err := probe.TryLock()
|
|
if errors.Is(err, lockfile.ErrHeld) {
|
|
return true
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[busdiscover] probe %s: %v\n", lockPath, err) //nolint:forbidigo // internal diagnostic; scanner has no IOStreams plumbing
|
|
return false
|
|
}
|
|
_ = probe.Unlock()
|
|
return false
|
|
}
|
|
|
|
func scanLiveBuses(eventsDir string) ([]Process, error) {
|
|
entries, err := vfs.ReadDir(eventsDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("busdiscover: read events dir: %w", err)
|
|
}
|
|
var result []Process
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
appID := e.Name()
|
|
appDir := filepath.Join(eventsDir, appID)
|
|
if !isBusAlive(appDir) {
|
|
continue
|
|
}
|
|
pid, startTime, err := readPIDFile(appDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "[busdiscover] live bus at %s but pid file unreadable: %v\n", appDir, err) //nolint:forbidigo // internal diagnostic; scanner has no IOStreams plumbing
|
|
result = append(result, Process{PID: 0, AppID: appID})
|
|
continue
|
|
}
|
|
result = append(result, Process{PID: pid, AppID: appID, StartTime: startTime})
|
|
}
|
|
return result, nil
|
|
}
|