gohrl/main.go

250 lines
4.3 KiB
Go

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
}