Compare commits
16 commits
dev
...
ax/review-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
861c88b8e8 | ||
|
|
2a0bc19f6a | ||
| 356a644918 | |||
|
|
94b99bfd18 | ||
| 8296075202 | |||
|
|
cca45eebcc | ||
| 542bf0db32 | |||
|
|
eefcb292c4 | ||
|
|
2ccd84b87a | ||
|
|
1425023862 | ||
|
|
62623ce0d6 | ||
|
|
e9bb6a8968 | ||
|
|
1b4efe0f67 | ||
|
|
5d316650e2 | ||
|
|
5a1812bfd3 | ||
|
|
87f53ad8dd |
29 changed files with 1087 additions and 868 deletions
|
|
@ -24,8 +24,7 @@ The package has three layers, all in the root `process` package (plus a `exec` s
|
|||
|
||||
`Service` is a Core service (`*core.ServiceRuntime[Options]`) that manages all `Process` instances. It spawns subprocesses, pipes stdout/stderr through goroutines, captures output to a `RingBuffer`, and broadcasts IPC actions (`ActionProcessStarted`, `ActionProcessOutput`, `ActionProcessExited`, `ActionProcessKilled` — defined in actions.go).
|
||||
|
||||
The legacy global singleton API (`process_global.go`) was removed in favor of
|
||||
explicit Core service registration.
|
||||
During `OnStartup`, the service registers the named Core actions `process.run`, `process.start`, `process.kill`, `process.list`, and `process.get`. Core's `Process` primitive delegates to those actions, so `c.Process().Run(...)` only works when this service has been registered.
|
||||
|
||||
### Layer 2: Daemon Lifecycle (daemon.go, pidfile.go, health.go, registry.go)
|
||||
|
||||
|
|
@ -53,12 +52,12 @@ Builder-pattern wrapper around `os/exec` with structured logging via a pluggable
|
|||
- **Graceful shutdown**: `Service.OnShutdown` kills all running processes. `Daemon.Stop()` performs ordered teardown: sets health to not-ready → shuts down health server → releases PID file → unregisters from registry. `DaemonOptions.ShutdownTimeout` (default 30 s) bounds the shutdown context.
|
||||
- **Auto-registration**: Pass a `Registry` and `RegistryEntry` in `DaemonOptions` to automatically register the daemon on `Start()` and unregister on `Stop()`.
|
||||
- **PID liveness checks**: Both `PIDFile` and `Registry` use `proc.Signal(syscall.Signal(0))` to check if a PID is alive before trusting stored state.
|
||||
- **Error handling**: All errors MUST use `core.E()`, never `fmt.Errorf` or
|
||||
`errors.New`. Sentinel errors are package-level vars created with `core.E("", "message", nil)`.
|
||||
- **Error handling**: All errors MUST use `coreerr.E()` from `go-log` (imported as `coreerr`), never `fmt.Errorf` or `errors.New`. Sentinel errors are package-level vars created with `coreerr.E("", "message", nil)`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `dappco.re/go/core` — Core DI framework, IPC actions, `ServiceRuntime`
|
||||
- `dappco.re/go/core/log` — Structured error constructor (`coreerr.E()`)
|
||||
- `dappco.re/go/core/io` — Filesystem abstraction (`coreio.Local`) used by PIDFile and Registry
|
||||
- `github.com/stretchr/testify` — test assertions (require/assert)
|
||||
|
||||
|
|
|
|||
119
actions.go
119
actions.go
|
|
@ -1,12 +1,6 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"dappco.re/go/core"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// --- ACTION messages (broadcast via Core.ACTION) ---
|
||||
|
||||
|
|
@ -41,114 +35,3 @@ type ActionProcessKilled struct {
|
|||
ID string
|
||||
Signal string
|
||||
}
|
||||
|
||||
// --- Core Action Handlers ---------------------------------------------------
|
||||
|
||||
func (s *Service) handleRun(ctx context.Context, opts core.Options) core.Result {
|
||||
command := opts.String("command")
|
||||
if command == "" {
|
||||
return core.Result{Value: core.E("process.run", "command is required", nil), OK: false}
|
||||
}
|
||||
|
||||
runOpts := RunOptions{
|
||||
Command: command,
|
||||
Dir: opts.String("dir"),
|
||||
}
|
||||
if r := opts.Get("args"); r.OK {
|
||||
runOpts.Args = optionStrings(r.Value)
|
||||
}
|
||||
if r := opts.Get("env"); r.OK {
|
||||
runOpts.Env = optionStrings(r.Value)
|
||||
}
|
||||
|
||||
return s.runCommand(ctx, runOpts)
|
||||
}
|
||||
|
||||
func (s *Service) handleStart(ctx context.Context, opts core.Options) core.Result {
|
||||
command := opts.String("command")
|
||||
if command == "" {
|
||||
return core.Result{Value: core.E("process.start", "command is required", nil), OK: false}
|
||||
}
|
||||
|
||||
runOpts := RunOptions{
|
||||
Command: command,
|
||||
Dir: opts.String("dir"),
|
||||
Detach: opts.Bool("detach"),
|
||||
}
|
||||
if r := opts.Get("args"); r.OK {
|
||||
runOpts.Args = optionStrings(r.Value)
|
||||
}
|
||||
if r := opts.Get("env"); r.OK {
|
||||
runOpts.Env = optionStrings(r.Value)
|
||||
}
|
||||
|
||||
r := s.StartWithOptions(ctx, runOpts)
|
||||
if !r.OK {
|
||||
return r
|
||||
}
|
||||
return core.Result{Value: r.Value.(*ManagedProcess).ID, OK: true}
|
||||
}
|
||||
|
||||
func (s *Service) handleKill(_ context.Context, opts core.Options) core.Result {
|
||||
id := opts.String("id")
|
||||
if id != "" {
|
||||
if err := s.Kill(id); err != nil {
|
||||
if core.Is(err, ErrProcessNotFound) {
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("not found: ", id), nil), OK: false}
|
||||
}
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("kill failed: ", id), err), OK: false}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
||||
pid := opts.Int("pid")
|
||||
if pid > 0 {
|
||||
proc, err := processHandle(pid)
|
||||
if err != nil {
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("find pid failed: ", core.Sprintf("%d", pid)), err), OK: false}
|
||||
}
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("signal failed: ", core.Sprintf("%d", pid)), err), OK: false}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
||||
return core.Result{Value: core.E("process.kill", "need id or pid", nil), OK: false}
|
||||
}
|
||||
|
||||
func (s *Service) handleList(_ context.Context, _ core.Options) core.Result {
|
||||
return core.Result{Value: s.managed.Names(), OK: true}
|
||||
}
|
||||
|
||||
func (s *Service) handleGet(_ context.Context, opts core.Options) core.Result {
|
||||
id := opts.String("id")
|
||||
if id == "" {
|
||||
return core.Result{Value: core.E("process.get", "id is required", nil), OK: false}
|
||||
}
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
return core.Result{Value: core.E("process.get", core.Concat("not found: ", id), err), OK: false}
|
||||
}
|
||||
return core.Result{Value: proc.Info(), OK: true}
|
||||
}
|
||||
|
||||
func optionStrings(value any) []string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case []string:
|
||||
return append([]string(nil), typed...)
|
||||
case []any:
|
||||
result := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
text, ok := item.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
result = append(result, text)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
buffer.go
16
buffer.go
|
|
@ -19,17 +19,13 @@ type RingBuffer struct {
|
|||
//
|
||||
// rb := process.NewRingBuffer(256)
|
||||
func NewRingBuffer(size int) *RingBuffer {
|
||||
if size <= 0 {
|
||||
size = 1
|
||||
}
|
||||
|
||||
return &RingBuffer{
|
||||
data: make([]byte, size),
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Write appends data to the buffer, overwriting oldest data if full.
|
||||
// _, _ = rb.Write([]byte("output line\n"))
|
||||
func (rb *RingBuffer) Write(p []byte) (n int, err error) {
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
|
|
@ -47,7 +43,7 @@ func (rb *RingBuffer) Write(p []byte) (n int, err error) {
|
|||
return len(p), nil
|
||||
}
|
||||
|
||||
// String returns the buffer contents as a string.
|
||||
// output := rb.String() // returns all buffered output as a string
|
||||
func (rb *RingBuffer) String() string {
|
||||
rb.mu.RLock()
|
||||
defer rb.mu.RUnlock()
|
||||
|
|
@ -66,7 +62,7 @@ func (rb *RingBuffer) String() string {
|
|||
return string(rb.data[rb.start:rb.end])
|
||||
}
|
||||
|
||||
// Bytes returns a copy of the buffer contents.
|
||||
// data := rb.Bytes() // returns nil if empty
|
||||
func (rb *RingBuffer) Bytes() []byte {
|
||||
rb.mu.RLock()
|
||||
defer rb.mu.RUnlock()
|
||||
|
|
@ -87,7 +83,7 @@ func (rb *RingBuffer) Bytes() []byte {
|
|||
return result
|
||||
}
|
||||
|
||||
// Len returns the current length of data in the buffer.
|
||||
// byteCount := rb.Len() // 0 when empty, Cap() when full
|
||||
func (rb *RingBuffer) Len() int {
|
||||
rb.mu.RLock()
|
||||
defer rb.mu.RUnlock()
|
||||
|
|
@ -101,12 +97,12 @@ func (rb *RingBuffer) Len() int {
|
|||
return rb.size - rb.start + rb.end
|
||||
}
|
||||
|
||||
// Cap returns the buffer capacity.
|
||||
// capacity := rb.Cap() // fixed at construction time
|
||||
func (rb *RingBuffer) Cap() int {
|
||||
return rb.size
|
||||
}
|
||||
|
||||
// Reset clears the buffer.
|
||||
// rb.Reset() // discard all buffered output
|
||||
func (rb *RingBuffer) Reset() {
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
|
|
|
|||
108
buffer_test.go
108
buffer_test.go
|
|
@ -1,18 +1,19 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRingBuffer_Basics_Good(t *testing.T) {
|
||||
func TestBuffer_Write_Good(t *testing.T) {
|
||||
t.Run("write and read", func(t *testing.T) {
|
||||
rb := NewRingBuffer(10)
|
||||
|
||||
n, err := rb.Write([]byte("hello"))
|
||||
itemCount, err := rb.Write([]byte("hello"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, n)
|
||||
assert.Equal(t, 5, itemCount)
|
||||
assert.Equal(t, "hello", rb.String())
|
||||
assert.Equal(t, 5, rb.Len())
|
||||
})
|
||||
|
|
@ -38,14 +39,79 @@ func TestRingBuffer_Basics_Good(t *testing.T) {
|
|||
assert.Equal(t, 10, rb.Len())
|
||||
})
|
||||
|
||||
t.Run("empty buffer", func(t *testing.T) {
|
||||
t.Run("bytes returns copy", func(t *testing.T) {
|
||||
rb := NewRingBuffer(10)
|
||||
_, _ = rb.Write([]byte("hello"))
|
||||
|
||||
contents := rb.Bytes()
|
||||
assert.Equal(t, []byte("hello"), contents)
|
||||
|
||||
// Modifying returned bytes shouldn't affect buffer
|
||||
contents[0] = 'x'
|
||||
assert.Equal(t, "hello", rb.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_Write_Bad(t *testing.T) {
|
||||
t.Run("empty write is a no-op", func(t *testing.T) {
|
||||
rb := NewRingBuffer(10)
|
||||
itemCount, err := rb.Write([]byte{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, itemCount)
|
||||
assert.Equal(t, "", rb.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_Write_Ugly(t *testing.T) {
|
||||
t.Run("concurrent writes do not race", func(t *testing.T) {
|
||||
rb := NewRingBuffer(64)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = rb.Write([]byte("data"))
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
// Buffer should not panic and length should be bounded by capacity
|
||||
assert.LessOrEqual(t, rb.Len(), rb.Cap())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_String_Good(t *testing.T) {
|
||||
t.Run("empty buffer returns empty string", func(t *testing.T) {
|
||||
rb := NewRingBuffer(10)
|
||||
assert.Equal(t, "", rb.String())
|
||||
assert.Equal(t, 0, rb.Len())
|
||||
assert.Nil(t, rb.Bytes())
|
||||
})
|
||||
|
||||
t.Run("reset", func(t *testing.T) {
|
||||
t.Run("full buffer wraps correctly", func(t *testing.T) {
|
||||
rb := NewRingBuffer(5)
|
||||
_, _ = rb.Write([]byte("abcde"))
|
||||
assert.Equal(t, "abcde", rb.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_String_Bad(t *testing.T) {
|
||||
t.Run("overflowed buffer reflects newest data", func(t *testing.T) {
|
||||
rb := NewRingBuffer(5)
|
||||
_, _ = rb.Write([]byte("hello"))
|
||||
_, _ = rb.Write([]byte("world"))
|
||||
// Oldest bytes ("hello") have been overwritten
|
||||
assert.Equal(t, "world", rb.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_String_Ugly(t *testing.T) {
|
||||
t.Run("size-1 buffer holds only last byte", func(t *testing.T) {
|
||||
rb := NewRingBuffer(1)
|
||||
_, _ = rb.Write([]byte("abc"))
|
||||
assert.Equal(t, "c", rb.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_Reset_Good(t *testing.T) {
|
||||
t.Run("clears all data", func(t *testing.T) {
|
||||
rb := NewRingBuffer(10)
|
||||
_, _ = rb.Write([]byte("hello"))
|
||||
rb.Reset()
|
||||
|
|
@ -53,20 +119,28 @@ func TestRingBuffer_Basics_Good(t *testing.T) {
|
|||
assert.Equal(t, 0, rb.Len())
|
||||
})
|
||||
|
||||
t.Run("cap", func(t *testing.T) {
|
||||
t.Run("cap returns buffer capacity", func(t *testing.T) {
|
||||
rb := NewRingBuffer(42)
|
||||
assert.Equal(t, 42, rb.Cap())
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("bytes returns copy", func(t *testing.T) {
|
||||
func TestBuffer_Reset_Bad(t *testing.T) {
|
||||
t.Run("reset on empty buffer is a no-op", func(t *testing.T) {
|
||||
rb := NewRingBuffer(10)
|
||||
_, _ = rb.Write([]byte("hello"))
|
||||
|
||||
bytes := rb.Bytes()
|
||||
assert.Equal(t, []byte("hello"), bytes)
|
||||
|
||||
// Modifying returned bytes shouldn't affect buffer
|
||||
bytes[0] = 'x'
|
||||
assert.Equal(t, "hello", rb.String())
|
||||
rb.Reset()
|
||||
assert.Equal(t, "", rb.String())
|
||||
assert.Equal(t, 0, rb.Len())
|
||||
})
|
||||
}
|
||||
|
||||
func TestBuffer_Reset_Ugly(t *testing.T) {
|
||||
t.Run("reset after overflow allows fresh writes", func(t *testing.T) {
|
||||
rb := NewRingBuffer(5)
|
||||
_, _ = rb.Write([]byte("hello"))
|
||||
_, _ = rb.Write([]byte("world"))
|
||||
rb.Reset()
|
||||
_, _ = rb.Write([]byte("new"))
|
||||
assert.Equal(t, "new", rb.String())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
14
daemon.go
14
daemon.go
|
|
@ -72,6 +72,8 @@ func NewDaemon(opts DaemonOptions) *Daemon {
|
|||
}
|
||||
|
||||
// Start initialises the daemon (PID file, health server).
|
||||
//
|
||||
// err := daemon.Start()
|
||||
func (d *Daemon) Start() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
|
@ -122,6 +124,8 @@ func (d *Daemon) Start() error {
|
|||
}
|
||||
|
||||
// Run blocks until the context is cancelled.
|
||||
//
|
||||
// err := daemon.Run(ctx)
|
||||
func (d *Daemon) Run(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
if !d.running {
|
||||
|
|
@ -136,6 +140,8 @@ func (d *Daemon) Run(ctx context.Context) error {
|
|||
}
|
||||
|
||||
// Stop performs graceful shutdown.
|
||||
//
|
||||
// err := daemon.Stop()
|
||||
func (d *Daemon) Stop() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
|
@ -164,9 +170,7 @@ func (d *Daemon) Stop() error {
|
|||
|
||||
// Auto-unregister
|
||||
if d.opts.Registry != nil {
|
||||
if err := d.opts.Registry.Unregister(d.opts.RegistryEntry.Code, d.opts.RegistryEntry.Daemon); err != nil {
|
||||
errs = append(errs, core.E("daemon.stop", "registry", err))
|
||||
}
|
||||
_ = d.opts.Registry.Unregister(d.opts.RegistryEntry.Code, d.opts.RegistryEntry.Daemon)
|
||||
}
|
||||
|
||||
d.running = false
|
||||
|
|
@ -178,6 +182,8 @@ func (d *Daemon) Stop() error {
|
|||
}
|
||||
|
||||
// SetReady sets the daemon readiness status for health checks.
|
||||
//
|
||||
// daemon.SetReady(true)
|
||||
func (d *Daemon) SetReady(ready bool) {
|
||||
if d.health != nil {
|
||||
d.health.SetReady(ready)
|
||||
|
|
@ -185,6 +191,8 @@ func (d *Daemon) SetReady(ready bool) {
|
|||
}
|
||||
|
||||
// HealthAddr returns the health server address, or empty if disabled.
|
||||
//
|
||||
// addr := daemon.HealthAddr() // e.g. "127.0.0.1:9090"
|
||||
func (d *Daemon) HealthAddr() string {
|
||||
if d.health != nil {
|
||||
return d.health.Addr()
|
||||
|
|
|
|||
|
|
@ -163,3 +163,28 @@ func TestDaemon_AutoRegister_Good(t *testing.T) {
|
|||
_, ok = reg.Get("test-app", "serve")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestDaemon_Lifecycle_Ugly(t *testing.T) {
|
||||
t.Run("stop called twice is safe", func(t *testing.T) {
|
||||
d := NewDaemon(DaemonOptions{
|
||||
HealthAddr: "127.0.0.1:0",
|
||||
})
|
||||
|
||||
err := d.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = d.Stop()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Second stop should be a no-op
|
||||
err = d.Stop()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("set ready with no health server is a no-op", func(t *testing.T) {
|
||||
d := NewDaemon(DaemonOptions{})
|
||||
// Should not panic
|
||||
d.SetReady(true)
|
||||
d.SetReady(false)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ go-process provides: c.Action("process.run", s.handleRun)
|
|||
|
||||
Without go-process registered, `c.Process().Run()` returns `Result{OK: false}`. Permission-by-registration.
|
||||
|
||||
### Current State (2026-03-30)
|
||||
### Current State (2026-03-25)
|
||||
|
||||
The codebase now matches the v0.8.0 target. The bullets below are the historical migration delta that was closed out:
|
||||
The codebase is PRE-migration. The RFC describes the v0.8.0 target. What exists today:
|
||||
|
||||
- `service.go` — `NewService(opts) func(*Core) (any, error)` — **old factory signature**. Change to `Register(c *Core) core.Result`
|
||||
- `OnStartup() error` / `OnShutdown() error` — **Change** to return `core.Result`
|
||||
|
|
@ -44,7 +44,7 @@ daemon.go — DaemonEntry, managed daemon lifecycle
|
|||
health.go — health check endpoints
|
||||
pidfile.go — PID file management
|
||||
buffer.go — output buffering
|
||||
actions.go — Action payloads and Core action handlers
|
||||
actions.go — WILL CONTAIN Action handlers after migration
|
||||
global.go — global Default() singleton — DELETE after migration
|
||||
```
|
||||
|
||||
|
|
|
|||
23
exec/exec.go
23
exec/exec.go
|
|
@ -19,6 +19,8 @@ type Options struct {
|
|||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
// If true, command will run in background (not implemented in this wrapper yet)
|
||||
// Background bool
|
||||
}
|
||||
|
||||
// Command wraps `os/exec.Command` with logging and context.
|
||||
|
|
@ -86,7 +88,7 @@ func (c *Cmd) Run() error {
|
|||
c.logDebug("executing command")
|
||||
|
||||
if err := c.cmd.Run(); err != nil {
|
||||
wrapped := wrapError("exec.cmd.run", err, c.name, c.args)
|
||||
wrapped := wrapError("Cmd.Run", err, c.name, c.args)
|
||||
c.logError("command failed", wrapped)
|
||||
return wrapped
|
||||
}
|
||||
|
|
@ -100,7 +102,7 @@ func (c *Cmd) Output() ([]byte, error) {
|
|||
|
||||
out, err := c.cmd.Output()
|
||||
if err != nil {
|
||||
wrapped := wrapError("exec.cmd.output", err, c.name, c.args)
|
||||
wrapped := wrapError("Cmd.Output", err, c.name, c.args)
|
||||
c.logError("command failed", wrapped)
|
||||
return nil, wrapped
|
||||
}
|
||||
|
|
@ -114,7 +116,7 @@ func (c *Cmd) CombinedOutput() ([]byte, error) {
|
|||
|
||||
out, err := c.cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
wrapped := wrapError("exec.cmd.combined_output", err, c.name, c.args)
|
||||
wrapped := wrapError("Cmd.CombinedOutput", err, c.name, c.args)
|
||||
c.logError("command failed", wrapped)
|
||||
return out, wrapped
|
||||
}
|
||||
|
|
@ -122,13 +124,16 @@ func (c *Cmd) CombinedOutput() ([]byte, error) {
|
|||
}
|
||||
|
||||
func (c *Cmd) prepare() {
|
||||
ctx := c.ctx
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
if c.ctx != nil {
|
||||
c.cmd = exec.CommandContext(c.ctx, c.name, c.args...)
|
||||
} else {
|
||||
// Should we enforce context? The issue says "Enforce context usage".
|
||||
// For now, let's allow nil but log a warning if we had a logger?
|
||||
// Or strictly panic/error?
|
||||
// Let's fallback to Background for now but maybe strict later.
|
||||
c.cmd = exec.Command(c.name, c.args...)
|
||||
}
|
||||
|
||||
c.cmd = exec.CommandContext(ctx, c.name, c.args...)
|
||||
|
||||
c.cmd.Dir = c.opts.Dir
|
||||
if len(c.opts.Env) > 0 {
|
||||
c.cmd.Env = append(os.Environ(), c.opts.Env...)
|
||||
|
|
@ -147,7 +152,7 @@ func RunQuiet(ctx context.Context, name string, args ...string) error {
|
|||
var stderr bytes.Buffer
|
||||
cmd := Command(ctx, name, args...).WithStderr(&stderr)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return core.E("exec.run_quiet", core.Trim(stderr.String()), err)
|
||||
return core.E("RunQuiet", core.Trim(stderr.String()), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,14 +71,6 @@ func TestCommand_Run_Bad(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCommand_Run_WithNilContext_Good(t *testing.T) {
|
||||
var ctx context.Context
|
||||
|
||||
if err := exec.Command(ctx, "echo", "hello").Run(); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommand_Output_Good(t *testing.T) {
|
||||
logger := &mockLogger{}
|
||||
ctx := context.Background()
|
||||
|
|
@ -219,3 +211,81 @@ func TestRunQuiet_Command_Bad(t *testing.T) {
|
|||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommand_Run_Ugly(t *testing.T) {
|
||||
t.Run("cancelled context terminates command", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
err := exec.Command(ctx, "sleep", "10").
|
||||
WithLogger(&mockLogger{}).
|
||||
Run()
|
||||
if err == nil {
|
||||
t.Fatal("expected error from cancelled context")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommand_Output_Bad(t *testing.T) {
|
||||
t.Run("non-zero exit returns error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, err := exec.Command(ctx, "sh", "-c", "exit 1").
|
||||
WithLogger(&mockLogger{}).
|
||||
Output()
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommand_Output_Ugly(t *testing.T) {
|
||||
t.Run("non-existent command returns error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, err := exec.Command(ctx, "nonexistent_command_xyz_abc").
|
||||
WithLogger(&mockLogger{}).
|
||||
Output()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent command")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommand_CombinedOutput_Bad(t *testing.T) {
|
||||
t.Run("non-zero exit returns output and error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
out, err := exec.Command(ctx, "sh", "-c", "echo stderr >&2; exit 1").
|
||||
WithLogger(&mockLogger{}).
|
||||
CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if string(out) == "" {
|
||||
t.Error("expected combined output even on failure")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommand_CombinedOutput_Ugly(t *testing.T) {
|
||||
t.Run("command with no output returns empty bytes", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
out, err := exec.Command(ctx, "true").
|
||||
WithLogger(&mockLogger{}).
|
||||
CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(out) != 0 {
|
||||
t.Errorf("expected empty output, got %q", string(out))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunQuiet_Command_Ugly(t *testing.T) {
|
||||
t.Run("non-existent command returns error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
err := exec.RunQuiet(ctx, "nonexistent_command_xyz_abc")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent command")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -6,15 +6,15 @@ require (
|
|||
dappco.re/go/core v0.8.0-alpha.1
|
||||
dappco.re/go/core/io v0.2.0
|
||||
dappco.re/go/core/ws v0.3.0
|
||||
dappco.re/go/core/api v0.1.5
|
||||
forge.lthn.ai/core/api v0.1.5
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
dappco.re/go/core/log v0.1.0 // indirect
|
||||
dappco.re/go/core/io v0.1.5 // indirect
|
||||
dappco.re/go/core/log v0.0.4 // indirect
|
||||
forge.lthn.ai/core/go-io v0.1.5 // indirect
|
||||
forge.lthn.ai/core/go-log v0.0.4 // indirect
|
||||
github.com/99designs/gqlgen v0.17.88 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||
|
|
|
|||
31
health.go
31
health.go
|
|
@ -38,13 +38,16 @@ func NewHealthServer(addr string) *HealthServer {
|
|||
}
|
||||
|
||||
// AddCheck registers a health check function.
|
||||
//
|
||||
// health.AddCheck(func() error { return db.Ping() })
|
||||
func (h *HealthServer) AddCheck(check HealthCheck) {
|
||||
h.mu.Lock()
|
||||
h.checks = append(h.checks, check)
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetReady sets the readiness status.
|
||||
// health.SetReady(true) // mark ready for traffic
|
||||
// health.SetReady(false) // mark not-ready during shutdown
|
||||
func (h *HealthServer) SetReady(ready bool) {
|
||||
h.mu.Lock()
|
||||
h.ready = ready
|
||||
|
|
@ -52,6 +55,8 @@ func (h *HealthServer) SetReady(ready bool) {
|
|||
}
|
||||
|
||||
// Start begins serving health check endpoints.
|
||||
//
|
||||
// err := health.Start()
|
||||
func (h *HealthServer) Start() error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
|
@ -92,33 +97,29 @@ func (h *HealthServer) Start() error {
|
|||
return core.E("health.start", core.Concat("failed to listen on ", h.addr), err)
|
||||
}
|
||||
|
||||
server := &http.Server{Handler: mux}
|
||||
h.listener = listener
|
||||
h.server = server
|
||||
h.server = &http.Server{Handler: mux}
|
||||
|
||||
go func(srv *http.Server, ln net.Listener) {
|
||||
_ = srv.Serve(ln)
|
||||
}(server, listener)
|
||||
go func() {
|
||||
_ = h.server.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully shuts down the health server.
|
||||
//
|
||||
// err := health.Stop(ctx)
|
||||
func (h *HealthServer) Stop(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
server := h.server
|
||||
h.server = nil
|
||||
h.listener = nil
|
||||
h.mu.Unlock()
|
||||
|
||||
if server == nil {
|
||||
if h.server == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return server.Shutdown(ctx)
|
||||
return h.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// Addr returns the actual address the server is listening on.
|
||||
//
|
||||
// addr := health.Addr() // e.g. "127.0.0.1:9090"
|
||||
func (h *HealthServer) Addr() string {
|
||||
if h.listener != nil {
|
||||
return h.listener.Addr().String()
|
||||
|
|
|
|||
|
|
@ -66,12 +66,6 @@ func TestHealthServer_WithChecks_Good(t *testing.T) {
|
|||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestHealthServer_StopImmediately_Good(t *testing.T) {
|
||||
hs := NewHealthServer("127.0.0.1:0")
|
||||
require.NoError(t, hs.Start())
|
||||
require.NoError(t, hs.Stop(context.Background()))
|
||||
}
|
||||
|
||||
func TestWaitForHealth_Reachable_Good(t *testing.T) {
|
||||
hs := NewHealthServer("127.0.0.1:0")
|
||||
require.NoError(t, hs.Start())
|
||||
|
|
@ -85,3 +79,33 @@ func TestWaitForHealth_Unreachable_Bad(t *testing.T) {
|
|||
ok := WaitForHealth("127.0.0.1:19999", 500)
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestHealthServer_Endpoints_Bad(t *testing.T) {
|
||||
t.Run("listen fails on invalid address", func(t *testing.T) {
|
||||
hs := NewHealthServer("invalid-addr-xyz:99999")
|
||||
err := hs.Start()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHealthServer_Endpoints_Ugly(t *testing.T) {
|
||||
t.Run("addr before start returns configured address", func(t *testing.T) {
|
||||
hs := NewHealthServer("127.0.0.1:0")
|
||||
// Before Start, Addr() returns the configured address (not yet bound)
|
||||
assert.Equal(t, "127.0.0.1:0", hs.Addr())
|
||||
})
|
||||
|
||||
t.Run("stop before start is a no-op", func(t *testing.T) {
|
||||
hs := NewHealthServer("127.0.0.1:0")
|
||||
err := hs.Stop(context.Background())
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWaitForHealth_Reachable_Ugly(t *testing.T) {
|
||||
t.Run("zero timeout returns false immediately", func(t *testing.T) {
|
||||
// With 0ms timeout, should return false without waiting
|
||||
ok := WaitForHealth("127.0.0.1:19998", 0)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,3 +68,37 @@ func TestReadPID_Stale_Bad(t *testing.T) {
|
|||
assert.Equal(t, 999999999, pid)
|
||||
assert.False(t, running)
|
||||
}
|
||||
|
||||
func TestPIDFile_Acquire_Ugly(t *testing.T) {
|
||||
t.Run("double acquire from same instance returns error", func(t *testing.T) {
|
||||
pidPath := core.JoinPath(t.TempDir(), "double.pid")
|
||||
pid := NewPIDFile(pidPath)
|
||||
|
||||
err := pid.Acquire()
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = pid.Release() }()
|
||||
|
||||
// Second acquire should fail — the current process is running
|
||||
err = pid.Acquire()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "another instance is running")
|
||||
})
|
||||
|
||||
t.Run("release of non-existent file returns error", func(t *testing.T) {
|
||||
pidPath := core.JoinPath(t.TempDir(), "gone.pid")
|
||||
pid := NewPIDFile(pidPath)
|
||||
// Release without acquire — file doesn't exist
|
||||
err := pid.Release()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadPID_Missing_Ugly(t *testing.T) {
|
||||
t.Run("zero byte pid file is invalid", func(t *testing.T) {
|
||||
pidPath := core.JoinPath(t.TempDir(), "empty.pid")
|
||||
require.NoError(t, os.WriteFile(pidPath, []byte(""), 0644))
|
||||
pid, running := ReadPID(pidPath)
|
||||
assert.Equal(t, 0, pid)
|
||||
assert.False(t, running)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,12 @@ package api
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
process "dappco.re/go/core/process"
|
||||
"dappco.re/go/core/ws"
|
||||
"forge.lthn.ai/core/api"
|
||||
"forge.lthn.ai/core/api/pkg/provider"
|
||||
process "dappco.re/go/core/process"
|
||||
"dappco.re/go/core/ws"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
|
@ -119,8 +117,6 @@ func (p *ProcessProvider) Describe() []api.RouteDescription {
|
|||
"daemon": map[string]any{"type": "string"},
|
||||
"pid": map[string]any{"type": "integer"},
|
||||
"health": map[string]any{"type": "string"},
|
||||
"project": map[string]any{"type": "string"},
|
||||
"binary": map[string]any{"type": "string"},
|
||||
"started": map[string]any{"type": "string", "format": "date-time"},
|
||||
},
|
||||
},
|
||||
|
|
@ -149,7 +145,6 @@ func (p *ProcessProvider) Describe() []api.RouteDescription {
|
|||
"properties": map[string]any{
|
||||
"healthy": map[string]any{"type": "boolean"},
|
||||
"address": map[string]any{"type": "string"},
|
||||
"reason": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -192,13 +187,8 @@ func (p *ProcessProvider) stopDaemon(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Send SIGTERM to the process
|
||||
proc, err := os.FindProcess(entry.PID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, api.Fail("signal_failed", err.Error()))
|
||||
return
|
||||
}
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
// Send SIGTERM to the process via the process package abstraction
|
||||
if err := process.KillPID(entry.PID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, api.Fail("signal_failed", err.Error()))
|
||||
return
|
||||
}
|
||||
|
|
@ -236,15 +226,10 @@ func (p *ProcessProvider) healthCheck(c *gin.Context) {
|
|||
}
|
||||
|
||||
healthy := process.WaitForHealth(entry.Health, 2000)
|
||||
reason := ""
|
||||
if !healthy {
|
||||
reason = "health endpoint did not report healthy"
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"healthy": healthy,
|
||||
"address": entry.Health,
|
||||
"reason": reason,
|
||||
}
|
||||
|
||||
// Emit health event
|
||||
|
|
@ -252,7 +237,6 @@ func (p *ProcessProvider) healthCheck(c *gin.Context) {
|
|||
"code": code,
|
||||
"daemon": daemon,
|
||||
"healthy": healthy,
|
||||
"reason": reason,
|
||||
})
|
||||
|
||||
statusCode := http.StatusOK
|
||||
|
|
@ -275,15 +259,10 @@ func (p *ProcessProvider) emitEvent(channel string, data any) {
|
|||
|
||||
// PIDAlive checks whether a PID is still running. Exported for use by
|
||||
// consumers that need to verify daemon liveness outside the REST API.
|
||||
//
|
||||
// alive := api.PIDAlive(entry.PID)
|
||||
func PIDAlive(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return proc.Signal(syscall.Signal(0)) == nil
|
||||
return process.IsPIDAlive(pid)
|
||||
}
|
||||
|
||||
// intParam parses a URL param as int, returning 0 on failure.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ package api_test
|
|||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
process "dappco.re/go/core/process"
|
||||
|
|
@ -50,14 +49,6 @@ func TestProcessProvider_Describe_Good(t *testing.T) {
|
|||
assert.NotEmpty(t, d.Summary)
|
||||
assert.NotEmpty(t, d.Tags)
|
||||
}
|
||||
|
||||
for _, d := range descs {
|
||||
if d.Path == "/daemons/:code/:daemon/health" {
|
||||
props, ok := d.Response["properties"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, props, "reason")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessProvider_ListDaemons_Good(t *testing.T) {
|
||||
|
|
@ -90,27 +81,6 @@ func TestProcessProvider_GetDaemon_Bad(t *testing.T) {
|
|||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestProcessProvider_HealthCheck_NoEndpoint_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
registry := newTestRegistry(dir)
|
||||
require.NoError(t, registry.Register(process.DaemonEntry{
|
||||
Code: "test",
|
||||
Daemon: "nohealth",
|
||||
PID: os.Getpid(),
|
||||
}))
|
||||
|
||||
p := processapi.NewProvider(registry, nil)
|
||||
|
||||
r := setupRouter(p)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/process/daemons/test/nohealth/health", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "no health endpoint configured")
|
||||
assert.Contains(t, w.Body.String(), "\"reason\"")
|
||||
}
|
||||
|
||||
func TestProcessProvider_RegistersAsRouteGroup_Good(t *testing.T) {
|
||||
p := processapi.NewProvider(nil, nil)
|
||||
|
||||
|
|
@ -135,6 +105,71 @@ func TestProcessProvider_StreamGroup_Good(t *testing.T) {
|
|||
assert.Contains(t, channels, "process.daemon.started")
|
||||
}
|
||||
|
||||
func TestProcessProvider_ListDaemons_Bad(t *testing.T) {
|
||||
t.Run("get non-existent daemon returns 404", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
registry := newTestRegistry(dir)
|
||||
p := processapi.NewProvider(registry, nil)
|
||||
|
||||
r := setupRouter(p)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/process/daemons/nope/missing", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessProvider_ListDaemons_Ugly(t *testing.T) {
|
||||
t.Run("nil registry falls back to default", func(t *testing.T) {
|
||||
p := processapi.NewProvider(nil, nil)
|
||||
r := setupRouter(p)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/process/daemons", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
// Should succeed — default registry returns empty list
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessProvider_Element_Good(t *testing.T) {
|
||||
p := processapi.NewProvider(nil, nil)
|
||||
element := p.Element()
|
||||
assert.Equal(t, "core-process-panel", element.Tag)
|
||||
assert.NotEmpty(t, element.Source)
|
||||
}
|
||||
|
||||
func TestProcessProvider_Element_Bad(t *testing.T) {
|
||||
t.Run("stop non-existent daemon returns 404", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
registry := newTestRegistry(dir)
|
||||
p := processapi.NewProvider(registry, nil)
|
||||
|
||||
r := setupRouter(p)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/process/daemons/nope/missing/stop", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcessProvider_Element_Ugly(t *testing.T) {
|
||||
t.Run("health check on non-existent daemon returns 404", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
registry := newTestRegistry(dir)
|
||||
p := processapi.NewProvider(registry, nil)
|
||||
|
||||
r := setupRouter(p)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/process/daemons/nope/missing/health", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
})
|
||||
}
|
||||
|
||||
// -- Test helpers -------------------------------------------------------------
|
||||
|
||||
func setupRouter(p *processapi.ProcessProvider) *gin.Engine {
|
||||
|
|
|
|||
525
pkg/api/ui/dist/core-process.js
vendored
525
pkg/api/ui/dist/core-process.js
vendored
|
|
@ -3,10 +3,10 @@
|
|||
* Copyright 2019 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const K = globalThis, se = K.ShadowRoot && (K.ShadyCSS === void 0 || K.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, ie = Symbol(), ae = /* @__PURE__ */ new WeakMap();
|
||||
let ye = class {
|
||||
const V = globalThis, se = V.ShadowRoot && (V.ShadyCSS === void 0 || V.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype, re = Symbol(), ne = /* @__PURE__ */ new WeakMap();
|
||||
let $e = class {
|
||||
constructor(e, t, i) {
|
||||
if (this._$cssResult$ = !0, i !== ie) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
|
||||
if (this._$cssResult$ = !0, i !== re) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");
|
||||
this.cssText = e, this.t = t;
|
||||
}
|
||||
get styleSheet() {
|
||||
|
|
@ -14,7 +14,7 @@ let ye = class {
|
|||
const t = this.t;
|
||||
if (se && e === void 0) {
|
||||
const i = t !== void 0 && t.length === 1;
|
||||
i && (e = ae.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), i && ae.set(t, e));
|
||||
i && (e = ne.get(t)), e === void 0 && ((this.o = e = new CSSStyleSheet()).replaceSync(this.cssText), i && ne.set(t, e));
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
|
@ -22,20 +22,20 @@ let ye = class {
|
|||
return this.cssText;
|
||||
}
|
||||
};
|
||||
const Ae = (s) => new ye(typeof s == "string" ? s : s + "", void 0, ie), q = (s, ...e) => {
|
||||
const t = s.length === 1 ? s[0] : e.reduce((i, o, n) => i + ((r) => {
|
||||
if (r._$cssResult$ === !0) return r.cssText;
|
||||
if (typeof r == "number") return r;
|
||||
throw Error("Value passed to 'css' function must be a 'css' function result: " + r + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
|
||||
})(o) + s[n + 1], s[0]);
|
||||
return new ye(t, s, ie);
|
||||
const Ae = (s) => new $e(typeof s == "string" ? s : s + "", void 0, re), B = (s, ...e) => {
|
||||
const t = s.length === 1 ? s[0] : e.reduce((i, r, n) => i + ((o) => {
|
||||
if (o._$cssResult$ === !0) return o.cssText;
|
||||
if (typeof o == "number") return o;
|
||||
throw Error("Value passed to 'css' function must be a 'css' function result: " + o + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.");
|
||||
})(r) + s[n + 1], s[0]);
|
||||
return new $e(t, s, re);
|
||||
}, ke = (s, e) => {
|
||||
if (se) s.adoptedStyleSheets = e.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet);
|
||||
else for (const t of e) {
|
||||
const i = document.createElement("style"), o = K.litNonce;
|
||||
o !== void 0 && i.setAttribute("nonce", o), i.textContent = t.cssText, s.appendChild(i);
|
||||
const i = document.createElement("style"), r = V.litNonce;
|
||||
r !== void 0 && i.setAttribute("nonce", r), i.textContent = t.cssText, s.appendChild(i);
|
||||
}
|
||||
}, le = se ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((e) => {
|
||||
}, ae = se ? (s) => s : (s) => s instanceof CSSStyleSheet ? ((e) => {
|
||||
let t = "";
|
||||
for (const i of e.cssRules) t += i.cssText;
|
||||
return Ae(t);
|
||||
|
|
@ -45,7 +45,7 @@ const Ae = (s) => new ye(typeof s == "string" ? s : s + "", void 0, ie), q = (s,
|
|||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const { is: Se, defineProperty: Pe, getOwnPropertyDescriptor: Ce, getOwnPropertyNames: Ee, getOwnPropertySymbols: Ue, getPrototypeOf: Oe } = Object, A = globalThis, ce = A.trustedTypes, ze = ce ? ce.emptyScript : "", X = A.reactiveElementPolyfillSupport, j = (s, e) => s, J = { toAttribute(s, e) {
|
||||
const { is: Se, defineProperty: Pe, getOwnPropertyDescriptor: Ce, getOwnPropertyNames: Ee, getOwnPropertySymbols: Ue, getPrototypeOf: Oe } = Object, A = globalThis, le = A.trustedTypes, ze = le ? le.emptyScript : "", X = A.reactiveElementPolyfillSupport, j = (s, e) => s, J = { toAttribute(s, e) {
|
||||
switch (e) {
|
||||
case Boolean:
|
||||
s = s ? ze : null;
|
||||
|
|
@ -73,7 +73,7 @@ const { is: Se, defineProperty: Pe, getOwnPropertyDescriptor: Ce, getOwnProperty
|
|||
}
|
||||
}
|
||||
return t;
|
||||
} }, oe = (s, e) => !Se(s, e), de = { attribute: !0, type: String, converter: J, reflect: !1, useDefault: !1, hasChanged: oe };
|
||||
} }, ie = (s, e) => !Se(s, e), ce = { attribute: !0, type: String, converter: J, reflect: !1, useDefault: !1, hasChanged: ie };
|
||||
Symbol.metadata ?? (Symbol.metadata = Symbol("metadata")), A.litPropertyMetadata ?? (A.litPropertyMetadata = /* @__PURE__ */ new WeakMap());
|
||||
let D = class extends HTMLElement {
|
||||
static addInitializer(e) {
|
||||
|
|
@ -82,25 +82,25 @@ let D = class extends HTMLElement {
|
|||
static get observedAttributes() {
|
||||
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
||||
}
|
||||
static createProperty(e, t = de) {
|
||||
static createProperty(e, t = ce) {
|
||||
if (t.state && (t.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(e) && ((t = Object.create(t)).wrapped = !0), this.elementProperties.set(e, t), !t.noAccessor) {
|
||||
const i = Symbol(), o = this.getPropertyDescriptor(e, i, t);
|
||||
o !== void 0 && Pe(this.prototype, e, o);
|
||||
const i = Symbol(), r = this.getPropertyDescriptor(e, i, t);
|
||||
r !== void 0 && Pe(this.prototype, e, r);
|
||||
}
|
||||
}
|
||||
static getPropertyDescriptor(e, t, i) {
|
||||
const { get: o, set: n } = Ce(this.prototype, e) ?? { get() {
|
||||
const { get: r, set: n } = Ce(this.prototype, e) ?? { get() {
|
||||
return this[t];
|
||||
}, set(r) {
|
||||
this[t] = r;
|
||||
}, set(o) {
|
||||
this[t] = o;
|
||||
} };
|
||||
return { get: o, set(r) {
|
||||
const l = o == null ? void 0 : o.call(this);
|
||||
n == null || n.call(this, r), this.requestUpdate(e, l, i);
|
||||
return { get: r, set(o) {
|
||||
const l = r == null ? void 0 : r.call(this);
|
||||
n == null || n.call(this, o), this.requestUpdate(e, l, i);
|
||||
}, configurable: !0, enumerable: !0 };
|
||||
}
|
||||
static getPropertyOptions(e) {
|
||||
return this.elementProperties.get(e) ?? de;
|
||||
return this.elementProperties.get(e) ?? ce;
|
||||
}
|
||||
static _$Ei() {
|
||||
if (this.hasOwnProperty(j("elementProperties"))) return;
|
||||
|
|
@ -111,17 +111,17 @@ let D = class extends HTMLElement {
|
|||
if (this.hasOwnProperty(j("finalized"))) return;
|
||||
if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(j("properties"))) {
|
||||
const t = this.properties, i = [...Ee(t), ...Ue(t)];
|
||||
for (const o of i) this.createProperty(o, t[o]);
|
||||
for (const r of i) this.createProperty(r, t[r]);
|
||||
}
|
||||
const e = this[Symbol.metadata];
|
||||
if (e !== null) {
|
||||
const t = litPropertyMetadata.get(e);
|
||||
if (t !== void 0) for (const [i, o] of t) this.elementProperties.set(i, o);
|
||||
if (t !== void 0) for (const [i, r] of t) this.elementProperties.set(i, r);
|
||||
}
|
||||
this._$Eh = /* @__PURE__ */ new Map();
|
||||
for (const [t, i] of this.elementProperties) {
|
||||
const o = this._$Eu(t, i);
|
||||
o !== void 0 && this._$Eh.set(o, t);
|
||||
const r = this._$Eu(t, i);
|
||||
r !== void 0 && this._$Eh.set(r, t);
|
||||
}
|
||||
this.elementStyles = this.finalizeStyles(this.styles);
|
||||
}
|
||||
|
|
@ -129,8 +129,8 @@ let D = class extends HTMLElement {
|
|||
const t = [];
|
||||
if (Array.isArray(e)) {
|
||||
const i = new Set(e.flat(1 / 0).reverse());
|
||||
for (const o of i) t.unshift(le(o));
|
||||
} else e !== void 0 && t.push(le(e));
|
||||
for (const r of i) t.unshift(ae(r));
|
||||
} else e !== void 0 && t.push(ae(e));
|
||||
return t;
|
||||
}
|
||||
static _$Eu(e, t) {
|
||||
|
|
@ -182,33 +182,33 @@ let D = class extends HTMLElement {
|
|||
}
|
||||
_$ET(e, t) {
|
||||
var n;
|
||||
const i = this.constructor.elementProperties.get(e), o = this.constructor._$Eu(e, i);
|
||||
if (o !== void 0 && i.reflect === !0) {
|
||||
const r = (((n = i.converter) == null ? void 0 : n.toAttribute) !== void 0 ? i.converter : J).toAttribute(t, i.type);
|
||||
this._$Em = e, r == null ? this.removeAttribute(o) : this.setAttribute(o, r), this._$Em = null;
|
||||
const i = this.constructor.elementProperties.get(e), r = this.constructor._$Eu(e, i);
|
||||
if (r !== void 0 && i.reflect === !0) {
|
||||
const o = (((n = i.converter) == null ? void 0 : n.toAttribute) !== void 0 ? i.converter : J).toAttribute(t, i.type);
|
||||
this._$Em = e, o == null ? this.removeAttribute(r) : this.setAttribute(r, o), this._$Em = null;
|
||||
}
|
||||
}
|
||||
_$AK(e, t) {
|
||||
var n, r;
|
||||
const i = this.constructor, o = i._$Eh.get(e);
|
||||
if (o !== void 0 && this._$Em !== o) {
|
||||
const l = i.getPropertyOptions(o), a = typeof l.converter == "function" ? { fromAttribute: l.converter } : ((n = l.converter) == null ? void 0 : n.fromAttribute) !== void 0 ? l.converter : J;
|
||||
this._$Em = o;
|
||||
var n, o;
|
||||
const i = this.constructor, r = i._$Eh.get(e);
|
||||
if (r !== void 0 && this._$Em !== r) {
|
||||
const l = i.getPropertyOptions(r), a = typeof l.converter == "function" ? { fromAttribute: l.converter } : ((n = l.converter) == null ? void 0 : n.fromAttribute) !== void 0 ? l.converter : J;
|
||||
this._$Em = r;
|
||||
const p = a.fromAttribute(t, l.type);
|
||||
this[o] = p ?? ((r = this._$Ej) == null ? void 0 : r.get(o)) ?? p, this._$Em = null;
|
||||
this[r] = p ?? ((o = this._$Ej) == null ? void 0 : o.get(r)) ?? p, this._$Em = null;
|
||||
}
|
||||
}
|
||||
requestUpdate(e, t, i, o = !1, n) {
|
||||
var r;
|
||||
requestUpdate(e, t, i, r = !1, n) {
|
||||
var o;
|
||||
if (e !== void 0) {
|
||||
const l = this.constructor;
|
||||
if (o === !1 && (n = this[e]), i ?? (i = l.getPropertyOptions(e)), !((i.hasChanged ?? oe)(n, t) || i.useDefault && i.reflect && n === ((r = this._$Ej) == null ? void 0 : r.get(e)) && !this.hasAttribute(l._$Eu(e, i)))) return;
|
||||
if (r === !1 && (n = this[e]), i ?? (i = l.getPropertyOptions(e)), !((i.hasChanged ?? ie)(n, t) || i.useDefault && i.reflect && n === ((o = this._$Ej) == null ? void 0 : o.get(e)) && !this.hasAttribute(l._$Eu(e, i)))) return;
|
||||
this.C(e, t, i);
|
||||
}
|
||||
this.isUpdatePending === !1 && (this._$ES = this._$EP());
|
||||
}
|
||||
C(e, t, { useDefault: i, reflect: o, wrapped: n }, r) {
|
||||
i && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(e) && (this._$Ej.set(e, r ?? t ?? this[e]), n !== !0 || r !== void 0) || (this._$AL.has(e) || (this.hasUpdated || i || (t = void 0), this._$AL.set(e, t)), o === !0 && this._$Em !== e && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(e));
|
||||
C(e, t, { useDefault: i, reflect: r, wrapped: n }, o) {
|
||||
i && !(this._$Ej ?? (this._$Ej = /* @__PURE__ */ new Map())).has(e) && (this._$Ej.set(e, o ?? t ?? this[e]), n !== !0 || o !== void 0) || (this._$AL.has(e) || (this.hasUpdated || i || (t = void 0), this._$AL.set(e, t)), r === !0 && this._$Em !== e && (this._$Eq ?? (this._$Eq = /* @__PURE__ */ new Set())).add(e));
|
||||
}
|
||||
async _$EP() {
|
||||
this.isUpdatePending = !0;
|
||||
|
|
@ -228,24 +228,24 @@ let D = class extends HTMLElement {
|
|||
if (!this.isUpdatePending) return;
|
||||
if (!this.hasUpdated) {
|
||||
if (this.renderRoot ?? (this.renderRoot = this.createRenderRoot()), this._$Ep) {
|
||||
for (const [n, r] of this._$Ep) this[n] = r;
|
||||
for (const [n, o] of this._$Ep) this[n] = o;
|
||||
this._$Ep = void 0;
|
||||
}
|
||||
const o = this.constructor.elementProperties;
|
||||
if (o.size > 0) for (const [n, r] of o) {
|
||||
const { wrapped: l } = r, a = this[n];
|
||||
l !== !0 || this._$AL.has(n) || a === void 0 || this.C(n, void 0, r, a);
|
||||
const r = this.constructor.elementProperties;
|
||||
if (r.size > 0) for (const [n, o] of r) {
|
||||
const { wrapped: l } = o, a = this[n];
|
||||
l !== !0 || this._$AL.has(n) || a === void 0 || this.C(n, void 0, o, a);
|
||||
}
|
||||
}
|
||||
let e = !1;
|
||||
const t = this._$AL;
|
||||
try {
|
||||
e = this.shouldUpdate(t), e ? (this.willUpdate(t), (i = this._$EO) == null || i.forEach((o) => {
|
||||
e = this.shouldUpdate(t), e ? (this.willUpdate(t), (i = this._$EO) == null || i.forEach((r) => {
|
||||
var n;
|
||||
return (n = o.hostUpdate) == null ? void 0 : n.call(o);
|
||||
return (n = r.hostUpdate) == null ? void 0 : n.call(r);
|
||||
}), this.update(t)) : this._$EM();
|
||||
} catch (o) {
|
||||
throw e = !1, this._$EM(), o;
|
||||
} catch (r) {
|
||||
throw e = !1, this._$EM(), r;
|
||||
}
|
||||
e && this._$AE(t);
|
||||
}
|
||||
|
|
@ -254,8 +254,8 @@ let D = class extends HTMLElement {
|
|||
_$AE(e) {
|
||||
var t;
|
||||
(t = this._$EO) == null || t.forEach((i) => {
|
||||
var o;
|
||||
return (o = i.hostUpdated) == null ? void 0 : o.call(i);
|
||||
var r;
|
||||
return (r = i.hostUpdated) == null ? void 0 : r.call(i);
|
||||
}), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(e)), this.updated(e);
|
||||
}
|
||||
_$EM() {
|
||||
|
|
@ -284,68 +284,68 @@ D.elementStyles = [], D.shadowRootOptions = { mode: "open" }, D[j("elementProper
|
|||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const N = globalThis, he = (s) => s, Z = N.trustedTypes, pe = Z ? Z.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, ve = "$lit$", x = `lit$${Math.random().toFixed(9).slice(2)}$`, we = "?" + x, De = `<${we}>`, U = document, I = () => U.createComment(""), L = (s) => s === null || typeof s != "object" && typeof s != "function", re = Array.isArray, Te = (s) => re(s) || typeof (s == null ? void 0 : s[Symbol.iterator]) == "function", Y = `[
|
||||
\f\r]`, H = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, ue = /-->/g, me = />/g, P = RegExp(`>|${Y}(?:([^\\s"'>=/]+)(${Y}*=${Y}*(?:[^
|
||||
\f\r"'\`<>=]|("|')|))|$)`, "g"), fe = /'/g, ge = /"/g, _e = /^(?:script|style|textarea|title)$/i, Me = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), c = Me(1), T = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), be = /* @__PURE__ */ new WeakMap(), C = U.createTreeWalker(U, 129);
|
||||
function xe(s, e) {
|
||||
if (!re(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||||
return pe !== void 0 ? pe.createHTML(e) : e;
|
||||
const N = globalThis, de = (s) => s, Z = N.trustedTypes, he = Z ? Z.createPolicy("lit-html", { createHTML: (s) => s }) : void 0, ye = "$lit$", x = `lit$${Math.random().toFixed(9).slice(2)}$`, ve = "?" + x, De = `<${ve}>`, E = document, I = () => E.createComment(""), L = (s) => s === null || typeof s != "object" && typeof s != "function", oe = Array.isArray, Te = (s) => oe(s) || typeof (s == null ? void 0 : s[Symbol.iterator]) == "function", Y = `[
|
||||
\f\r]`, H = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, pe = /-->/g, ue = />/g, S = RegExp(`>|${Y}(?:([^\\s"'>=/]+)(${Y}*=${Y}*(?:[^
|
||||
\f\r"'\`<>=]|("|')|))|$)`, "g"), me = /'/g, fe = /"/g, _e = /^(?:script|style|textarea|title)$/i, Me = (s) => (e, ...t) => ({ _$litType$: s, strings: e, values: t }), c = Me(1), T = Symbol.for("lit-noChange"), d = Symbol.for("lit-nothing"), ge = /* @__PURE__ */ new WeakMap(), P = E.createTreeWalker(E, 129);
|
||||
function we(s, e) {
|
||||
if (!oe(s) || !s.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||||
return he !== void 0 ? he.createHTML(e) : e;
|
||||
}
|
||||
const Re = (s, e) => {
|
||||
const t = s.length - 1, i = [];
|
||||
let o, n = e === 2 ? "<svg>" : e === 3 ? "<math>" : "", r = H;
|
||||
let r, n = e === 2 ? "<svg>" : e === 3 ? "<math>" : "", o = H;
|
||||
for (let l = 0; l < t; l++) {
|
||||
const a = s[l];
|
||||
let p, m, h = -1, $ = 0;
|
||||
for (; $ < a.length && (r.lastIndex = $, m = r.exec(a), m !== null); ) $ = r.lastIndex, r === H ? m[1] === "!--" ? r = ue : m[1] !== void 0 ? r = me : m[2] !== void 0 ? (_e.test(m[2]) && (o = RegExp("</" + m[2], "g")), r = P) : m[3] !== void 0 && (r = P) : r === P ? m[0] === ">" ? (r = o ?? H, h = -1) : m[1] === void 0 ? h = -2 : (h = r.lastIndex - m[2].length, p = m[1], r = m[3] === void 0 ? P : m[3] === '"' ? ge : fe) : r === ge || r === fe ? r = P : r === ue || r === me ? r = H : (r = P, o = void 0);
|
||||
const _ = r === P && s[l + 1].startsWith("/>") ? " " : "";
|
||||
n += r === H ? a + De : h >= 0 ? (i.push(p), a.slice(0, h) + ve + a.slice(h) + x + _) : a + x + (h === -2 ? l : _);
|
||||
let p, m, h = -1, b = 0;
|
||||
for (; b < a.length && (o.lastIndex = b, m = o.exec(a), m !== null); ) b = o.lastIndex, o === H ? m[1] === "!--" ? o = pe : m[1] !== void 0 ? o = ue : m[2] !== void 0 ? (_e.test(m[2]) && (r = RegExp("</" + m[2], "g")), o = S) : m[3] !== void 0 && (o = S) : o === S ? m[0] === ">" ? (o = r ?? H, h = -1) : m[1] === void 0 ? h = -2 : (h = o.lastIndex - m[2].length, p = m[1], o = m[3] === void 0 ? S : m[3] === '"' ? fe : me) : o === fe || o === me ? o = S : o === pe || o === ue ? o = H : (o = S, r = void 0);
|
||||
const w = o === S && s[l + 1].startsWith("/>") ? " " : "";
|
||||
n += o === H ? a + De : h >= 0 ? (i.push(p), a.slice(0, h) + ye + a.slice(h) + x + w) : a + x + (h === -2 ? l : w);
|
||||
}
|
||||
return [xe(s, n + (s[t] || "<?>") + (e === 2 ? "</svg>" : e === 3 ? "</math>" : "")), i];
|
||||
return [we(s, n + (s[t] || "<?>") + (e === 2 ? "</svg>" : e === 3 ? "</math>" : "")), i];
|
||||
};
|
||||
class W {
|
||||
class q {
|
||||
constructor({ strings: e, _$litType$: t }, i) {
|
||||
let o;
|
||||
let r;
|
||||
this.parts = [];
|
||||
let n = 0, r = 0;
|
||||
let n = 0, o = 0;
|
||||
const l = e.length - 1, a = this.parts, [p, m] = Re(e, t);
|
||||
if (this.el = W.createElement(p, i), C.currentNode = this.el.content, t === 2 || t === 3) {
|
||||
if (this.el = q.createElement(p, i), P.currentNode = this.el.content, t === 2 || t === 3) {
|
||||
const h = this.el.content.firstChild;
|
||||
h.replaceWith(...h.childNodes);
|
||||
}
|
||||
for (; (o = C.nextNode()) !== null && a.length < l; ) {
|
||||
if (o.nodeType === 1) {
|
||||
if (o.hasAttributes()) for (const h of o.getAttributeNames()) if (h.endsWith(ve)) {
|
||||
const $ = m[r++], _ = o.getAttribute(h).split(x), V = /([.?@])?(.*)/.exec($);
|
||||
a.push({ type: 1, index: n, name: V[2], strings: _, ctor: V[1] === "." ? je : V[1] === "?" ? Ne : V[1] === "@" ? Ie : G }), o.removeAttribute(h);
|
||||
} else h.startsWith(x) && (a.push({ type: 6, index: n }), o.removeAttribute(h));
|
||||
if (_e.test(o.tagName)) {
|
||||
const h = o.textContent.split(x), $ = h.length - 1;
|
||||
if ($ > 0) {
|
||||
o.textContent = Z ? Z.emptyScript : "";
|
||||
for (let _ = 0; _ < $; _++) o.append(h[_], I()), C.nextNode(), a.push({ type: 2, index: ++n });
|
||||
o.append(h[$], I());
|
||||
for (; (r = P.nextNode()) !== null && a.length < l; ) {
|
||||
if (r.nodeType === 1) {
|
||||
if (r.hasAttributes()) for (const h of r.getAttributeNames()) if (h.endsWith(ye)) {
|
||||
const b = m[o++], w = r.getAttribute(h).split(x), K = /([.?@])?(.*)/.exec(b);
|
||||
a.push({ type: 1, index: n, name: K[2], strings: w, ctor: K[1] === "." ? je : K[1] === "?" ? Ne : K[1] === "@" ? Ie : G }), r.removeAttribute(h);
|
||||
} else h.startsWith(x) && (a.push({ type: 6, index: n }), r.removeAttribute(h));
|
||||
if (_e.test(r.tagName)) {
|
||||
const h = r.textContent.split(x), b = h.length - 1;
|
||||
if (b > 0) {
|
||||
r.textContent = Z ? Z.emptyScript : "";
|
||||
for (let w = 0; w < b; w++) r.append(h[w], I()), P.nextNode(), a.push({ type: 2, index: ++n });
|
||||
r.append(h[b], I());
|
||||
}
|
||||
}
|
||||
} else if (o.nodeType === 8) if (o.data === we) a.push({ type: 2, index: n });
|
||||
} else if (r.nodeType === 8) if (r.data === ve) a.push({ type: 2, index: n });
|
||||
else {
|
||||
let h = -1;
|
||||
for (; (h = o.data.indexOf(x, h + 1)) !== -1; ) a.push({ type: 7, index: n }), h += x.length - 1;
|
||||
for (; (h = r.data.indexOf(x, h + 1)) !== -1; ) a.push({ type: 7, index: n }), h += x.length - 1;
|
||||
}
|
||||
n++;
|
||||
}
|
||||
}
|
||||
static createElement(e, t) {
|
||||
const i = U.createElement("template");
|
||||
const i = E.createElement("template");
|
||||
return i.innerHTML = e, i;
|
||||
}
|
||||
}
|
||||
function M(s, e, t = s, i) {
|
||||
var r, l;
|
||||
var o, l;
|
||||
if (e === T) return e;
|
||||
let o = i !== void 0 ? (r = t._$Co) == null ? void 0 : r[i] : t._$Cl;
|
||||
let r = i !== void 0 ? (o = t._$Co) == null ? void 0 : o[i] : t._$Cl;
|
||||
const n = L(e) ? void 0 : e._$litDirective$;
|
||||
return (o == null ? void 0 : o.constructor) !== n && ((l = o == null ? void 0 : o._$AO) == null || l.call(o, !1), n === void 0 ? o = void 0 : (o = new n(s), o._$AT(s, t, i)), i !== void 0 ? (t._$Co ?? (t._$Co = []))[i] = o : t._$Cl = o), o !== void 0 && (e = M(s, o._$AS(s, e.values), o, i)), e;
|
||||
return (r == null ? void 0 : r.constructor) !== n && ((l = r == null ? void 0 : r._$AO) == null || l.call(r, !1), n === void 0 ? r = void 0 : (r = new n(s), r._$AT(s, t, i)), i !== void 0 ? (t._$Co ?? (t._$Co = []))[i] = r : t._$Cl = r), r !== void 0 && (e = M(s, r._$AS(s, e.values), r, i)), e;
|
||||
}
|
||||
class He {
|
||||
constructor(e, t) {
|
||||
|
|
@ -358,30 +358,30 @@ class He {
|
|||
return this._$AM._$AU;
|
||||
}
|
||||
u(e) {
|
||||
const { el: { content: t }, parts: i } = this._$AD, o = ((e == null ? void 0 : e.creationScope) ?? U).importNode(t, !0);
|
||||
C.currentNode = o;
|
||||
let n = C.nextNode(), r = 0, l = 0, a = i[0];
|
||||
const { el: { content: t }, parts: i } = this._$AD, r = ((e == null ? void 0 : e.creationScope) ?? E).importNode(t, !0);
|
||||
P.currentNode = r;
|
||||
let n = P.nextNode(), o = 0, l = 0, a = i[0];
|
||||
for (; a !== void 0; ) {
|
||||
if (r === a.index) {
|
||||
if (o === a.index) {
|
||||
let p;
|
||||
a.type === 2 ? p = new B(n, n.nextSibling, this, e) : a.type === 1 ? p = new a.ctor(n, a.name, a.strings, this, e) : a.type === 6 && (p = new Le(n, this, e)), this._$AV.push(p), a = i[++l];
|
||||
a.type === 2 ? p = new W(n, n.nextSibling, this, e) : a.type === 1 ? p = new a.ctor(n, a.name, a.strings, this, e) : a.type === 6 && (p = new Le(n, this, e)), this._$AV.push(p), a = i[++l];
|
||||
}
|
||||
r !== (a == null ? void 0 : a.index) && (n = C.nextNode(), r++);
|
||||
o !== (a == null ? void 0 : a.index) && (n = P.nextNode(), o++);
|
||||
}
|
||||
return C.currentNode = U, o;
|
||||
return P.currentNode = E, r;
|
||||
}
|
||||
p(e) {
|
||||
let t = 0;
|
||||
for (const i of this._$AV) i !== void 0 && (i.strings !== void 0 ? (i._$AI(e, i, t), t += i.strings.length - 2) : i._$AI(e[t])), t++;
|
||||
}
|
||||
}
|
||||
class B {
|
||||
class W {
|
||||
get _$AU() {
|
||||
var e;
|
||||
return ((e = this._$AM) == null ? void 0 : e._$AU) ?? this._$Cv;
|
||||
}
|
||||
constructor(e, t, i, o) {
|
||||
this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = e, this._$AB = t, this._$AM = i, this.options = o, this._$Cv = (o == null ? void 0 : o.isConnected) ?? !0;
|
||||
constructor(e, t, i, r) {
|
||||
this.type = 2, this._$AH = d, this._$AN = void 0, this._$AA = e, this._$AB = t, this._$AM = i, this.options = r, this._$Cv = (r == null ? void 0 : r.isConnected) ?? !0;
|
||||
}
|
||||
get parentNode() {
|
||||
let e = this._$AA.parentNode;
|
||||
|
|
@ -404,33 +404,33 @@ class B {
|
|||
this._$AH !== e && (this._$AR(), this._$AH = this.O(e));
|
||||
}
|
||||
_(e) {
|
||||
this._$AH !== d && L(this._$AH) ? this._$AA.nextSibling.data = e : this.T(U.createTextNode(e)), this._$AH = e;
|
||||
this._$AH !== d && L(this._$AH) ? this._$AA.nextSibling.data = e : this.T(E.createTextNode(e)), this._$AH = e;
|
||||
}
|
||||
$(e) {
|
||||
var n;
|
||||
const { values: t, _$litType$: i } = e, o = typeof i == "number" ? this._$AC(e) : (i.el === void 0 && (i.el = W.createElement(xe(i.h, i.h[0]), this.options)), i);
|
||||
if (((n = this._$AH) == null ? void 0 : n._$AD) === o) this._$AH.p(t);
|
||||
const { values: t, _$litType$: i } = e, r = typeof i == "number" ? this._$AC(e) : (i.el === void 0 && (i.el = q.createElement(we(i.h, i.h[0]), this.options)), i);
|
||||
if (((n = this._$AH) == null ? void 0 : n._$AD) === r) this._$AH.p(t);
|
||||
else {
|
||||
const r = new He(o, this), l = r.u(this.options);
|
||||
r.p(t), this.T(l), this._$AH = r;
|
||||
const o = new He(r, this), l = o.u(this.options);
|
||||
o.p(t), this.T(l), this._$AH = o;
|
||||
}
|
||||
}
|
||||
_$AC(e) {
|
||||
let t = be.get(e.strings);
|
||||
return t === void 0 && be.set(e.strings, t = new W(e)), t;
|
||||
let t = ge.get(e.strings);
|
||||
return t === void 0 && ge.set(e.strings, t = new q(e)), t;
|
||||
}
|
||||
k(e) {
|
||||
re(this._$AH) || (this._$AH = [], this._$AR());
|
||||
oe(this._$AH) || (this._$AH = [], this._$AR());
|
||||
const t = this._$AH;
|
||||
let i, o = 0;
|
||||
for (const n of e) o === t.length ? t.push(i = new B(this.O(I()), this.O(I()), this, this.options)) : i = t[o], i._$AI(n), o++;
|
||||
o < t.length && (this._$AR(i && i._$AB.nextSibling, o), t.length = o);
|
||||
let i, r = 0;
|
||||
for (const n of e) r === t.length ? t.push(i = new W(this.O(I()), this.O(I()), this, this.options)) : i = t[r], i._$AI(n), r++;
|
||||
r < t.length && (this._$AR(i && i._$AB.nextSibling, r), t.length = r);
|
||||
}
|
||||
_$AR(e = this._$AA.nextSibling, t) {
|
||||
var i;
|
||||
for ((i = this._$AP) == null ? void 0 : i.call(this, !1, !0, t); e !== this._$AB; ) {
|
||||
const o = he(e).nextSibling;
|
||||
he(e).remove(), e = o;
|
||||
const r = de(e).nextSibling;
|
||||
de(e).remove(), e = r;
|
||||
}
|
||||
}
|
||||
setConnected(e) {
|
||||
|
|
@ -445,19 +445,19 @@ class G {
|
|||
get _$AU() {
|
||||
return this._$AM._$AU;
|
||||
}
|
||||
constructor(e, t, i, o, n) {
|
||||
this.type = 1, this._$AH = d, this._$AN = void 0, this.element = e, this.name = t, this._$AM = o, this.options = n, i.length > 2 || i[0] !== "" || i[1] !== "" ? (this._$AH = Array(i.length - 1).fill(new String()), this.strings = i) : this._$AH = d;
|
||||
constructor(e, t, i, r, n) {
|
||||
this.type = 1, this._$AH = d, this._$AN = void 0, this.element = e, this.name = t, this._$AM = r, this.options = n, i.length > 2 || i[0] !== "" || i[1] !== "" ? (this._$AH = Array(i.length - 1).fill(new String()), this.strings = i) : this._$AH = d;
|
||||
}
|
||||
_$AI(e, t = this, i, o) {
|
||||
_$AI(e, t = this, i, r) {
|
||||
const n = this.strings;
|
||||
let r = !1;
|
||||
if (n === void 0) e = M(this, e, t, 0), r = !L(e) || e !== this._$AH && e !== T, r && (this._$AH = e);
|
||||
let o = !1;
|
||||
if (n === void 0) e = M(this, e, t, 0), o = !L(e) || e !== this._$AH && e !== T, o && (this._$AH = e);
|
||||
else {
|
||||
const l = e;
|
||||
let a, p;
|
||||
for (e = n[0], a = 0; a < n.length - 1; a++) p = M(this, l[i + a], t, a), p === T && (p = this._$AH[a]), r || (r = !L(p) || p !== this._$AH[a]), p === d ? e = d : e !== d && (e += (p ?? "") + n[a + 1]), this._$AH[a] = p;
|
||||
for (e = n[0], a = 0; a < n.length - 1; a++) p = M(this, l[i + a], t, a), p === T && (p = this._$AH[a]), o || (o = !L(p) || p !== this._$AH[a]), p === d ? e = d : e !== d && (e += (p ?? "") + n[a + 1]), this._$AH[a] = p;
|
||||
}
|
||||
r && !o && this.j(e);
|
||||
o && !r && this.j(e);
|
||||
}
|
||||
j(e) {
|
||||
e === d ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, e ?? "");
|
||||
|
|
@ -480,13 +480,13 @@ class Ne extends G {
|
|||
}
|
||||
}
|
||||
class Ie extends G {
|
||||
constructor(e, t, i, o, n) {
|
||||
super(e, t, i, o, n), this.type = 5;
|
||||
constructor(e, t, i, r, n) {
|
||||
super(e, t, i, r, n), this.type = 5;
|
||||
}
|
||||
_$AI(e, t = this) {
|
||||
if ((e = M(this, e, t, 0) ?? d) === T) return;
|
||||
const i = this._$AH, o = e === d && i !== d || e.capture !== i.capture || e.once !== i.once || e.passive !== i.passive, n = e !== d && (i === d || o);
|
||||
o && this.element.removeEventListener(this.name, this, i), n && this.element.addEventListener(this.name, this, e), this._$AH = e;
|
||||
const i = this._$AH, r = e === d && i !== d || e.capture !== i.capture || e.once !== i.once || e.passive !== i.passive, n = e !== d && (i === d || r);
|
||||
r && this.element.removeEventListener(this.name, this, i), n && this.element.addEventListener(this.name, this, e), this._$AH = e;
|
||||
}
|
||||
handleEvent(e) {
|
||||
var t;
|
||||
|
|
@ -505,23 +505,23 @@ class Le {
|
|||
}
|
||||
}
|
||||
const ee = N.litHtmlPolyfillSupport;
|
||||
ee == null || ee(W, B), (N.litHtmlVersions ?? (N.litHtmlVersions = [])).push("3.3.2");
|
||||
const We = (s, e, t) => {
|
||||
ee == null || ee(q, W), (N.litHtmlVersions ?? (N.litHtmlVersions = [])).push("3.3.2");
|
||||
const qe = (s, e, t) => {
|
||||
const i = (t == null ? void 0 : t.renderBefore) ?? e;
|
||||
let o = i._$litPart$;
|
||||
if (o === void 0) {
|
||||
let r = i._$litPart$;
|
||||
if (r === void 0) {
|
||||
const n = (t == null ? void 0 : t.renderBefore) ?? null;
|
||||
i._$litPart$ = o = new B(e.insertBefore(I(), n), n, void 0, t ?? {});
|
||||
i._$litPart$ = r = new W(e.insertBefore(I(), n), n, void 0, t ?? {});
|
||||
}
|
||||
return o._$AI(s), o;
|
||||
return r._$AI(s), r;
|
||||
};
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const E = globalThis;
|
||||
class y extends D {
|
||||
const C = globalThis;
|
||||
class $ extends D {
|
||||
constructor() {
|
||||
super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0;
|
||||
}
|
||||
|
|
@ -532,7 +532,7 @@ class y extends D {
|
|||
}
|
||||
update(e) {
|
||||
const t = this.render();
|
||||
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = We(t, this.renderRoot, this.renderOptions);
|
||||
this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(e), this._$Do = qe(t, this.renderRoot, this.renderOptions);
|
||||
}
|
||||
connectedCallback() {
|
||||
var e;
|
||||
|
|
@ -546,11 +546,11 @@ class y extends D {
|
|||
return T;
|
||||
}
|
||||
}
|
||||
var $e;
|
||||
y._$litElement$ = !0, y.finalized = !0, ($e = E.litElementHydrateSupport) == null || $e.call(E, { LitElement: y });
|
||||
const te = E.litElementPolyfillSupport;
|
||||
te == null || te({ LitElement: y });
|
||||
(E.litElementVersions ?? (E.litElementVersions = [])).push("4.2.2");
|
||||
var be;
|
||||
$._$litElement$ = !0, $.finalized = !0, (be = C.litElementHydrateSupport) == null || be.call(C, { LitElement: $ });
|
||||
const te = C.litElementPolyfillSupport;
|
||||
te == null || te({ LitElement: $ });
|
||||
(C.litElementVersions ?? (C.litElementVersions = [])).push("4.2.2");
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2017 Google LLC
|
||||
|
|
@ -566,31 +566,31 @@ const F = (s) => (e, t) => {
|
|||
* Copyright 2017 Google LLC
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
const qe = { attribute: !0, type: String, converter: J, reflect: !1, hasChanged: oe }, Be = (s = qe, e, t) => {
|
||||
const { kind: i, metadata: o } = t;
|
||||
let n = globalThis.litPropertyMetadata.get(o);
|
||||
if (n === void 0 && globalThis.litPropertyMetadata.set(o, n = /* @__PURE__ */ new Map()), i === "setter" && ((s = Object.create(s)).wrapped = !0), n.set(t.name, s), i === "accessor") {
|
||||
const { name: r } = t;
|
||||
const Be = { attribute: !0, type: String, converter: J, reflect: !1, hasChanged: ie }, We = (s = Be, e, t) => {
|
||||
const { kind: i, metadata: r } = t;
|
||||
let n = globalThis.litPropertyMetadata.get(r);
|
||||
if (n === void 0 && globalThis.litPropertyMetadata.set(r, n = /* @__PURE__ */ new Map()), i === "setter" && ((s = Object.create(s)).wrapped = !0), n.set(t.name, s), i === "accessor") {
|
||||
const { name: o } = t;
|
||||
return { set(l) {
|
||||
const a = e.get.call(this);
|
||||
e.set.call(this, l), this.requestUpdate(r, a, s, !0, l);
|
||||
e.set.call(this, l), this.requestUpdate(o, a, s, !0, l);
|
||||
}, init(l) {
|
||||
return l !== void 0 && this.C(r, void 0, s, l), l;
|
||||
return l !== void 0 && this.C(o, void 0, s, l), l;
|
||||
} };
|
||||
}
|
||||
if (i === "setter") {
|
||||
const { name: r } = t;
|
||||
const { name: o } = t;
|
||||
return function(l) {
|
||||
const a = this[r];
|
||||
e.call(this, l), this.requestUpdate(r, a, s, !0, l);
|
||||
const a = this[o];
|
||||
e.call(this, l), this.requestUpdate(o, a, s, !0, l);
|
||||
};
|
||||
}
|
||||
throw Error("Unsupported decorator location: " + i);
|
||||
};
|
||||
function f(s) {
|
||||
return (e, t) => typeof t == "object" ? Be(s, e, t) : ((i, o, n) => {
|
||||
const r = o.hasOwnProperty(n);
|
||||
return o.constructor.createProperty(n, i), r ? Object.getOwnPropertyDescriptor(o, n) : void 0;
|
||||
return (e, t) => typeof t == "object" ? We(s, e, t) : ((i, r, n) => {
|
||||
const o = r.hasOwnProperty(n);
|
||||
return r.constructor.createProperty(n, i), o ? Object.getOwnPropertyDescriptor(r, n) : void 0;
|
||||
})(s, e, t);
|
||||
}
|
||||
/**
|
||||
|
|
@ -601,13 +601,13 @@ function f(s) {
|
|||
function u(s) {
|
||||
return f({ ...s, state: !0, attribute: !1 });
|
||||
}
|
||||
function ne(s, e) {
|
||||
function xe(s, e) {
|
||||
const t = new WebSocket(s);
|
||||
return t.onmessage = (i) => {
|
||||
var o, n, r, l;
|
||||
var r, n, o, l;
|
||||
try {
|
||||
const a = JSON.parse(i.data);
|
||||
((n = (o = a.type) == null ? void 0 : o.startsWith) != null && n.call(o, "process.") || (l = (r = a.channel) == null ? void 0 : r.startsWith) != null && l.call(r, "process.")) && e(a);
|
||||
((n = (r = a.type) == null ? void 0 : r.startsWith) != null && n.call(r, "process.") || (l = (o = a.channel) == null ? void 0 : o.startsWith) != null && l.call(o, "process.")) && e(a);
|
||||
} catch {
|
||||
}
|
||||
}, t;
|
||||
|
|
@ -621,10 +621,10 @@ class Fe {
|
|||
}
|
||||
async request(e, t) {
|
||||
var n;
|
||||
const o = await (await fetch(`${this.base}${e}`, t)).json();
|
||||
if (!o.success)
|
||||
throw new Error(((n = o.error) == null ? void 0 : n.message) ?? "Request failed");
|
||||
return o.data;
|
||||
const r = await (await fetch(`${this.base}${e}`, t)).json();
|
||||
if (!r.success)
|
||||
throw new Error(((n = r.error) == null ? void 0 : n.message) ?? "Request failed");
|
||||
return r.data;
|
||||
}
|
||||
/** List all alive daemons from the registry. */
|
||||
listDaemons() {
|
||||
|
|
@ -645,12 +645,12 @@ class Fe {
|
|||
return this.request(`/daemons/${e}/${t}/health`);
|
||||
}
|
||||
}
|
||||
var Ve = Object.defineProperty, Ke = Object.getOwnPropertyDescriptor, k = (s, e, t, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? Ke(e, t) : e, n = s.length - 1, r; n >= 0; n--)
|
||||
(r = s[n]) && (o = (i ? r(e, t, o) : r(o)) || o);
|
||||
return i && o && Ve(e, t, o), o;
|
||||
var Ke = Object.defineProperty, Ve = Object.getOwnPropertyDescriptor, k = (s, e, t, i) => {
|
||||
for (var r = i > 1 ? void 0 : i ? Ve(e, t) : e, n = s.length - 1, o; n >= 0; n--)
|
||||
(o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r);
|
||||
return i && r && Ke(e, t, r), r;
|
||||
};
|
||||
let g = class extends y {
|
||||
let g = class extends $ {
|
||||
constructor() {
|
||||
super(...arguments), this.apiUrl = "", this.daemons = [], this.loading = !0, this.error = "", this.stopping = /* @__PURE__ */ new Set(), this.checking = /* @__PURE__ */ new Set(), this.healthResults = /* @__PURE__ */ new Map();
|
||||
}
|
||||
|
|
@ -770,7 +770,7 @@ let g = class extends y {
|
|||
`;
|
||||
}
|
||||
};
|
||||
g.styles = q`
|
||||
g.styles = B`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
|
|
@ -947,30 +947,20 @@ k([
|
|||
g = k([
|
||||
F("core-process-daemons")
|
||||
], g);
|
||||
var Je = Object.defineProperty, Ze = Object.getOwnPropertyDescriptor, S = (s, e, t, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? Ze(e, t) : e, n = s.length - 1, r; n >= 0; n--)
|
||||
(r = s[n]) && (o = (i ? r(e, t, o) : r(o)) || o);
|
||||
return i && o && Je(e, t, o), o;
|
||||
var Je = Object.defineProperty, Ze = Object.getOwnPropertyDescriptor, U = (s, e, t, i) => {
|
||||
for (var r = i > 1 ? void 0 : i ? Ze(e, t) : e, n = s.length - 1, o; n >= 0; n--)
|
||||
(o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r);
|
||||
return i && r && Je(e, t, r), r;
|
||||
};
|
||||
let b = class extends y {
|
||||
let y = class extends $ {
|
||||
constructor() {
|
||||
super(...arguments), this.apiUrl = "", this.wsUrl = "", this.selectedId = "", this.processes = [], this.loading = !1, this.error = "", this.connected = !1, this.ws = null;
|
||||
super(...arguments), this.apiUrl = "", this.selectedId = "", this.processes = [], this.loading = !1, this.error = "", this.killing = /* @__PURE__ */ new Set();
|
||||
}
|
||||
connectedCallback() {
|
||||
super.connectedCallback(), this.loadProcesses();
|
||||
}
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback(), this.disconnect();
|
||||
}
|
||||
updated(s) {
|
||||
s.has("wsUrl") && (this.disconnect(), this.processes = [], this.loadProcesses());
|
||||
}
|
||||
async loadProcesses() {
|
||||
if (this.error = "", this.loading = !1, !this.wsUrl) {
|
||||
this.processes = [];
|
||||
return;
|
||||
}
|
||||
this.connect();
|
||||
this.loading = !1, this.processes = [];
|
||||
}
|
||||
handleSelect(s) {
|
||||
this.dispatchEvent(
|
||||
|
|
@ -991,60 +981,13 @@ let b = class extends y {
|
|||
return "unknown";
|
||||
}
|
||||
}
|
||||
connect() {
|
||||
this.ws = ne(this.wsUrl, (s) => {
|
||||
this.applyEvent(s);
|
||||
}), this.ws.onopen = () => {
|
||||
this.connected = !0;
|
||||
}, this.ws.onclose = () => {
|
||||
this.connected = !1;
|
||||
};
|
||||
}
|
||||
disconnect() {
|
||||
this.ws && (this.ws.close(), this.ws = null), this.connected = !1;
|
||||
}
|
||||
applyEvent(s) {
|
||||
const e = s.channel ?? s.type ?? "", t = s.data ?? {};
|
||||
if (!t.id)
|
||||
return;
|
||||
const i = new Map(this.processes.map((n) => [n.id, n])), o = i.get(t.id);
|
||||
if (e === "process.started") {
|
||||
i.set(t.id, this.normalizeProcess(t, o, "running")), this.processes = this.sortProcesses(i);
|
||||
return;
|
||||
}
|
||||
if (e === "process.exited") {
|
||||
i.set(t.id, this.normalizeProcess(t, o, "exited")), this.processes = this.sortProcesses(i);
|
||||
return;
|
||||
}
|
||||
if (e === "process.killed") {
|
||||
i.set(t.id, this.normalizeProcess(t, o, "killed")), this.processes = this.sortProcesses(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
normalizeProcess(s, e, t) {
|
||||
return {
|
||||
id: s.id,
|
||||
command: s.command ?? (e == null ? void 0 : e.command) ?? "",
|
||||
args: s.args ?? (e == null ? void 0 : e.args) ?? [],
|
||||
dir: s.dir ?? (e == null ? void 0 : e.dir) ?? "",
|
||||
startedAt: s.startedAt ?? (e == null ? void 0 : e.startedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
||||
status: t,
|
||||
exitCode: s.exitCode ?? (e == null ? void 0 : e.exitCode) ?? (t === "killed" ? -1 : 0),
|
||||
duration: s.duration ?? (e == null ? void 0 : e.duration) ?? 0,
|
||||
pid: s.pid ?? (e == null ? void 0 : e.pid) ?? 0
|
||||
};
|
||||
}
|
||||
sortProcesses(s) {
|
||||
return [...s.values()].sort(
|
||||
(e, t) => new Date(t.startedAt).getTime() - new Date(e.startedAt).getTime()
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return this.loading ? c`<div class="loading">Loading processes\u2026</div>` : c`
|
||||
${this.error ? c`<div class="error">${this.error}</div>` : d}
|
||||
${this.processes.length === 0 ? c`
|
||||
<div class="info-notice">
|
||||
${this.wsUrl ? this.connected ? "Waiting for process events from the WebSocket feed." : "Connecting to the process event stream..." : "Set a WebSocket URL to receive live process events."}
|
||||
Process list endpoints are pending. Processes will appear here once
|
||||
the REST API for managed processes is available.
|
||||
</div>
|
||||
<div class="empty">No managed processes.</div>
|
||||
` : c`
|
||||
|
|
@ -1076,12 +1019,12 @@ let b = class extends y {
|
|||
<div class="item-actions">
|
||||
<button
|
||||
class="kill-btn"
|
||||
disabled
|
||||
?disabled=${this.killing.has(s.id)}
|
||||
@click=${(t) => {
|
||||
t.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Live only
|
||||
${this.killing.has(s.id) ? "Killing…" : "Kill"}
|
||||
</button>
|
||||
</div>
|
||||
` : d}
|
||||
|
|
@ -1094,7 +1037,7 @@ let b = class extends y {
|
|||
`;
|
||||
}
|
||||
};
|
||||
b.styles = q`
|
||||
y.styles = B`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
|
|
@ -1258,36 +1201,33 @@ b.styles = q`
|
|||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
S([
|
||||
U([
|
||||
f({ attribute: "api-url" })
|
||||
], b.prototype, "apiUrl", 2);
|
||||
S([
|
||||
f({ attribute: "ws-url" })
|
||||
], b.prototype, "wsUrl", 2);
|
||||
S([
|
||||
], y.prototype, "apiUrl", 2);
|
||||
U([
|
||||
f({ attribute: "selected-id" })
|
||||
], b.prototype, "selectedId", 2);
|
||||
S([
|
||||
], y.prototype, "selectedId", 2);
|
||||
U([
|
||||
u()
|
||||
], b.prototype, "processes", 2);
|
||||
S([
|
||||
], y.prototype, "processes", 2);
|
||||
U([
|
||||
u()
|
||||
], b.prototype, "loading", 2);
|
||||
S([
|
||||
], y.prototype, "loading", 2);
|
||||
U([
|
||||
u()
|
||||
], b.prototype, "error", 2);
|
||||
S([
|
||||
], y.prototype, "error", 2);
|
||||
U([
|
||||
u()
|
||||
], b.prototype, "connected", 2);
|
||||
b = S([
|
||||
], y.prototype, "killing", 2);
|
||||
y = U([
|
||||
F("core-process-list")
|
||||
], b);
|
||||
], y);
|
||||
var Ge = Object.defineProperty, Qe = Object.getOwnPropertyDescriptor, O = (s, e, t, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? Qe(e, t) : e, n = s.length - 1, r; n >= 0; n--)
|
||||
(r = s[n]) && (o = (i ? r(e, t, o) : r(o)) || o);
|
||||
return i && o && Ge(e, t, o), o;
|
||||
for (var r = i > 1 ? void 0 : i ? Qe(e, t) : e, n = s.length - 1, o; n >= 0; n--)
|
||||
(o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r);
|
||||
return i && r && Ge(e, t, r), r;
|
||||
};
|
||||
let v = class extends y {
|
||||
let v = class extends $ {
|
||||
constructor() {
|
||||
super(...arguments), this.apiUrl = "", this.wsUrl = "", this.processId = "", this.lines = [], this.autoScroll = !0, this.connected = !1, this.ws = null;
|
||||
}
|
||||
|
|
@ -1301,7 +1241,7 @@ let v = class extends y {
|
|||
(s.has("processId") || s.has("wsUrl")) && (this.disconnect(), this.lines = [], this.wsUrl && this.processId && this.connect()), this.autoScroll && this.scrollToBottom();
|
||||
}
|
||||
connect() {
|
||||
this.ws = ne(this.wsUrl, (s) => {
|
||||
this.ws = xe(this.wsUrl, (s) => {
|
||||
const e = s.data;
|
||||
if (!e) return;
|
||||
(s.channel ?? s.type ?? "") === "process.output" && e.id === this.processId && (this.lines = [
|
||||
|
|
@ -1360,7 +1300,7 @@ let v = class extends y {
|
|||
` : c`<div class="empty">Select a process to view its output.</div>`;
|
||||
}
|
||||
};
|
||||
v.styles = q`
|
||||
v.styles = B`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
|
|
@ -1486,11 +1426,11 @@ v = O([
|
|||
F("core-process-output")
|
||||
], v);
|
||||
var Xe = Object.defineProperty, Ye = Object.getOwnPropertyDescriptor, Q = (s, e, t, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? Ye(e, t) : e, n = s.length - 1, r; n >= 0; n--)
|
||||
(r = s[n]) && (o = (i ? r(e, t, o) : r(o)) || o);
|
||||
return i && o && Xe(e, t, o), o;
|
||||
for (var r = i > 1 ? void 0 : i ? Ye(e, t) : e, n = s.length - 1, o; n >= 0; n--)
|
||||
(o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r);
|
||||
return i && r && Xe(e, t, r), r;
|
||||
};
|
||||
let R = class extends y {
|
||||
let R = class extends $ {
|
||||
constructor() {
|
||||
super(...arguments), this.apiUrl = "", this.result = null, this.expandedOutputs = /* @__PURE__ */ new Set();
|
||||
}
|
||||
|
|
@ -1519,7 +1459,7 @@ let R = class extends y {
|
|||
</div>
|
||||
<div class="empty">No pipeline results.</div>
|
||||
`;
|
||||
const { results: s, duration: e, passed: t, failed: i, skipped: o, success: n } = this.result;
|
||||
const { results: s, duration: e, passed: t, failed: i, skipped: r, success: n } = this.result;
|
||||
return c`
|
||||
<div class="summary">
|
||||
<span class="overall-badge ${n ? "success" : "failure"}">
|
||||
|
|
@ -1534,7 +1474,7 @@ let R = class extends y {
|
|||
<span class="summary-label">Failed</span>
|
||||
</div>
|
||||
<div class="summary-stat">
|
||||
<span class="summary-value skipped">${o}</span>
|
||||
<span class="summary-value skipped">${r}</span>
|
||||
<span class="summary-label">Skipped</span>
|
||||
</div>
|
||||
<span class="summary-duration">${this.formatDuration(e)}</span>
|
||||
|
|
@ -1542,24 +1482,24 @@ let R = class extends y {
|
|||
|
||||
<div class="list">
|
||||
${s.map(
|
||||
(r) => c`
|
||||
(o) => c`
|
||||
<div class="spec">
|
||||
<div class="spec-header">
|
||||
<div class="spec-name">
|
||||
<span>${r.name}</span>
|
||||
<span class="result-badge ${this.resultStatus(r)}">${this.resultStatus(r)}</span>
|
||||
<span>${o.name}</span>
|
||||
<span class="result-badge ${this.resultStatus(o)}">${this.resultStatus(o)}</span>
|
||||
</div>
|
||||
<span class="duration">${this.formatDuration(r.duration)}</span>
|
||||
<span class="duration">${this.formatDuration(o.duration)}</span>
|
||||
</div>
|
||||
<div class="spec-meta">
|
||||
${r.exitCode !== 0 && !r.skipped ? c`<span>exit ${r.exitCode}</span>` : d}
|
||||
${o.exitCode !== 0 && !o.skipped ? c`<span>exit ${o.exitCode}</span>` : d}
|
||||
</div>
|
||||
${r.error ? c`<div class="spec-error">${r.error}</div>` : d}
|
||||
${r.output ? c`
|
||||
<button class="toggle-output" @click=${() => this.toggleOutput(r.name)}>
|
||||
${this.expandedOutputs.has(r.name) ? "Hide output" : "Show output"}
|
||||
${o.error ? c`<div class="spec-error">${o.error}</div>` : d}
|
||||
${o.output ? c`
|
||||
<button class="toggle-output" @click=${() => this.toggleOutput(o.name)}>
|
||||
${this.expandedOutputs.has(o.name) ? "Hide output" : "Show output"}
|
||||
</button>
|
||||
${this.expandedOutputs.has(r.name) ? c`<div class="spec-output">${r.output}</div>` : d}
|
||||
${this.expandedOutputs.has(o.name) ? c`<div class="spec-output">${o.output}</div>` : d}
|
||||
` : d}
|
||||
</div>
|
||||
`
|
||||
|
|
@ -1568,7 +1508,7 @@ let R = class extends y {
|
|||
`;
|
||||
}
|
||||
};
|
||||
R.styles = q`
|
||||
R.styles = B`
|
||||
:host {
|
||||
display: block;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
|
|
@ -1776,11 +1716,11 @@ R = Q([
|
|||
F("core-process-runner")
|
||||
], R);
|
||||
var et = Object.defineProperty, tt = Object.getOwnPropertyDescriptor, z = (s, e, t, i) => {
|
||||
for (var o = i > 1 ? void 0 : i ? tt(e, t) : e, n = s.length - 1, r; n >= 0; n--)
|
||||
(r = s[n]) && (o = (i ? r(e, t, o) : r(o)) || o);
|
||||
return i && o && et(e, t, o), o;
|
||||
for (var r = i > 1 ? void 0 : i ? tt(e, t) : e, n = s.length - 1, o; n >= 0; n--)
|
||||
(o = s[n]) && (r = (i ? o(e, t, r) : o(r)) || r);
|
||||
return i && r && et(e, t, r), r;
|
||||
};
|
||||
let w = class extends y {
|
||||
let _ = class extends $ {
|
||||
constructor() {
|
||||
super(...arguments), this.apiUrl = "", this.wsUrl = "", this.activeTab = "daemons", this.wsConnected = !1, this.lastEvent = "", this.selectedProcessId = "", this.ws = null, this.tabs = [
|
||||
{ id: "daemons", label: "Daemons" },
|
||||
|
|
@ -1795,7 +1735,7 @@ let w = class extends y {
|
|||
super.disconnectedCallback(), this.ws && (this.ws.close(), this.ws = null);
|
||||
}
|
||||
connectWs() {
|
||||
this.ws = ne(this.wsUrl, (s) => {
|
||||
this.ws = xe(this.wsUrl, (s) => {
|
||||
this.lastEvent = s.channel ?? s.type ?? "", this.requestUpdate();
|
||||
}), this.ws.onopen = () => {
|
||||
this.wsConnected = !0;
|
||||
|
|
@ -1825,7 +1765,6 @@ let w = class extends y {
|
|||
return c`
|
||||
<core-process-list
|
||||
api-url=${this.apiUrl}
|
||||
ws-url=${this.wsUrl}
|
||||
@process-selected=${this.handleProcessSelected}
|
||||
></core-process-list>
|
||||
${this.selectedProcessId ? c`<core-process-output
|
||||
|
|
@ -1873,7 +1812,7 @@ let w = class extends y {
|
|||
`;
|
||||
}
|
||||
};
|
||||
w.styles = q`
|
||||
_.styles = B`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1989,31 +1928,31 @@ w.styles = q`
|
|||
`;
|
||||
z([
|
||||
f({ attribute: "api-url" })
|
||||
], w.prototype, "apiUrl", 2);
|
||||
], _.prototype, "apiUrl", 2);
|
||||
z([
|
||||
f({ attribute: "ws-url" })
|
||||
], w.prototype, "wsUrl", 2);
|
||||
], _.prototype, "wsUrl", 2);
|
||||
z([
|
||||
u()
|
||||
], w.prototype, "activeTab", 2);
|
||||
], _.prototype, "activeTab", 2);
|
||||
z([
|
||||
u()
|
||||
], w.prototype, "wsConnected", 2);
|
||||
], _.prototype, "wsConnected", 2);
|
||||
z([
|
||||
u()
|
||||
], w.prototype, "lastEvent", 2);
|
||||
], _.prototype, "lastEvent", 2);
|
||||
z([
|
||||
u()
|
||||
], w.prototype, "selectedProcessId", 2);
|
||||
w = z([
|
||||
], _.prototype, "selectedProcessId", 2);
|
||||
_ = z([
|
||||
F("core-process-panel")
|
||||
], w);
|
||||
], _);
|
||||
export {
|
||||
Fe as ProcessApi,
|
||||
g as ProcessDaemons,
|
||||
b as ProcessList,
|
||||
y as ProcessList,
|
||||
v as ProcessOutput,
|
||||
w as ProcessPanel,
|
||||
_ as ProcessPanel,
|
||||
R as ProcessRunner,
|
||||
ne as connectProcessEvents
|
||||
xe as connectProcessEvents
|
||||
};
|
||||
|
|
|
|||
29
process.go
29
process.go
|
|
@ -38,13 +38,13 @@ type ManagedProcess struct {
|
|||
gracePeriod time.Duration
|
||||
killGroup bool
|
||||
lastSignal string
|
||||
killEmitted bool
|
||||
}
|
||||
|
||||
// Process is kept as a compatibility alias for ManagedProcess.
|
||||
type Process = ManagedProcess
|
||||
|
||||
// Info returns a snapshot of process state.
|
||||
// info := proc.Info()
|
||||
// fmt.Println(info.Status, info.ExitCode)
|
||||
func (p *ManagedProcess) Info() ProcessInfo {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
|
@ -63,7 +63,7 @@ func (p *ManagedProcess) Info() ProcessInfo {
|
|||
}
|
||||
}
|
||||
|
||||
// Output returns the captured output as a string.
|
||||
// output := proc.Output() // returns combined stdout+stderr
|
||||
func (p *ManagedProcess) Output() string {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
|
@ -73,7 +73,7 @@ func (p *ManagedProcess) Output() string {
|
|||
return p.output.String()
|
||||
}
|
||||
|
||||
// OutputBytes returns the captured output as bytes.
|
||||
// data := proc.OutputBytes() // nil if capture is disabled
|
||||
func (p *ManagedProcess) OutputBytes() []byte {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
|
@ -83,7 +83,7 @@ func (p *ManagedProcess) OutputBytes() []byte {
|
|||
return p.output.Bytes()
|
||||
}
|
||||
|
||||
// IsRunning returns true if the process is still executing.
|
||||
// if proc.IsRunning() { log.Println("still running") }
|
||||
func (p *ManagedProcess) IsRunning() bool {
|
||||
select {
|
||||
case <-p.done:
|
||||
|
|
@ -93,7 +93,7 @@ func (p *ManagedProcess) IsRunning() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Wait blocks until the process exits.
|
||||
// if err := proc.Wait(); err != nil { /* non-zero exit or killed */ }
|
||||
func (p *ManagedProcess) Wait() error {
|
||||
<-p.done
|
||||
p.mu.RLock()
|
||||
|
|
@ -110,7 +110,7 @@ func (p *ManagedProcess) Wait() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Done returns a channel that closes when the process exits.
|
||||
// <-proc.Done() // blocks until process exits
|
||||
func (p *ManagedProcess) Done() <-chan struct{} {
|
||||
return p.done
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ func (p *ManagedProcess) terminate() error {
|
|||
return syscall.Kill(pid, syscall.SIGTERM)
|
||||
}
|
||||
|
||||
// SendInput writes to the process stdin.
|
||||
// _ = proc.SendInput("yes\n") // write to process stdin
|
||||
func (p *ManagedProcess) SendInput(input string) error {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
|
@ -201,7 +201,7 @@ func (p *ManagedProcess) SendInput(input string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// CloseStdin closes the process stdin pipe.
|
||||
// _ = proc.CloseStdin() // signals EOF to the subprocess
|
||||
func (p *ManagedProcess) CloseStdin() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
|
@ -220,14 +220,3 @@ func (p *ManagedProcess) requestedSignal() string {
|
|||
defer p.mu.RUnlock()
|
||||
return p.lastSignal
|
||||
}
|
||||
|
||||
func (p *ManagedProcess) markKillEmitted() bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.killEmitted {
|
||||
return false
|
||||
}
|
||||
p.killEmitted = true
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
122
process_test.go
122
process_test.go
|
|
@ -26,18 +26,6 @@ func TestProcess_Info_Good(t *testing.T) {
|
|||
assert.Greater(t, info.Duration, time.Duration(0))
|
||||
}
|
||||
|
||||
func TestProcess_Info_Pending_Good(t *testing.T) {
|
||||
proc := &ManagedProcess{
|
||||
ID: "pending",
|
||||
Status: StatusPending,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
info := proc.Info()
|
||||
assert.Equal(t, StatusPending, info.Status)
|
||||
assert.False(t, info.Running)
|
||||
}
|
||||
|
||||
func TestProcess_Output_Good(t *testing.T) {
|
||||
t.Run("captures stdout", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
|
|
@ -331,3 +319,113 @@ func TestProcess_TimeoutWithGrace_Good(t *testing.T) {
|
|||
assert.Equal(t, StatusKilled, proc.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Info_Bad(t *testing.T) {
|
||||
t.Run("failed process has StatusFailed", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
r := svc.Start(context.Background(), "nonexistent_command_xyz")
|
||||
assert.False(t, r.OK)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Info_Ugly(t *testing.T) {
|
||||
t.Run("info is safe to call concurrently", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
proc := startProc(t, svc, context.Background(), "sleep", "1")
|
||||
defer func() { _ = proc.Kill() }()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 50; i++ {
|
||||
_ = proc.Info()
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 50; i++ {
|
||||
_ = proc.Info()
|
||||
}
|
||||
<-done
|
||||
_ = proc.Kill()
|
||||
<-proc.Done()
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Wait_Bad(t *testing.T) {
|
||||
t.Run("returns error for non-zero exit code", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
proc := startProc(t, svc, context.Background(), "sh", "-c", "exit 2")
|
||||
err := proc.Wait()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Wait_Ugly(t *testing.T) {
|
||||
t.Run("wait on killed process returns error", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
proc := startProc(t, svc, context.Background(), "sleep", "60")
|
||||
_ = proc.Kill()
|
||||
err := proc.Wait()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Kill_Bad(t *testing.T) {
|
||||
t.Run("kill after kill is idempotent", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
proc := startProc(t, svc, ctx, "sleep", "60")
|
||||
_ = proc.Kill()
|
||||
<-proc.Done()
|
||||
|
||||
// Second kill should be a no-op (process not running)
|
||||
err := proc.Kill()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Kill_Ugly(t *testing.T) {
|
||||
t.Run("shutdown immediate when grace period is zero", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
proc := startProc(t, svc, context.Background(), "sleep", "60")
|
||||
|
||||
err := proc.Shutdown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("should have been killed immediately")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_SendInput_Ugly(t *testing.T) {
|
||||
t.Run("send to nil stdin returns error", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
r := svc.StartWithOptions(context.Background(), RunOptions{
|
||||
Command: "echo",
|
||||
Args: []string{"hi"},
|
||||
})
|
||||
require.True(t, r.OK)
|
||||
proc := r.Value.(*Process)
|
||||
<-proc.Done()
|
||||
|
||||
err := proc.SendInput("data")
|
||||
assert.ErrorIs(t, err, ErrProcessNotRunning)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProcess_Signal_Ugly(t *testing.T) {
|
||||
t.Run("multiple signals to completed process return error", func(t *testing.T) {
|
||||
svc, _ := newTestService(t)
|
||||
proc := startProc(t, svc, context.Background(), "echo", "done")
|
||||
<-proc.Done()
|
||||
|
||||
err := proc.Signal(os.Interrupt)
|
||||
assert.ErrorIs(t, err, ErrProcessNotRunning)
|
||||
err = proc.Signal(os.Interrupt)
|
||||
assert.ErrorIs(t, err, ErrProcessNotRunning)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
10
program.go
10
program.go
|
|
@ -3,7 +3,6 @@ package process
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"dappco.re/go/core"
|
||||
|
|
@ -37,12 +36,6 @@ func (p *Program) Find() error {
|
|||
if err != nil {
|
||||
return core.E("program.find", core.Concat(strconv.Quote(p.Name), ": not found in PATH"), ErrProgramNotFound)
|
||||
}
|
||||
if !filepath.IsAbs(path) {
|
||||
path, err = filepath.Abs(path)
|
||||
if err != nil {
|
||||
return core.E("program.find", core.Concat(strconv.Quote(p.Name), ": failed to resolve absolute path"), err)
|
||||
}
|
||||
}
|
||||
p.Path = path
|
||||
return nil
|
||||
}
|
||||
|
|
@ -65,9 +58,6 @@ func (p *Program) RunDir(ctx context.Context, dir string, args ...string) (strin
|
|||
if binary == "" {
|
||||
binary = p.Name
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd := execCommandContext(ctx, binary, args...)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package process_test
|
|||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -25,7 +24,6 @@ func TestProgram_Find_Good(t *testing.T) {
|
|||
p := &process.Program{Name: "echo"}
|
||||
require.NoError(t, p.Find())
|
||||
assert.NotEmpty(t, p.Path)
|
||||
assert.True(t, filepath.IsAbs(p.Path))
|
||||
}
|
||||
|
||||
func TestProgram_FindUnknown_Bad(t *testing.T) {
|
||||
|
|
@ -58,14 +56,6 @@ func TestProgram_RunFallback_Good(t *testing.T) {
|
|||
assert.Equal(t, "fallback", out)
|
||||
}
|
||||
|
||||
func TestProgram_RunNilContext_Good(t *testing.T) {
|
||||
p := &process.Program{Name: "echo"}
|
||||
|
||||
out, err := p.Run(nil, "nil-context")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "nil-context", out)
|
||||
}
|
||||
|
||||
func TestProgram_RunDir_Good(t *testing.T) {
|
||||
p := &process.Program{Name: "pwd"}
|
||||
require.NoError(t, p.Find())
|
||||
|
|
@ -88,3 +78,22 @@ func TestProgram_RunFailure_Bad(t *testing.T) {
|
|||
_, err := p.Run(testCtx(t))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestProgram_Find_Ugly(t *testing.T) {
|
||||
t.Run("find then run in non-existent dir returns error", func(t *testing.T) {
|
||||
p := &process.Program{Name: "echo"}
|
||||
require.NoError(t, p.Find())
|
||||
|
||||
_, err := p.RunDir(testCtx(t), "/nonexistent-dir-xyz-abc")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("path set directly skips PATH lookup", func(t *testing.T) {
|
||||
p := &process.Program{Name: "echo", Path: "/bin/echo"}
|
||||
out, err := p.Run(testCtx(t), "direct")
|
||||
// Only assert no panic; binary may be at different location on some systems
|
||||
if err == nil {
|
||||
assert.Equal(t, "direct", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
10
registry.go
10
registry.go
|
|
@ -51,6 +51,8 @@ func DefaultRegistry() *Registry {
|
|||
// Register writes a daemon entry to the registry directory.
|
||||
// If Started is zero, it is set to the current time.
|
||||
// The directory is created if it does not exist.
|
||||
//
|
||||
// err := registry.Register(process.DaemonEntry{Code: "agent", Daemon: "core-agent", PID: os.Getpid()})
|
||||
func (r *Registry) Register(entry DaemonEntry) error {
|
||||
if entry.Started.IsZero() {
|
||||
entry.Started = time.Now()
|
||||
|
|
@ -72,6 +74,8 @@ func (r *Registry) Register(entry DaemonEntry) error {
|
|||
}
|
||||
|
||||
// Unregister removes a daemon entry from the registry.
|
||||
//
|
||||
// err := registry.Unregister("agent", "core-agent")
|
||||
func (r *Registry) Unregister(code, daemon string) error {
|
||||
if err := coreio.Local.Delete(r.entryPath(code, daemon)); err != nil {
|
||||
return core.E("registry.unregister", "failed to delete entry file", err)
|
||||
|
|
@ -79,8 +83,8 @@ func (r *Registry) Unregister(code, daemon string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Get reads a single daemon entry and checks whether its process is alive.
|
||||
// If the process is dead, the stale file is removed and (nil, false) is returned.
|
||||
// entry, alive := registry.Get("agent", "core-agent")
|
||||
// if !alive { fmt.Println("daemon not running") }
|
||||
func (r *Registry) Get(code, daemon string) (*DaemonEntry, bool) {
|
||||
path := r.entryPath(code, daemon)
|
||||
|
||||
|
|
@ -104,6 +108,8 @@ func (r *Registry) Get(code, daemon string) (*DaemonEntry, bool) {
|
|||
}
|
||||
|
||||
// List returns all alive daemon entries, pruning any with dead PIDs.
|
||||
//
|
||||
// entries, err := registry.List()
|
||||
func (r *Registry) List() ([]DaemonEntry, error) {
|
||||
if !coreio.Local.Exists(r.dir) {
|
||||
return nil, nil
|
||||
|
|
|
|||
|
|
@ -125,3 +125,40 @@ func TestRegistry_Default_Good(t *testing.T) {
|
|||
reg := DefaultRegistry()
|
||||
assert.NotNil(t, reg)
|
||||
}
|
||||
|
||||
func TestRegistry_Register_Bad(t *testing.T) {
|
||||
t.Run("unregister non-existent entry returns error", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg := NewRegistry(dir)
|
||||
|
||||
err := reg.Unregister("ghost", "proc")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistry_Register_Ugly(t *testing.T) {
|
||||
t.Run("code with slashes is sanitised in filename", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
reg := NewRegistry(dir)
|
||||
|
||||
err := reg.Register(DaemonEntry{
|
||||
Code: "org/app",
|
||||
Daemon: "serve",
|
||||
PID: os.Getpid(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
entry, ok := reg.Get("org/app", "serve")
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "org/app", entry.Code)
|
||||
})
|
||||
|
||||
t.Run("list on empty directory returns nil", func(t *testing.T) {
|
||||
dir := core.JoinPath(t.TempDir(), "nonexistent-registry")
|
||||
reg := NewRegistry(dir)
|
||||
|
||||
entries, err := reg.List()
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, entries)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
81
runner.go
81
runner.go
|
|
@ -13,10 +13,8 @@ type Runner struct {
|
|||
service *Service
|
||||
}
|
||||
|
||||
// ErrRunnerNoService is returned when a runner was created without a service.
|
||||
var ErrRunnerNoService = core.E("", "runner service is nil", nil)
|
||||
|
||||
// NewRunner creates a runner for the given service.
|
||||
// runner := process.NewRunner(svc)
|
||||
// result, _ := runner.RunAll(ctx, specs)
|
||||
func NewRunner(svc *Service) *Runner {
|
||||
return &Runner{service: svc}
|
||||
}
|
||||
|
|
@ -50,7 +48,7 @@ type RunResult struct {
|
|||
Skipped bool
|
||||
}
|
||||
|
||||
// Passed returns true if the process succeeded.
|
||||
// if result.Passed() { fmt.Println("ok:", result.Name) }
|
||||
func (r RunResult) Passed() bool {
|
||||
return !r.Skipped && r.Error == nil && r.ExitCode == 0
|
||||
}
|
||||
|
|
@ -64,31 +62,27 @@ type RunAllResult struct {
|
|||
Skipped int
|
||||
}
|
||||
|
||||
// Success returns true if all non-skipped specs passed.
|
||||
// if !result.Success() { fmt.Println("failed:", result.Failed) }
|
||||
func (r RunAllResult) Success() bool {
|
||||
return r.Failed == 0
|
||||
}
|
||||
|
||||
// RunAll executes specs respecting dependencies, parallelising where possible.
|
||||
// result, err := runner.RunAll(ctx, []process.RunSpec{{Name: "build"}, {Name: "test", After: []string{"build"}}})
|
||||
func (r *Runner) RunAll(ctx context.Context, specs []RunSpec) (*RunAllResult, error) {
|
||||
if err := r.ensureService(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.Now()
|
||||
|
||||
// Build dependency graph
|
||||
specMap := make(map[string]RunSpec)
|
||||
indexMap := make(map[string]int)
|
||||
for i, spec := range specs {
|
||||
for _, spec := range specs {
|
||||
specMap[spec.Name] = spec
|
||||
indexMap[spec.Name] = i
|
||||
}
|
||||
|
||||
// Track completion
|
||||
completed := make(map[string]*RunResult)
|
||||
var completedMu sync.Mutex
|
||||
|
||||
results := make([]RunResult, len(specs))
|
||||
results := make([]RunResult, 0, len(specs))
|
||||
var resultsMu sync.Mutex
|
||||
|
||||
// Process specs in waves
|
||||
remaining := make(map[string]RunSpec)
|
||||
|
|
@ -107,13 +101,13 @@ func (r *Runner) RunAll(ctx context.Context, specs []RunSpec) (*RunAllResult, er
|
|||
|
||||
if len(ready) == 0 && len(remaining) > 0 {
|
||||
// Deadlock — circular dependency or missing specs. Mark as failed, not skipped.
|
||||
for name, spec := range remaining {
|
||||
results[indexMap[name]] = RunResult{
|
||||
for name := range remaining {
|
||||
results = append(results, RunResult{
|
||||
Name: name,
|
||||
Spec: spec,
|
||||
Spec: remaining[name],
|
||||
ExitCode: 1,
|
||||
Error: core.E("runner.run_all", "circular dependency or missing dependency", nil),
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
@ -154,7 +148,9 @@ func (r *Runner) RunAll(ctx context.Context, specs []RunSpec) (*RunAllResult, er
|
|||
completed[spec.Name] = &result
|
||||
completedMu.Unlock()
|
||||
|
||||
results[indexMap[spec.Name]] = result
|
||||
resultsMu.Lock()
|
||||
results = append(results, result)
|
||||
resultsMu.Unlock()
|
||||
}(spec)
|
||||
}
|
||||
wg.Wait()
|
||||
|
|
@ -166,22 +162,22 @@ func (r *Runner) RunAll(ctx context.Context, specs []RunSpec) (*RunAllResult, er
|
|||
}
|
||||
|
||||
// Build aggregate result
|
||||
aggResult := &RunAllResult{
|
||||
aggregate := &RunAllResult{
|
||||
Results: results,
|
||||
Duration: time.Since(start),
|
||||
}
|
||||
|
||||
for _, res := range results {
|
||||
if res.Skipped {
|
||||
aggResult.Skipped++
|
||||
aggregate.Skipped++
|
||||
} else if res.Passed() {
|
||||
aggResult.Passed++
|
||||
aggregate.Passed++
|
||||
} else {
|
||||
aggResult.Failed++
|
||||
aggregate.Failed++
|
||||
}
|
||||
}
|
||||
|
||||
return aggResult, nil
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
// canRun checks if all dependencies are completed.
|
||||
|
|
@ -230,11 +226,8 @@ func (r *Runner) runSpec(ctx context.Context, spec RunSpec) RunResult {
|
|||
}
|
||||
}
|
||||
|
||||
// RunSequential executes specs one after another, stopping on first failure.
|
||||
// result, _ := runner.RunSequential(ctx, []process.RunSpec{{Name: "lint"}, {Name: "test"}})
|
||||
func (r *Runner) RunSequential(ctx context.Context, specs []RunSpec) (*RunAllResult, error) {
|
||||
if err := r.ensureService(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.Now()
|
||||
results := make([]RunResult, 0, len(specs))
|
||||
|
||||
|
|
@ -255,29 +248,26 @@ func (r *Runner) RunSequential(ctx context.Context, specs []RunSpec) (*RunAllRes
|
|||
}
|
||||
}
|
||||
|
||||
aggResult := &RunAllResult{
|
||||
aggregate := &RunAllResult{
|
||||
Results: results,
|
||||
Duration: time.Since(start),
|
||||
}
|
||||
|
||||
for _, res := range results {
|
||||
if res.Skipped {
|
||||
aggResult.Skipped++
|
||||
aggregate.Skipped++
|
||||
} else if res.Passed() {
|
||||
aggResult.Passed++
|
||||
aggregate.Passed++
|
||||
} else {
|
||||
aggResult.Failed++
|
||||
aggregate.Failed++
|
||||
}
|
||||
}
|
||||
|
||||
return aggResult, nil
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
// RunParallel executes all specs concurrently, regardless of dependencies.
|
||||
// result, _ := runner.RunParallel(ctx, []process.RunSpec{{Name: "a"}, {Name: "b"}, {Name: "c"}})
|
||||
func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResult, error) {
|
||||
if err := r.ensureService(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
start := time.Now()
|
||||
results := make([]RunResult, len(specs))
|
||||
|
||||
|
|
@ -291,27 +281,20 @@ func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResul
|
|||
}
|
||||
wg.Wait()
|
||||
|
||||
aggResult := &RunAllResult{
|
||||
aggregate := &RunAllResult{
|
||||
Results: results,
|
||||
Duration: time.Since(start),
|
||||
}
|
||||
|
||||
for _, res := range results {
|
||||
if res.Skipped {
|
||||
aggResult.Skipped++
|
||||
aggregate.Skipped++
|
||||
} else if res.Passed() {
|
||||
aggResult.Passed++
|
||||
aggregate.Passed++
|
||||
} else {
|
||||
aggResult.Failed++
|
||||
aggregate.Failed++
|
||||
}
|
||||
}
|
||||
|
||||
return aggResult, nil
|
||||
}
|
||||
|
||||
func (r *Runner) ensureService() error {
|
||||
if r == nil || r.service == nil {
|
||||
return ErrRunnerNoService
|
||||
}
|
||||
return nil
|
||||
return aggregate, nil
|
||||
}
|
||||
|
|
|
|||
107
runner_test.go
107
runner_test.go
|
|
@ -146,24 +146,6 @@ func TestRunner_RunAll_Good(t *testing.T) {
|
|||
assert.True(t, result.Success())
|
||||
assert.Equal(t, 4, result.Passed)
|
||||
})
|
||||
|
||||
t.Run("preserves input order", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
specs := []RunSpec{
|
||||
{Name: "third", Command: "echo", Args: []string{"3"}, After: []string{"second"}},
|
||||
{Name: "first", Command: "echo", Args: []string{"1"}},
|
||||
{Name: "second", Command: "echo", Args: []string{"2"}, After: []string{"first"}},
|
||||
}
|
||||
|
||||
result, err := runner.RunAll(context.Background(), specs)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, result.Results, len(specs))
|
||||
for i, res := range result.Results {
|
||||
assert.Equal(t, specs[i].Name, res.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunner_CircularDeps_Bad(t *testing.T) {
|
||||
|
|
@ -204,18 +186,83 @@ func TestRunResult_Passed_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestRunner_NilService_Bad(t *testing.T) {
|
||||
runner := NewRunner(nil)
|
||||
func TestRunner_RunSequential_Bad(t *testing.T) {
|
||||
t.Run("invalid command fails", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
_, err := runner.RunAll(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrRunnerNoService)
|
||||
result, err := runner.RunSequential(context.Background(), []RunSpec{
|
||||
{Name: "bad", Command: "nonexistent_command_xyz"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = runner.RunSequential(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrRunnerNoService)
|
||||
|
||||
_, err = runner.RunParallel(context.Background(), nil)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrRunnerNoService)
|
||||
assert.False(t, result.Success())
|
||||
assert.Equal(t, 1, result.Failed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunner_RunSequential_Ugly(t *testing.T) {
|
||||
t.Run("empty spec list succeeds with no results", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
result, err := runner.RunSequential(context.Background(), []RunSpec{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, result.Success())
|
||||
assert.Equal(t, 0, result.Passed)
|
||||
assert.Len(t, result.Results, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunner_RunParallel_Bad(t *testing.T) {
|
||||
t.Run("invalid command fails without stopping others", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
result, err := runner.RunParallel(context.Background(), []RunSpec{
|
||||
{Name: "ok", Command: "echo", Args: []string{"1"}},
|
||||
{Name: "bad", Command: "nonexistent_command_xyz"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, result.Success())
|
||||
assert.Equal(t, 1, result.Passed)
|
||||
assert.Equal(t, 1, result.Failed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunner_RunParallel_Ugly(t *testing.T) {
|
||||
t.Run("empty spec list succeeds", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
result, err := runner.RunParallel(context.Background(), []RunSpec{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, result.Success())
|
||||
assert.Len(t, result.Results, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunner_RunAll_Bad(t *testing.T) {
|
||||
t.Run("missing dependency name counts as deadlock", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
result, err := runner.RunAll(context.Background(), []RunSpec{
|
||||
{Name: "first", Command: "echo", Args: []string{"1"}, After: []string{"missing"}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, result.Success())
|
||||
assert.Equal(t, 1, result.Failed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunner_RunAll_Ugly(t *testing.T) {
|
||||
t.Run("empty spec list succeeds", func(t *testing.T) {
|
||||
runner := newTestRunner(t)
|
||||
|
||||
result, err := runner.RunAll(context.Background(), []RunSpec{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, result.Success())
|
||||
assert.Len(t, result.Results, 0)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
188
service.go
188
service.go
|
|
@ -155,7 +155,7 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) core.Re
|
|||
Dir: opts.Dir,
|
||||
Env: append([]string(nil), opts.Env...),
|
||||
StartedAt: time.Now(),
|
||||
Status: StatusPending,
|
||||
Status: StatusRunning,
|
||||
cmd: cmd,
|
||||
ctx: procCtx,
|
||||
cancel: cancel,
|
||||
|
|
@ -168,16 +168,10 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) core.Re
|
|||
|
||||
// Start the process
|
||||
if err := cmd.Start(); err != nil {
|
||||
proc.mu.Lock()
|
||||
proc.Status = StatusFailed
|
||||
proc.mu.Unlock()
|
||||
cancel()
|
||||
return core.Result{Value: core.E("process.start", core.Concat("command failed: ", opts.Command), err), OK: false}
|
||||
}
|
||||
proc.PID = cmd.Process.Pid
|
||||
proc.mu.Lock()
|
||||
proc.Status = StatusRunning
|
||||
proc.mu.Unlock()
|
||||
|
||||
// Store process
|
||||
if r := s.managed.Set(id, proc); !r.OK {
|
||||
|
|
@ -224,7 +218,7 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) core.Re
|
|||
waitErr := cmd.Wait()
|
||||
|
||||
duration := time.Since(proc.StartedAt)
|
||||
status, exitCode, _, killedSignal := classifyProcessExit(proc, waitErr)
|
||||
status, exitCode, actionErr, killedSignal := classifyProcessExit(proc, waitErr)
|
||||
|
||||
proc.mu.Lock()
|
||||
proc.PID = cmd.Process.Pid
|
||||
|
|
@ -236,13 +230,16 @@ func (s *Service) StartWithOptions(ctx context.Context, opts RunOptions) core.Re
|
|||
close(proc.done)
|
||||
|
||||
if status == StatusKilled {
|
||||
s.emitKilledAction(proc, killedSignal)
|
||||
_ = s.Core().ACTION(ActionProcessKilled{
|
||||
ID: id,
|
||||
Signal: killedSignal,
|
||||
})
|
||||
}
|
||||
s.Core().ACTION(ActionProcessExited{
|
||||
ID: id,
|
||||
ExitCode: exitCode,
|
||||
Duration: duration,
|
||||
Error: nil,
|
||||
Error: actionErr,
|
||||
})
|
||||
}()
|
||||
|
||||
|
|
@ -273,6 +270,8 @@ func (s *Service) streamOutput(proc *ManagedProcess, r streamReader, stream Stre
|
|||
}
|
||||
|
||||
// Get returns a process by ID.
|
||||
//
|
||||
// proc, err := svc.Get("abc123")
|
||||
func (s *Service) Get(id string) (*ManagedProcess, error) {
|
||||
r := s.managed.Get(id)
|
||||
if !r.OK {
|
||||
|
|
@ -282,6 +281,8 @@ func (s *Service) Get(id string) (*ManagedProcess, error) {
|
|||
}
|
||||
|
||||
// List returns all processes.
|
||||
//
|
||||
// procs := svc.List()
|
||||
func (s *Service) List() []*ManagedProcess {
|
||||
result := make([]*ManagedProcess, 0, s.managed.Len())
|
||||
s.managed.Each(func(_ string, proc *ManagedProcess) {
|
||||
|
|
@ -291,6 +292,8 @@ func (s *Service) List() []*ManagedProcess {
|
|||
}
|
||||
|
||||
// Running returns all currently running processes.
|
||||
//
|
||||
// active := svc.Running()
|
||||
func (s *Service) Running() []*ManagedProcess {
|
||||
result := make([]*ManagedProcess, 0, s.managed.Len())
|
||||
s.managed.Each(func(_ string, proc *ManagedProcess) {
|
||||
|
|
@ -302,6 +305,8 @@ func (s *Service) Running() []*ManagedProcess {
|
|||
}
|
||||
|
||||
// Kill terminates a process by ID.
|
||||
//
|
||||
// err := svc.Kill("abc123")
|
||||
func (s *Service) Kill(id string) error {
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
|
|
@ -311,11 +316,12 @@ func (s *Service) Kill(id string) error {
|
|||
if err := proc.Kill(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.emitKilledAction(proc, proc.requestedSignal())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes a completed process from the list.
|
||||
//
|
||||
// err := svc.Remove("abc123")
|
||||
func (s *Service) Remove(id string) error {
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
|
|
@ -332,6 +338,8 @@ func (s *Service) Remove(id string) error {
|
|||
}
|
||||
|
||||
// Clear removes all completed processes.
|
||||
//
|
||||
// svc.Clear()
|
||||
func (s *Service) Clear() {
|
||||
ids := make([]string, 0)
|
||||
s.managed.Each(func(id string, proc *ManagedProcess) {
|
||||
|
|
@ -345,6 +353,8 @@ func (s *Service) Clear() {
|
|||
}
|
||||
|
||||
// Output returns the captured output of a process.
|
||||
//
|
||||
// output, err := svc.Output("abc123")
|
||||
func (s *Service) Output(id string) (string, error) {
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
|
|
@ -374,6 +384,84 @@ func (s *Service) RunWithOptions(ctx context.Context, opts RunOptions) core.Resu
|
|||
return s.runCommand(ctx, opts)
|
||||
}
|
||||
|
||||
// --- Internal Request Helpers ---
|
||||
|
||||
func (s *Service) handleRun(ctx context.Context, opts core.Options) core.Result {
|
||||
command := opts.String("command")
|
||||
if command == "" {
|
||||
return core.Result{Value: core.E("process.run", "command is required", nil), OK: false}
|
||||
}
|
||||
|
||||
runOpts := RunOptions{
|
||||
Command: command,
|
||||
Dir: opts.String("dir"),
|
||||
}
|
||||
if r := opts.Get("args"); r.OK {
|
||||
runOpts.Args = optionStrings(r.Value)
|
||||
}
|
||||
if r := opts.Get("env"); r.OK {
|
||||
runOpts.Env = optionStrings(r.Value)
|
||||
}
|
||||
|
||||
return s.runCommand(ctx, runOpts)
|
||||
}
|
||||
|
||||
func (s *Service) handleStart(ctx context.Context, opts core.Options) core.Result {
|
||||
command := opts.String("command")
|
||||
if command == "" {
|
||||
return core.Result{Value: core.E("process.start", "command is required", nil), OK: false}
|
||||
}
|
||||
|
||||
runOpts := RunOptions{
|
||||
Command: command,
|
||||
Dir: opts.String("dir"),
|
||||
Detach: true,
|
||||
}
|
||||
if r := opts.Get("args"); r.OK {
|
||||
runOpts.Args = optionStrings(r.Value)
|
||||
}
|
||||
if r := opts.Get("env"); r.OK {
|
||||
runOpts.Env = optionStrings(r.Value)
|
||||
}
|
||||
|
||||
startResult := s.StartWithOptions(ctx, runOpts)
|
||||
if !startResult.OK {
|
||||
return startResult
|
||||
}
|
||||
return core.Result{Value: startResult.Value.(*ManagedProcess).ID, OK: true}
|
||||
}
|
||||
|
||||
func (s *Service) handleKill(ctx context.Context, opts core.Options) core.Result {
|
||||
id := opts.String("id")
|
||||
if id != "" {
|
||||
if err := s.Kill(id); err != nil {
|
||||
if core.Is(err, ErrProcessNotFound) {
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("not found: ", id), nil), OK: false}
|
||||
}
|
||||
return core.Result{Value: err, OK: false}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
||||
pid := opts.Int("pid")
|
||||
if pid > 0 {
|
||||
proc, err := processHandle(pid)
|
||||
if err != nil {
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("find pid failed: ", core.Sprintf("%d", pid)), err), OK: false}
|
||||
}
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
return core.Result{Value: core.E("process.kill", core.Concat("signal failed: ", core.Sprintf("%d", pid)), err), OK: false}
|
||||
}
|
||||
return core.Result{OK: true}
|
||||
}
|
||||
|
||||
return core.Result{Value: core.E("process.kill", "need id or pid", nil), OK: false}
|
||||
}
|
||||
|
||||
func (s *Service) handleList(ctx context.Context, opts core.Options) core.Result {
|
||||
return core.Result{Value: s.managed.Names(), OK: true}
|
||||
}
|
||||
|
||||
func (s *Service) runCommand(ctx context.Context, opts RunOptions) core.Result {
|
||||
if opts.Command == "" {
|
||||
return core.Result{Value: core.E("process.run", "command is required", nil), OK: false}
|
||||
|
|
@ -398,6 +486,8 @@ func (s *Service) runCommand(ctx context.Context, opts RunOptions) core.Result {
|
|||
}
|
||||
|
||||
// Signal sends a signal to the process.
|
||||
//
|
||||
// err := proc.Signal(syscall.SIGTERM)
|
||||
func (p *ManagedProcess) Signal(sig os.Signal) error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
|
@ -444,6 +534,69 @@ func isNotExist(err error) bool {
|
|||
return os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// KillPID sends SIGTERM to the process identified by pid.
|
||||
// Use this instead of os.FindProcess+syscall in consumer packages.
|
||||
//
|
||||
// err := process.KillPID(entry.PID)
|
||||
func KillPID(pid int) error {
|
||||
proc, err := processHandle(pid)
|
||||
if err != nil {
|
||||
return core.E("process.kill_pid", core.Sprintf("find pid %d failed", pid), err)
|
||||
}
|
||||
if err := proc.Signal(syscall.SIGTERM); err != nil {
|
||||
return core.E("process.kill_pid", core.Sprintf("signal pid %d failed", pid), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsPIDAlive returns true if the process with the given pid is running.
|
||||
// Use this instead of os.FindProcess+syscall.Signal(0) in consumer packages.
|
||||
//
|
||||
// alive := process.IsPIDAlive(entry.PID)
|
||||
func IsPIDAlive(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
proc, err := processHandle(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return proc.Signal(syscall.Signal(0)) == nil
|
||||
}
|
||||
|
||||
func (s *Service) handleGet(ctx context.Context, opts core.Options) core.Result {
|
||||
id := opts.String("id")
|
||||
if id == "" {
|
||||
return core.Result{Value: core.E("process.get", "id is required", nil), OK: false}
|
||||
}
|
||||
proc, err := s.Get(id)
|
||||
if err != nil {
|
||||
return core.Result{Value: core.E("process.get", core.Concat("not found: ", id), err), OK: false}
|
||||
}
|
||||
return core.Result{Value: proc.Info(), OK: true}
|
||||
}
|
||||
|
||||
func optionStrings(value any) []string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case []string:
|
||||
return append([]string(nil), typed...)
|
||||
case []any:
|
||||
result := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
text, ok := item.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
result = append(result, text)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func classifyProcessExit(proc *ManagedProcess, err error) (Status, int, error, string) {
|
||||
if err == nil {
|
||||
return StatusExited, 0, nil, ""
|
||||
|
|
@ -494,16 +647,3 @@ func normalizeSignalName(sig syscall.Signal) string {
|
|||
return sig.String()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) emitKilledAction(proc *ManagedProcess, signal string) {
|
||||
if proc == nil || !proc.markKillEmitted() {
|
||||
return
|
||||
}
|
||||
if signal == "" {
|
||||
signal = "SIGKILL"
|
||||
}
|
||||
_ = s.Core().ACTION(ActionProcessKilled{
|
||||
ID: proc.ID,
|
||||
Signal: signal,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,52 +102,6 @@ func TestService_HandleStart_Good(t *testing.T) {
|
|||
))
|
||||
require.True(t, kill.OK)
|
||||
<-proc.Done()
|
||||
|
||||
t.Run("respects detach=false", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
start := c.Action("process.start").Run(ctx, framework.NewOptions(
|
||||
framework.Option{Key: "command", Value: "sleep"},
|
||||
framework.Option{Key: "args", Value: []string{"60"}},
|
||||
framework.Option{Key: "detach", Value: false},
|
||||
))
|
||||
require.True(t, start.OK)
|
||||
|
||||
id := start.Value.(string)
|
||||
proc, err := svc.Get(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should honor detached=false context cancellation")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("defaults to non-detached", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
start := c.Action("process.start").Run(ctx, framework.NewOptions(
|
||||
framework.Option{Key: "command", Value: "sleep"},
|
||||
framework.Option{Key: "args", Value: []string{"60"}},
|
||||
))
|
||||
require.True(t, start.OK)
|
||||
|
||||
id := start.Value.(string)
|
||||
proc, err := svc.Get(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-proc.Done():
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("process should honor context cancellation by default")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_HandleStart_Bad(t *testing.T) {
|
||||
|
|
@ -474,7 +428,6 @@ func TestService_Actions_Good(t *testing.T) {
|
|||
|
||||
assert.Len(t, exited, 1)
|
||||
assert.Equal(t, 0, exited[0].ExitCode)
|
||||
assert.Nil(t, exited[0].Error)
|
||||
})
|
||||
|
||||
t.Run("broadcasts killed event", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Exported fields:
|
|||
### Package Functions
|
||||
|
||||
- `func Command(ctx context.Context, name string, args ...string) *Cmd`: Returns a `Cmd` for the supplied context, executable name, and arguments.
|
||||
- `func RunQuiet(ctx context.Context, name string, args ...string) error`: Runs a command with stderr captured into a buffer and returns `core.E("exec.run_quiet", core.Trim(stderr.String()), err)` on failure.
|
||||
- `func RunQuiet(ctx context.Context, name string, args ...string) error`: Runs a command with stderr captured into a buffer and returns `core.E("RunQuiet", core.Trim(stderr.String()), err)` on failure.
|
||||
- `func SetDefaultLogger(l Logger)`: Sets the package-level default logger. Passing `nil` replaces it with `NopLogger`.
|
||||
- `func DefaultLogger() Logger`: Returns the package-level default logger.
|
||||
|
||||
|
|
@ -58,9 +58,9 @@ Exported fields:
|
|||
- `func (c *Cmd) WithStdout(w io.Writer) *Cmd`: Sets `Options.Stdout` and returns the same command.
|
||||
- `func (c *Cmd) WithStderr(w io.Writer) *Cmd`: Sets `Options.Stderr` and returns the same command.
|
||||
- `func (c *Cmd) WithLogger(l Logger) *Cmd`: Sets a command-specific logger and returns the same command.
|
||||
- `func (c *Cmd) Run() error`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, runs it, and wraps failures with `wrapError("exec.cmd.run", ...)`.
|
||||
- `func (c *Cmd) Output() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns stdout bytes, and wraps failures with `wrapError("exec.cmd.output", ...)`.
|
||||
- `func (c *Cmd) CombinedOutput() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns combined stdout and stderr, and wraps failures with `wrapError("exec.cmd.combined_output", ...)`.
|
||||
- `func (c *Cmd) Run() error`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, runs it, and wraps failures with `wrapError("Cmd.Run", ...)`.
|
||||
- `func (c *Cmd) Output() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns stdout bytes, and wraps failures with `wrapError("Cmd.Output", ...)`.
|
||||
- `func (c *Cmd) CombinedOutput() ([]byte, error)`: Prepares the underlying `exec.Cmd`, logs `"executing command"`, returns combined stdout and stderr, and wraps failures with `wrapError("Cmd.CombinedOutput", ...)`.
|
||||
|
||||
### `NopLogger` Methods
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { connectProcessEvents, type ProcessEvent } from './shared/events.js';
|
||||
import type { ProcessInfo } from './shared/api.js';
|
||||
|
||||
/**
|
||||
|
|
@ -186,45 +185,23 @@ export class ProcessList extends LitElement {
|
|||
`;
|
||||
|
||||
@property({ attribute: 'api-url' }) apiUrl = '';
|
||||
@property({ attribute: 'ws-url' }) wsUrl = '';
|
||||
@property({ attribute: 'selected-id' }) selectedId = '';
|
||||
|
||||
@state() private processes: ProcessInfo[] = [];
|
||||
@state() private loading = false;
|
||||
@state() private error = '';
|
||||
@state() private connected = false;
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
@state() private killing = new Set<string>();
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadProcesses();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
updated(changed: Map<string, unknown>) {
|
||||
if (changed.has('wsUrl')) {
|
||||
this.disconnect();
|
||||
this.processes = [];
|
||||
this.loadProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
async loadProcesses() {
|
||||
// The process list is built from the shared process event stream.
|
||||
this.error = '';
|
||||
// Process-level REST endpoints are not yet available.
|
||||
// This element will populate via WS events once endpoints exist.
|
||||
this.loading = false;
|
||||
|
||||
if (!this.wsUrl) {
|
||||
this.processes = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.connect();
|
||||
this.processes = [];
|
||||
}
|
||||
|
||||
private handleSelect(proc: ProcessInfo) {
|
||||
|
|
@ -251,84 +228,6 @@ export class ProcessList extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private connect() {
|
||||
this.ws = connectProcessEvents(this.wsUrl, (event: ProcessEvent) => {
|
||||
this.applyEvent(event);
|
||||
});
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.connected = true;
|
||||
};
|
||||
this.ws.onclose = () => {
|
||||
this.connected = false;
|
||||
};
|
||||
}
|
||||
|
||||
private disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
private applyEvent(event: ProcessEvent) {
|
||||
const channel = event.channel ?? event.type ?? '';
|
||||
const data = (event.data ?? {}) as Partial<ProcessInfo> & {
|
||||
id?: string;
|
||||
signal?: string;
|
||||
};
|
||||
|
||||
if (!data.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = new Map(this.processes.map((proc) => [proc.id, proc] as const));
|
||||
const current = next.get(data.id);
|
||||
|
||||
if (channel === 'process.started') {
|
||||
next.set(data.id, this.normalizeProcess(data, current, 'running'));
|
||||
this.processes = this.sortProcesses(next);
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel === 'process.exited') {
|
||||
next.set(data.id, this.normalizeProcess(data, current, 'exited'));
|
||||
this.processes = this.sortProcesses(next);
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel === 'process.killed') {
|
||||
next.set(data.id, this.normalizeProcess(data, current, 'killed'));
|
||||
this.processes = this.sortProcesses(next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeProcess(
|
||||
data: Partial<ProcessInfo> & { id: string; signal?: string },
|
||||
current: ProcessInfo | undefined,
|
||||
status: ProcessInfo['status'],
|
||||
): ProcessInfo {
|
||||
return {
|
||||
id: data.id,
|
||||
command: data.command ?? current?.command ?? '',
|
||||
args: data.args ?? current?.args ?? [],
|
||||
dir: data.dir ?? current?.dir ?? '',
|
||||
startedAt: data.startedAt ?? current?.startedAt ?? new Date().toISOString(),
|
||||
status,
|
||||
exitCode: data.exitCode ?? current?.exitCode ?? (status === 'killed' ? -1 : 0),
|
||||
duration: data.duration ?? current?.duration ?? 0,
|
||||
pid: data.pid ?? current?.pid ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
private sortProcesses(processes: Map<string, ProcessInfo>): ProcessInfo[] {
|
||||
return [...processes.values()].sort(
|
||||
(a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
return html`<div class="loading">Loading processes\u2026</div>`;
|
||||
|
|
@ -339,11 +238,8 @@ export class ProcessList extends LitElement {
|
|||
${this.processes.length === 0
|
||||
? html`
|
||||
<div class="info-notice">
|
||||
${this.wsUrl
|
||||
? this.connected
|
||||
? 'Waiting for process events from the WebSocket feed.'
|
||||
: 'Connecting to the process event stream...'
|
||||
: 'Set a WebSocket URL to receive live process events.'}
|
||||
Process list endpoints are pending. Processes will appear here once
|
||||
the REST API for managed processes is available.
|
||||
</div>
|
||||
<div class="empty">No managed processes.</div>
|
||||
`
|
||||
|
|
@ -379,12 +275,12 @@ export class ProcessList extends LitElement {
|
|||
<div class="item-actions">
|
||||
<button
|
||||
class="kill-btn"
|
||||
disabled
|
||||
?disabled=${this.killing.has(proc.id)}
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
Live only
|
||||
${this.killing.has(proc.id) ? 'Killing\u2026' : 'Kill'}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
|
|
|
|||
|
|
@ -206,7 +206,6 @@ export class ProcessPanel extends LitElement {
|
|||
return html`
|
||||
<core-process-list
|
||||
api-url=${this.apiUrl}
|
||||
ws-url=${this.wsUrl}
|
||||
@process-selected=${this.handleProcessSelected}
|
||||
></core-process-list>
|
||||
${this.selectedProcessId
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue