Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pkg/client/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ func (c *ControlClient) ChangeControlPosition(tcp string, keepTracks bool) error
return err
}

func (c *ControlClient) KickController(targetTCP string, callback func(error)) {
c.addCall(makeRPCCall(c.client.Go("Sim.KickController", &server.KickControllerArgs{
ControllerToken: c.controllerToken,
TargetTCP: targetTCP,
}, nil, nil), callback))
}

func (c *ControlClient) CreateDeparture(airport, runway, category string, rules av.FlightRules, ac *sim.Aircraft,
callback func(error)) {
c.addCall(makeRPCCall(c.client.Go("Sim.CreateDeparture", &server.CreateDepartureArgs{
Expand Down
5 changes: 5 additions & 0 deletions pkg/panes/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,11 @@ func (mp *MessagesPane) processEvents(ctx *Context) {
}

case sim.StatusMessageEvent:
// Only show status messages if they're for us (when ToController is set) or for everyone (when ToController is empty)
if event.ToController != "" && event.ToController != ctx.UserTCP {
break
}

// Don't spam the same message repeatedly; look in the most recent 5.
n := len(mp.messages)
start := max(0, n-5)
Expand Down
33 changes: 33 additions & 0 deletions pkg/panes/stars/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,39 @@ func (sp *STARSPane) executeSTARSCommand(ctx *panes.Context, cmd string, tracks
return
}

if len(cmd) >= 3 && cmd[:2] == "*K" {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you switch *K to be .KICK? I'd like to keep the stuff that aren't real-world STARS commands to all start with ..

// Kick controller command: *K <controller_tcp>
targetTCP := strings.TrimSpace(cmd[2:])
if targetTCP == "" {
status.err = ErrSTARSCommandFormat
return
}

// Any signed-in controller can kick others (no instructor/RPO restriction)

// Verify the target controller exists and is signed in
if !slices.Contains(ctx.Client.State.HumanControllers, targetTCP) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general statement, this sort of check is better to perform on the server side: since the user's view of the current state of the world in ctx.Client.State is only updated once a second (and there's some network latency when it's sent), it's usually slightly out of date and so it's possible that the controller has just signed out. It's harmless in this case, but in that you need this check on the server side anyway, might as well remove this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where would this be better suited?

status.err = ErrSTARSIllegalPosition
return
}

// Don't allow kicking yourself
if targetTCP == ctx.UserTCP {
status.err = ErrSTARSIllegalParam
return
}

// Kick the controller
ctx.Client.KickController(targetTCP, func(err error) {
if err != nil {
sp.displayError(err, ctx, "")
}
})

status.clear = true
return
}

if len(cmd) > 3 && cmd[:3] == "*F " && sp.wipSignificantPoint != nil {
if sig, ok := sp.significantPoints[cmd[3:]]; ok {
status = sp.displaySignificantPointInfo(*sp.wipSignificantPoint, sig.Location,
Expand Down
19 changes: 18 additions & 1 deletion pkg/server/dispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ func (sd *dispatcher) SignOff(token string, _ *struct{}) error {
return sd.sm.SignOff(token)
}

type KickControllerArgs struct {
ControllerToken string
TargetTCP string
}

func (sd *dispatcher) KickController(kc *KickControllerArgs, _ *struct{}) error {
defer sd.sm.lg.CatchAndReportCrash()

return sd.sm.KickController(kc.ControllerToken, kc.TargetTCP)
}

type ChangeControlPositionArgs struct {
ControllerToken string
TCP string
Expand All @@ -46,7 +57,13 @@ func (sd *dispatcher) ChangeControlPosition(cs *ChangeControlPositionArgs, _ *st
if ctrl, s, ok := sd.sm.LookupController(cs.ControllerToken); !ok {
return ErrNoSimForControllerToken
} else {
return s.ChangeControlPosition(ctrl.tcp, cs.TCP, cs.KeepTracks)
oldTCP := ctrl.tcp
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the kick stuff?

if err := s.ChangeControlPosition(ctrl.tcp, cs.TCP, cs.KeepTracks); err != nil {
return err
}
// Update the controller mappings in SimManager after successful position change
sd.sm.UpdateControllerPosition(cs.ControllerToken, oldTCP, cs.TCP)
return nil
}
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/server/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ var (
ErrRPCTimeout = errors.New("RPC call timed out")
ErrRPCVersionMismatch = errors.New("Client and server RPC versions don't match")
ErrServerDisconnected = errors.New("Server disconnected")
ErrInsufficientPermissions = errors.New("Insufficient permissions to perform this action")
ErrNoControllerToKick = errors.New("No controller found with that position")
ErrCannotKickSelf = errors.New("Cannot kick yourself")
)

var errorStringToError = map[string]error{
Expand Down Expand Up @@ -87,6 +90,9 @@ var errorStringToError = map[string]error{
ErrRPCTimeout.Error(): ErrRPCTimeout,
ErrRPCVersionMismatch.Error(): ErrRPCVersionMismatch,
ErrServerDisconnected.Error(): ErrServerDisconnected,
ErrInsufficientPermissions.Error(): ErrInsufficientPermissions,
ErrNoControllerToKick.Error(): ErrNoControllerToKick,
ErrCannotKickSelf.Error(): ErrCannotKickSelf,
}

func TryDecodeError(e error) error {
Expand Down
83 changes: 83 additions & 0 deletions pkg/server/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,72 @@ func (sm *SimManager) SignOff(token string) error {
return sm.signOff(token)
}

func (sm *SimManager) KickController(adminToken, targetTCP string) error {
sm.mu.Lock(sm.lg)
defer sm.mu.Unlock(sm.lg)

// First verify that the admin has permission to kick
adminCtrl, adminSim, ok := sm.lookupController(adminToken)
if !ok {
return ErrNoSimForControllerToken
}

// Any signed-in controller can kick others (no instructor/RPO restriction)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to cause mayhem (vs. e.g. only allowing the primary controller who is presumably the one who created the sim) to do this? I'm fine with leaving it as is and seeing how it goes, though.


// Find the target controller in the same sim
var targetToken string
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can get rid of the for loop and if test and just do targetToken := adminCtrl.asim.controllersByTCP[targetTCP] ?

for tcp, ctrl := range adminCtrl.asim.controllersByTCP {
if tcp == targetTCP {
targetToken = ctrl.token
break
}
}

if targetToken == "" {
return ErrNoControllerToKick
}

// Don't allow kicking yourself
if targetToken == adminToken {
return ErrCannotKickSelf
}

// Send immediate feedback to the admin
adminSim.PostEvent(sim.Event{
Type: sim.StatusMessageEvent,
WrittenText: fmt.Sprintf("Kicking %s...", targetTCP),
ToController: adminCtrl.tcp,
})

// Send notification to the kicked controller
adminSim.PostEvent(sim.Event{
Type: sim.StatusMessageEvent,
WrittenText: fmt.Sprintf("You have been kicked by %s", adminCtrl.tcp),
ToController: targetTCP,
})

// Perform the kick after a short delay to allow notification delivery
go func() {
time.Sleep(500 * time.Millisecond) // Give time for notification to be processed
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that in some cases this may not be enough time and that in general it could be brittle. How about this for an alternate approach?

  • Add the KickedController event type, as suggested above.
  • Remove this server-side code to call signOff after a little while
  • Then in cmd/vice/ui.go around line 300 where it processes all of the events, it checks for a kicked event and sees if the controller matches UserTCP. In that case, the dialog is displayed and then you call ControlClient Disconnect(), which in turn calls back to the server with a SignOff request. So it's kind of odd in that the user's own computer is seeing they were kicked and then asking to be kicked, but that way we know 100% that they got the message.

(Though having suggested that, there is some risk that someone could be unkickable if they disabled that in their local vice build. Maybe another alternative is to defer the kick until Sim GetStateUpdate is called by that user and we know that they will be getting the event that triggers the notification.)


sm.mu.Lock(sm.lg)
defer sm.mu.Unlock(sm.lg)

err := sm.signOff(targetToken)
if err == nil {
sm.lg.Infof("%s: kicked by %s", targetTCP, adminCtrl.tcp)
adminSim.PostEvent(sim.Event{
Type: sim.StatusMessageEvent,
WrittenText: fmt.Sprintf("%s was kicked by %s", targetTCP, adminCtrl.tcp),
})
} else {
sm.lg.Errorf("Failed to kick %s: %v", targetTCP, err)
}
}()

return nil
}

func (sm *SimManager) signOff(token string) error {
if ctrl, s, ok := sm.lookupController(token); !ok {
return ErrNoSimForControllerToken
Expand Down Expand Up @@ -597,6 +663,23 @@ func (sm *SimManager) lookupController(token string) (*humanController, *sim.Sim
return nil, nil, false
}

// UpdateControllerPosition updates the controller mappings after a position change
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this part of the kick changes?

func (sm *SimManager) UpdateControllerPosition(token, oldTCP, newTCP string) {
sm.mu.Lock(sm.lg)
defer sm.mu.Unlock(sm.lg)

if ctrl, ok := sm.controllersByToken[token]; ok {
// Remove from old TCP mapping
delete(ctrl.asim.controllersByTCP, oldTCP)

// Update the controller's TCP field
ctrl.tcp = newTCP

// Add to new TCP mapping
ctrl.asim.controllersByTCP[newTCP] = ctrl
}
}

const simIdleLimit = 4 * time.Hour

func (sm *SimManager) SimShouldExit(sim *sim.Sim) bool {
Expand Down
5 changes: 5 additions & 0 deletions pkg/sim/sim.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,11 @@ func (s *Sim) signOn(tcp string, instructor bool, disableTextToSpeech bool) erro
s.State.Controllers[tcp] = s.SignOnPositions[tcp]
s.State.HumanControllers = append(s.State.HumanControllers, tcp)

// Set instructor status if the flag is true
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the kick command? If not it should probably be in a separate PR. (Is there a bug this is fixing?)

if instructor {
s.Instructors[tcp] = true
}

if tcp == s.State.PrimaryController {
// The primary controller signed in so the sim will resume.
// Reset lastUpdateTime so that the next time Update() is
Expand Down
40 changes: 40 additions & 0 deletions ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,18 @@ func uiDraw(mgr *client.ConnectionManager, config *Config, p platform.Platform,
for _, event := range ui.eventsSubscription.Get() {
if event.Type == sim.ServerBroadcastMessageEvent {
uiShowModalDialog(NewModalDialogBox(&BroadcastModalDialog{Message: event.WrittenText}, p), false)
} else if event.Type == sim.StatusMessageEvent && event.ToController != "" && controlClient != nil && event.ToController == controlClient.State.UserTCP {
// Check if this is a kick notification
if strings.Contains(event.WrittenText, "You have been kicked by") {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a little brittle to match the text here since it's non-obvious that changing it in server/manager.go would break anything. I think it would be better and more clear to add a new KickedControllerEvent in sim/eventstream.go and to trigger on that here.

client := &KickNotificationModalClient{
message: event.WrittenText,
mgr: mgr,
config: config,
platform: p,
logger: lg,
}
uiShowModalDialog(NewModalDialogBox(client, p), true) // Show at front for visibility
}
}
}

Expand Down Expand Up @@ -944,6 +956,34 @@ func (e *ErrorModalClient) Draw() int {
return -1
}

type KickNotificationModalClient struct {
message string
mgr *client.ConnectionManager
config *Config
platform platform.Platform
logger *log.Logger
}

func (k *KickNotificationModalClient) Title() string { return "Kicked from Simulation" }
func (k *KickNotificationModalClient) Opening() {}

func (k *KickNotificationModalClient) Buttons() []ModalDialogButton {
return []ModalDialogButton{{
text: "Ok",
action: func() bool {
// When the user clicks OK, show the connection dialog
uiShowConnectDialog(k.mgr, true, k.config, k.platform, k.logger)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, is this necessary? I'd have guessed that after they were kicked the code that checks if you're connected to a sim would notice they weren't and would put this up on its own. (Then that'd be nice so you wouldn't have to carry along all of the extra members in the KickNotificationModalClient struct.)

return true
},
}}
}

func (k *KickNotificationModalClient) Draw() int {
text, _ := util.WrapText(k.message, 80, 0, true)
imgui.Text("\n\n" + text + "\n\n")
return -1
}

func ShowErrorDialog(p platform.Platform, lg *log.Logger, s string, args ...interface{}) {
d := NewModalDialogBox(&ErrorModalClient{message: fmt.Sprintf(s, args...)}, p)
uiShowModalDialog(d, true)
Expand Down
Loading