Merge pull request '[agent/codex:gpt-5.4-mini] Read ~/spec/code/core/go/cli/RFC.md fully. Find ONE feature ...' (#33) from agent/read---spec-code-core-go-cli-rfc-md-full into dev
All checks were successful
Security Scan / security (push) Successful in 21s
All checks were successful
Security Scan / security (push) Successful in 21s
This commit is contained in:
commit
b34d36ea41
2 changed files with 130 additions and 1 deletions
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"dappco.re/go/core"
|
"dappco.re/go/core"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
@ -145,6 +146,55 @@ func Execute() error {
|
||||||
return instance.root.Execute()
|
return instance.root.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run executes the CLI and watches an external context for cancellation.
|
||||||
|
// If the context is cancelled first, the runtime is shut down and the
|
||||||
|
// command error is returned if execution failed during shutdown.
|
||||||
|
func Run(ctx context.Context) error {
|
||||||
|
mustInit()
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
errCh <- Execute()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
Shutdown()
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWithTimeout returns a shutdown helper that waits for the runtime to stop
|
||||||
|
// for up to timeout before giving up. It is intended for deferred cleanup.
|
||||||
|
func RunWithTimeout(timeout time.Duration) func() {
|
||||||
|
return func() {
|
||||||
|
if timeout <= 0 {
|
||||||
|
Shutdown()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
Shutdown()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(timeout):
|
||||||
|
// Give up waiting, but let the shutdown goroutine finish in the background.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Context returns the CLI's root context.
|
// Context returns the CLI's root context.
|
||||||
// Cancelled on SIGINT/SIGTERM.
|
// Cancelled on SIGINT/SIGTERM.
|
||||||
func Context() context.Context {
|
func Context() context.Context {
|
||||||
|
|
@ -158,7 +208,7 @@ func Shutdown() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
instance.cancel()
|
instance.cancel()
|
||||||
_ = instance.core.ServiceShutdown(instance.ctx)
|
_ = instance.core.ServiceShutdown(context.WithoutCancel(instance.ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Signal Srv (internal) ---
|
// --- Signal Srv (internal) ---
|
||||||
|
|
|
||||||
79
pkg/cli/runtime_run_test.go
Normal file
79
pkg/cli/runtime_run_test.go
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dappco.re/go/core"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRun_Good_ReturnsCommandError(t *testing.T) {
|
||||||
|
resetGlobals(t)
|
||||||
|
|
||||||
|
require.NoError(t, Init(Options{AppName: "test"}))
|
||||||
|
|
||||||
|
RootCmd().AddCommand(NewCommand("boom", "Boom", "", func(_ *Command, _ []string) error {
|
||||||
|
return errors.New("boom")
|
||||||
|
}))
|
||||||
|
RootCmd().SetArgs([]string{"boom"})
|
||||||
|
|
||||||
|
err := Run(context.Background())
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "boom")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRun_Good_CancelledContext(t *testing.T) {
|
||||||
|
resetGlobals(t)
|
||||||
|
|
||||||
|
require.NoError(t, Init(Options{AppName: "test"}))
|
||||||
|
|
||||||
|
RootCmd().AddCommand(NewCommand("wait", "Wait", "", func(_ *Command, _ []string) error {
|
||||||
|
<-Context().Done()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
RootCmd().SetArgs([]string{"wait"})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
time.AfterFunc(25*time.Millisecond, cancel)
|
||||||
|
|
||||||
|
err := Run(ctx)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, context.Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunWithTimeout_Good_ReturnsHelper(t *testing.T) {
|
||||||
|
resetGlobals(t)
|
||||||
|
|
||||||
|
finished := make(chan struct{})
|
||||||
|
var finishedOnce sync.Once
|
||||||
|
require.NoError(t, Init(Options{
|
||||||
|
AppName: "test",
|
||||||
|
Services: []core.Service{
|
||||||
|
{
|
||||||
|
Name: "slow-stop",
|
||||||
|
OnStop: func() core.Result {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
finishedOnce.Do(func() {
|
||||||
|
close(finished)
|
||||||
|
})
|
||||||
|
return core.Result{OK: true}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
RunWithTimeout(20 * time.Millisecond)()
|
||||||
|
require.Less(t, time.Since(start), 80*time.Millisecond)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-finished:
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("shutdown did not complete")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue