Skip to content

Instantly share code, notes, and snippets.

@josephspurrier
Last active October 13, 2023 16:19
Show Gist options
  • Save josephspurrier/7d6ff5d8176ccf02a3c3bd85328c3882 to your computer and use it in GitHub Desktop.
Save josephspurrier/7d6ff5d8176ccf02a3c3bd85328c3882 to your computer and use it in GitHub Desktop.
Golang HTTP Redirect using JavaScript and Long Polling
package main
import (
"bytes"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
// FileMonitor keeps a record of files and their timestamps.
type FileMonitor struct {
rootDir string
files map[string]time.Time
cmd *exec.Cmd
}
// NewFileMonitor creates a new file monitor.
func NewFileMonitor(rootDir string) *FileMonitor {
return &FileMonitor{
rootDir: rootDir,
files: make(map[string]time.Time),
}
}
func main() {
if len(os.Args) < 2 {
log.Fatalln("Missing the Go application to run.")
}
dir, err := os.Getwd()
if err != nil {
log.Println(err)
}
fmt.Println("** Monitoring:", dir)
fm := NewFileMonitor(dir)
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("\nTerminating, please wait...")
fmt.Printf("State: %#v\n", fm.cmd.Process.Pid)
err := fm.Kill()
if err != nil {
fmt.Println("Could not kill final:", err)
}
fmt.Println("Done.")
os.Exit(1)
}()
fm.Watch()
}
// Start will start the file monitor.
func (m *FileMonitor) Start() error {
m.cmd = exec.Command("go", "run", os.Args[1])
m.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Setsid: true}
err := m.cmd.Start()
return err
}
// Kill will kill the application.
func (m *FileMonitor) Kill() error {
err := syscall.Kill(-m.cmd.Process.Pid, syscall.SIGKILL)
return err
}
// Watch files for changes.
func (m *FileMonitor) Watch() {
m.RunCommandForUser()
for {
err := filepath.Walk(m.rootDir, m.Visit)
if err != nil {
err = m.Kill()
if err != nil {
fmt.Println("Could not kill:", err)
}
m.RunCommandForUser()
}
time.Sleep(5 * time.Millisecond)
}
}
// Visit will look at each file in the directory.
func (m *FileMonitor) Visit(path string, f os.FileInfo, err error) error {
if f.IsDir() {
return nil
}
// Skip JSON files.
if strings.HasSuffix(f.Name(), ".json") {
return nil
}
oldTime, found := m.files[path]
if !found {
m.files[path] = f.ModTime()
} else {
if f.ModTime() != oldTime {
m.files[path] = f.ModTime()
fmt.Println("reloading...", path)
return errors.New("kill")
}
}
return nil
}
// RunCommandForUser outputs commands to the screen.
func (m *FileMonitor) RunCommandForUser() {
var stdoutBuf, stderrBuf bytes.Buffer
m.cmd = exec.Command("go", "run", os.Args[1])
m.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdoutIn, _ := m.cmd.StdoutPipe()
stderrIn, _ := m.cmd.StderrPipe()
var errStdout, errStderr error
stdout := io.MultiWriter(os.Stdout, &stdoutBuf)
stderr := io.MultiWriter(os.Stderr, &stderrBuf)
err := m.cmd.Start()
if err != nil {
log.Fatalf("cmd.Start() failed with '%s'\n", err)
}
go func() {
_, errStdout = io.Copy(stdout, stdoutIn)
}()
go func() {
_, errStderr = io.Copy(stderr, stderrIn)
}()
if errStdout != nil || errStderr != nil {
log.Printf("failed to capture stdout or stderr. stdout: %v | sterr: %v\n", errStdout, errStderr)
}
}
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
SetupReload()
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title of the document</title>
</head>
<body>
<h1>This is the title</h1>
<div>Content goes here.</div>
<script>
`+ReloadJavaScript()+`
</script>
</body>
</html>`)
})
http.ListenAndServe(":8080", nil)
}
// SetupReload will set up the reload endpoint.
func SetupReload() {
t := time.Now().Format("2006-01-02 03:04:05 PM")
http.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("initial") == t {
time.Sleep(30 * time.Second)
}
fmt.Fprint(w, t)
})
}
// ReloadJavaScript returns JavaScript reload code.
func ReloadJavaScript() string {
return `var previous = 0;
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
if (previous === 0) {
previous = this.responseText;
} else if (this.responseText != previous) {
location.reload(true);
return;
}
checkStatus();
} else if (this.readyState == 4) {
// This the request is an error, wait and retry.
setTimeout(function(){
checkStatus();
}, 250);
}
};
function checkStatus() {
xhttp.open("GET", "/status?initial="+previous, true);
xhttp.send();
}
// On page load, wait and then get the version number.
checkStatus();`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment