package main import ( // External "github.com/chromedp/cdproto/target" "github.com/chromedp/chromedp" "github.com/google/uuid" // Standard "context" "fmt" "log" "os/exec" "sync" ) var ( sites map[string]*Site siteLock *sync.Mutex ) type Site struct { UUID string URL string Watch string WatchLoop *exec.Cmd `json:"-"` StopLoop bool Context context.Context `json:"-"` } func init() { siteLock = &sync.Mutex{} sites = make(map[string]*Site) } func NewSite(wsURL, url, watch string) (s Site, err error) { s.UUID = uuid.NewString() s.URL = url s.Watch = watch allocatorContext, _ := chromedp.NewRemoteAllocator(context.Background(), wsURL) // Used to optionally enable debugging output. var opts []chromedp.ContextOption if flagVerbose { opts = append(opts, chromedp.WithDebugf(log.Printf)) } s.Context, _ = chromedp.NewContext(allocatorContext, opts...) // A tab is started up and navigated to in order to get a context for this instance. if err = chromedp.Run( s.Context, chromedp.Navigate(url), ); err != nil { err = fmt.Errorf("Failed getting body of %s: %v", url, err) return } siteLock.Lock() sites[s.UUID] = &s defer siteLock.Unlock() // chromedp.Run crashes something really hard and undetectable with recover(). // ListenBrowser gives the possibility to cancel the watch loop. chromedp.ListenBrowser(s.Context, func(ev any) { _, event1 := ev.(*target.EventDetachedFromTarget) _, event2 := ev.(*target.EventTargetCrashed) _, event3 := ev.(*target.EventTargetDestroyed) if event1 || event2 || event3 { log.Printf("Stopping loop for %s\n", s.UUID) s.StopLoop = true if s.WatchLoop != nil { log.Printf("Killing inotifywait for %s\n", s.UUID) s.WatchLoop.Process.Kill() } } }) // Start watching file/dir for changes. go s.watchLoop() return } func (s *Site) watchLoop() { for !s.StopLoop { log.Println("Starting watching " + s.Watch) s.WatchLoop = exec.Command("inotifywait", "-e", "close_write", s.Watch) if err := s.WatchLoop.Run(); err != nil { log.Println(err) } s.ReloadCSS() } log.Printf("Stopping watch loop [%s]\n", s.UUID) } func (s *Site) ReloadCSS() { var buf []byte cssReloadScript := chromedp.Evaluate(` (()=>{ const stylesheets = document.querySelectorAll('link[rel="stylesheet"]') for (const ss of stylesheets) { const url = URL.parse(ss.href, location.protocol + '//' + location.host) const nextReloadCounter = parseInt(url.searchParams.get('reload') || 0) + 1 url.searchParams.set('reload', nextReloadCounter) ss.href = url.toString() } })() `, &buf) err := chromedp.Run(s.Context, cssReloadScript) if err != nil { fmt.Println(err) } }