// am-dbg-ssh is an SSH version of asyncmachine-go debugger.
package main import ( ss ) type Params struct { types.Params SshAddr string } func main() { := rootCmd(cliRun) := .Execute() if != nil { panic() } } type rootFn func(cmd *cobra.Command, args []string, params Params) var cliParamServerAddr = "ssh-addr" var cliParamServerAddrShort = "s" func rootCmd( rootFn) *cobra.Command { := &cobra.Command{ Use: "am-dbg-ssh -s localhost:4444", // nolint:lll Long: dedent.Dedent(` am-dbg-ssh is an SSH version of asyncmachine-go debugger serving local dumps via --import-file. You can connect to a running instance with any SSH client. `), Run: func( *cobra.Command, []string) { (, , parseParams(, types.ParseParams(, ))) }, } .Flags().StringP(cliParamServerAddr, cliParamServerAddrShort, "localhost:4444", "SSH host:port to listen on") types.AddFlags() // TODO validate --import-file passed return } func parseParams( *cobra.Command, types.Params) Params { := .Flag(cliParamServerAddr).Value.String() return Params{ Params: , SshAddr: , } } // TODO error msgs func cliRun( *cobra.Command, []string, Params) { := utils.GetVersion() if .Version { println() os.Exit(0) } else if .ImportData == "" { println("error: --import-file is required") os.Exit(1) } // init the debugger ssh.Handle(func( ssh.Session) { println("New session " + .RemoteAddr().String()) , := NewSessionScreen() if != nil { _, _ = fmt.Fprintln(.Stderr(), "unable to create screen:", ) return } // Init is required for cview, but not for tview if := .Init(); != nil { _, _ = fmt.Fprintln(.Stderr(), "unable to init screen:", ) return } , := debugger.New(.Context(), debugger.Opts{ // ssh screen Screen: , DbgLogLevel: .LogLevel, DbgRace: .RaceDetector, DbgLogger: types.GetLogger(&.Params, .OutputDir), ImportData: .ImportData, // ServerAddr is disabled AddrRpc: .ListenAddr, EnableMouse: .EnableMouse, EnableClipboard: .EnableClipboard, Version: , }) if != nil { _, _ = fmt.Fprintln(.Stderr(), "error: dbg", ) return } // rpc client if .DebugAddr != "" { := telemetry.TransitionsToDbg(.Mach, .DebugAddr) // TODO retries if != nil { panic() } } // start and wait till the end .Start(.StartupMachine, .StartupTx, .StartupView, .StartupGroup) select { // TODO handle timeouts better case <-time.After(10 * time.Minute): _, _ = fmt.Fprintln(.Stderr(), "hard timeout") case <-.Mach.WhenDisposed(): case <-.Mach.WhenNot1(ss.Start, nil): } := .P .Dispose() = nil runtime.GC() fmt.Println("Dispose " + .RemoteAddr().String()) var runtime.MemStats runtime.ReadMemStats(&) _, _ = .Printf("Memory: %d bytes\n", .Alloc) _ = .Exit(0) }) := make(chan struct{}) var *ssh.Server go func() { := func( *ssh.Server) error { = return nil } fmt.Printf("SSH: listening on %s\n", .SshAddr) := ssh.ListenAndServe(.SshAddr, nil, ) if != nil { log.Printf("ssh.ListenAndServe: %v", ) close() } }() // wait := make(chan os.Signal, 1) signal.Notify(, syscall.SIGINT, syscall.SIGTERM) select { case <-: _ = .Close() case <-: } } // ///// ///// ///// // ///// SSH // ///// ///// ///// func ( ssh.Session) (tcell.Screen, error) { , , := .Pty() if ! { return nil, errors.New("no pty requested") } , := terminfo.LookupTerminfo(.Term) if != nil { return nil, } , := tcell.NewTerminfoScreenFromTtyTerminfo(&tty{ Session: , size: .Window, ch: , }, ) if != nil { return nil, } return , nil } type tty struct { ssh.Session size ssh.Window ch <-chan ssh.Window resizecb func() mu sync.Mutex } func ( *tty) () error { go func() { for := range .ch { .size = .notifyResize() } }() return nil } func ( *tty) () error { return nil } func ( *tty) () error { return nil } func ( *tty) () ( tcell.WindowSize, error) { return tcell.WindowSize{ Width: .size.Width, Height: .size.Height, }, nil } func ( *tty) ( func()) { .mu.Lock() defer .mu.Unlock() .resizecb = } func ( *tty) () { .mu.Lock() defer .mu.Unlock() if .resizecb != nil { .resizecb() } }