package backend import ( "encoding/json" "errors" "fmt" "os" "io/fs" "os/exec" "path/filepath" "strings" "time" "git.noctra.dev/noctra/servtex/globals" "github.com/fsnotify/fsnotify" ) // 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) } // Returns the current localtime in the RFC3339 format func GetLocalTimeRFC() string { return GetLocalTime().Format(time.RFC3339) } func GetLocalTimeLog() string { return GetLocalTime().Format("2006/01/02 15:04:05") } // Returns the current localtime in custom pretty print format func GetLocalTimePretty() string { return GetLocalTime().Format("02.01.2006 15:04") } // 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) } // 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) 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 } // Reads config file and stores the options in configOptionStorage // // 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 { path := filepath.Join(filepath.Dir(exePath), configFileName) err = configReaderParse(path, configOptionStorage) if err == nil { return nil } } localappdata := os.Getenv("LOCALAPPDATA") if localappdata != "" { path := filepath.Join(localappdata, "servtex", configFileName) err = configReaderParse(path, configOptionStorage) if err == nil { return nil } } path := filepath.Join("~", ".config", "servtex", configFileName) err = configReaderParse(path, configOptionStorage) if err == nil { return nil } return err } // Contents copied from https://github.com/fsnotify/fsnotify/blob/main/cmd/fsnotify/watch.go func watchLoop(watcher *fsnotify.Watcher, callOnChange func() error) { defer watcher.Close() for { select { // Errors case err, ok := <-watcher.Errors: if !ok { LogLine(fmt.Sprintf("Watcher crashed on error, manual compile only: %s", err), 4) return } LogLine(fmt.Sprintf("Watcher Error: %s", err), 3) // Events case event, ok := <-watcher.Events: if !ok { LogLine(fmt.Sprintf("Watcher crashed on event, manual compile only: %s", event), 4) return } LogLine(fmt.Sprintf("File change noticed: %s", event.Name), 2) err := callOnChange() if err != nil { LogLine(fmt.Sprintf("Latex compilation failed: %s", err), 4) } if err = watcher.Add(event.Name); err != nil { LogLine(fmt.Sprintf("File could not be re-added to watcher: %s", event.Name), 4) } } } } func texFinder(path string) []string { var texFiles []string err := filepath.WalkDir(path, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if !d.IsDir() && filepath.Ext(path) == ".tex" { texFiles = append(texFiles, path) } return nil }) if err != nil { LogLine("Some paths could not be traversed while search for .tex files", 3) } return texFiles } // 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() error) { watcher, err := fsnotify.NewWatcher() if err != nil { LogLine("File system watcher could not be initialised, manual compile only", 4) return } for _, file := range texFinder(path) { if err = watcher.Add(file); err != nil { LogLine(fmt.Sprintf("File could not be added to watcher: %s", err), 4) } LogLine(fmt.Sprintf("File added to watcher: %s", file), 1) } go watchLoop(watcher, callOnChange) LogLine("Watcher initialised", 1) } func updateExecutionTimestamp(execution *globals.LatexExecution) { execution.Timestamp = GetLocalTimePretty() execution.TimestampRFC = GetLocalTimeRFC() } // Intended to be run as goroutine func LatexCompile() error { execution := &globals.LatexExec config := globals.AppConfig if execution.ExecutionLock.TryLock() { defer execution.ExecutionLock.Unlock() } else { LogLine("LaTeX execution already underway", 2) return errors.New("Execution already in progress") } LogLine("LaTeX execution started", 2) execution.ExecutionState = "Started" var latexCommand *exec.Cmd switch config.LatexEngine { case "lualatex": latexCommand = exec.Command( "lualatex", "-output-directory=" + config.LatexOutputPath, "-jobname=servtex", config.LatexSourceFilePath, ) LogLine(fmt.Sprintf("Command: %s", latexCommand.String()), 1) default: execution.ExecutionState = "Unknown Latex Engine" execution.Output = []byte{} updateExecutionTimestamp(execution) LogLine("Unsupported LaTeX Engine", 4) return errors.New("Unsupported LaTeX Engine") } execution.ExecutionState = "Running" stdout, err := latexCommand.Output() if err != nil { execution.ExecutionState = "Failed" return err } execution.Output = stdout execution.ExecutionState = "Done" updateExecutionTimestamp(execution) LogLine("LaTeX execution finished", 2) return nil }