implemented SSE logic

This commit is contained in:
Maximilian Wagner
2025-12-25 21:10:26 +01:00
parent 891ebe0107
commit a54c4cf9ff
3 changed files with 101 additions and 31 deletions

View File

@@ -10,21 +10,29 @@ import (
"time" "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) timezone, _ := time.LoadLocation(globals.AppConfig.Timezone)
return time.Now().In(timezone) return time.Now().In(timezone)
} }
func getLocalTimePretty() string { // Returns the current localtime in the RFC3339 format
return getLocalTime().Format("02.01.2006 15:04") 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) { func LogLine(message string) {
logline := "\n" + getLocalTime().Format(time.RFC3339) + " - " + message logline := "\n" + GetLocalTimeRFC() + " - " + message
globals.LogFile.WriteString(logline) globals.LogFile.WriteString(logline)
} }
// Helper for configReader() that does the actual reading // Helper for configReader() that does the actual reading
//
// Also validates the populated fields // Also validates the populated fields
func configReaderParse(filePath string, configOptionStorage *globals.Config) error { func configReaderParse(filePath string, configOptionStorage *globals.Config) error {
jsonData, err := os.ReadFile(filePath) jsonData, err := os.ReadFile(filePath)
@@ -45,7 +53,9 @@ func configValidator(config globals.Config) error {
} }
// Reads config file and stores the options in configOptionStorage // Reads config file and stores the options in configOptionStorage
// Tries executable/path/configFileName //
// Tries the following paths
// executable/path/configFileName
// %LOCALAPPDATA%\servtex\configFileName // %LOCALAPPDATA%\servtex\configFileName
// ~/.config/servtex/configFileName // ~/.config/servtex/configFileName
func ConfigReader(configFileName string, configOptionStorage *globals.Config) error { func ConfigReader(configFileName string, configOptionStorage *globals.Config) error {
@@ -71,6 +81,7 @@ func ConfigReader(configFileName string, configOptionStorage *globals.Config) er
} }
// Watches a specified directory and calls a function on change // Watches a specified directory and calls a function on change
//
// Optionally, a minimum time to wait for consecutive changes can be specified // Optionally, a minimum time to wait for consecutive changes can be specified
func ChangeWatch(path string, callOnChange func()) { func ChangeWatch(path string, callOnChange func()) {
LogLine("File change noticed") LogLine("File change noticed")
@@ -97,7 +108,7 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro
default: default:
execution.ExecutionState = "Unknown Latex Engine" execution.ExecutionState = "Unknown Latex Engine"
execution.Output = []byte{} execution.Output = []byte{}
execution.Timestamp = getLocalTimePretty() execution.Timestamp = GetLocalTimePretty()
execution.ExecutionLock.Unlock() execution.ExecutionLock.Unlock()
return errors.New("Unknown Latex Engine") return errors.New("Unknown Latex Engine")
} }
@@ -110,7 +121,7 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro
} }
execution.Output = stdout execution.Output = stdout
execution.Timestamp = getLocalTimePretty() execution.Timestamp = GetLocalTimePretty()
execution.ExecutionState = "Done" execution.ExecutionState = "Done"
execution.ExecutionLock.Unlock() execution.ExecutionLock.Unlock()
return nil return nil

View File

@@ -2,17 +2,17 @@
<body> <body>
<div class="container-fluid h-100 d-flex flex-column"> <div class="container-fluid h-100 d-flex flex-column">
<div class="row output flex-grow-1"> <div class="row output flex-grow-1">
<div class="col-3 left-sidebar" hx-sse="connect:/sse/status" hx-swap="innerHTML"> <div class="col-3 left-sidebar" hx-sse="connect:/sse on:status" hx-swap="innerHTML">
Status <br> Status <br>
Last Compilation <br> Last Compilation <br>
Such Info <br> Such Info <br>
Much wow Much wow
</div> </div>
<div class="col-9 p-0"> <div class="col-9 p-0">
<iframe class="pdf-frame" src="servetex.pdf?ts=0" hx-sse="connect:/sse/pdf" hx-swap="outerHTML"></iframe> <iframe class="pdf-frame" src="servetex.pdf?ts=0" hx-sse="connect:/sse on:pdf" hx-swap="outerHTML"></iframe>
</div> </div>
</div> </div>
<div class="command-out" hx-sse="connect:/sse/status" hx-swap="innerHTML"> <div class="command-out" hx-sse="connect:/sse on:output" hx-swap="innerHTML">
compile start compile start
compile file-x compile file-x
compile file-y compile file-y

View File

@@ -1,32 +1,91 @@
package frontend package frontend
import ( import (
"net/http"
"fmt" "fmt"
"time"
"strings"
"net/http"
"net/url"
"git.noctra.dev/noctra/servtex/backend"
"git.noctra.dev/noctra/servtex/globals" "git.noctra.dev/noctra/servtex/globals"
) )
// Publishes Meta Information relating to last execution // Adds necessary Headers for SSE
// Does not use contained Mutex, ignore warning func sseHeadersAdd(writer *http.ResponseWriter) {
func SSEStatus(writer http.ResponseWriter, request *http.Request) { (*writer).Header().Set("Content-Type", "text/event-stream")
writer.Header().Set("Content-Type", "text/event-stream") (*writer).Header().Set("Cache-Control", "no-cache")
writer.Header().Set("Cache-Control", "no-cache") (*writer).Header().Set("Connection", "keep-alive")
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()
} }
func SSEOutput(writer http.ResponseWriter, request *http.Request) { // Sends a Ping to keep the connection alive
writer.Header().Set("Content-Type", "text/event-stream") func ssePing(writer *http.ResponseWriter) {
writer.Header().Set("Cache-Control", "no-cache") sseHeadersAdd(writer)
writer.Header().Set("Connection", "keep-alive")
fmt.Fprintf(writer, "data: Execution Time: %s\n", globals.LatexExec.Timestamp) fmt.Fprintf((*writer), ": ping\n\n")
fmt.Fprintf(writer, "data: Execution State: %s\n\n", globals.LatexExec.ExecutionState)
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(
`<iframe class="pdf-frame" src="servetex.pdf?ts=%s" hx-sse="connect:/sse/pdf" hx-swap="outerHTML"></iframe>`,
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
}
}
} }