package main import ( "bytes" "fmt" "hash/fnv" "io" "io/fs" "log" "os" "os/exec" "os/signal" "path/filepath" "strings" "sync" "syscall" "time" "github.com/mqu/go-notify" ) const iconpath = "/data/drive/jan/hotreload/icon.png" var enableNotify = os.Getenv("GOHRL_NO_NOTIFY") == "" func logg(v ...any) { spr := fmt.Sprintln(v...) fmt.Print("\x1B[32m", time.Now().Format("15:04:05.000"), " \x1B[93m", spr[:len(spr)-1], "\x1B[0m\n") } func main() { notify.Init("gohrl") name := os.Args[1] args := os.Args[2:] cont := true sc := make(chan os.Signal, 1) exit := make(chan struct{}, 1) change := make(chan struct{}, 1) updated := make(chan struct{}, 1) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) tdir, err := os.MkdirTemp("", "gohrl-build-*") if err != nil { log.Fatalln(err) } defer func() { logg(os.RemoveAll(tdir)) }() var changes []string go func() { hash() var ok bool for { time.Sleep(time.Second) changes, ok = hash() if !ok { continue } change <- struct{}{} } }() // build loop go func() { //var not = notify.NotificationNew("updating", strings.Join(changes, "\n"), iconpath) change <- struct{}{} // make sure it builds once off the bat for cont { <-change logg("change detected", changes, "building") //not.Update("updating", strings.Join(changes, "\n"), iconpath) //not.SetTimeout(500) //not.Show() cmd := exec.Command("go", "build", "-o", tdir+"/build", name) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin err := cmd.Run() if err != nil { logg("possible error, waiting for next update") continue } updated <- struct{}{} } }() select { case <-sc: return case <-updated: } var wg sync.WaitGroup wg.Add(1) // application run loop go func() { var not = notify.NotificationNew("app running", "", iconpath) var rapid = 0 for cont { logg("starting:", name, args) cmd := exec.Command(tdir+"/build", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin kill := func() { syscall.Kill(cmd.Process.Pid, syscall.SIGINT) t := time.NewTimer(time.Second * 10) select { case <-exit: return case <-t.C: logg("program didn't exit: killing") syscall.Kill(cmd.Process.Pid, syscall.SIGTERM) } } startedAt := time.Now() cmd.Start() go func() { cmd.Wait() exit <- struct{}{} }() if not != nil && enableNotify { not.Update("update applied", strings.Join(changes, "\n"), iconpath) not.SetTimeout(1000) not.Show() } select { case <-sc: logg("inturrupting") cont = false kill() case <-updated: logg("restarting to apply update") //not.Update("restarting to apply updates", strings.Join(changes, "\n"), iconpath) //not.SetTimeout(1000) //not.Show() kill() case <-exit: logg("program exited on its own...") switch t := time.Since(startedAt); { case t < time.Second*3: rapid++ case t > time.Minute: rapid = 0 default: rapid-- } if rapid > 2 { multip := rapid * (rapid / 2) if enableNotify { not.Update("delaying restart due to rapid exiting", fmt.Sprint(time.Second*time.Duration(multip)), iconpath) not.Show() } timer := time.NewTimer(time.Second * time.Duration(multip)) select { case <-timer.C: case <-updated: case <-sc: os.Exit(1) } } } time.Sleep(time.Second) logg("releasing") cmd.Process.Release() } wg.Done() }() wg.Wait() } var hashes = make(map[string][]byte) var pt = false func hash() ([]string, bool) { dif := false var changes = []string{} err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { if err != nil { logg(err) } if !strings.HasSuffix(d.Name(), ".go") { return nil } dat, err := os.Open(filepath.Join(path)) if err != nil { return err } defer dat.Close() h := fnv.New128() _, err = io.Copy(h, dat) if err != nil { return err } var b []byte b = h.Sum(b) if !bytes.Equal(hashes[dat.Name()], b) { if pt { logg(dat.Name(), "changed, recompiling and reloading") changes = append(changes, dat.Name()) } dif = true } hashes[dat.Name()] = b return nil }) if err != nil { log.Fatalln(err) } pt = true return changes, dif }