Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions backend/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand All @@ -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 {
Expand All @@ -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)
Expand Down
14 changes: 6 additions & 8 deletions backend/cmd/orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ 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"
adj_module "github.com/HyperloopUPV-H8/h9-backend/pkg/adj"
"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"
)

Expand Down Expand Up @@ -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
}
7 changes: 5 additions & 2 deletions backend/pkg/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
27 changes: 7 additions & 20 deletions backend/pkg/logger/protection/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
}
}

Expand All @@ -63,6 +54,7 @@ func (sublogger *Logger) PushRecord(record abstraction.LoggerRecord) error {
}

infoRecord, ok := record.(*Record)

if !ok {
return logger.ErrWrongRecordType{
Name: Name,
Expand All @@ -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
}
Expand All @@ -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()

Expand All @@ -101,22 +93,17 @@ 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
}

// 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),
Expand Down
5 changes: 3 additions & 2 deletions backend/pkg/vehicle/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 0 additions & 21 deletions backend/protection_message.json

This file was deleted.

2 changes: 2 additions & 0 deletions electron-app/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 20 additions & 1 deletion electron-app/src/ipc/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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", () => {
Expand Down
22 changes: 12 additions & 10 deletions electron-app/src/processes/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand Down Expand Up @@ -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;
}
}

Expand Down
21 changes: 18 additions & 3 deletions frontend/testing-view/src/components/Error.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
Expand Down Expand Up @@ -72,11 +86,12 @@ export const Error = ({ error: propError, componentStack }: ErrorProps) => {
<div className="flex items-center gap-3">
<Button
onClick={handleReload}
disabled={countdown > 0}
className="shadow-primary/20 gap-2 px-8 font-semibold shadow-lg"
variant="default"
>
<RefreshCw className="h-4 w-4" />
Reload App
{countdown > 0 ? `Reload App (${countdown})` : "Reload App"}
</Button>

{error?.stack && (
Expand Down
1 change: 1 addition & 0 deletions frontend/testing-view/src/vite-end.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface ElectronAPI {
importConfig: () => Promise<void>;
selectFolder: () => Promise<string>;
openFolder: (path: string) => Promise<void>;
restartBackend: () => Promise<void>;
blcuSelectFile?: () => Promise<string | null>;
blcuUpload?: (request: {
host: string;
Expand Down
Loading