diff --git a/.gitignore b/.gitignore index 3b1931e..29df7c6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ # Custom Additions servtex -servtex.log -output +*.aux +*.log +*.pdf diff --git a/backend/backend.go b/backend/backend.go index 7b4ad1e..018dba7 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -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 } diff --git a/config.json b/config.json index 3f2632c..d079421 100644 --- a/config.json +++ b/config.json @@ -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" } diff --git a/frontend/jscss/custom-view.css b/frontend/jscss/custom-view.css index e08e099..45d2a7a 100644 --- a/frontend/jscss/custom-view.css +++ b/frontend/jscss/custom-view.css @@ -3,6 +3,7 @@ } .left-sidebar { padding: 15px; + height: 100%; } .pdf-frame { width: 100%; diff --git a/frontend/templates/main.html b/frontend/templates/main.html index c2ce990..86b0666 100644 --- a/frontend/templates/main.html +++ b/frontend/templates/main.html @@ -12,26 +12,23 @@ -
+
- diff --git a/frontend/webserver.go b/frontend/webserver.go index 03ce086..1bca983 100644 --- a/frontend/webserver.go +++ b/frontend/webserver.go @@ -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
\n", globals.LatexExec.Timestamp) + fmt.Fprintf((*writer), "data: Execution State: %s
\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( - ``, + ``, 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
\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) { - http.NotFound(writer, 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) +} + diff --git a/globals/memory.go b/globals/memory.go index f01fedc..cffad4d 100644 --- a/globals/memory.go +++ b/globals/memory.go @@ -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 - diff --git a/main.go b/main.go index 9175d21..008c660 100644 --- a/main.go +++ b/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) } diff --git a/testfiles/Example.tex b/testfiles/Example.tex new file mode 100644 index 0000000..6fa16b1 --- /dev/null +++ b/testfiles/Example.tex @@ -0,0 +1,4 @@ +\documentclass{article} +\begin{document} +(This is an Example Document) +\end{document} diff --git a/testing.crt b/testfiles/tls/testing.crt similarity index 100% rename from testing.crt rename to testfiles/tls/testing.crt diff --git a/testing.key b/testfiles/tls/testing.key similarity index 100% rename from testing.key rename to testfiles/tls/testing.key