1package serve23import (4 "errors"5 "fmt"6 "io"7 "path/filepath"8 "strings"9 "time"10 "unicode"1112 "charm.land/ssh"1314 "src.rest/cat/012/internal/confine"15 "src.rest/cat/012/internal/sheet"16 "src.rest/cat/012/internal/ui"17)1819// Opening a file named on the ssh command line, and keeping a session's20// unsaved work when it ends without the user quitting. See docs/terminal/ssh.md.2122// fileArg is the file an exec request names (ssh -t host file.012), ""23// for a shell request. The request is never run: its one word is a file24// name, which must resolve inside the served directory as names typed in25// File > Open do. allowSession has refused anything but one word.26func (s *Server) fileArg(sess ssh.Session) (string, error) {27 if sess.RawCommand() == "" {28 return "", nil29 }30 args := sess.Command()31 if len(args) != 1 {32 return "", errors.New("give one file name: ssh -t host name.012")33 }34 name := args[0]35 if name == "" || strings.ContainsFunc(name, unicode.IsControl) {36 return "", errors.New("that's not a file name")37 }38 names := []string{name}39 if filepath.Ext(name) == "" {40 names = append(names, name+sheet.FileExt) // as the session will open it41 }42 for _, n := range names {43 if _, err := s.root.Resolve(n); err != nil {44 return "", fmt.Errorf("can't open %s: %s", name, s.root.Scrub(err.Error()))45 }46 }47 return name, nil48}4950// ending is how a session ended, and where its unsaved work was kept.51type ending struct {52 idle, stopped bool // it idled out; the server stopped53 crash *ui.Crash // it stopped on an internal error54 kept string // the recovery file, relative to the served directory55 keptFor string // the file name it was kept for, "" for a book never saved56 err error // keeping it failed57 others string // who still has the shared workbook open, "" when nobody58}5960// keep writes the unsaved work of the program g ran to a recovery file61// when the session ended by idling out, by the server stopping or on an62// internal error; when the user quits, the app has already asked about63// unsaved changes.64func (e *ending) keep(g *ui.Guarded) {65 if !e.idle && !e.stopped && e.crash == nil {66 return67 }68 e.keptFor = g.Model().Filename()69 e.kept, e.err = g.Keep(time.Now())70}7172// tell says on the client's terminal why the session ended and where its73// unsaved work went.74func (e ending) tell(w io.Writer, idle time.Duration, root confine.Root) {75 switch {76 case e.stopped:77 fmt.Fprint(w, "012: the server is stopping\r\n")78 case e.idle:79 fmt.Fprintf(w, "012: closed after %v without input\r\n", idle)80 case e.crash != nil:81 fmt.Fprint(w, "012: stopped on an internal error; the server's log has a report\r\n")82 }83 switch {84 case e.others != "" && (e.idle || e.crash != nil):85 fmt.Fprintf(w, "012: the workbook stays open with %s\r\n", e.others)86 case e.err != nil:87 fmt.Fprintf(w, "012: couldn't keep the unsaved changes: %s\r\n", root.Scrub(e.err.Error()))88 case e.kept != "" && e.keptFor == "":89 fmt.Fprintf(w, "012: unsaved changes kept in %s; connect again to restore them\r\n", e.kept)90 case e.kept != "":91 fmt.Fprintf(w, "012: unsaved changes kept in %s; open %s again to restore them\r\n", e.kept, e.keptFor)92 }93}