feat: safely control Shell Target

This commit is contained in:
proxyctl coordinator
2026-07-17 02:59:41 +08:00
parent 4e3f780eba
commit 12e857932b
15 changed files with 1552 additions and 16 deletions
+82 -8
View File
@@ -1,10 +1,12 @@
package app
import (
"context"
"errors"
"fmt"
"proxyctl/internal/profile"
"proxyctl/internal/target"
)
type IntentKind int
@@ -14,18 +16,31 @@ const (
UpdateProfile
DeleteProfile
ActivateProfile
SetTargetDesired
)
type Intent struct {
Kind IntentKind
Name string
Profile profile.Profile
Confirm bool
Kind IntentKind
Name string
Profile profile.Profile
Confirm bool
TargetID string
Enabled bool
}
type TargetSnapshot struct {
ID string
Description target.Description
Available bool
AvailabilityEvidence string
State target.State
Evidence string
}
type Snapshot struct {
Profiles []profile.Profile
ActiveProfile string
Targets []TargetSnapshot
}
type Result struct {
@@ -40,24 +55,41 @@ type Module interface {
}
type Application struct {
store profile.Store
state profile.State
store profile.Store
state profile.State
targets []target.Target
inspections map[string]target.Inspection
}
func New(store profile.Store) (*Application, error) {
return NewWithTargets(store, nil)
}
func NewWithTargets(store profile.Store, targets []target.Target) (*Application, error) {
state, err := store.Load()
if err != nil {
return nil, err
}
return &Application{store: store, state: state}, nil
a := &Application{store: store, state: state, targets: append([]target.Target(nil), targets...), inspections: make(map[string]target.Inspection)}
a.refreshTargets()
return a, nil
}
func (a *Application) Snapshot() Snapshot {
profiles := append([]profile.Profile(nil), a.state.Profiles...)
return Snapshot{Profiles: profiles, ActiveProfile: a.state.ActiveProfile}
snapshot := Snapshot{Profiles: profiles, ActiveProfile: a.state.ActiveProfile}
for _, adapter := range a.targets {
availability := adapter.Availability(context.Background())
inspection := a.inspections[adapter.ID()]
snapshot.Targets = append(snapshot.Targets, TargetSnapshot{ID: adapter.ID(), Description: adapter.Description(), Available: availability.Available, AvailabilityEvidence: availability.Evidence, State: inspection.State, Evidence: inspection.Evidence})
}
return snapshot
}
func (a *Application) Dispatch(intent Intent) Result {
if intent.Kind == SetTargetDesired {
return a.setTargetDesired(intent)
}
var notice string
next, err := a.store.Mutate(func(current profile.State) (profile.State, error) {
var mutationErr error
@@ -77,6 +109,7 @@ func (a *Application) Dispatch(intent Intent) Result {
})
if err == nil {
a.state = next
a.refreshTargets()
} else {
var committed *profile.CommitError
if errors.As(err, &committed) {
@@ -94,6 +127,47 @@ func (a *Application) Dispatch(intent Intent) Result {
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err}
}
func (a *Application) activeProfile() *profile.Profile {
for i := range a.state.Profiles {
if a.state.Profiles[i].Name == a.state.ActiveProfile {
value := a.state.Profiles[i]
return &value
}
}
return nil
}
func (a *Application) refreshTargets() {
active := a.activeProfile()
for _, adapter := range a.targets {
a.inspections[adapter.ID()] = adapter.Inspect(context.Background(), active)
}
}
func (a *Application) setTargetDesired(intent Intent) Result {
var selected target.Target
for _, adapter := range a.targets {
if adapter.ID() == intent.TargetID {
selected = adapter
break
}
}
if selected == nil {
return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("Target %q does not exist", intent.TargetID)}
}
active := a.activeProfile()
if intent.Enabled && active == nil {
return Result{Snapshot: a.Snapshot(), Err: errors.New("no Active Profile; open Profile management before enabling a Target")}
}
result := selected.SetDesired(context.Background(), intent.Enabled, active)
a.inspections[selected.ID()] = result.Inspection
notice := ""
if result.Err == nil {
notice = fmt.Sprintf("%s Target is now %s", selected.Description().Name, result.Inspection.State)
}
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: result.Err}
}
func create(state profile.State, input profile.Profile) (profile.State, string, error) {
p, err := profile.ValidateAndNormalize(input)
if err != nil {