SSE in working state
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
|
||||
# Custom Additions
|
||||
servtex
|
||||
servtex.log
|
||||
output
|
||||
*.aux
|
||||
*.log
|
||||
*.pdf
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.noctra.dev/noctra/servtex/globals"
|
||||
)
|
||||
|
||||
@@ -32,8 +32,16 @@ func GetLocalTimePretty() string {
|
||||
return GetLocalTime().Format("02.01.2006 15:04")
|
||||
}
|
||||
|
||||
// Writes to log and prints to screen
|
||||
func LogLine(message string) {
|
||||
// Writes to log file and prints to screen
|
||||
//
|
||||
// Levels:
|
||||
// 1 - Debug
|
||||
// 2 - Info
|
||||
// 3 - Warning
|
||||
// 4 - Error
|
||||
func LogLine(message string, level int) {
|
||||
if level < globals.AppConfig.LogLevelNumeric { return }
|
||||
|
||||
logline := GetLocalTimeLog() + " ServTeX: " + message + "\n"
|
||||
fmt.Print(logline)
|
||||
globals.LogFile.WriteString(logline)
|
||||
@@ -46,15 +54,31 @@ func configReaderParse(filePath string, configOptionStorage *globals.Config) err
|
||||
jsonData, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
if err = json.Unmarshal(jsonData, &configOptionStorage); err != nil {
|
||||
LogLine("Configuration file is invaldi JSON", 5)
|
||||
return errors.New("Config file could not be read")
|
||||
}
|
||||
} else {
|
||||
LogLine(fmt.Sprintf("Config at %s does not exist", filePath), 1)
|
||||
return errors.New("Config file does not exist")
|
||||
}
|
||||
|
||||
switch strings.ToLower(configOptionStorage.LogLevel) {
|
||||
case "debug":
|
||||
configOptionStorage.LogLevelNumeric = 1
|
||||
case "info":
|
||||
configOptionStorage.LogLevelNumeric = 2
|
||||
case "warning":
|
||||
configOptionStorage.LogLevelNumeric = 3
|
||||
case "error":
|
||||
configOptionStorage.LogLevelNumeric = 4
|
||||
default:
|
||||
configOptionStorage.LogLevelNumeric = 3
|
||||
LogLine("Defaulting to log level warning", 3)
|
||||
}
|
||||
return configValidator(*configOptionStorage)
|
||||
}
|
||||
|
||||
// tbd what exactly happens here
|
||||
func configValidator(config globals.Config) error {
|
||||
return nil
|
||||
}
|
||||
@@ -91,7 +115,7 @@ func ConfigReader(configFileName string, configOptionStorage *globals.Config) er
|
||||
//
|
||||
// Optionally, a minimum time to wait for consecutive changes can be specified
|
||||
func ChangeWatch(path string, callOnChange func()) {
|
||||
LogLine("File change noticed")
|
||||
LogLine("File change noticed", 2)
|
||||
}
|
||||
|
||||
// Intended to be run as goroutine
|
||||
@@ -99,11 +123,11 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro
|
||||
if execution.ExecutionLock.TryLock() {
|
||||
defer execution.ExecutionLock.Unlock()
|
||||
} else {
|
||||
LogLine("LaTeX execution already underway")
|
||||
LogLine("LaTeX execution already underway", 2)
|
||||
return errors.New("Execution already in progress")
|
||||
}
|
||||
|
||||
LogLine("LaTeX execution started")
|
||||
LogLine("LaTeX execution started", 2)
|
||||
execution.ExecutionState = "Started"
|
||||
|
||||
var latexCommand *exec.Cmd
|
||||
@@ -115,11 +139,13 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro
|
||||
"-jobname=servtex",
|
||||
config.LatexSourceFilePath,
|
||||
)
|
||||
LogLine(fmt.Sprintf("Command: %s", latexCommand.String()), 1)
|
||||
default:
|
||||
execution.ExecutionState = "Unknown Latex Engine"
|
||||
execution.Output = []byte{}
|
||||
execution.Timestamp = GetLocalTimePretty()
|
||||
return errors.New("Unknown Latex Engine")
|
||||
LogLine("Unsupported LaTeX Engine", 4)
|
||||
return errors.New("Unsupported LaTeX Engine")
|
||||
}
|
||||
|
||||
execution.ExecutionState = "Running"
|
||||
@@ -132,6 +158,7 @@ func LatexCompile(config globals.Config, execution *globals.LatexExecution) erro
|
||||
execution.Output = stdout
|
||||
execution.Timestamp = GetLocalTimePretty()
|
||||
execution.ExecutionState = "Done"
|
||||
LogLine("LaTeX execution finished", 2)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
"logFilePath": "./servtex.log",
|
||||
|
||||
"latexEngine": "lualatex",
|
||||
"latexSourceFilePath": "",
|
||||
"latexOutputPath": "./output",
|
||||
"latexSourceFilePath": "./testfiles/Example.tex",
|
||||
"latexOutputPath": "./testfiles/output",
|
||||
|
||||
"webserverDomain": "localhost",
|
||||
"webserverPort": "8080",
|
||||
"webserverSecure": true,
|
||||
"webserverPortSecure": "8443",
|
||||
"certificatePath": "./testing.crt",
|
||||
"certificateKeyPath": "./testing.key",
|
||||
"certificatePath": "./testfiles/tls/testing.crt",
|
||||
"certificateKeyPath": "./testfiles/tls/testing.key",
|
||||
"timezone": "Europe/Berlin"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
}
|
||||
.left-sidebar {
|
||||
padding: 15px;
|
||||
height: 100%;
|
||||
}
|
||||
.pdf-frame {
|
||||
width: 100%;
|
||||
|
||||
@@ -12,26 +12,23 @@
|
||||
|
||||
|
||||
<body>
|
||||
<div class="container-fluid h-100 d-flex flex-column">
|
||||
<div class="container-fluid h-100 d-flex flex-column" hx-ext="sse" sse-connect="/sse">
|
||||
<div class="row output flex-grow-1">
|
||||
<div class="col-3 left-sidebar" hx-sse="connect:/sse on:status" hx-swap="innerHTML">
|
||||
Status <br>
|
||||
Last Compilation <br>
|
||||
Such Info <br>
|
||||
Much wow
|
||||
</div>
|
||||
<div class="col-9 p-0">
|
||||
<iframe class="pdf-frame" src="/pdf?ts=0" hx-sse="connect:/sse on:pdf" hx-swap="outerHTML"></iframe>
|
||||
<div class="col-3 left-sidebar d-flex flex-column justify-content-between">
|
||||
<div>
|
||||
<h4>ServTeX Status</h4> <br>
|
||||
<div sse-swap="status">
|
||||
Compilation has not run yet.
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-out" hx-sse="connect:/sse on:output" hx-swap="innerHTML">
|
||||
compile start<br>
|
||||
compile file-x<br>
|
||||
compile file-y<br>
|
||||
example output<br>
|
||||
warning but who cares<br>
|
||||
finished compilation<br>
|
||||
output file created<br>
|
||||
<br>
|
||||
<button class="btn btn-primary mt-2" hx-post="/compile" hx-trigger="click" hx-swap="none">Recompile</button>
|
||||
</div>
|
||||
<div class="col-9 p-0" sse-swap="pdf">
|
||||
<iframe class="pdf-frame" src="/pdf?ts=0"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-out" sse-swap="output">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package frontend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"embed"
|
||||
"strings"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.noctra.dev/noctra/servtex/backend"
|
||||
"git.noctra.dev/noctra/servtex/globals"
|
||||
)
|
||||
@@ -18,17 +21,8 @@ import (
|
||||
//go:embed templates/main.html
|
||||
var WebFiles embed.FS
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// Sends a Ping to keep the connection alive
|
||||
func ssePing(writer *http.ResponseWriter) {
|
||||
sseHeadersAdd(writer)
|
||||
|
||||
fmt.Fprintf((*writer), ": ping\n\n")
|
||||
|
||||
(*writer).(http.Flusher).Flush()
|
||||
@@ -38,52 +32,52 @@ func ssePing(writer *http.ResponseWriter) {
|
||||
//
|
||||
// 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)
|
||||
fmt.Fprintf((*writer), "data: Execution Time: %s<br>\n", globals.LatexExec.Timestamp)
|
||||
fmt.Fprintf((*writer), "data: Execution State: %s<br>\n\n", globals.LatexExec.ExecutionState)
|
||||
|
||||
(*writer).(http.Flusher).Flush()
|
||||
backend.LogLine("Status Event has been sent", 1)
|
||||
}
|
||||
|
||||
// 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>`,
|
||||
`<iframe class="pdf-frame" src="/pdf?ts=%s"></iframe>`,
|
||||
url.QueryEscape(backend.GetLocalTimeRFC()),
|
||||
)
|
||||
fmt.Fprintf((*writer), "data: %s\n\n", iframe)
|
||||
|
||||
(*writer).(http.Flusher).Flush()
|
||||
backend.LogLine("PDF Event has been sent", 1)
|
||||
}
|
||||
|
||||
// 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), "data: %s<br>\n", line)
|
||||
}
|
||||
fmt.Fprintf((*writer), "\n")
|
||||
|
||||
(*writer).(http.Flusher).Flush()
|
||||
backend.LogLine("Output Event has been sent", 1)
|
||||
}
|
||||
|
||||
// Server Side Event Handler
|
||||
//
|
||||
// Sends a Ping instead of actual data when no new data available to save bandwidth
|
||||
func SSEventHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
lastExecution := ""
|
||||
writer.Header().Set("Content-Type", "text/event-stream")
|
||||
writer.Header().Set("Cache-Control", "no-cache")
|
||||
writer.Header().Set("Connection", "keep-alive")
|
||||
lastExecution := "startup"
|
||||
for range time.Tick(time.Second) {
|
||||
if lastExecution == globals.LatexExec.Timestamp {
|
||||
ssePing(&writer)
|
||||
@@ -97,9 +91,19 @@ func SSEventHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Serves the compiled PDF file
|
||||
func PDFHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
pdfPath := filepath.Join(globals.AppConfig.LatexOutputPath, "servtex.pdf")
|
||||
pdf, err := os.Open(pdfPath)
|
||||
if err != nil {
|
||||
backend.LogLine("PDF file could not be found or read", 1)
|
||||
http.NotFound(writer, request)
|
||||
return
|
||||
}
|
||||
defer pdf.Close()
|
||||
|
||||
writer.Header().Set("Content-Type", "application/pdf")
|
||||
http.ServeFile(writer, request, pdfPath)
|
||||
}
|
||||
|
||||
// Serves the main page of ServTeX
|
||||
@@ -109,3 +113,7 @@ func MainHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Write(main)
|
||||
}
|
||||
|
||||
func PDFCompile(writer http.ResponseWriter, request *http.Request) {
|
||||
backend.LatexCompile(globals.AppConfig, &globals.LatexExec)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
type Config struct {
|
||||
// general
|
||||
LogLevel string `json:"logLevel"`
|
||||
LogLevelNumeric int
|
||||
LogFilePath string `json:"logFilePath"`
|
||||
|
||||
// latex
|
||||
@@ -37,4 +38,3 @@ var LatexExec LatexExecution
|
||||
|
||||
var LogFile *os.File
|
||||
|
||||
|
||||
|
||||
39
main.go
39
main.go
@@ -16,52 +16,59 @@ import (
|
||||
|
||||
// Exit Codes:
|
||||
// 0 - ok
|
||||
// 1 - config file could not be read
|
||||
// 2 - log file could not be accessed
|
||||
// 1 - log file could not be accessed
|
||||
// 2 - config file could not be read
|
||||
func main() {
|
||||
// application init
|
||||
err := backend.ConfigReader("config.json", &globals.AppConfig)
|
||||
// temporary logfile
|
||||
var err error
|
||||
globals.LogFile, err = os.OpenFile("./servtex.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
defer globals.LogFile.Close()
|
||||
|
||||
// parse configuration
|
||||
err = backend.ConfigReader("config.json", &globals.AppConfig)
|
||||
if err != nil { os.Exit(1) }
|
||||
|
||||
// user-chosen logfile
|
||||
globals.LogFile, err = os.OpenFile(globals.AppConfig.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil { os.Exit(2) }
|
||||
|
||||
globals.LatexExec.Timestamp = "mytimestamp"
|
||||
|
||||
// webserver init
|
||||
server := &http.Server{Addr: globals.AppConfig.WebserverDomain + ":" + globals.AppConfig.WebserverPort}
|
||||
serverSecure := &http.Server{Addr: globals.AppConfig.WebserverDomain + ":" + globals.AppConfig.WebserverPortSecure}
|
||||
|
||||
// website url paths
|
||||
http.HandleFunc("/", frontend.MainHandler)
|
||||
http.HandleFunc("/sse", frontend.SSEventHandler)
|
||||
http.HandleFunc("/pdf", frontend.PDFHandler)
|
||||
http.HandleFunc("/compile", frontend.PDFCompile)
|
||||
jscss, _ := fs.Sub(frontend.WebFiles, "jscss")
|
||||
http.Handle("/jscss/", http.StripPrefix("/jscss/", http.FileServer(http.FS(jscss))))
|
||||
|
||||
// rocket
|
||||
go server.ListenAndServe()
|
||||
if globals.AppConfig.WebserverSecure {
|
||||
go serverSecure.ListenAndServeTLS(globals.AppConfig.CertificatePath, globals.AppConfig.CertificateKeyPath)
|
||||
}
|
||||
backend.LogLine("Started")
|
||||
|
||||
// shutdown logic
|
||||
backend.LogLine("Started", 2)
|
||||
fmt.Println("Press CTRL-C to Exit ServTeX")
|
||||
|
||||
// wait for signal to quit
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
<-stop
|
||||
|
||||
context, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// remove ^C from stdout
|
||||
fmt.Print("\r")
|
||||
|
||||
// shutdown
|
||||
context, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err = server.Shutdown(context); err != nil {
|
||||
backend.LogLine("Graceful Shutdown failed")
|
||||
backend.LogLine("Graceful Shutdown failed", 4)
|
||||
}
|
||||
if err = serverSecure.Shutdown(context); err != nil {
|
||||
backend.LogLine("Graceful Shutdown failed")
|
||||
backend.LogLine("Graceful Shutdown failed", 4)
|
||||
}
|
||||
backend.LogLine("Stopped")
|
||||
backend.LogLine("Stopped", 2)
|
||||
}
|
||||
|
||||
|
||||
4
testfiles/Example.tex
Normal file
4
testfiles/Example.tex
Normal file
@@ -0,0 +1,4 @@
|
||||
\documentclass{article}
|
||||
\begin{document}
|
||||
(This is an Example Document)
|
||||
\end{document}
|
||||
Reference in New Issue
Block a user