Skip to content

Commit

Permalink
feat(serve): Split timeout to idle timeout and max timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe565 committed Apr 18, 2024
1 parent de55015 commit f613742
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 46 deletions.
1 change: 1 addition & 0 deletions cmd/serve/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func run(cmd *cobra.Command, args []string) (err error) {

if telnet := server.NewTelnet(cmd.Flags()); telnet.Enabled {
api.TelnetEnabled = true
server.LoadDeprecated(cmd.Flags())
group.Go(func() error {
return telnet.Listen(ctx, &m)
})
Expand Down
3 changes: 2 additions & 1 deletion docs/ascii-movie_serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ ascii-movie serve [movie] [flags]
--api-enabled Enables API listener (default true)
--concurrent-streams uint Number of concurrent streams allowed from an IP address. Set to 0 to disable. (default 10)
-h, --help help for serve
--idle-timeout duration Idle connection timeout. (default 15m0s)
--max-timeout duration Absolute connection timeout. (default 2h0m0s)
--speed float Playback speed multiplier. Must be greater than 0. (default 1)
--ssh-address string SSH listen address (default ":22")
--ssh-enabled Enables SSH listener (default true)
--ssh-host-key strings SSH host key file path
--ssh-host-key-data strings SSH host key PEM data
--telnet-address string Telnet listen address (default ":23")
--telnet-enabled Enables Telnet listener (default true)
--timeout duration Maximum amount of time that a connection may stay active. (default 1h0m0s)
```

### Options inherited from parent commands
Expand Down
16 changes: 2 additions & 14 deletions internal/movie/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,6 @@ type Player struct {

keymap keymap
helpViewCache string

timeoutCtx context.Context
timeoutCancel context.CancelFunc
}

func (p Player) Init() tea.Cmd {
Expand Down Expand Up @@ -148,12 +145,7 @@ func (p Player) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if p.log != nil {
p.log.Info("Disconnected early")
}
if p.playCancel != nil {
p.playCancel()
}
if p.timeoutCancel != nil {
p.timeoutCancel()
}
p.clearTimeouts()
return p, tea.Quit
case PlayerOption:
p.optionViewStale = true
Expand Down Expand Up @@ -227,8 +219,7 @@ func (p *Player) OptionsView() string {

func (p *Player) pause() tea.Cmd {
p.clearTimeouts()
p.timeoutCtx, p.timeoutCancel = context.WithCancel(context.Background())
return tick(p.timeoutCtx, 15*time.Minute, Quit())
return nil
}

func (p *Player) play() tea.Cmd {
Expand All @@ -247,7 +238,4 @@ func (p *Player) clearTimeouts() {
if p.playCancel != nil {
p.playCancel()
}
if p.timeoutCancel != nil {
p.timeoutCancel()
}
}
23 changes: 21 additions & 2 deletions internal/server/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ const (

ConcurrentStreamsFlag = "concurrent-streams"
TimeoutFlag = "timeout"
IdleTimeoutFlag = "idle-timeout"
MaxTimeoutFlag = "max-timeout"
)

var (
concurrentStreams uint
timeout time.Duration
idleTimeout time.Duration
maxTimeout time.Duration
)

func Flags(flags *flag.FlagSet) {
Expand All @@ -38,5 +41,21 @@ func Flags(flags *flag.FlagSet) {
flags.String(ApiFlagPrefix+AddressFlag, "127.0.0.1:1977", "API listen address")

flags.UintVar(&concurrentStreams, ConcurrentStreamsFlag, 10, "Number of concurrent streams allowed from an IP address. Set to 0 to disable.")
flags.DurationVar(&timeout, TimeoutFlag, time.Hour, "Maximum amount of time that a connection may stay active.")
flags.DurationVar(&idleTimeout, IdleTimeoutFlag, 15*time.Minute, "Idle connection timeout.")
flags.DurationVar(&maxTimeout, MaxTimeoutFlag, 2*time.Hour, "Absolute connection timeout.")

flags.Duration(TimeoutFlag, time.Hour, "Maximum amount of time that a connection may stay active.")
if err := flags.MarkDeprecated(TimeoutFlag, "please use --idle-timeout and --max-timeout instead."); err != nil {
panic(err)
}
}

func LoadDeprecated(flags *flag.FlagSet) {
if flags.Lookup(TimeoutFlag).Changed {
d, err := flags.GetDuration(TimeoutFlag)
if err == nil {
idleTimeout = d
maxTimeout = d
}
}
}
62 changes: 62 additions & 0 deletions internal/server/idleconn/conn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package idleconn

import (
"context"
"errors"
"net"
"os"
"time"
)

func New(conn net.Conn, idleTimeout, maxTimeout time.Duration, cancel context.CancelFunc) net.Conn {
return &Conn{
Conn: conn,
idleTimeout: idleTimeout,
maxDeadline: time.Now().Add(maxTimeout),
cancel: cancel,
}
}

type Conn struct {
net.Conn
idleTimeout time.Duration
maxDeadline time.Time
cancel context.CancelFunc
}

func (c *Conn) Write(p []byte) (int, error) {
_ = c.updateDeadline()

n, err := c.Conn.Write(p)
if errors.Is(err, os.ErrDeadlineExceeded) {
c.cancel()
}
return n, err
}

func (c *Conn) Read(b []byte) (int, error) {
_ = c.updateDeadline()

n, err := c.Conn.Read(b)
if errors.Is(err, os.ErrDeadlineExceeded) {
c.cancel()
}
return n, err
}

func (c *Conn) Close() error {
if c.cancel != nil {
c.cancel()
}
return c.Conn.Close()
}

func (c *Conn) updateDeadline() error {
if c.idleTimeout != 0 {
idleDeadline := time.Now().Add(c.idleTimeout)
if idleDeadline.Before(c.maxDeadline) || c.maxDeadline.IsZero() {
return c.Conn.SetDeadline(idleDeadline)
}
}
return c.Conn.SetDeadline(c.maxDeadline)
}
19 changes: 3 additions & 16 deletions internal/server/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func (s *SSHServer) Listen(ctx context.Context, m *movie.Movie) error {

sshOptions := []ssh.Option{
wish.WithAddress(s.Address),
wish.WithIdleTimeout(idleTimeout),
wish.WithMaxTimeout(maxTimeout),
wish.WithMiddleware(
bubbletea.MiddlewareWithProgramHandler(s.Handler(m), termenv.ANSI256),
s.TrackStream,
Expand Down Expand Up @@ -117,27 +119,12 @@ func (s *SSHServer) Handler(m *movie.Movie) bubbletea.ProgramHandler {
profile := util.Profile(pty.Term)

player := movie.NewPlayer(m, logger, profile)
program := tea.NewProgram(
return tea.NewProgram(
player,
tea.WithInput(session),
tea.WithOutput(session),
tea.WithFPS(30),
)

if timeout != 0 {
go func() {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-timer.C:
program.Send(movie.Quit())
case <-session.Context().Done():

}
}()
}

return program
}
}

Expand Down
17 changes: 4 additions & 13 deletions internal/server/telnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

tea "github.com/charmbracelet/bubbletea"
"github.com/gabe565/ascii-movie/internal/movie"
"github.com/gabe565/ascii-movie/internal/server/idleconn"
"github.com/gabe565/ascii-movie/internal/server/telnet"
"github.com/gabe565/ascii-movie/internal/util"
flag "github.com/spf13/pflag"
Expand Down Expand Up @@ -61,7 +62,9 @@ func (s *TelnetServer) Listen(ctx context.Context, m *movie.Movie) error {
serveGroup.Add(1)
go func() {
defer serveGroup.Done()
s.Handler(serveCtx, conn, m)
ctx, cancel := context.WithCancel(serveCtx)
conn = idleconn.New(conn, idleTimeout, maxTimeout, cancel)
s.Handler(ctx, conn, m)
}()
}
}()
Expand Down Expand Up @@ -125,18 +128,6 @@ func (s *TelnetServer) Handler(ctx context.Context, conn net.Conn, m *movie.Movi
tea.WithFPS(30),
)

if timeout != 0 {
go func() {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-timer.C:
cancel()
case <-ctx.Done():
}
}()
}

go func() {
<-ctx.Done()
program.Send(movie.Quit())
Expand Down

0 comments on commit f613742

Please sign in to comment.