package arthur import ( "errors" "fmt" "io/ioutil" "net" "os" "path/filepath" "sort" "strings" "text/tabwriter" "time" "git.csclub.uwaterloo.ca/public/merlin/config" "git.csclub.uwaterloo.ca/public/merlin/logger" "git.csclub.uwaterloo.ca/public/merlin/sync" ) // Reads and parses the message sent over the accepted connection func GetCommand(conn net.Conn) (command, repoName string) { command = "" repoName = "" buf, err := ioutil.ReadAll(conn) if err != nil { logger.ErrLog(err.Error()) return } args := strings.Split(string(buf), ":") if len(args) >= 1 { command = args[0] } if len(args) >= 2 { repoName = args[1] } return } // Receives an message and writes it to both stdout and the accepted connection func SendAndLog(conn net.Conn, msg string) { logger.OutLog(msg) conn.Write([]byte(msg)) } // Writes the status of the repos to the accepted connection func SendStatus(conn net.Conn) { // Force arthur to send back time information in America/Toronto time location, err := time.LoadLocation("America/Toronto") if err != nil { logger.ErrLog(err) } status := tabwriter.NewWriter(conn, 5, 5, 5, ' ', 0) fmt.Fprintf(status, "Repository\tLast Synced\tNext Expected Sync\tRunning\n") // get the names of all of the repos in the config var keys []string for name, _ := range config.RepoMap { keys = append(keys, name) } sort.Strings(keys) // print out the state of each repo in the config (last and next sync time + if it is currently running) // for other ways to format the time see: https://pkg.go.dev/time#pkg-constants for _, name := range keys { repo := config.RepoMap[name] lastSync := repo.State.LastAttemptStartTime nextSync := lastSync + int64(repo.Frequency) fmt.Fprintf(status, "%s\t%s\t%s\t%t\n", name, time.Unix(lastSync, 0).In(location).Format(time.RFC1123), time.Unix(nextSync, 0).In(location).Format(time.RFC1123), repo.State.IsRunning, ) } status.Flush() } // Attempt to force the sync of the repo. Returns true iff a sync was started. func ForceSync(conn net.Conn, repoName string) (newSync bool) { newSync = false if repo, isInMap := config.RepoMap[repoName]; isInMap { logger.OutLog("Attempting to force sync of " + repoName) if sync.SyncIfPossible(repo, true) { conn.Write([]byte("Forced sync for " + repoName)) newSync = true } else { SendAndLog(conn, "Could not force sync: "+repoName+" is already syncing.") } } else { SendAndLog(conn, repoName+" is not tracked so cannot sync") } return } // Create and start listening to a unix socket and accept and forward connections to the connChan channel func StartListener(connChan chan net.Conn, stopLisChan chan struct{}) { sockpath := config.Conf.SockPath if filepath.Ext(sockpath) != ".sock" { panic(fmt.Errorf("socket file must end with .sock")) } // Attempt to remove sockpath if exists, continue if does not exist. // If old sockpath is not removed then will get "bind: address already in use". if _, err := os.Stat(sockpath); err == nil { // sockpath exists if err := os.Remove(sockpath); err != nil { panic(err) } } else if !errors.Is(err, os.ErrNotExist) { // error is not that the sockpath does not exist panic(err) } // creates and starts listening to a unix socket ear, err := net.Listen("unix", sockpath) if err != nil { panic(err) } logger.OutLog("Listening to unix socket at " + sockpath) go func() { for { // Note: cannot use select with ear.Accept() for some reason (it does block until connection is made) conn, err := ear.Accept() if err != nil { // attempting to accept on a closed net.Listener will return a non-temporary error if netErr, isTemp := err.(net.Error); isTemp && netErr.Temporary() { logger.ErrLog("Accepted socket error: " + err.Error()) continue } logger.ErrLog("Unhandlable socket error: " + err.Error()) return } connChan <- conn } }() // wait for stopLisChan to close or signal to close the socket listener <-stopLisChan ear.Close() }