main.go (5436B)
1 // log2life 2 // by Brian C. Lane <bcl@brianlane.com> 3 package main 4 5 import ( 6 "bufio" 7 "flag" 8 "fmt" 9 "io" 10 "log" 11 "net" 12 "net/http" 13 "os" 14 "strings" 15 "time" 16 ) 17 18 /* commandline flags */ 19 type cmdlineArgs struct { 20 Logfile string // Logfile to read 21 Speed float64 // Playback speed factor 1.0 == realtime 22 Columns int // Columns of Life world in cells 23 Rows int // Rows of Life world in cells 24 Port int // Port to connect to 25 Host string // Host IP to connect to 26 } 27 28 /* commandline defaults */ 29 var cfg = cmdlineArgs{ 30 Logfile: "", 31 Speed: 1.0, 32 Columns: 100, 33 Rows: 100, 34 Port: 3051, 35 Host: "127.0.0.1", 36 } 37 38 /* parseArgs handles parsing the cmdline args and setting values in the global cfg struct */ 39 func init() { 40 flag.Float64Var(&cfg.Speed, "speed", cfg.Speed, "Playback speed. 1.0 is realtime") 41 flag.IntVar(&cfg.Columns, "columns", cfg.Columns, "Width of Life world in cells") 42 flag.IntVar(&cfg.Rows, "rows", cfg.Rows, "Height of Life world in cells") 43 flag.IntVar(&cfg.Port, "port", cfg.Port, "Port to listen to") 44 flag.StringVar(&cfg.Host, "host", cfg.Host, "Host IP to bind to") 45 46 // first non flag argument is the logfile name 47 flag.Usage = func() { 48 fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [options] logfile:\n", os.Args[0]) 49 flag.PrintDefaults() 50 } 51 52 flag.Parse() 53 } 54 55 func main() { 56 if flag.NArg() == 0 { 57 flag.Usage() 58 os.Exit(1) 59 } 60 filename := flag.Arg(0) 61 62 var f *os.File 63 var err error 64 if filename == "-" { 65 f = os.Stdin 66 67 // When feeding log lines we don't want to delay 68 cfg.Speed = 0 69 fmt.Printf("Playback of <stdin> to %s:%d in realtime\n", cfg.Host, cfg.Port) 70 } else { 71 72 // Read logfile line by line 73 f, err = os.Open(filename) 74 if err != nil { 75 log.Fatal(err) 76 } 77 fmt.Printf("Playback of %s to %s:%d at %0.1fx speed\n", filename, cfg.Host, cfg.Port, cfg.Speed) 78 } 79 80 lastTime := time.Time{} 81 scanner := bufio.NewScanner(f) 82 for scanner.Scan() { 83 pattern, timestamp, err := LineToPattern(scanner.Text(), cfg.Columns, cfg.Rows) 84 if err != nil { 85 log.Print(err) 86 continue 87 } 88 89 // When reading from stdin speed is set to 0 for no delay. When replaying a 90 // log file it will use the timestamps to replay it in realtime unless -speed is passed 91 // with a different value. 92 noTime := time.Time{} 93 if lastTime != noTime { 94 delay := time.Duration(float64(timestamp.Sub(lastTime).Microseconds())*1/cfg.Speed) * time.Microsecond 95 fmt.Printf("delaying %s\n", delay) 96 time.Sleep(delay) 97 } 98 lastTime = timestamp 99 100 fmt.Printf("%s\n", strings.Join(pattern, "\n")) 101 err = SendPattern(cfg.Host, cfg.Port, pattern) 102 if err != nil { 103 fmt.Printf("ERROR: %s\n", err) 104 } 105 106 } 107 if err = scanner.Err(); err != nil { 108 log.Fatal(err) 109 } 110 } 111 112 // LineToPattern converts a log line to a Life 1.05 pattern with position based on the client IP 113 func LineToPattern(line string, width, height int) ([]string, time.Time, error) { 114 115 // Get the IP and convert to x, y coordinated, scaled by columns, rows and 0, 0 at the center 116 fields := strings.SplitN(line, " ", 4) 117 if fields[0] == "-" || strings.TrimSpace(fields[0]) == "" { 118 return []string{}, time.Time{}, fmt.Errorf("No client IP address") 119 } 120 x, y := IPToXY(fields[0], width, height) 121 122 // Get the timestamp (will eventually return this and use it for timing) 123 // [20/Nov/2022:02:27:49 +0000] 124 fields = strings.SplitN(fields[3], "]", 2) 125 // timestamp := fields[0][1:] 126 timestamp, err := time.Parse("02/Jan/2006:15:04:05 -0700", fields[0][1:]) 127 if err != nil { 128 return []string{}, time.Time{}, err 129 } 130 131 // XOR the data into an 8x8 bitpattern 132 // TODO Scramble this a bit more, all the log data is 7 bit 133 var data [8]byte 134 var idx int 135 for _, b := range []byte(fields[1]) { 136 // Skip quotes 137 if b == byte('"') { 138 continue 139 } 140 data[idx] = data[idx] ^ b 141 idx = (idx + 1) % 8 142 } 143 144 // Convert the data to a Life 1.05 pattern 145 return MakeLife105(x, y, data), timestamp, nil 146 } 147 148 // IPToXY convert an IPv4 dotted quad into an X, Y coordinate 149 func IPToXY(addr string, width, height int) (x, y int) { 150 ip := net.ParseIP(addr) 151 if ip == nil { 152 return 0, 0 153 } 154 155 // Only using IPv4 right now so 4 bytes from the ip which are at the end 156 // because it converts it to a IPv6 encoded IPv4 157 // Use the upper 16 bits as x and lower 16 as y, scaled to the life world size 158 // and with 0,0 at the center 159 x = int(float64(int(ip[12])<<8+int(ip[13]))/0xffff*float64(width)) - width/2 160 y = int(float64(int(ip[14])<<8+int(ip[15]))/0xffff*float64(height)) - height/2 161 162 return x, y 163 } 164 165 // SendPattern POSTs a pattern to the life server and returns any errors 166 func SendPattern(host string, port int, pattern []string) error { 167 data := strings.NewReader(strings.Join(pattern, "\n")) 168 resp, err := http.Post(fmt.Sprintf("http://%s:%d", host, port), "text/plain", data) 169 if err != nil { 170 return err 171 } 172 defer resp.Body.Close() 173 174 _, err = io.ReadAll(resp.Body) 175 return err 176 } 177 178 // MakeLife105 converts an array of 8 bytes into a life 1.05 pattern string 179 func MakeLife105(x, y int, data [8]byte) []string { 180 var pattern []string 181 182 pattern = append(pattern, "#Life 1.05") 183 pattern = append(pattern, "#D log2life ouput") 184 pattern = append(pattern, "#N") 185 pattern = append(pattern, fmt.Sprintf("#P %d %d", x, y)) 186 187 for _, b := range data { 188 var line string 189 for i := 0; i < 8; i++ { 190 if b&0x80 == 0x80 { 191 line = line + "*" 192 } else { 193 line = line + "." 194 } 195 196 b = b << 1 197 } 198 pattern = append(pattern, line) 199 } 200 201 return pattern 202 }