Last active
October 13, 2023 16:19
-
-
Save josephspurrier/7d6ff5d8176ccf02a3c3bd85328c3882 to your computer and use it in GitHub Desktop.
Golang HTTP Redirect using JavaScript and Long Polling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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