From a54c4cf9ff1cd5c6a0ac464064b7ab6255cc7e8b Mon Sep 17 00:00:00 2001 From: Maximilian Wagner Date: Thu, 25 Dec 2025 21:10:26 +0100 Subject: [PATCH] implemented SSE logic --- backend/backend.go | 29 +++++++---- frontend/templates/body.html | 6 +-- frontend/webserver.go | 97 +++++++++++++++++++++++++++++------- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index a55a83d..9ba9ae4 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -10,21 +10,29 @@ import ( "time" ) -func getLocalTime() time.Time { +// Returns the current time in the timezone specified in the config file +func GetLocalTime() time.Time { timezone, _ := time.LoadLocation(globals.AppConfig.Timezone) return time.Now().In(timezone) } -func getLocalTimePretty() string { - return getLocalTime().Format("02.01.2006 15:04") +// Returns the current localtime in the RFC3339 format +func GetLocalTimeRFC() string { + return GetLocalTime().Format(time.RFC3339) +} + +// Returns the current localtime in custom pretty print format +func GetLocalTimePretty() string { + return GetLocalTime().Format("02.01.2006 15:04") } func LogLine(message string) { - logline := "\n" + getLocalTime().Format(time.RFC3339) + " - " + message + logline := "\n" + GetLocalTimeRFC() + " - " + message globals.LogFile.WriteString(logline) } // Helper for configReader() that does the actual reading +// // Also validates the populated fields func configReaderParse(filePath string, configOptionStorage *globals.Config) error { jsonData, err := os.ReadFile(filePath) @@ -45,9 +53,11 @@ func configValidator(config globals.Config) error { } // Reads config file and stores the options in configOptionStorage -// Tries executable/path/configFileName -// %LOCALAPPDATA%\servtex\configFileName -// ~/.config/servtex/configFileName +// +// Tries the following paths +// executable/path/configFileName +// %LOCALAPPDATA%\servtex\configFileName +// ~/.config/servtex/configFileName func ConfigReader(configFileName string, configOptionStorage *globals.Config) error { exePath, err := os.Executable() if err == nil { @@ -71,6 +81,7 @@ func ConfigReader(configFileName string, configOptionStorage *globals.Config) er } // Watches a specified directory and calls a function on change +// // Optionally, a minimum time to wait for consecutive changes can be specified func ChangeWatch(path string, callOnChange func()) { LogLine("File change noticed") @@ -97,7 +108,7 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro default: execution.ExecutionState = "Unknown Latex Engine" execution.Output = []byte{} - execution.Timestamp = getLocalTimePretty() + execution.Timestamp = GetLocalTimePretty() execution.ExecutionLock.Unlock() return errors.New("Unknown Latex Engine") } @@ -110,7 +121,7 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro } execution.Output = stdout - execution.Timestamp = getLocalTimePretty() + execution.Timestamp = GetLocalTimePretty() execution.ExecutionState = "Done" execution.ExecutionLock.Unlock() return nil diff --git a/frontend/templates/body.html b/frontend/templates/body.html index 8544776..5d12c8e 100644 --- a/frontend/templates/body.html +++ b/frontend/templates/body.html @@ -2,17 +2,17 @@
- -
+
compile start compile file-x compile file-y diff --git a/frontend/webserver.go b/frontend/webserver.go index a48aaa8..8ae4941 100644 --- a/frontend/webserver.go +++ b/frontend/webserver.go @@ -1,32 +1,91 @@ package frontend import ( - "net/http" "fmt" + "time" + "strings" + "net/http" + "net/url" + "git.noctra.dev/noctra/servtex/backend" "git.noctra.dev/noctra/servtex/globals" ) -// Publishes Meta Information relating to last execution -// Does not use contained Mutex, ignore warning -func SSEStatus(writer http.ResponseWriter, request *http.Request) { - writer.Header().Set("Content-Type", "text/event-stream") - writer.Header().Set("Cache-Control", "no-cache") - writer.Header().Set("Connection", "keep-alive") - - fmt.Fprintf(writer, "data: Execution Time: %s\n", globals.LatexExec.Timestamp) - fmt.Fprintf(writer, "data: Execution State: %s\n\n", globals.LatexExec.ExecutionState) - - writer.(http.Flusher).Flush() +// Adds necessary Headers for SSE +func sseHeadersAdd(writer *http.ResponseWriter) { + (*writer).Header().Set("Content-Type", "text/event-stream") + (*writer).Header().Set("Cache-Control", "no-cache") + (*writer).Header().Set("Connection", "keep-alive") } -func SSEOutput(writer http.ResponseWriter, request *http.Request) { - writer.Header().Set("Content-Type", "text/event-stream") - writer.Header().Set("Cache-Control", "no-cache") - writer.Header().Set("Connection", "keep-alive") +// Sends a Ping to keep the connection alive +func ssePing(writer *http.ResponseWriter) { + sseHeadersAdd(writer) - fmt.Fprintf(writer, "data: Execution Time: %s\n", globals.LatexExec.Timestamp) - fmt.Fprintf(writer, "data: Execution State: %s\n\n", globals.LatexExec.ExecutionState) + fmt.Fprintf((*writer), ": ping\n\n") - writer.(http.Flusher).Flush() + (*writer).(http.Flusher).Flush() +} + +// Sends new Event +// +// Reads globals.LatexExec +func sseStatusSend(writer *http.ResponseWriter) { + sseHeadersAdd(writer) + + fmt.Fprintf((*writer), "event: status\n") + fmt.Fprintf((*writer), "data: Execution Time: %s\n", globals.LatexExec.Timestamp) + fmt.Fprintf((*writer), "data: Execution State: %s\n\n", globals.LatexExec.ExecutionState) + + (*writer).(http.Flusher).Flush() +} + +// Sends new Event +// +// Reads globals.LatexExec +func ssePDFSend(writer *http.ResponseWriter) { + sseHeadersAdd(writer) + + fmt.Fprintf((*writer), "event: pdf\n") + iframe := fmt.Sprintf( + ``, + url.QueryEscape(backend.GetLocalTimeRFC()), + ) + fmt.Fprintf((*writer), "data: %s\n\n", iframe) + + (*writer).(http.Flusher).Flush() +} + +// Sends new Event +// +// Reads globals.LatexExec +func sseOutputSend(writer *http.ResponseWriter) { + sseHeadersAdd(writer) + + fmt.Fprintf((*writer), "event: output\n") + lines := strings.SplitSeq(string(globals.LatexExec.Output), "\n") + for line := range lines { + fmt.Fprintf((*writer), "data: %s\n", line) + } + fmt.Fprintf((*writer), "\n") + + (*writer).(http.Flusher).Flush() +} + +// Server Side Event Handler +// +// Sends a Ping instead of actual data when no new data available to save bandwidth +func SSEEventSender(writer http.ResponseWriter, request *http.Request) { + lastExecution := "" + for range time.Tick(time.Second) { + if lastExecution == globals.LatexExec.Timestamp { + ssePing(&writer) + } else { + sseStatusSend(&writer) + ssePDFSend(&writer) + sseOutputSend(&writer) + // comment out for testing purposes + lastExecution = globals.LatexExec.Timestamp + } + } }