diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 1cdbafefc..5ce0e5126 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -9,6 +9,7 @@ import ( "github.com/HyperloopUPV-H8/h9-backend/internal/flags" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/internal/update_factory" + "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" tracelogger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/trace" vehicle_models "github.com/HyperloopUPV-H8/h9-backend/internal/vehicle/models" @@ -34,12 +35,6 @@ func main() { flags.Init() handleVersionFlag() - // Configure trace - traceFile := tracelogger.InitTrace(flags.TraceLevel) - if traceFile != nil { - defer traceFile.Close() - } - // Set use to all available CPUs and setup CPU profiling if enabled cleanup := setupRuntimeCPU() defer cleanup() @@ -50,12 +45,26 @@ func main() { trace.Fatal().Err(err).Msg("error unmarshaling toml file") } + // Configure BasePath before InitTrace and NewADJ so all "others" files land in the right place + if err := logger.ConfigureLogger(config.Logging.TimeUnit, config.Logging.LoggingPath, ""); err != nil { + trace.Fatal().Err(err).Msg("configuring logger") + } + + // Configure trace + traceFile := tracelogger.InitTrace(flags.TraceLevel) + if traceFile != nil { + defer traceFile.Close() + } + // <--- ADJ ---> adj, err := adj_module.NewADJ(config.Adj) if err != nil { trace.Fatal().Err(err).Msg("setting up ADJ") } + // Now that we have the commit hash, update it in the logger + logger.CommitHash = adj.Commit + // <--- pod data ---> podData, err := pod_data.NewPodData(adj.Boards, adj.Info.Units) if err != nil { @@ -75,10 +84,7 @@ func main() { updateFactory := update_factory.NewFactory(boardToPackets) // <--- logger ---> - loggerHandler, subloggers, err := setUpLogger(config, adj.Commit) - if err != nil { - trace.Fatal().Err(err).Msg("setting up logger") - } + loggerHandler, subloggers := setUpLogger() // <-- connections & upgrader --> connections := make(chan *websocket.Client) diff --git a/backend/cmd/orchestrator.go b/backend/cmd/orchestrator.go index 53e9f6cac..0c804356d 100644 --- a/backend/cmd/orchestrator.go +++ b/backend/cmd/orchestrator.go @@ -7,7 +7,6 @@ import ( "runtime/pprof" "strings" - "github.com/HyperloopUPV-H8/h9-backend/internal/config" "github.com/HyperloopUPV-H8/h9-backend/internal/flags" "github.com/HyperloopUPV-H8/h9-backend/internal/pod_data" "github.com/HyperloopUPV-H8/h9-backend/pkg/abstraction" @@ -15,6 +14,7 @@ import ( "github.com/HyperloopUPV-H8/h9-backend/pkg/logger" data_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/data" order_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/order" + protection_logger "github.com/HyperloopUPV-H8/h9-backend/pkg/logger/protection" trace "github.com/rs/zerolog/log" ) @@ -134,17 +134,15 @@ func createLookupTables( createBoardToPackets(podData) } -func setUpLogger(config config.Config, commitHash string) (*logger.Logger, abstraction.SubloggersMap, error) { +func setUpLogger() (*logger.Logger, abstraction.SubloggersMap) { var subloggers = abstraction.SubloggersMap{ - data_logger.Name: data_logger.NewLogger(), - order_logger.Name: order_logger.NewLogger(), + data_logger.Name: data_logger.NewLogger(), + protection_logger.Name: protection_logger.NewLogger(), + order_logger.Name: order_logger.NewLogger(), } - err := logger.ConfigureLogger(config.Logging.TimeUnit, config.Logging.LoggingPath, commitHash) - loggerHandler := logger.NewLogger(subloggers, trace.Logger) - return loggerHandler, subloggers, err - + return loggerHandler, subloggers } diff --git a/backend/pkg/logger/logger.go b/backend/pkg/logger/logger.go index 511d2e1db..ad411be13 100644 --- a/backend/pkg/logger/logger.go +++ b/backend/pkg/logger/logger.go @@ -207,7 +207,7 @@ type LoggerSettings struct { } // WriteLoggerSettings writes the logger settings to a JSON file in the logger directory -func WriteLoggerSettings(path string) error { +func WriteLoggerSettings(filePath string) error { settings := LoggerSettings{ AdjCommitHash: CommitHash, TimeUnit: TimestampUnit, @@ -219,6 +219,9 @@ func WriteLoggerSettings(path string) error { return err } - return os.WriteFile(path, settingsBytes, 0644) + if err := os.MkdirAll(path.Dir(filePath), os.ModePerm); err != nil { + return err + } + return os.WriteFile(filePath, settingsBytes, 0644) } diff --git a/backend/pkg/logger/protection/logger.go b/backend/pkg/logger/protection/logger.go index 17b81d762..a92148515 100644 --- a/backend/pkg/logger/protection/logger.go +++ b/backend/pkg/logger/protection/logger.go @@ -19,17 +19,10 @@ const ( ) type Logger struct { - - // embed the base logger *loggerbase.BaseLogger - // An atomic boolean is used in order to use CompareAndSwap in the Start and Stop methods - fileLock *sync.Mutex - // saveFiles is a map that contains the file of each info packet + fileLock *sync.Mutex saveFiles map[abstraction.BoardId]*file.CSV - // BoardNames is a map that contains the common name of each board - boardNames map[abstraction.BoardId]string - // save the starting time of the logger in Unix microseconds in order to log relative timestamps } // Record is a struct that implements the abstraction.LoggerRecord interface @@ -43,14 +36,12 @@ type Record struct { func (*Record) Name() abstraction.LoggerName { return Name } -func NewLogger(boardMap map[abstraction.BoardId]string) *Logger { +func NewLogger() *Logger { - fmt.Print("ssfs") return &Logger{ BaseLogger: loggerbase.NewBaseLogger(Name), fileLock: &sync.Mutex{}, saveFiles: make(map[abstraction.BoardId]*file.CSV), - boardNames: boardMap, } } @@ -63,6 +54,7 @@ func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { } infoRecord, ok := record.(*Record) + if !ok { return logger.ErrWrongRecordType{ Name: Name, @@ -72,7 +64,7 @@ func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { } } - saveFile, err := sublogger.getFile(infoRecord.BoardId) + saveFile, err := sublogger.getFile(infoRecord.BoardId, infoRecord.From) if err != nil { return err } @@ -92,7 +84,7 @@ func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error { return err } -func (sublogger *Logger) getFile(boardId abstraction.BoardId) (*file.CSV, error) { +func (sublogger *Logger) getFile(boardId abstraction.BoardId, boardName string) (*file.CSV, error) { sublogger.fileLock.Lock() defer sublogger.fileLock.Unlock() @@ -101,7 +93,7 @@ func (sublogger *Logger) getFile(boardId abstraction.BoardId) (*file.CSV, error) return valueFile, nil } - valueFileRaw, err := sublogger.createFile(boardId) + valueFileRaw, err := sublogger.createFile(boardId, boardName) sublogger.saveFiles[boardId] = file.NewCSV(valueFileRaw) return sublogger.saveFiles[boardId], err @@ -109,14 +101,9 @@ func (sublogger *Logger) getFile(boardId abstraction.BoardId) (*file.CSV, error) // override createFile from BaseLogger to add specific path // and filename structure -func (sublogger *Logger) createFile(boardId abstraction.BoardId) (*os.File, error) { - boardName, ok := sublogger.boardNames[boardId] - if !ok { - boardName = fmt.Sprint(boardId) - } +func (sublogger *Logger) createFile(boardId abstraction.BoardId, boardName string) (*os.File, error) { filename := path.Join( - "logger", logger.Timestamp.Format(logger.TimestampFormat), "protections", fmt.Sprintf("%s.csv", boardName), diff --git a/backend/pkg/vehicle/notification.go b/backend/pkg/vehicle/notification.go index 4bf0d9d05..52e4a9948 100644 --- a/backend/pkg/vehicle/notification.go +++ b/backend/pkg/vehicle/notification.go @@ -82,16 +82,17 @@ func (vehicle *Vehicle) handlePacketNotification(notification transport.PacketNo } case *protection.Packet: - boardId := vehicle.ipToBoardId[strings.Split(notification.From, ":")[0]] + boardID := vehicle.ipToBoardId[strings.Split(notification.From, ":")[0]] err := vehicle.broker.Push(message_topic.Push(p, vehicle.idToBoardName[p.Id()])) if err != nil { vehicle.trace.Error().Stack().Err(err).Msg("broker push") return errors.Join(fmt.Errorf("update protection to frontend (%s protection with id %d and kind %d from %s to %s)", p.Severity(), p.Id(), p.Kind, notification.From, notification.To), err) } + // Log protection err = vehicle.logger.PushRecord(&protection_logger.Record{ Packet: p, - BoardId: boardId, + BoardId: boardID, From: notification.From, To: notification.To, Timestamp: notification.Timestamp, diff --git a/backend/protection_message.json b/backend/protection_message.json deleted file mode 100644 index f65822bdf..000000000 --- a/backend/protection_message.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "boardId": 12, - "timestamp": { - "counter": 38923221, - "second": 32, - "minute": 12, - "hour": 8, - "day": 25, - "month": 7, - "year": 2023 - }, - "name": "VCELL1", - "protection": { - "type": 4412323, - "data": { - "value": 3.3, - "boundary_1": 2.5, - "boundary_2": 3.2 - } - } -} diff --git a/electron-app/preload.js b/electron-app/preload.js index 871721de9..3dce66219 100644 --- a/electron-app/preload.js +++ b/electron-app/preload.js @@ -44,6 +44,8 @@ contextBridge.exposeInMainWorld("electronAPI", { blcuReadFile: (path) => ipcRenderer.invoke("blcu-read-file", path), // Get the application version from the main process getAppVersion: () => ipcRenderer.invoke("get-app-version"), + // Restart the backend process and reload the renderer when ready + restartBackend: () => ipcRenderer.invoke("restart-backend"), // Get the list of views available in this build getAvailableViews: () => ipcRenderer.invoke("get-available-views"), // Set initial mode (used by mode selector renderer) diff --git a/electron-app/src/ipc/handlers.js b/electron-app/src/ipc/handlers.js index 9e984a57e..973623e18 100644 --- a/electron-app/src/ipc/handlers.js +++ b/electron-app/src/ipc/handlers.js @@ -16,13 +16,17 @@ import { readConfig, writeConfig, } from "../config/configInstance.js"; -import { getBackendWorkingDir } from "../processes/backend.js"; +import { + getBackendWorkingDir, + restartBackend, +} from "../processes/backend.js"; import { logger } from "../utils/logger.js"; import { getAppPath } from "../utils/paths.js"; import { getCurrentView, getMainWindow, loadView, + reloadWindow, } from "../windows/mainWindow.js"; /** @@ -41,6 +45,21 @@ function setupIpcHandlers() { */ ipcMain.handle("get-current-view", () => getCurrentView()); + /** + * @event restart-backend + * @async + * @description Stops the backend process, restarts it, and reloads the renderer once ready. + */ + ipcMain.handle("restart-backend", async () => { + try { + await restartBackend(); + reloadWindow(); + } catch (error) { + logger.electron.error("Failed to restart backend:", error); + dialog.showErrorBox("Restart Failed", `Could not restart backend:\n\n${error.message}`); + } + }); + ipcMain.handle("get-app-version", () => app.getVersion()); ipcMain.handle("get-available-views", () => { diff --git a/electron-app/src/processes/backend.js b/electron-app/src/processes/backend.js index 64b51c838..c314a4637 100644 --- a/electron-app/src/processes/backend.js +++ b/electron-app/src/processes/backend.js @@ -165,10 +165,17 @@ async function startBackend(logWindow = null) { } if (code === null || code === 0) { - logger.backend.warning("Backend closed before ready signal - likely port conflict or initialization error"); + let errorMessage = "Backend process closed before initialization completed"; + if (lastBackendError) { + const stripped = lastBackendError.replace(/\x1b\[[0-9;]*m/g, ""); + errorMessage += `\n\n${stripped}`; + lastBackendError = null; + } + logger.backend.warning(errorMessage); + dialog.showErrorBox("Backend Failed to Start", errorMessage); backendProcess = null; resolved = true; - return reject(new Error("Backend process closed before initialization completed")); + return reject(new Error(errorMessage)); } } @@ -242,20 +249,15 @@ async function stopBackend() { * restartBackend(); */ async function restartBackend() { - // Stop current process first await stopBackend(); - - if (localBackendProcess.stdin) { - localBackendProcess.stdin.end(); - } - - // Start a new process + // Brief pause so the OS fully releases ports before the new process binds them + await new Promise((resolve) => setTimeout(resolve, 500)); try { await startBackend(); logger.electron.info("Backend restarted successfully"); } catch (error) { logger.electron.error("Failed to restart backend:", error); - throw error; // Let the IPC handler know it failed + throw error; } } diff --git a/frontend/testing-view/src/components/Error.tsx b/frontend/testing-view/src/components/Error.tsx index fbec77e4b..645015c39 100644 --- a/frontend/testing-view/src/components/Error.tsx +++ b/frontend/testing-view/src/components/Error.tsx @@ -1,9 +1,11 @@ import { Button } from "@workspace/ui"; import { RefreshCw, Terminal } from "@workspace/ui/icons"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import errorGif from "../assets/error.gif"; import { useStore } from "../store/store"; +const RELOAD_COOLDOWN = 8; + interface ErrorProps { /** Optional error to display. Can be null or undefined. In this case component will show default error message */ error?: Error | null; @@ -21,9 +23,21 @@ export const Error = ({ error: propError, componentStack }: ErrorProps) => { const storeError = useStore((s) => s.error); const error = propError || storeError; const [showDetails, setShowDetails] = useState(false); + const [countdown, setCountdown] = useState(RELOAD_COOLDOWN); + + useEffect(() => { + if (countdown <= 0) return; + const id = setTimeout(() => setCountdown((c) => c - 1), 1000); + return () => clearTimeout(id); + }, [countdown]); const handleReload = () => { - window.location.reload(); + setCountdown(RELOAD_COOLDOWN); + if (window.electronAPI) { + window.electronAPI.restartBackend(); + } else { + window.location.reload(); + } }; return ( @@ -72,11 +86,12 @@ export const Error = ({ error: propError, componentStack }: ErrorProps) => {