diff --git a/spongycli/network.go b/spongycli/network.go new file mode 100644 index 0000000..15929d1 --- /dev/null +++ b/spongycli/network.go @@ -0,0 +1,171 @@ +package main + +import ( + "bufio" + "fmt" + "github.com/go-fsnotify/fsnotify" + "io/ioutil" + "os" + "path" + "strconv" + "strings" + "time" +) + +const eventIdSep = "/" + +type Network struct { + running bool + + Name string + currentLog string + lineno int64 + + basePath string + seq int +} + +func NewNetwork(basePath string) (*Network) { + return &Network{ + running: true, + Name: path.Base(basePath), + basePath: basePath, + } +} + +func (nw *Network) Close() { + nw.running = false +} + +func (nw *Network) ReadLastEventId(lastEventId string) { + for _, eventId := range strings.Split(lastEventId, " ") { + parts := strings.Split(eventId, eventIdSep) + if len(parts) != 3 { + continue + } + + if parts[0] != nw.Name { + continue + } + nw.currentLog = parts[1] + nw.lineno, _ = strconv.ParseInt(parts[2], 10, 64) + return + } +} + +func (nw *Network) LastEventId() string { + parts := []string{nw.Name, nw.currentLog, strconv.FormatInt(nw.lineno, 10)} + return strings.Join(parts, eventIdSep) +} + +func (nw *Network) errmsg(err error) string { + return fmt.Sprintf("ERROR: %s", err.Error()) +} + +func (nw *Network) Tail(out chan<- string) { + if nw.currentLog == "" { + var err error + + currentfn := path.Join(nw.basePath, "log", "current") + nw.currentLog, err = os.Readlink(currentfn) + if err != nil { + out <- nw.errmsg(err) + return + } + } + + filepath := path.Join(nw.basePath, "log", nw.currentLog) + f, err := os.Open(filepath) + if err != nil { + out <- nw.errmsg(err) + return + } + defer f.Close() + + watcher, err := fsnotify.NewWatcher() + if err != nil { + out <- nw.errmsg(err) + return + } + defer watcher.Close() + + watcher.Add(filepath) + lineno := int64(0) + + // XXX: some way to stop this? + for nw.running { + bf := bufio.NewScanner(f) + for bf.Scan() { + lineno += 1 + if lineno <= nw.lineno { + continue + } else { + nw.lineno = lineno + } + + t := bf.Text() + + parts := strings.Split(t, " ") + if (len(parts) >= 3) && (parts[1] == "NEXTLOG") { + watcher.Remove(filepath) + filename := parts[2] + filepath = path.Join(nw.basePath, "log", filename) + f.Close() + f, err = os.Open(filepath) + if err != nil { + out <- nw.errmsg(err) + return + } + watcher.Add(filepath) + lineno = 0 + nw.lineno = 0 + } + out <- t + } + + select { + case _ = <-watcher.Events: + // Somethin' happened! + case err := <-watcher.Errors: + out <- nw.errmsg(err) + return + } + } +} + +func (nw *Network) Write(data []byte) { + epoch := time.Now().Unix() + pid := os.Getpid() + filename := fmt.Sprintf("%d-%d-%d.txt", epoch, pid, nw.seq) + + filepath := path.Join(nw.basePath, "outq", filename) + ioutil.WriteFile(filepath, data, 0750) + nw.seq += 1 +} + + +func Networks(basePath string) (found []*Network) { + + dir, err := os.Open(basePath) + if err != nil { + return + } + defer dir.Close() + + + entities, _ := dir.Readdirnames(0) + for _, fn := range entities { + netdir := path.Join(basePath, fn) + + _, err = os.Stat(path.Join(netdir, "nick")) + if err != nil { + continue + } + + nw := NewNetwork(netdir) + found = append(found, nw) + } + + return +} + diff --git a/spongycli/spongycli.go b/spongycli/spongycli.go new file mode 100644 index 0000000..23edfcd --- /dev/null +++ b/spongycli/spongycli.go @@ -0,0 +1,55 @@ +package main + +import ( + "bufio" + "fmt" + "flag" + "log" + "os" + "path/filepath" +) + +var playback int +var running bool = true + +func inputLoop(nw *Network) { + bf := bufio.NewScanner(os.Stdin) + for bf.Scan() { + line := bf.Bytes() + nw.Write(line) + } +} + +func usage() { + fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] NETDIR\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "NETDIR is the path to your IRC directory (see README)\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "OPTIONS:\n") + flag.PrintDefaults() +} + +func main() { + flag.Usage = usage + flag.IntVar(&playback, "playback", 0, "Number of lines to play back on startup") + + flag.Parse() + if flag.NArg() != 1 { + usage() + os.Exit(2) + } + netDir, err := filepath.Abs(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + + nw := NewNetwork(netDir) + defer nw.Close() + go inputLoop(nw) + + outq := make(chan string, 50) // to stdout + go nw.Tail(outq) + for line := range outq { + fmt.Println(line) + } +} diff --git a/spongy/irc.go b/spongyd/irc.go similarity index 93% rename from spongy/irc.go rename to spongyd/irc.go index 04688b0..28381a5 100644 --- a/spongy/irc.go +++ b/spongyd/irc.go @@ -3,10 +3,10 @@ package main import ( "strconv" "strings" - "fmt" ) type Message struct { + Unparsed string Command string FullSender string Sender string @@ -20,6 +20,7 @@ func NewMessage(v string) (Message, error) { var parts []string var lhs string + m.Unparsed = v parts = strings.SplitN(v, " :", 2) if len(parts) == 2 { lhs = parts[0] @@ -98,11 +99,9 @@ func NewMessage(v string) (Message, error) { } func (m Message) String() string { - args := strings.Join(m.Args, " ") - return fmt.Sprintf("%s %s %s %s %s :%s", m.FullSender, m.Command, m.Sender, m.Forum, args, m.Text) + return m.Unparsed } - func SplitTarget(s string) (string, string, string) { var parts []string @@ -130,4 +129,3 @@ func IsChannel(s string) bool { return false } } - diff --git a/spongy/logfile.go b/spongyd/logfile.go similarity index 53% rename from spongy/logfile.go rename to spongyd/logfile.go index 0044891..bde06a2 100644 --- a/spongy/logfile.go +++ b/spongyd/logfile.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "log" "path" "time" ) @@ -13,25 +14,54 @@ type Logfile struct { name string nlines int maxlines int + outq chan string +} + +func timestamp(s string) string { + ret := fmt.Sprintf("%d %s", time.Now().Unix(), s) + return ret } func NewLogfile(baseDir string, maxlines int) (*Logfile) { - return &Logfile{baseDir, nil, "", 0, maxlines} + lf := Logfile{baseDir, nil, "", 0, maxlines, make(chan string, 50)} + go lf.processQueue(); + return &lf } func (lf *Logfile) Close() { if lf.file != nil { - lf.writeln("EXIT") - lf.file.Close() + lf.Log("EXIT") + close(lf.outq) } } -func (lf *Logfile) writeln(s string) error { - _, err := fmt.Fprintf(lf.file, "%d %s\n", time.Now().Unix(), s) - if err == nil { +func (lf *Logfile) Log(s string) error { + lf.outq <- timestamp(s) + return nil +} + +// +// + +func (lf *Logfile) processQueue() { + for line := range lf.outq { + if (lf.file == nil) || (lf.nlines >= lf.maxlines) { + if err := lf.rotate(); err != nil { + // Just keep trying, I guess. + log.Print(err) + continue + } + lf.nlines = 0 + } + + if _, err := fmt.Fprintln(lf.file, line); err != nil { + log.Print(err) + continue + } lf.nlines += 1 } - return err + + lf.file.Close() } func (lf *Logfile) rotate() error { @@ -45,15 +75,15 @@ func (lf *Logfile) rotate() error { currentPath := path.Join(lf.baseDir, "log", "current") if lf.file == nil { - // Set lf.file just so we can write out NEXTLOG. - // If this fails, that's okay + // Open "current" to append a NEXTLOG line. + // If there's no "current", that's okay lf.file, _ = os.OpenFile(currentPath, os.O_WRONLY|os.O_APPEND, 0666) } if lf.file != nil { // Note location of new log - logmsg := fmt.Sprintf(". NEXTLOG %s", fn) - lf.writeln(logmsg) + logmsg := fmt.Sprintf("NEXTLOG %s", fn) + fmt.Fprintln(lf.file, timestamp(logmsg)) // All done with the current log lf.file.Close() @@ -66,27 +96,10 @@ func (lf *Logfile) rotate() error { os.Remove(currentPath) os.Symlink(fn, currentPath) - logmsg := fmt.Sprintf(". PREVLOG %s", lf.name) - lf.writeln(logmsg) + logmsg := fmt.Sprintf("PREVLOG %s", lf.name) + fmt.Fprintln(lf.file, timestamp(logmsg)) lf.name = fn return nil } - -func (lf *Logfile) Log(s string) error { - if lf.file == nil { - lf.rotate() - } - - err := lf.writeln(s) - if err == nil { - return err - } - - if lf.nlines >= lf.maxlines { - return lf.rotate() - } - - return nil -} diff --git a/spongy/network.go b/spongyd/network.go similarity index 75% rename from spongy/network.go rename to spongyd/network.go index bd895c7..d9be315 100644 --- a/spongy/network.go +++ b/spongyd/network.go @@ -9,6 +9,7 @@ import ( "net" "os" "os/user" + "os/exec" "path" "strings" "time" @@ -48,7 +49,9 @@ type Network struct { serverIndex int conn io.ReadWriteCloser - logq chan Message + + logf *Logfile + inq chan string outq chan string } @@ -57,23 +60,21 @@ func NewNetwork(basePath string) *Network { nw := Network{ running: true, basePath: basePath, - logq: make(chan Message, 20), } - - go nw.LogLoop() + nw.logf = NewLogfile(nw.basePath, int(maxlogsize)) return &nw } func (nw *Network) Close() { nw.running = false - close(nw.logq) if nw.conn != nil { nw.conn.Close() } + nw.logf.Close() } -func (nw *Network) WatchOutqDirectory() { +func (nw *Network) watchOutqDirectory() { outqDirname := path.Join(nw.basePath, "outq") dir, err := os.Open(outqDirname) @@ -113,20 +114,12 @@ func (nw *Network) HandleInfile(fn string) { } } -func (nw *Network) LogLoop() { - logf := NewLogfile(nw.basePath, int(maxlogsize)) - defer logf.Close() - - for m := range nw.logq { - logf.Log(m.String()) - } -} - -func (nw *Network) ServerWriteLoop() { +func (nw *Network) serverWriteLoop() { for v := range nw.outq { - m, _ := NewMessage(v) - nw.logq <- m + debug("ยป %s", v) + nw.logf.Log(v) fmt.Fprintln(nw.conn, v) + time.Sleep(500 * time.Millisecond) } } @@ -163,21 +156,20 @@ func (nw *Network) JoinChannels() { } for _, ch := range chans { + debug("Joining %s", ch) nw.outq <- "JOIN " + ch } } -func (nw *Network) MessageDispatch() { +func (nw *Network) messageDispatchLoop() { for line := range nw.inq { + nw.logf.Log(line) + m, err := NewMessage(line) if err != nil { log.Print(err) continue } - - nw.logq <- m - - // XXX: Add in a handler subprocess call switch m.Command { case "PING": @@ -196,6 +188,32 @@ func (nw *Network) MessageDispatch() { nw.outq <- "NOTICE " + m.Sender + " :\001 VERSION end\001" } } + + handlerPath := path.Join(nw.basePath, "handler") + cmd := exec.Command(handlerPath, m.Args...) + cmd.Env = []string{ + "command=" + m.Command, + "fullsender=" + m.FullSender, + "sender=" + m.Sender, + "forum=" + m.Forum, + "text=" + m.Text, + "raw=" + line, + } + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil { + log.Print(err) + continue + } + + if len(out) > 0 { + outlines := strings.Split(string(out), "\n") + for _, line := range outlines { + if len(line) > 0 { + nw.outq <- line + } + } + } } } @@ -211,6 +229,7 @@ func (nw *Network) ConnectToNextServer() bool { } server := servers[nw.serverIndex] + debug("Connecting to %s", server) switch (server[0]) { case '|': parts := strings.Split(server[1:], " ") @@ -229,12 +248,21 @@ func (nw *Network) ConnectToNextServer() bool { log.Print(err) return false } + debug("Connected") return true } func (nw *Network) login() { var name string + var username string + + usernames, err := ReadLines(path.Join(nw.basePath, "username")) + if err == nil { + username = usernames[0] + } else { + username = "sponge" + } passwd, err := ReadLines(path.Join(nw.basePath, "passwd")) if err == nil { @@ -255,15 +283,24 @@ func (nw *Network) login() { } if name == "" { - name = "Charlie" + // Rogue used "Rodney" if you didn't give it a name. + // This one works for the ladies, too. + name = "Ronnie" } - nw.outq <- "USER g g g :" + name + nw.outq <- "USER " + username + " g g :" + name nw.NextNick() } +func (nw *Network) keepaliveLoop() { + for nw.running { + time.Sleep(1 * time.Minute) + nw.outq <- "PING :keepalive" + } +} -func (nw *Network) Connect(){ + +func (nw *Network) Connect() { for nw.running { if ! nw.ConnectToNextServer() { time.Sleep(8 * time.Second) @@ -273,9 +310,10 @@ func (nw *Network) Connect(){ nw.inq = make(chan string, 20) nw.outq = make(chan string, 20) - go nw.ServerWriteLoop() - go nw.MessageDispatch() - go nw.WatchOutqDirectory() + go nw.serverWriteLoop() + go nw.messageDispatchLoop() + go nw.watchOutqDirectory() + go nw.keepaliveLoop() nw.login() diff --git a/spongy/network_test.go b/spongyd/network_test.go similarity index 100% rename from spongy/network_test.go rename to spongyd/network_test.go diff --git a/spongy/readwritecloserwrapper.go b/spongyd/readwritecloserwrapper.go similarity index 100% rename from spongy/readwritecloserwrapper.go rename to spongyd/readwritecloserwrapper.go diff --git a/spongy/readwritecloserwrapper_test.go b/spongyd/readwritecloserwrapper_test.go similarity index 100% rename from spongy/readwritecloserwrapper_test.go rename to spongyd/readwritecloserwrapper_test.go diff --git a/spongy/spongy_test.go b/spongyd/spongy_test.go similarity index 100% rename from spongy/spongy_test.go rename to spongyd/spongy_test.go diff --git a/spongy/spongy.go b/spongyd/spongyd.go similarity index 84% rename from spongy/spongy.go rename to spongyd/spongyd.go index 30b18a8..bc6f85c 100644 --- a/spongy/spongy.go +++ b/spongyd/spongyd.go @@ -11,8 +11,15 @@ import ( ) var running bool = true +var verbose bool = false var maxlogsize uint +func debug(format string, a ...interface{}) { + if verbose { + log.Printf(format, a...) + } +} + func exists(filename string) bool { _, err := os.Stat(filename); if err != nil { return false @@ -78,8 +85,9 @@ func usage() { func main() { flag.Usage = usage - flag.UintVar(&maxlogsize, "logsize", 1000, "Log entries before rotating") - notime := flag.Bool("notime", false, "Don't timestamp log messages") + flag.UintVar(&maxlogsize, "logsize", 6000, "Log entries before rotating") + flag.BoolVar(&verbose, "verbose", false, "Verbose logging") + notime := flag.Bool("notime", false, "Don't timestamp debugging messages") flag.Parse() if flag.NArg() != 1 { usage()