diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3631d7d..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "internal/resmanager/agent-resources"] - path = pkg/agent-resources - url = git@github.com:paradigm-ehb/agent-resources.git diff --git a/build.sh b/build.sh index 0eabe9e..fc3a710 100755 --- a/build.sh +++ b/build.sh @@ -83,20 +83,19 @@ echo "-------------------------------------------------------------------------" CC=cc AR=ar -AGENT_RES_DIR="pkg/agent-resources" -# SRC="$AGENT_RES_DIR/resources.c" +AGENT_RES_DIR="pkg/resources" OUT_DIR="$AGENT_RES_DIR/build" -# OUT_OBJ="$OUT_DIR/resources.o" -# OUT_LIB="$OUT_DIR/libagent_resources.a" SRC_RES="$AGENT_RES_DIR/resources.c" SRC_ARENA="$AGENT_RES_DIR/arena.c" +SRC_BASE="$AGENT_RES_DIR/base.c" OUT_OBJ_RES="$OUT_DIR/resources.o" OUT_OBJ_ARENA="$OUT_DIR/arena.o" +OUT_OBJ_BASE="$OUT_DIR/base.o" -OUT_LIB="$OUT_DIR/libagent_resources.a" +OUT_LIB="$OUT_DIR/libresources.a" CFLAGS=" -std=c99 @@ -116,7 +115,6 @@ CFLAGS=" -Wmisleading-indentation -Wunused -Wuninitialized --Werror -Wdouble-promotion -Wstrict-overflow=2 -D_POSIX_C_SOURCE=200809L @@ -133,10 +131,13 @@ $CC $CFLAGS -c "$SRC_RES" -o "$OUT_OBJ_RES" echo "Compiling arena.c..." $CC $CFLAGS -c "$SRC_ARENA" -o "$OUT_OBJ_ARENA" +echo "Compiling base.c..." +$CC $CFLAGS -c "$SRC_BASE" -o "$OUT_OBJ_BASE" + echo "Creating static library..." # $AR rcs "$OUT_LIB" "$OUT_OBJ" -$AR rcs "$OUT_LIB" "$OUT_OBJ_RES" "$OUT_OBJ_ARENA" +$AR rcs "$OUT_LIB" "$OUT_OBJ_RES" "$OUT_OBJ_ARENA" "$OUT_OBJ_BASE" echo "Done: $OUT_LIB" diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 0ee0202..0bf0755 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -1,16 +1,18 @@ -// Package main boots the Paradigm Agent gRPC server. -// -// Responsibilities: -// - Parse runtime flags (IP, port, diagnostics) -// - Bind a TCP listener with automatic port fallback -// - Initialize and register all gRPC services -// - Expose health and reflection endpoints -// -// Design notes: -// - The server must be able to start even if the preferred port is occupied. -// - Reflection is enabled by default for debugging and introspection. -// - Diagnostics are intentionally decoupled from server startup to avoid -// blocking gRPC reflection and request handling. +/* +Package main boots the Paradigm Agent gRPC server. + +Responsibilities: +- Parse runtime flags (IP, port, diagnostics) +- Bind a TCP listener with automatic port fallback +- Initialize and register all gRPC services +- Expose health and reflection endpoints + +Design notes: +- The server must be able to start even if the preferred port is occupied. +- Reflection is enabled by default for debugging and introspection. +- Diagnostics are intentionally decoupled from server startup to avoid + blocking gRPC reflection and request handling. +*/ package main import ( @@ -22,9 +24,15 @@ import ( devacpb "paradigm-ehb/agent/gen/actions/v1" "paradigm-ehb/agent/gen/greet" "paradigm-ehb/agent/gen/journal/v1" - "paradigm-ehb/agent/gen/resources/v1" - services_v1 "paradigm-ehb/agent/gen/services/v1" - services_v2 "paradigm-ehb/agent/gen/services/v2" + + "log" + + servicesV1 "paradigm-ehb/agent/gen/services/v1" + servicesV2 "paradigm-ehb/agent/gen/services/v2" + servicesV3 "paradigm-ehb/agent/gen/services/v3" + + resourcesv1 "paradigm-ehb/agent/gen/resources/v1" + resourcesv2 "paradigm-ehb/agent/gen/resources/v2" "google.golang.org/grpc" "google.golang.org/grpc/health" @@ -33,54 +41,82 @@ import ( "paradigm-ehb/agent/internal/platform" - "paradigm-ehb/agent/pkg/grpc_handler" + "paradigm-ehb/agent/pkg/grpchandler" + + resourcesHandlerV1 "paradigm-ehb/agent/pkg/grpchandler/resources/v1" + resourcesHandlerV2 "paradigm-ehb/agent/pkg/grpchandler/resources/v2" + + servicesHandlerV1 "paradigm-ehb/agent/pkg/grpchandler/services/v1" + servicesHandlerV2 "paradigm-ehb/agent/pkg/grpchandler/services/v2" + servicesHandlerV3 "paradigm-ehb/agent/pkg/grpchandler/services/v3" + "syscall" "time" ) var ( - /** - diagnostics enables periodic runtime diagnostics such as - resource usage, process health, and connectivity checks. - NOTE: When enabled, diagnostics must never block the gRPC server. - **/ + /* + diagnostics enables periodic runtime diagnostics such as + resource usage, process health, and connectivity checks. - diagnostics = flag.Bool("diagnostics", true, "run runtime diagnostics") + TODO: + - Add structured configuration for diagnostics intervals. + - Allow diagnostics to be toggled or reconfigured at runtime. + - Ensure diagnostics respect context cancellation on shutdown. - /** - portFlag is the preferred TCP port to bind the gRPC server to. - If unavailable, the server will increment the port until a free - one is found. + NOTE: + When enabled, diagnostics must never block the gRPC server. */ + diagnostics = flag.Bool("diagnostics", false, "run runtime diagnostics") + /* + portFlag is the preferred TCP port to bind the gRPC server to. + + If unavailable: + - The server increments the port until a free one is found. + + TODO: + - Log the final selected port explicitly. + - Optionally expose the selected port via diagnostics or metadata. + */ portFlag = flag.Int("port", 5000, "port to listen on") - /** - ipFlag defines the IP address diagnostics may use when reporting - or exposing runtime information. + /* + ipFlag defines the IP address diagnostics may use when reporting + or exposing runtime information. + + TODO: + - Validate IP format early. + - Clarify distinction between bind address vs diagnostics address. */ ipFlag = flag.String("ip", "0.0.0.0", "ip addr") ) func main() { - /** + /* Parse command-line flags before any runtime behavior. - */ + TODO: + - Add validation for flag combinations. + - Print effective configuration at startup. + */ flag.Parse() - /** + /* Attempt to bind a TCP listener. Strategy: - - Start with the requested port - - If EADDRINUSE is encountered, increment the port and retry - - Fail hard on any other error + - Start with the requested port + - If EADDRINUSE is encountered, increment the port and retry + - Fail hard on any other error This guarantees the agent can always start, even in constrained or multi-agent environments. + TODO: + - Add an upper bound to port scanning. + - Support IPv6 or configurable network protocols. */ var lis net.Listener var err error @@ -100,83 +136,125 @@ func main() { } } - /** + /* Any error other than "address already in use" is fatal. + + TODO: + - Emit structured logs. + - Exit with non-zero status code. */ - fmt.Println("failed to listen:", err) + log.Printf("failed to listen: %v", err) return } break } - /** + /* Create the gRPC server instance. + + TODO: + - Configure server options (timeouts, interceptors, limits). + - Add graceful shutdown handling. */ server := grpc.NewServer() - /** + /* Health server is used by orchestration systems (systemd, Kubernetes, external monitors) to determine liveness and readiness. + + TODO: + - Set explicit serving statuses per service. */ healthServer := health.NewServer() grpc_health_v1.RegisterHealthServer(server, healthServer) - /** - Register all application services. + /* + Register all application services. - Each service implements a distinct responsibility: - - Greeter: connectivity / handshake testing - - HandlerService: service lifecycle and orchestration - - JournalService: event and state journaling - - ResourcesService: system resource inspection and reporting + Each service implements a distinct responsibility: + - Greeter: connectivity / handshake testing + - HandlerService: service lifecycle and orchestration + - JournalService: event and state journaling + - ResourcesService: system resource inspection and reporting + TODO: + - Centralize service registration. + - Version-gate deprecated service versions. */ - greet.RegisterGreeterServer(server, &grpc_handler.GreeterServer{}) - services_v1.RegisterHandlerServiceServer(server, &grpc_handler.HandlerService{}) - services_v2.RegisterHandlerServiceServer(server, &grpc_handler.HandlerServiceV2{}) - journal.RegisterJournalServiceServer(server, &grpc_handler.JournalService{}) - resourcespb.RegisterResourcesServiceServer(server, &grpc_handler.ResourcesService{}) - devacpb.RegisterActionServiceServer(server, &grpc_handler.DeviceActionsService{}) + greet.RegisterGreeterServer( + server, + &grpc_handler.GreeterServer{}, + ) + + servicesV1.RegisterHandlerServiceServer( + server, + &servicesHandlerV1.HandlerService{}, + ) + + servicesV2.RegisterHandlerServiceServer( + server, + &servicesHandlerV2.HandlerServiceV2{}, + ) + + servicesV3.RegisterHandlerServiceServer( + server, + &servicesHandlerV3.HandlerServicev3{}, + ) + + journal.RegisterJournalServiceServer( + server, + &grpc_handler.JournalService{}, + ) + + resourcesv1.RegisterResourcesServiceServer( + server, + &resourcesHandlerV1.ResourcesService{}, + ) + + resourcesv2.RegisterResourcesServiceServer( + server, + &resourcesHandlerV2.ResourcesServiceV2{}, + ) + + devacpb.RegisterActionServiceServer( + server, + &grpc_handler.DeviceActionsService{}, + ) /* - Diagnostics mode (disabled for now) - - Problem: - - Running diagnostics synchronously introduces an infinite loop - that blocks gRPC reflection and request handling. - - Important invariant: - - Diagnostics must run asynchronously and must never interfere - with server startup, reflection, or request processing. - - TODO(nasr): - - Move diagnostics into a separate goroutine - - Introduce a proper shutdown context - - Ensure diagnostics respect server lifecycle - */ - - /** Enable gRPC reflection unconditionally. This allows tools such as grpcurl and gRPC UI to inspect services and message schemas at runtime. + + TODO: + - Make reflection configurable for production environments. */ reflection.Register(server) - fmt.Printf("\nserver listening at %v\n", lis.Addr()) + + log.Printf("\nserver listening at %v\n", lis.Addr()) if *diagnostics { + /* + TODO: + - Tie diagnostics lifecycle to server context. + - Ensure diagnostics terminate on server shutdown. + */ go platform.RunRuntimeDiagnostics(time.Second*2, *ipFlag, *portFlag) - } - - /** + /* Start serving requests. + This call blocks until the server is stopped or encounters a fatal error. + + TODO: + - Implement graceful shutdown (signals, context). + - Flush diagnostics and logs before exit. */ if err := server.Serve(lis); err != nil { - fmt.Println("failed to serve:", err) + log.Printf("failed to serve: %v", err) } } diff --git a/compile.sh b/compile.sh index d19a4f5..456bd75 100755 --- a/compile.sh +++ b/compile.sh @@ -1,129 +1,9 @@ #!/bin/sh -# echo ------------------------------------------------------------------------- -# echo Description: build go binary with flags -# echo ------------------------------------------------------------------------- -VERSION=1.0 -prod() { - go build \ - -ldflags="-s -w -X main.version=${VERSION}" \ - -o agent \ - ./cmd/agent/agent.go -} +go clean -cache -debug() { - go build \ - -race \ - -gcflags="all=-N -l" \ - -o debug \ - ./cmd/agent/agent.go -} - -valgrind() { - echo "Building with debug symbols for valgrind..." - CGO_CFLAGS="-g -O0" \ - CGO_LDFLAGS="-g" \ - go build \ - -gcflags="all=-N -l" \ - -o debug_valgrind \ - ./cmd/agent/agent.go - - echo "Running valgrind..." - valgrind \ - --leak-check=full \ - --show-leak-kinds=all \ - --track-origins=yes \ - --verbose \ - --log-file=valgrind-out.txt \ - ./debug_valgrind - - echo "Valgrind output saved to valgrind-out.txt" -} - -gdb() { - echo "Building with debug symbols for gdb..." - CGO_CFLAGS="-g -O0" \ - CGO_LDFLAGS="-g" \ - go build \ - -gcflags="all=-N -l" \ - -o debug_gdb \ - ./cmd/agent/agent.go - - echo "Starting gdb..." - gdb ./debug_gdb -} - -core() { - echo "Building with debug symbols..." - CGO_CFLAGS="-g -O0" \ - CGO_LDFLAGS="-g" \ - go build \ - -gcflags="all=-N -l" \ - -o debug_core \ - ./cmd/agent/agent.go - - echo "Enabling core dumps..." - ulimit -c unlimited - - echo "Running program (will generate core dump on crash)..." - ./debug_core - - # Check if core dump was created - if [ -f core ]; then - echo "Core dump generated. Starting gdb..." - gdb ./debug_core core - elif [ -f core.* ]; then - CORE_FILE=$(ls -t core.* | head -n1) - echo "Core dump generated: $CORE_FILE. Starting gdb..." - gdb ./debug_core "$CORE_FILE" - else - echo "No core dump found. Program may have exited normally." - echo "Core dumps might be in: /var/lib/systemd/coredump/ or /var/crash/" - echo "Check with: coredumpctl list" - fi -} - -sanitize() { - echo "Building with address sanitizer..." - CGO_CFLAGS="-fsanitize=address -g -O0" \ - CGO_LDFLAGS="-fsanitize=address" \ - go build \ - -gcflags="all=-N -l" \ - -o debug_asan \ - ./cmd/agent/agent.go - - echo "Running with address sanitizer..." - ASAN_OPTIONS=detect_leaks=1:halt_on_error=0 ./debug_asan -} - -case "$1" in - prod) - prod - ;; - debug) - debug - ;; - valgrind) - valgrind - ;; - gdb) - gdb - ;; - core) - core - ;; - sanitize|asan) - sanitize - ;; - *) - echo "Usage: $0 [prod|debug|valgrind|gdb|core|sanitize]" - echo "" - echo " prod - Production build (stripped, optimized)" - echo " debug - Debug build with race detector" - echo " valgrind - Build and run with valgrind memory checker" - echo " gdb - Build and start gdb debugger" - echo " core - Build, enable core dumps, run and analyze crash" - echo " sanitize - Build and run with address sanitizer (ASAN)" - exit 1 - ;; -esac +if CGO_ENABLED=1 go build -race -gcflags="all=-N -l" -o debug ./cmd/agent/agent.go; then + exec ./debug +else + exit 1 +fi diff --git a/go.mod b/go.mod index 8fb194a..34322c6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,26 @@ require ( google.golang.org/protobuf v1.36.10 ) +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect +) + require ( github.com/godbus/dbus v4.1.0+incompatible golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect diff --git a/go.sum b/go.sum index 2391d25..c15576e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,21 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -12,6 +28,25 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= @@ -26,6 +61,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo= golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= diff --git a/internal/dbusservices/dbus/call.go b/internal/dbusservices/dbus/call.go index 9f9ac0b..5b0391a 100644 --- a/internal/dbusservices/dbus/call.go +++ b/internal/dbusservices/dbus/call.go @@ -1,6 +1,7 @@ package dbushandler import ( + "fmt" "github.com/godbus/dbus" ) @@ -9,7 +10,35 @@ import ( * used to handle services * @return BusObjcet */ -func CreateSystemdObject(conn *dbus.Conn) dbus.BusObject { +func CreateSystemdObject(conn *dbus.Conn) (dbus.BusObject, error) { + + obj := conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") + if obj == nil { + + return nil, fmt.Errorf("failed to create a systemd object") + } + + return obj, nil +} + +/** + +* CreateLoginObject +* used to handle services +* @return BusObject + +*/ +func CreateLoginObject(conn *dbus.Conn) (dbus.BusObject, error) { + obj := conn.Object( + "org.freedesktop.login1", + "/org/freedesktop/login1", + ) + + if obj == nil { + + return nil, fmt.Errorf("failed to create a login object") + } + + return obj, nil - return conn.Object("org.freedesktop.systemd1", "/org/freedesktop/systemd1") } diff --git a/internal/dbusservices/dbus/parse.go b/internal/dbusservices/dbus/parse.go index fd31e91..6f9fce0 100644 --- a/internal/dbusservices/dbus/parse.go +++ b/internal/dbusservices/dbus/parse.go @@ -2,42 +2,40 @@ package dbushandler import ( - "fmt" - svctypes "paradigm-ehb/agent/internal/dbusservices/types" + types "paradigm-ehb/agent/internal/dbusservices/types" + "strings" ) -// TODO: implement interfaces maybe - -// Method -// @param chan a(ss), chan a(ss) -// @param chan UnitFileEntry, chan UnitFileEntry -// @return nil -func ParseUnitFileEntries(in chan []svctypes.UnitFileEntry, out chan []svctypes.UnitFileEntry) { - - input := <-in - - for i := range input { - - if input[i].State == "enabled" { - fmt.Println(input[i].Name) - } else { - - fmt.Println(input[i].Name) +// TODO(nasr): implement interfaces maybe + +// ParseUnits filters units to only include services +// @param input []types.Unit +// @return []types.Unit, error +func ParseUnits(input []types.Unit) ([]types.Unit, error) { + /** + filter the units on services and remove devices etc + */ + buffer := make([]types.Unit, 0, len(input)) + for _, value := range input { + if strings.HasSuffix(value.Name, ".service") { + buffer = append(buffer, value) } } - out <- input - + return buffer, nil } -func ParseLoadedUnits(in chan []svctypes.LoadedUnit, out chan []svctypes.LoadedUnit) { - - input := <-in - - for i := range input { - - fmt.Println(input[i]) +// ParseLoadedUnits filters loaded units to only include services +// @param input []types.LoadedUnit +// @return []types.LoadedUnit, error +func ParseLoadedUnits(input []types.LoadedUnit) ([]types.LoadedUnit, error) { + /** + filter the units on services and remove devices etc + */ + buffer := make([]types.LoadedUnit, 0, len(input)) + for _, value := range input { + if strings.HasSuffix(value.Name, ".service") { + buffer = append(buffer, value) + } } - - out <- input - + return buffer, nil } diff --git a/internal/dbusservices/manager.go b/internal/dbusservices/manager.go index 9931950..c801519 100644 --- a/internal/dbusservices/manager.go +++ b/internal/dbusservices/manager.go @@ -3,18 +3,37 @@ package dbus_services import ( "fmt" - v2 "paradigm-ehb/agent/gen/services/v2" - dh "paradigm-ehb/agent/internal/dbusservices/dbus" - svc "paradigm-ehb/agent/internal/dbusservices/systemd" - svctypes "paradigm-ehb/agent/internal/dbusservices/types" + dbushelper "paradigm-ehb/agent/internal/dbusservices/dbus" + systemd "paradigm-ehb/agent/internal/dbusservices/systemd" + types "paradigm-ehb/agent/internal/dbusservices/types" "github.com/godbus/dbus" ) -// @param, action [start, stop, restart], symLinkAction [enable, disable], service name format "example.service" -func RunAction(conn *dbus.Conn, ac svc.UnitAction, service string) error { +/* +RunAction executes a systemd unit action (start, stop, restart). + +Parameters: +- conn: + Active D-Bus connection. +- ac: + Unit action to execute (start, stop, restart). +- service: + Unit name in systemd format (e.g. "example.service"). + +TODO: +- Validate service name format before invoking D-Bus. +- Propagate context / cancellation support. +- Replace raw string conversion with strongly typed D-Bus method mapping. +*/ +func RunAction( + conn *dbus.Conn, + ac systemd.UnitAction, + service string, +) error { + + obj, _ := dbushelper.CreateSystemdObject(conn) - obj := dh.CreateSystemdObject(conn) if !obj.Path().IsValid() { return fmt.Errorf("object path is invalid") } @@ -22,33 +41,86 @@ func RunAction(conn *dbus.Conn, ac svc.UnitAction, service string) error { call := obj.Call(string(ac), 0, service, "replace") if call.Err != nil { - return fmt.Errorf("failed to execute object on in unit action, %v", call.Err) + return fmt.Errorf("failed to execute object on unit action, %v", call.Err) } return nil } -// @param, action [start, stop, restart], symLinkAction [enable, disable], service name format "example.service" -func RunSymlinkAction(conn *dbus.Conn, sc svc.UnitFileAction, enableForRunTime bool, enableForce bool, service []string) error { - - obj := dh.CreateSystemdObject(conn) +/* +RunSymlinkAction executes systemd unit file actions (enable / disable). + +Parameters: +- conn: + Active D-Bus connection. +- sc: + Unit file action (enable or disable). +- enableForRunTime: + Whether the action applies only at runtime. +- enableForce: + Whether to force-enable units (only relevant for enable). +- service: + Slice of unit names. + +TODO: +- Validate service slice is non-empty. +- Clarify runtime vs persistent semantics in API naming. +- Normalize error messages. +*/ +func RunSymlinkAction( + conn *dbus.Conn, + sc systemd.UnitFileAction, + enableForRunTime bool, + enableForce bool, + service []string, +) error { + + obj, err := dbushelper.CreateSystemdObject(conn) + if err != nil { + return fmt.Errorf("failed to create systemd object %v", err) + } if !obj.Path().IsValid() { - fmt.Println("invalid systemd path") + return fmt.Errorf("invalid systemd path") } - /** EnableUnitFiles(in as files, in b runtime, in b force, out b carries_install_info, out a(sss) changes); */ - /** DisableUnitFiles(in as files, in b runtime, out a(sss) changes); */ + /* + EnableUnitFiles( + in as files, + in b runtime, + in b force, + out b carries_install_info, + out a(sss) changes + ) + + DisableUnitFiles( + in as files, + in b runtime, + out a(sss) changes + ) + */ switch sc { - case svc.UnitFileActionEnable: - call := obj.Call(string(sc), dbus.FlagAllowInteractiveAuthorization, service, enableForRunTime, enableForce) + case systemd.UnitFileActionEnable: + call := obj.Call( + string(sc), + dbus.FlagAllowInteractiveAuthorization, + service, + enableForRunTime, + enableForce, + ) if call.Err != nil { return fmt.Errorf("error %v", call.Err) } - case svc.UnitFileActionDisable: - call := obj.Call(string(sc), dbus.FlagAllowInteractiveAuthorization, service, enableForRunTime) + + case systemd.UnitFileActionDisable: + call := obj.Call( + string(sc), + dbus.FlagAllowInteractiveAuthorization, + service, + enableForRunTime, + ) if call.Err != nil { return fmt.Errorf("something happened here %v", call.Err) } @@ -57,88 +129,205 @@ func RunSymlinkAction(conn *dbus.Conn, sc svc.UnitFileAction, enableForRunTime b return nil } -// @param, true for all on disk, false for loaded units -func RunRetrieval( - conn *dbus.Conn, - all bool, -) ([]*v2.LoadedUnit, error) { - - obj := dh.CreateSystemdObject(conn) - - if all { - ch := make(chan []svctypes.UnitFileEntry) - parse := make(chan []svctypes.UnitFileEntry) - - go svc.GetAllUnits(obj, ch) - go dh.ParseUnitFileEntries(ch, parse) - - entries := <-parse - - units := make([]*v2.LoadedUnit, 0, len(entries)) - for _, e := range entries { - units = append(units, &v2.LoadedUnit{ - Name: e.Name, - Description: "", - LoadState: e.State, - SubState: "", - ActiveState: "", - DepUnit: "", - ObjectPath: "", - QueuedJob: 0, - JobType: "", - JobPath: "", - }) - } - - return units, nil +/* +UnitStatus retrieves the status of a single systemd unit. + +Parameters: +- obj: + Systemd D-Bus object. +- name: + Unit name. + +TODO: +- Replace string status with structured state representation. +- Normalize error return values. +*/ +func UnitStatus( + obj dbus.BusObject, + name string, +) (string, error) { + + out, err := systemd.GetStatusCall(obj, name) + if err != nil { + return "Failed", fmt.Errorf("failed to execute status call %v", err) } - ch := make(chan []svctypes.LoadedUnit) - parse := make(chan []svctypes.LoadedUnit) + return out, nil +} + +/* +MapLoadedUnits maps loaded systemd units to internal LoadedUnit types. - go svc.GetLoadedUnits(obj, ch) - go dh.ParseLoadedUnits(ch, parse) +TODO: +- Remove duplicated mapping logic across unit retrieval functions. +- Fix QueudJob typo once wire format compatibility is resolved. +*/ +func MapLoadedUnits(conn *dbus.Conn) ([]*types.LoadedUnit, error) { - loaded := <-parse + obj, err := dbushelper.CreateSystemdObject(conn) + if err != nil { + return nil, fmt.Errorf("failed to create systemd object: %w", err) + } - units := make([]*v2.LoadedUnit, 0, len(loaded)) - for _, u := range loaded { - units = append(units, &v2.LoadedUnit{ + loaded, err := systemd.GetLoadedUnits(obj) + if err != nil { + return nil, fmt.Errorf("failed to get loaded units: %w", err) + } + + parsed, err := dbushelper.ParseLoadedUnits(loaded) + if err != nil { + return nil, fmt.Errorf("failed to parse loaded units: %w", err) + } + + units := make([]*types.LoadedUnit, 0, len(parsed)) + for _, u := range parsed { + units = append(units, &types.LoadedUnit{ Name: u.Name, Description: u.Description, LoadState: u.LoadState, SubState: u.SubState, ActiveState: u.ActiveState, DepUnit: u.DepUnit, - ObjectPath: string(u.ObjectPath), - /*oops typo in queued job :)*/ - QueuedJob: u.QueudJob, + ObjectPath: u.ObjectPath, + QueudJob: u.QueudJob, /* keep typo for consistency */ JobType: u.JobType, - JobPath: string(u.JobPath), + JobPath: u.JobPath, }) } return units, nil } -func GetStatus(obj dbus.BusObject, name string) (string, error) { - var result string +/* +MapFilteredUnits retrieves and maps filtered systemd units. - call := obj.Call( - "org.freedesktop.systemd1.Manager.GetUnitFileState", - 0, - name, - ) +TODO: +- Replace placeholder "Not Available" strings with optional fields. +- Clarify which fields are guaranteed by GetUnitsFiltered. +*/ +func MapFilteredUnits( + conn *dbus.Conn, + filters []string, +) ([]*types.LoadedUnit, error) { - if call.Err != nil { - return "call error: ", call.Err + obj, err := dbushelper.CreateSystemdObject(conn) + if err != nil { + return nil, fmt.Errorf("failed to create systemd object: %w", err) } - if err := call.Store(&result); err != nil { - return "call store: ", err + entries, err := systemd.GetUnitsFiltered(obj, filters) + if err != nil { + return nil, fmt.Errorf("failed to get filtered units: %w", err) } - fmt.Println("status:", result) + parsed, err := dbushelper.ParseLoadedUnits(entries) + if err != nil { + return nil, fmt.Errorf("failed to parse filtered units: %w", err) + } + + units := make([]*types.LoadedUnit, 0, len(parsed)) + for _, e := range parsed { + units = append(units, &types.LoadedUnit{ + Name: e.Name, + Description: "Not available", + LoadState: e.LoadState, + SubState: "Not Available", + ActiveState: "Not Available", + DepUnit: "Not Available", + ObjectPath: "Not Available", + QueudJob: 0, + JobType: "Not Available", + JobPath: "Not Available", + }) + } + + return units, nil +} + +/* +MapUnits retrieves and maps all systemd units. + +TODO: +- Distinguish unit-on-disk vs loaded semantics at the type level. +- Avoid repeating placeholder field values. +*/ +func MapUnits(conn *dbus.Conn) ([]*types.LoadedUnit, error) { + + obj, err := dbushelper.CreateSystemdObject(conn) + if err != nil { + return nil, fmt.Errorf("failed to create systemd object: %w", err) + } + + result, err := systemd.GetUnits(obj) + if err != nil { + return nil, fmt.Errorf("failed to get units: %w", err) + } + + parsedUnits, err := dbushelper.ParseUnits(result) + if err != nil { + return nil, fmt.Errorf("failed to parse units: %w", err) + } + + units := make([]*types.LoadedUnit, 0, len(parsedUnits)) + for _, e := range parsedUnits { + units = append(units, &types.LoadedUnit{ + Name: e.Name, + Description: "Not available", + LoadState: e.State, + SubState: "Not Available", + ActiveState: "Not Available", + DepUnit: "Not Available", + ObjectPath: "Not Available", + QueudJob: 0, + JobType: "Not Available", + JobPath: "Not Available", + }) + } + + return units, nil +} + +/* +RunRetrieval retrieves units using an internally created system bus connection. + +Parameters: +- requestAllUnitsOnDisk: + true -> retrieve all units on disk + false -> retrieve only loaded units + +TODO: +- Close system bus connection explicitly. +- Propagate context. +*/ +func RunRetrieval(requestAllUnitsOnDisk bool) ([]*types.LoadedUnit, error) { + + conn, err := dbushelper.CreateSystemBus() + if err != nil { + return nil, fmt.Errorf("failed to create system bus connection: %w", err) + } + + if requestAllUnitsOnDisk { + return MapUnits(conn) + } + + return MapLoadedUnits(conn) +} + +/* +RunRetrievalDeprecated performs unit retrieval using a caller-provided connection. + +TODO: +- Remove once all call sites migrate to RunRetrieval. +- Clearly document ownership of conn lifecycle. +*/ +func RunRetrievalDeprecated( + conn *dbus.Conn, + requestAllUnitsOnDisk bool, +) ([]*types.LoadedUnit, error) { + + if requestAllUnitsOnDisk { + return MapUnits(conn) + } - return result, nil + return MapLoadedUnits(conn) } diff --git a/internal/dbusservices/systemd/list.go b/internal/dbusservices/systemd/list.go index bbf46dd..f509f56 100644 --- a/internal/dbusservices/systemd/list.go +++ b/internal/dbusservices/systemd/list.go @@ -2,7 +2,7 @@ package servicecontrol import ( "fmt" - svctypes "paradigm-ehb/agent/internal/dbusservices/types" + types "paradigm-ehb/agent/internal/dbusservices/types" "github.com/godbus/dbus" ) @@ -14,45 +14,70 @@ import ( // files (templates) cannot directly be loaded as units but need to be instantiated. // --------------------------------------------------------------------------------------- // Method returns an array of all currently loaded units, -// -func GetLoadedUnits(obj dbus.BusObject, out chan []svctypes.LoadedUnit) { - - var result []svctypes.LoadedUnit +func GetLoadedUnits(obj dbus.BusObject) ([]types.LoadedUnit, error) { + var result []types.LoadedUnit call := obj.Call("org.freedesktop.systemd1.Manager.ListUnits", 0) if call.Err != nil { - fmt.Printf("failed to list unit files that are loaded in memory %v", call.Err) - return + return nil, fmt.Errorf("failed to list unit files that are loaded in memory: %w", call.Err) } - - + err := call.Store(&result) - if err != nil { - return + return nil, fmt.Errorf("failed to store loaded units: %w", err) } - - out <- result - + + return result, nil } -func GetAllUnits(obj dbus.BusObject, out chan []svctypes.UnitFileEntry) { - +func GetUnits(obj dbus.BusObject) ([]types.Unit, error) { // ListUnitFiles(out a(ss) files); // an array of struct string string - - var result []svctypes.UnitFileEntry - + var result []types.Unit call := obj.Call("org.freedesktop.systemd1.Manager.ListUnitFiles", 0) - if call.Err != nil { - fmt.Println("failed to call all units loaded on disk") - return + return nil, fmt.Errorf("failed to call all units loaded on disk: %w", call.Err) + } + + err := call.Store(&result) + if err != nil { + return nil, fmt.Errorf("failed to store services: %w", err) } + + return result, nil +} +func GetUnitsFiltered(obj dbus.BusObject, states []string) ([]types.LoadedUnit, error) { + var result []types.LoadedUnit + call := obj.Call("org.freedesktop.systemd1.Manager.ListUnitsFiltered", 0, states) + if call.Err != nil { + return nil, fmt.Errorf("failed to call filtered list of units: %w", call.Err) + } + err := call.Store(&result) if err != nil { - return + return nil, fmt.Errorf("failed to store filtered units: %w", err) + } + + return result, nil +} + +func GetStatusCall(obj dbus.BusObject, name string) (string, error) { + var result string + + call := obj.Call( + "org.freedesktop.systemd1.Manager.GetUnitFileState", + 0, + name, + ) + + if call.Err != nil { + return "call error: ", call.Err } - out <- result + + if err := call.Store(&result); err != nil { + return "call store: ", err + } + + return result, nil } diff --git a/internal/dbusservices/types/types.go b/internal/dbusservices/types/types.go index 4f01975..83c2df1 100644 --- a/internal/dbusservices/types/types.go +++ b/internal/dbusservices/types/types.go @@ -27,7 +27,7 @@ type Service struct { } // type a(ss) -type UnitFileEntry struct { +type Unit struct { Name string State string } diff --git a/internal/journal/journal.go b/internal/journal/journal.go index e963cf2..dcbb9b0 100644 --- a/internal/journal/journal.go +++ b/internal/journal/journal.go @@ -4,34 +4,38 @@ import ( "io" "time" - "fmt" + "log" - jrnl "github.com/coreos/go-systemd/v22/journal" - sdj "github.com/coreos/go-systemd/v22/sdjournal" + "github.com/coreos/go-systemd/v22/journal" + "github.com/coreos/go-systemd/v22/sdjournal" ) -// checkJournal reports whether systemd’s journal is available and enabled -// on the current system. -// -// It is a lightweight capability check and does not open or read the journal. -// Internally, this relies on libsystemd to detect whether journald is usable -// (for example, not present on non-systemd systems). +/* +checkJournal reports whether systemd’s journal is available and enabled +on the current system. + +It is a lightweight capability check and does not open or read the journal. +Internally, this relies on libsystemd to detect whether journald is usable +(for example, not present on non-systemd systems). +*/ func checkJournal() bool { - return jrnl.Enabled() + return journal.Enabled() } -// systemdID returns the boot ID associated with the currently running system. -// -// The boot ID uniquely identifies the current boot session and is useful for -// correlating journal entries to a specific system start. The function opens -// the journal, queries the boot ID, and then closes the journal handle. -// -// If the journal cannot be opened or the boot ID cannot be retrieved, the -// returned string may be empty. +/* +systemdID returns the boot ID associated with the currently running system. + +The boot ID uniquely identifies the current boot session and is useful for +correlating journal entries to a specific system start. The function opens +the journal, queries the boot ID, and then closes the journal handle. + +If the journal cannot be opened or the boot ID cannot be retrieved, the +returned string may be empty. +*/ func systemdID() (string, error) { - j, err := sdj.NewJournal() + j, err := sdjournal.NewJournal() if err != nil { return "not available", err } @@ -43,35 +47,68 @@ func systemdID() (string, error) { } return bid, nil - } -// TODO(nasr): checkout formatters - -// GetJournalInformation GetJournaldInformation reads entries from the systemd journal and returns -// them as a single concatenated string. -// -// The journal reader is configured through the provided parameters: -// - since: limits entries to those newer than the given duration -// relative now -// - numFromTail: limits the number of entries read from the end of the journal -// - cursor: reserved for future cursor-based positioning (currently unused) -// - matches: filters entries using systemd journal match rules -// - path: optionally specifies a custom journal path -// -// Internally, this function uses a JournalReader and performs sequential reads -// into a fixed-size buffer until no more data is available or an error occurs. -// The caller receives raw journal output as text, without further parsing or -// field-level decoding. -// -// Example Matches: []sdj.Match{{Field: "_SYSTEMD_UNIT", Value: "ssh.service"}}} -func GetJournalInformation(since time.Duration, numFromTail uint64, cursor string, matches []sdj.Match, path string, out chan []byte) { - +/* +TODO(nasr): +- Evaluate and implement journal output formatters. +- Decide whether formatting should be: + - raw text (current behavior), + - structured (map / proto), + - or selectable via configuration. +*/ + +/* +GetJournalInformation reads entries from the systemd journal and streams them +through the provided output channel. + +Parameters: +- since: + Duration relative to "now" used to limit journal entries. + (Currently not implemented.) +- numFromTail: + Limits the number of entries read from the end of the journal. +- cursor: + Reserved for cursor-based positioning (currently unused). +- matches: + Journal match filters (field/value pairs). +- path: + Optional custom journal path. +- out: + Output channel receiving raw journal byte slices. + +Behavior: +- Opens a JournalReader with the provided configuration. +- Reads sequentially into a fixed-size buffer. +- Streams raw journal output without parsing or decoding fields. +- Closes the output channel before returning. + +Example: + []sdj.Match{{Field: "_SYSTEMD_UNIT", Value: "ssh.service"}} +*/ +func GetJournalInformation( + since time.Duration, + numFromTail uint64, + cursor string, + matches []sdjournal.Match, + path string, + out chan []byte, +) { + + /* + TODO(nasr): + - Define ownership and lifecycle rules for `out`. + - Document that this function is responsible for closing the channel. + */ defer close(out) - config := sdj.JournalReaderConfig{ - // TODO(nasr): fix time imlementation - //Since: since, + config := sdjournal.JournalReaderConfig{ + /* + TODO(nasr): + - Implement time-based filtering using `since`. + - Decide whether `since` should override cursor semantics. + */ + // Since: since, NumFromTail: numFromTail, Cursor: cursor, Matches: matches, @@ -79,10 +116,14 @@ func GetJournalInformation(since time.Duration, numFromTail uint64, cursor strin Formatter: nil, } - reader, err := sdj.NewJournalReader(config) - + reader, err := sdjournal.NewJournalReader(config) if err != nil { - fmt.Println("failed to open the journal reader") + /* + TODO(nasr): + - Replace stdout logging with structured error propagation. + - Decide whether to terminate early or send error markers via channel. + */ + } defer reader.Close() @@ -93,23 +134,36 @@ func GetJournalInformation(since time.Duration, numFromTail uint64, cursor strin c, err := reader.Read(b) if err == io.EOF { - fmt.Println("End of journal") + /* + TODO(nasr): + - Decide whether EOF should be silent. + - Avoid stdout logging in library code. + */ + log.Printf("end of journal %v, ", err) break } if c == 0 { - fmt.Println(" data") + /* + TODO(nasr): + - Clarify whether zero-length reads are expected. + - Remove noisy logging or replace with debug-level tracing. + */ continue } if err != nil { + /* + TODO(nasr): + - Avoid sending placeholder data on error. + - Define a structured error signaling mechanism. + - Consider context cancellation or error channels. + */ out <- []byte("nothing in here") - fmt.Println("failed to read from the journal reader", err) + log.Printf("no more data to read %v, ", err) break } - out <- b[:c] - } } diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 9323a61..54fb879 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -6,6 +6,9 @@ import ( "runtime" "runtime/debug" "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) func AssertLinux() error { @@ -15,65 +18,117 @@ func AssertLinux() error { return nil } -func RunRuntimeDiagnostics(interval time.Duration, ip string, port int) { - ticker := time.NewTicker(interval) - defer ticker.Stop() +type tickMsg time.Time - for range ticker.C { +type model struct { + ip string + port int + interval time.Duration +} - // clear screen - fmt.Print("\033[2J") - fmt.Print("\033[H") +func (m model) Init() tea.Cmd { + return tick(m.interval) +} - var m runtime.MemStats - runtime.ReadMemStats(&m) +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + case tickMsg: + return m, tick(m.interval) + } + return m, nil +} - fmt.Println("runtime diagnostics") - fmt.Println("-------------------") +func (m model) View() string { + var mem runtime.MemStats + runtime.ReadMemStats(&mem) - fmt.Printf("%-20s : %s\n", "go version", runtime.Version()) - fmt.Printf("%-20s : %s\n", "os", runtime.GOOS) - fmt.Printf("%-20s : %s\n", "architecture", runtime.GOARCH) - fmt.Printf("%-20s : %s\n", "compiler", runtime.Compiler) + titleStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("39")). + MarginBottom(1) - fmt.Printf("%-20s : %d\n", "cpu cores", runtime.NumCPU()) - fmt.Printf("%-20s : %d\n", "gomaxprocs", runtime.GOMAXPROCS(0)) - fmt.Printf("%-20s : %d\n", "goroutines", runtime.NumGoroutine()) + sectionStyle := lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("86")). + MarginTop(1) - fmt.Println() - fmt.Println("memory") - fmt.Println("-----------------") + labelStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")) - fmt.Printf("%-20s : %d KB\n", "heap alloc", m.HeapAlloc/1024) - fmt.Printf("%-20s : %d KB\n", "heap sys", m.HeapSys/1024) - fmt.Printf("%-20s : %d KB\n", "heap in use", m.HeapInuse/1024) - fmt.Printf("%-20s : %d KB\n", "heap idle", m.HeapIdle/1024) - fmt.Printf("%-20s : %d KB\n", "stack in use", m.StackInuse/1024) - fmt.Printf("%-20s : %d KB\n", "stack sys", m.StackSys/1024) + valueStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("white")) - fmt.Printf("%-20s : %d\n", "gc cycles", m.NumGC) - fmt.Printf("%-20s : %d ms\n", "gc pause total", m.PauseTotalNs/1e6) + helpStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("241")). + MarginTop(1) - fmt.Printf("%-20s : %d\n", "gc next", m.NextGC/1024) + row := func(label, value string) string { + return fmt.Sprintf("%s %s", + labelStyle.Width(20).Render(label+":"), + valueStyle.Render(value)) + } - fmt.Println() - fmt.Println("server info") - fmt.Println("-----------------") - fmt.Printf("port listening on : %d\n", port) - fmt.Printf("ip address : %s\n", ip) + var s string + + s += titleStyle.Render("Runtime Diagnostics") + "\n" + + s += sectionStyle.Render("System") + "\n" + s += row("Go Version", runtime.Version()) + "\n" + s += row("OS", runtime.GOOS) + "\n" + s += row("Architecture", runtime.GOARCH) + "\n" + s += row("Compiler", runtime.Compiler) + "\n" + s += row("CPU Cores", fmt.Sprintf("%d", runtime.NumCPU())) + "\n" + s += row("GOMAXPROCS", fmt.Sprintf("%d", runtime.GOMAXPROCS(0))) + "\n" + s += row("Goroutines", fmt.Sprintf("%d", runtime.NumGoroutine())) + "\n" + + s += sectionStyle.Render("Memory") + "\n" + s += row("Heap Alloc", fmt.Sprintf("%d KB", mem.HeapAlloc/1024)) + "\n" + s += row("Heap Sys", fmt.Sprintf("%d KB", mem.HeapSys/1024)) + "\n" + s += row("Heap In Use", fmt.Sprintf("%d KB", mem.HeapInuse/1024)) + "\n" + s += row("Heap Idle", fmt.Sprintf("%d KB", mem.HeapIdle/1024)) + "\n" + s += row("Stack In Use", fmt.Sprintf("%d KB", mem.StackInuse/1024)) + "\n" + s += row("Stack Sys", fmt.Sprintf("%d KB", mem.StackSys/1024)) + "\n" + s += row("GC Cycles", fmt.Sprintf("%d", mem.NumGC)) + "\n" + s += row("GC Pause Total", fmt.Sprintf("%d ms", mem.PauseTotalNs/1e6)) + "\n" + s += row("GC Next", fmt.Sprintf("%d KB", mem.NextGC/1024)) + "\n" + + s += sectionStyle.Render("Server Info") + "\n" + s += row("IP Address", m.ip) + "\n" + s += row("Port", fmt.Sprintf("%d", m.port)) + "\n" + + if info, ok := debug.ReadBuildInfo(); ok { + s += sectionStyle.Render("Build Info") + "\n" + s += row("Module", info.Path) + "\n" + s += row("Go Version", info.GoVersion) + "\n" + if info.Main.Version != "(devel)" { + s += row("Version", info.Main.Version) + "\n" + } + } - if info, ok := debug.ReadBuildInfo(); ok { - fmt.Println() - fmt.Println("information") - fmt.Println("-----------------") + s += helpStyle.Render("\nPress 'q' or 'ctrl+c' to quit") - fmt.Printf("%-20s : %s\n", "module", info.Path) - fmt.Printf("%-20s : %s\n", "go version", info.GoVersion) + return s +} - if info.Main.Version != "(devel)" { - fmt.Printf("%-20s : %s\n", "version", info.Main.Version) - } +func tick(interval time.Duration) tea.Cmd { + return tea.Tick(interval, func(t time.Time) tea.Msg { + return tickMsg(t) + }) +} - } +func RunRuntimeDiagnostics(interval time.Duration, ip string, port int) { + p := tea.NewProgram(model{ + ip: ip, + port: port, + interval: interval, + }) + + if _, err := p.Run(); err != nil { + fmt.Printf("Error running diagnostics: %v\n", err) } } diff --git a/pkg/agent-resources b/pkg/agent-resources deleted file mode 160000 index 5dc30a0..0000000 --- a/pkg/agent-resources +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5dc30a052160c8234393641db3521b59e26f4fb9 diff --git a/pkg/cgowrap/types.go b/pkg/cgowrap/types.go index 0b6a8e2..30d8492 100644 --- a/pkg/cgowrap/types.go +++ b/pkg/cgowrap/types.go @@ -6,12 +6,14 @@ type Cpu struct { Model string Frequency string MaxCore uint32 + TotalTime uint64 + IdleTime uint64 } // Ram represents RAM information including total and free memory. type Ram struct { - Total string - Free string + Total uint64 + Free uint64 } // DiskPartition represents a single disk partition with device identifiers and block count. @@ -33,48 +35,4 @@ type Device struct { Uptime string } -// ProcessState represents the state of a process. -type ProcessState uint32 -const ( - ProcessUndefined ProcessState = 0 - ProcessRunning ProcessState = 1 - ProcessSleeping ProcessState = 2 - ProcessDiskSleep ProcessState = 3 - ProcessStopped ProcessState = 4 - ProcessTracingStopped ProcessState = 5 - ProcessZombie ProcessState = 6 - ProcessDead ProcessState = 7 -) - -// String returns a human-readable representation of the process state. -func (ps ProcessState) String() string { - switch ps { - case ProcessRunning: - return "Running" - case ProcessSleeping: - return "Sleeping" - case ProcessDiskSleep: - return "Disk Sleep" - case ProcessStopped: - return "Stopped" - case ProcessTracingStopped: - return "Tracing Stopped" - case ProcessZombie: - return "Zombie" - case ProcessDead: - return "Dead" - default: - return "Undefined" - } -} - -// Process represents a single process with its attributes. -type Process struct { - PID uint32 - Name string - State ProcessState - UTime uint64 - STime uint64 - NumThreads uint32 -} diff --git a/pkg/cgowrap/wrapper.go b/pkg/cgowrap/wrapper.go index 4283da8..bb54842 100644 --- a/pkg/cgowrap/wrapper.go +++ b/pkg/cgowrap/wrapper.go @@ -1,9 +1,133 @@ package wrapper /* -#cgo CFLAGS: -I${SRCDIR}/../agent-resources -#cgo LDFLAGS: -L${SRCDIR}/../agent-resources/build -lagent_resources +#cgo CFLAGS: -I${SRCDIR}/../resources +#cgo LDFLAGS: -L${SRCDIR}/../resources/build -lresources + +#include +#include +#include #include "resources.h" + +int +process_read2(i32 pid, Process *out) +{ + char path[PATH_MAX_LEN]; + snprintf(path, sizeof(path), "/proc/%d/status", pid); + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return ERR_IO; + } + + char buf[BUFFER_SIZE_LARGE]; + + out->pid = pid; + + while (fgets( + buf, + sizeof(buf), + fp)) + { + char *colon = strchr(buf, ':'); + if (!colon) + { + continue; + } + + char *val = colon + 1; + while (*val == ' ' || *val == '\t') + { + ++val; + } + + size_t len = strcspn(val, "\n"); + + + if (!strncmp(buf, "Name:", 5)) + { + + memcpy(out->name, val, len); + } + if (!strncmp(buf, "State:", 6)) + { + char state_char = 0; + for (char *p = val; *p; ++p) + { + if (*p >= 'A' && *p <= 'Z' || *p == 't') + { + state_char = *p; + break; + } + } + + + switch (state_char) + { + case 'R': + { + out->state = PROCESS_RUNNING; + break; + } + case 'S': + { + out->state = PROCESS_SLEEPING; + break; + } + case 'D': + { + out->state = PROCESS_DISK_SLEEP; + break; + } + case 'T': + { + out->state = PROCESS_STOPPED; + break; + } + case 't': + { + out->state = PROCESS_TRACING_STOPPED; + break; + } + case 'Z': + { + out->state = PROCESS_ZOMBIE; + break; + } + case 'X': + { + out->state = PROCESS_DEAD; + break; + } + case 'I': + { + out->state = PROCESS_IDLE; + break; + } + default: + { + out->state = PROCESS_UNDEFINED; + break; + } + } + } + + if (!strncmp(buf, "Threads:", 8)) + { + out->num_threads = (u32)strtoul(val, 0, 10); + } + } + + + int error = fclose(fp); + if (error != 0) + { + return ERR_IO; + } + + return OK; +} */ import "C" import ( @@ -13,6 +137,56 @@ import ( "golang.org/x/sys/unix" ) +// ProcessState represents the state of a process. + +type ProcessState C.int32_t + +const ( + ProcessUndefined ProcessState = ProcessState(C.PROCESS_UNDEFINED) + ProcessRunning ProcessState = ProcessState(C.PROCESS_RUNNING) + ProcessSleeping ProcessState = ProcessState(C.PROCESS_SLEEPING) + ProcessDiskSleep ProcessState = ProcessState(C.PROCESS_DISK_SLEEP) + ProcessStopped ProcessState = ProcessState(C.PROCESS_STOPPED) + ProcessTracingStopped ProcessState = ProcessState(C.PROCESS_TRACING_STOPPED) + ProcessZombie ProcessState = ProcessState(C.PROCESS_ZOMBIE) + ProcessDead ProcessState = ProcessState(C.PROCESS_DEAD) + ProcessIdle ProcessState = ProcessState(C.PROCESS_IDLE) +) + +// Process represents a single process with its attributes. +type Process struct { + PID int32 + State ProcessState + UTime uint64 + STime uint64 + NumThreads uint32 + Name string +} + +func (s ProcessState) String() string { + + switch s { + case ProcessRunning: + return "Running" + case ProcessSleeping: + return "Sleeping" + case ProcessDiskSleep: + return "Disk Sleep" + case ProcessStopped: + return "Stopped" + case ProcessTracingStopped: + return "Tracing Stopped" + case ProcessZombie: + return "Zombie" + case ProcessDead: + return "Dead" + case ProcessIdle: + return "Idle" + default: + return "Undefined" + } +} + // TODO(nasr): research this, interesting, alias vs true aliasing type Arena = C.mem_arena @@ -59,6 +233,14 @@ func AllocateArena(size uint64) (*C.mem_arena, error) { return arena, nil } +func PushArena(arena *C.mem_arena, size uint64) { + + if arena != nil { + C.arena_push(arena, C.ulong(size), 1) + } + +} + /* * DestroyArena unmaps and destroys the memory arena. @@ -127,11 +309,17 @@ func CpuRead(c *C.Cpu) (Cpu, error) { return Cpu{}, fmt.Errorf("failed to read CPU information") } + if C.cpu_read_usage(c) != C.OK { + return Cpu{}, fmt.Errorf("failed to read CPU information") + } + cpu := Cpu{ Vendor: C.GoString(&c.vendor[0]), Model: C.GoString(&c.model[0]), Frequency: C.GoString(&c.frequency[0]), MaxCore: uint32(c.cores), + TotalTime: uint64(c.total_time), + IdleTime: uint64(c.idle_time), } return cpu, nil } @@ -176,9 +364,10 @@ func RamRead(ram *C.Ram) (Ram, error) { } r := Ram{ - Total: C.GoString(&ram.total[0]), - Free: C.GoString(&ram.free[0]), + Total: uint64(ram.total), + Free: uint64(ram.free), } + return r, nil } @@ -224,10 +413,10 @@ func DiskRead(disk *C.Disk) (Disk, error) { } d := Disk{ - Partitions: make([]DiskPartition, 0, disk.count), + Partitions: make([]DiskPartition, 0, disk.part_count), } - for i := C.size_t(0); i < disk.count; i++ { + for i := C.size_t(0); i < disk.part_count; i++ { part := (*C.Partition)( unsafe.Pointer( uintptr(unsafe.Pointer(disk.partitions)) + @@ -334,39 +523,34 @@ Returns: - error: Error if device pointer is nil */ func ReadProcesses(device *C.Device) ([]Process, error) { + if device == nil { - return nil, fmt.Errorf("nil Device pointer") + return nil, fmt.Errorf("dvice null pointer") } - procs := make([]Process, 0, device.processes.count) + count := int(device.processes.count) items := device.processes.items - for i := C.size_t(0); i < device.processes.count; i++ { - p := (*C.Process)( - unsafe.Pointer( - uintptr(unsafe.Pointer(items)) + - uintptr(i)*unsafe.Sizeof(*items), - ), - ) + procs := make([]Process, 0, count) + + for i := 0; i < count; i++ { + + p := (*C.Process)(unsafe.Pointer(uintptr(unsafe.Pointer(items)) + uintptr(i)*unsafe.Sizeof(*items))) - /** - Read detailed process information - Skip processes that can't be read - */ - if C.process_read(p.pid, p) != C.OK { - continue + err := C.process_read2(p.pid, p) + if err != C.OK { + return nil, fmt.Errorf("failed reading processes %v", err) } procs = append(procs, Process{ - PID: uint32(p.pid), - Name: C.GoString(&p.name[0]), + PID: int32(p.pid), State: ProcessState(p.state), UTime: uint64(p.utime), STime: uint64(p.stime), NumThreads: uint32(p.num_threads), + Name: C.GoString(&p.name[0]), }) } - return procs, nil } @@ -380,12 +564,13 @@ Parameters: Returns: - error: Error if the process cannot be killed or doesn't exist */ -func KillProcess(pid int) error { +func ProcessAction(pid int, action unix.Signal) error { + if pid <= 0 { return fmt.Errorf("invalid PID: %d", pid) } - if C.process_kill(C.int(pid), C.int(unix.SIGTERM)) != C.OK { + if C.process_kill(C.int(pid), C.int(action)) != C.OK { return fmt.Errorf("failed to kill process %d", pid) } return nil diff --git a/pkg/grpc_handler/journal.go b/pkg/grpc_handler/journal.go deleted file mode 100644 index 8f60f58..0000000 --- a/pkg/grpc_handler/journal.go +++ /dev/null @@ -1,68 +0,0 @@ -package grpc_handler - -import ( - "log" - "paradigm-ehb/agent/gen/journal/v1" - j "paradigm-ehb/agent/internal/journal" - "sync" - - "github.com/coreos/go-systemd/v22/sdjournal" -) - -type JournalService struct { - journal.UnimplementedJournalServiceServer -} - -func (s *JournalService) Action(in *journal.JournalRequest, srv journal.JournalService_ActionServer) error { - - var val string - // TODO(nasr): remove the magic number enums, horrible code practice - switch in.Field { - - case 0: - val = sdjournal.SD_JOURNAL_FIELD_SYSTEMD_UNIT - case 1: - val = sdjournal.SD_JOURNAL_FIELD_PID - case 2: - val = sdjournal.SD_JOURNAL_FIELD_UID - case 3: - val = sdjournal.SD_JOURNAL_FIELD_GID - } - - m := []sdjournal.Match{{Field: val, Value: in.Value}} - - // Generated time. testing issue - // TODO(nasr): fix the time - //sinceTime, err := time.Parse(time.RFC3339, "0") - // - //if err != nil { - // fmt.Println("error parsing time:", err) - // return nil, nil - //} - // - //duration := time.Since(sinceTime) - - var wg sync.WaitGroup - - ch := make(chan []byte) - - wg.Add(2) - go func() { - defer wg.Done() - j.GetJournalInformation(0, in.NumFromTail, in.Cursor, m, in.Path, ch) - }() - - go func() { - defer wg.Done() - - for range ch { - resp := journal.JournalChunk{Reply: <-ch} - if err := srv.Send(&resp); err != nil { - log.Printf("send error %v", err) - } - } - }() - - wg.Wait() - return nil -} diff --git a/pkg/grpc_handler/deviceactions.go b/pkg/grpchandler/deviceactions.go similarity index 91% rename from pkg/grpc_handler/deviceactions.go rename to pkg/grpchandler/deviceactions.go index baf9b58..334b2eb 100644 --- a/pkg/grpc_handler/deviceactions.go +++ b/pkg/grpchandler/deviceactions.go @@ -25,18 +25,13 @@ func (s *DeviceActionsService) Action(ctx context.Context, req *actions.ActionRe } var out string - bus, err := dh.CreateSystemBus() + conn, err := dh.CreateSystemBus() if err != nil { log.Println("failed to create systembus") out = "failed system bus" } - - obj := bus.Object( - "org.freedesktop.login1", - "/org/freedesktop/login1", - ) - + obj, _ := dh.CreateLoginObject(conn) switch req.GetDeviceAction() { case actions.DeviceAction_DEVICE_ACTION_SHUTDOWN: out = string(da.PerformDeviceAction(obj, da.DeviceActionShutdown)) diff --git a/pkg/grpc_handler/greeter.go b/pkg/grpchandler/greeter.go similarity index 100% rename from pkg/grpc_handler/greeter.go rename to pkg/grpchandler/greeter.go diff --git a/pkg/grpchandler/journal.go b/pkg/grpchandler/journal.go new file mode 100644 index 0000000..7bf9801 --- /dev/null +++ b/pkg/grpchandler/journal.go @@ -0,0 +1,103 @@ +package grpc_handler + +import ( + "log" + "paradigm-ehb/agent/gen/journal/v1" + j "paradigm-ehb/agent/internal/journal" + "sync" + + "github.com/coreos/go-systemd/v22/sdjournal" +) + +type JournalService struct { + journal.UnimplementedJournalServiceServer +} + +func (s *JournalService) Action(in *journal.JournalRequest, srv journal.JournalService_ActionServer) error { + + var val string + /* + TODO(nasr): + - Remove magic-number–based enum handling. + - Replace `switch in.Field` with: + - a typed protobuf enum, or + - a map[JournalField]string lookup, or + - explicit constants with semantic names. + - Ensure invalid enum values are handled (default case + error). + */ + switch in.Field { + + case 0: + val = sdjournal.SD_JOURNAL_FIELD_SYSTEMD_UNIT + case 1: + val = sdjournal.SD_JOURNAL_FIELD_PID + case 2: + val = sdjournal.SD_JOURNAL_FIELD_UID + case 3: + val = sdjournal.SD_JOURNAL_FIELD_GID + } + + m := []sdjournal.Match{{Field: val, Value: in.Value}} + + /* + TODO(nasr): + - Implement proper time filtering support. + - Decide on API semantics: + - absolute timestamp vs relative duration + - server-side vs client-provided time window + - Validate time parsing errors and propagate them via gRPC status. + - Remove dead/commented-out code once design is finalized. + */ + + var wg sync.WaitGroup + + ch := make(chan []byte) + + /* + TODO(nasr): + - Define clear channel ownership: + - who closes `ch` and when. + - Consider making channel buffered to avoid producer/consumer blocking. + - Document lifetime guarantees between goroutines. + */ + + wg.Add(2) + go func() { + defer wg.Done() + /* + TODO(nasr): + - Propagate context cancellation into GetJournalInformation. + - Return errors instead of silent failure. + - Clarify meaning of leading `0` argument. + */ + j.GetJournalInformation(0, in.NumFromTail, in.Cursor, m, in.Path, ch) + }() + + go func() { + defer wg.Done() + + /* + TODO(nasr): + - Fix channel consumption logic: + current code ranges over `ch` but also performs `<-ch` again. + - Avoid double reads from the same channel. + - Handle channel close explicitly. + - Propagate send errors via context cancellation or status return. + */ + for range ch { + resp := journal.JournalChunk{Reply: <-ch} + if err := srv.Send(&resp); err != nil { + log.Printf("send error %v", err) + } + } + }() + + /* + TODO(nasr): + - Consider early exit on client disconnect. + - Avoid waiting indefinitely if goroutines deadlock. + - Evaluate replacing WaitGroup + channels with errgroup + context. + */ + wg.Wait() + return nil +} diff --git a/pkg/grpc_handler/resources.go b/pkg/grpchandler/resources/v1/resources.go similarity index 98% rename from pkg/grpc_handler/resources.go rename to pkg/grpchandler/resources/v1/resources.go index 25f2117..0a60456 100644 --- a/pkg/grpc_handler/resources.go +++ b/pkg/grpchandler/resources/v1/resources.go @@ -97,8 +97,8 @@ func mapCPU(c wr.Cpu) *proto.Cpu { */ func mapMemory(m wr.Ram) *proto.Memory { return &proto.Memory{ - Total: m.Total, - Free: m.Free, + Total: string(m.Total), + Free: string(m.Free), } } @@ -152,7 +152,7 @@ func mapProcesses(ps []wr.Process) []*proto.Process { for _, p := range ps { out = append(out, &proto.Process{ - Pid: p.PID, + Pid: uint32(p.PID), Name: p.Name, State: mapProcessState(p.State), Utime: p.UTime, diff --git a/pkg/grpchandler/resources/v2/resources.go b/pkg/grpchandler/resources/v2/resources.go new file mode 100644 index 0000000..5da3509 --- /dev/null +++ b/pkg/grpchandler/resources/v2/resources.go @@ -0,0 +1,265 @@ +package grpc_handler + +import ( + "context" + + proto "paradigm-ehb/agent/gen/resources/v2" + "paradigm-ehb/agent/internal/resources" + cgo "paradigm-ehb/agent/pkg/cgowrap" + + "golang.org/x/sys/unix" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +/** + * ResourcesServiceV2 implements the gRPC ResourcesServiceServer. + * + * It exposes endpoints that provide a snapshot of system-level resources + * such as CPU, memory, disks, and running processes. All data collection + * is delegated to the internal resources package and then mapped to + * protobuf-defined response types. + */ +type ResourcesServiceV2 struct { + proto.UnimplementedResourcesServiceServer +} + +/** + * GetSystemResources returns a full snapshot of system resources. + * + * The method: + * - Respects context cancellation to avoid unnecessary work + * - Collects system information via the internal resources layer + * - Maps internal domain structures to protobuf response messages + * + * Errors: + * - Returns codes.Canceled if the request context is canceled + * - Returns codes.Internal if resource collection fails + */ +func (s *ResourcesServiceV2) GetSystemResources( + ctx context.Context, + req *proto.GetSystemResourcesRequest, +) (*proto.GetSystemResourcesResponse, error) { + + select { + case <-ctx.Done(): + return nil, status.Error(codes.Canceled, "request canceled") + default: + } + + snap, err := resources.GetCompleteSystemResources() + if err != nil { + return nil, status.Errorf( + codes.Internal, + "failed to collect system resources: %v", + err, + ) + } + + return &proto.GetSystemResourcesResponse{ + Resources: mapSystemResources(snap), + }, nil +} + +/** +* TODO(nasr): update individual resources + */ + +func (s *ResourcesServiceV2) ProcessAction( + ctx context.Context, + req *proto.ProcessActionRequest, +) (*proto.ProcessActionReply, error) { + + err := cgo.ProcessAction(int(req.Pid), unix.Signal(req.Signal)) + if err != nil { + + return &proto.ProcessActionReply{ + Succes: false, + }, nil + } + + return &proto.ProcessActionReply{ + Succes: true, + }, nil +} + +/** + * mapSystemResources converts an internal SystemResources snapshot + * into its protobuf representation. + * + * This acts as the top-level aggregation mapper, delegating + * to more specific mapping functions per subsystem. + */ +func mapSystemResources(s *resources.SystemResources) *proto.SystemResources { + + return &proto.SystemResources{ + Cpu: mapCPU(s.CPU), + Memory: mapMemory(s.Memory), + Device: mapDevice(s.Device), + Disks: mapDisks(s.Disks), + Processes: mapProcesses(s.Procs), + } +} + +/** + * mapCPU maps CPU metadata from the cgo wrapper type + * into the protobuf Cpu message. + */ +func mapCPU(c cgo.Cpu) *proto.Cpu { + + return &proto.Cpu{ + Vendor: c.Vendor, + Model: c.Model, + Frequency: c.Frequency, + MaxCore: c.MaxCore, + TotalTime: c.TotalTime, + IdleTime: c.IdleTime, + } + +} + +/** + * mapMemory maps RAM usage information into the protobuf Memory message. + * + * Values are expected to be raw byte counts as reported by the system. + */ +func mapMemory(m cgo.Ram) *proto.Memory { + + return &proto.Memory{ + Total: m.Total, + Free: m.Free, + } + +} + +/** + * mapDevice maps general device and OS-level metadata + * into the protobuf Device message. + */ +func mapDevice(d cgo.Device) *proto.Device { + + return &proto.Device{ + OsVersion: d.OsVersion, + Uptime: d.Uptime, + } + +} + +/** + * mapDisks maps a slice of disk descriptors into protobuf Disk messages. + * + * Each disk contains a list of partitions, which are also converted + * field-by-field into their protobuf equivalents. + */ +func mapDisks(disks []cgo.Disk) []*proto.Disk { + + out := make([]*proto.Disk, 0, len(disks)) + + for _, d := range disks { + + parts := make([]*proto.DiskPartition, 0, len(d.Partitions)) + + for _, p := range d.Partitions { + + parts = append(parts, &proto.DiskPartition{ + + Name: p.Name, + Major: p.Major, + Minor: p.Minor, + Blocks: p.Blocks, + }) + } + + out = append(out, &proto.Disk{ + Partitions: parts, + }) + } + + return out +} + +/** + * mapProcessState converts an internal ProcessState enum + * into the corresponding protobuf ProcessState value. + * + * Unknown or unmapped states are converted to PROCESS_STATE_UNSPECIFIED + * to preserve forward compatibility. + */ +func mapProcessState(s cgo.ProcessState) proto.ProcessState { + + switch s { + + case cgo.ProcessRunning: + { + + return proto.ProcessState_PROCESS_STATE_RUNNING + } + case cgo.ProcessSleeping: + { + + return proto.ProcessState_PROCESS_STATE_SLEEPING + } + case cgo.ProcessDiskSleep: + { + + return proto.ProcessState_PROCESS_STATE_DISK_SLEEPING + } + case cgo.ProcessStopped: + { + + return proto.ProcessState_PROCESS_STATE_STOPPED + } + case cgo.ProcessTracingStopped: + { + + return proto.ProcessState_PROCESS_STATE_TRACING_STOPPED + } + case cgo.ProcessZombie: + { + + return proto.ProcessState_PROCESS_STATE_ZOMBIE + } + case cgo.ProcessDead: + { + + return proto.ProcessState_PROCESS_STATE_DEAD + } + case cgo.ProcessIdle: + { + return proto.ProcessState_PROCESS_STATE_IDLE + } + + case cgo.ProcessUndefined: + { + return proto.ProcessState_PROCESS_STATE_UNSPECIFIED + + } + default: + + return proto.ProcessState_PROCESS_STATE_UNSPECIFIED + } +} + +/** + * mapProcesses maps a slice of process descriptors into protobuf Process messages. + * + * Each process includes basic scheduling and accounting information + * such as PID, state, CPU time, and thread count. + */ +func mapProcesses(ps []cgo.Process) []*proto.Process { + + out := make([]*proto.Process, 0, len(ps)) + + for _, p := range ps { + out = append(out, &proto.Process{ + Pid: int32(p.PID), + Name: p.Name, + State: mapProcessState(p.State), + Utime: p.UTime, + NumThreads: p.NumThreads, + }) + } + + return out +} diff --git a/pkg/grpc_handler/services.go b/pkg/grpchandler/services/v1/services.go similarity index 97% rename from pkg/grpc_handler/services.go rename to pkg/grpchandler/services/v1/services.go index 719e0ed..ef1e423 100644 --- a/pkg/grpc_handler/services.go +++ b/pkg/grpchandler/services/v1/services.go @@ -91,7 +91,7 @@ func (s *HandlerService) UnitAction(_ context.Context, in *v1.ServiceActionReque out = "external bad input" } - _, err = manager.RunRetrieval(conn, true) + _, err = manager.RunRetrievalDeprecated(conn, true) if err != nil { log.Println("failed to do everything") out = "failed even more" diff --git a/pkg/grpc_handler/services_v2.go b/pkg/grpchandler/services/v2/services.go similarity index 87% rename from pkg/grpc_handler/services_v2.go rename to pkg/grpchandler/services/v2/services.go index a7549a7..6ede873 100644 --- a/pkg/grpc_handler/services_v2.go +++ b/pkg/grpchandler/services/v2/services.go @@ -128,7 +128,13 @@ func (s *HandlerServiceV2) GetAllUnits( }, nil } - units, err := manager.RunRetrieval(conn, true) + /** + + TODO(nasr): fix the design issue and return the correct mapped types + from the correct namespace + + */ + _, err = manager.RunRetrievalDeprecated(conn, true) if err != nil { return &v2.GetUnitsReply{ Success: false, @@ -137,7 +143,7 @@ func (s *HandlerServiceV2) GetAllUnits( } return &v2.GetUnitsReply{ - Units: units, + Units: nil, Success: true, }, nil } @@ -154,8 +160,13 @@ func (s *HandlerServiceV2) GetLoadedUnits( ErrorMessage: err.Error(), }, nil } + /** + + TODO(nasr): fix the design issue and return the correct mapped types + from the correct namespace - units, err := manager.RunRetrieval(conn, false) + */ + _, err = manager.RunRetrievalDeprecated(conn, false) if err != nil { return &v2.GetUnitsReply{ Success: false, @@ -164,7 +175,7 @@ func (s *HandlerServiceV2) GetLoadedUnits( } return &v2.GetUnitsReply{ - Units: units, + Units: nil, Success: true, }, nil } @@ -183,9 +194,12 @@ func (s *HandlerServiceV2) GetUnitStatus( }, nil } - obj := dh.CreateSystemdObject(conn) + obj, err := dh.CreateSystemdObject(conn) + if err != nil { + return nil, fmt.Errorf("failed to create system object") + } - state, err := manager.GetStatus(obj, in.UnitName) + state, err := manager.UnitStatus(obj, in.UnitName) if err != nil { return &v2.GetUnitStatusReply{ Success: false, diff --git a/pkg/grpchandler/services/v3/services.go b/pkg/grpchandler/services/v3/services.go new file mode 100644 index 0000000..d569141 --- /dev/null +++ b/pkg/grpchandler/services/v3/services.go @@ -0,0 +1,297 @@ +package grpc_handler + +import ( + "context" + "fmt" + + v3 "paradigm-ehb/agent/gen/services/v3" + + manager "paradigm-ehb/agent/internal/dbusservices" + dh "paradigm-ehb/agent/internal/dbusservices/dbus" + servicecontrol "paradigm-ehb/agent/internal/dbusservices/systemd" + types "paradigm-ehb/agent/internal/dbusservices/types" +) + +type HandlerServicev3 struct { + v3.UnimplementedHandlerServiceServer +} + +func toGrpcUnits(units []*types.LoadedUnit) ([]*v3.LoadedUnit, error) { + + if units == nil { + return nil, fmt.Errorf("passed input is nil") + } + + + out := make([]*v3.LoadedUnit, 0, len(units)) + + for _, u := range units { + if u == nil { + continue + } + + out = append(out, &v3.LoadedUnit{ + Name: u.Name, + Description: u.Description, + LoadState: u.LoadState, + SubState: u.SubState, + ActiveState: u.ActiveState, + DepUnit: u.DepUnit, + ObjectPath: string(u.ObjectPath), + QueuedJob: u.QueudJob, + JobType: u.JobType, + JobPath: string(u.JobPath), + }) + } + + + return out, nil +} +func (s *HandlerServicev3) PerformUnitAction( + _ context.Context, + in *v3.UnitActionRequest, +) (*v3.UnitActionReply, error) { + + conn, err := dh.CreateSystemBus() + if err != nil { + return &v3.UnitActionReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + var action servicecontrol.UnitAction + var actionName string + + switch in.Action { + case v3.UnitActionRequest_UNIT_ACTION_START: + action = servicecontrol.UnitActionStart + actionName = "start" + case v3.UnitActionRequest_UNIT_ACTION_STOP: + action = servicecontrol.UnitActionStop + actionName = "stop" + case v3.UnitActionRequest_UNIT_ACTION_RESTART: + action = servicecontrol.UnitActionRestart + actionName = "restart" + default: + return &v3.UnitActionReply{ + Success: false, + ErrorMessage: "unspecified unit action", + }, nil + } + + if err := manager.RunAction(conn, action, in.UnitName); err != nil { + return &v3.UnitActionReply{ + Status: []byte(fmt.Sprintf("failed to %s unit", actionName)), + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + return &v3.UnitActionReply{ + Status: []byte("success"), + Success: true, + }, nil +} + +func (s *HandlerServicev3) PerformUnitFileAction( + _ context.Context, + in *v3.UnitFileActionRequest, +) (*v3.UnitFileActionReply, error) { + + conn, err := dh.CreateSystemBus() + if err != nil { + return &v3.UnitFileActionReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + var action servicecontrol.UnitFileAction + + switch in.Action { + case v3.UnitFileActionRequest_UNIT_FILE_ACTION_ENABLE: + action = servicecontrol.UnitFileActionEnable + case v3.UnitFileActionRequest_UNIT_FILE_ACTION_DISABLE: + action = servicecontrol.UnitFileActionDisable + default: + return &v3.UnitFileActionReply{ + Success: false, + ErrorMessage: "unspecified unit file action", + }, nil + } + + if err := manager.RunSymlinkAction( + conn, + action, + in.Runtime, + in.Force, + []string{in.UnitName}, + ); err != nil { + return &v3.UnitFileActionReply{ + Status: []byte("unit file action failed"), + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + return &v3.UnitFileActionReply{ + Status: []byte("success"), + Success: true, + }, nil +} + +func (s *HandlerServicev3) GetAllUnits( + _ context.Context, + _ *v3.GetUnitsRequest, +) (*v3.GetUnitsReply, error) { + + units, err := manager.RunRetrieval(true) + if err != nil { + return &v3.GetUnitsReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + + mapped, err := toGrpcUnits(units) + if err != nil { + return &v3.GetUnitsReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + + return &v3.GetUnitsReply{ + Units: mapped, + Success: true, + }, nil +} + +func (s *HandlerServicev3) GetLoadedUnits( + _ context.Context, + _ *v3.GetUnitsRequest, +) (*v3.GetUnitsReply, error) { + + in, err := manager.RunRetrieval(false) + + if err != nil { + + return &v3.GetUnitsReply{ + Units: nil, + Success: true, + }, fmt.Errorf("failed to run retrieval") + } + + out, err := toGrpcUnits(in) + + if err != nil { + + return &v3.GetUnitsReply{ + Units: nil, + Success: true, + }, fmt.Errorf("failed to map to grpc types") + } + + return &v3.GetUnitsReply{ + Units: out, + Success: true, + }, nil +} + +func (s *HandlerServicev3) GetFilteredUnits( + _ context.Context, + in *v3.GetUnitsFilteredRequest, +) (*v3.GetUnitsReply, error) { + + conn, err := dh.CreateSystemBus() + if err != nil { + return &v3.GetUnitsReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + filters := make([]string, 0, len(in.Filters)) + + for _, f := range in.Filters { + switch f { + case v3.GetUnitsFilteredRequest_LOADED: + filters = append(filters, "loaded") + case v3.GetUnitsFilteredRequest_NOT_FOUND: + filters = append(filters, "not-found") + case v3.GetUnitsFilteredRequest_BAD_SETTING: + filters = append(filters, "bad-setting") + case v3.GetUnitsFilteredRequest_ERROR: + filters = append(filters, "error") + case v3.GetUnitsFilteredRequest_MASKED: + filters = append(filters, "masked") + default: + return &v3.GetUnitsReply{ + Success: false, + ErrorMessage: "unspecified filter state", + }, nil + } + } + + units, err := manager.MapFilteredUnits(conn, filters) + if err != nil { + return &v3.GetUnitsReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + result, err := toGrpcUnits(units) + + fmt.Print("result: ", result) + + if err != nil { + return &v3.GetUnitsReply{ + Units: nil, + Success: false, + }, fmt.Errorf("failed to parse to grpc units") + } + + return &v3.GetUnitsReply{ + Units: result, + Success: true, + }, nil +} + +func (s *HandlerServicev3) GetUnitStatus( + _ context.Context, + in *v3.GetUnitStatusRequest, +) (*v3.GetUnitStatusReply, error) { + + conn, err := dh.CreateSystemBus() + if err != nil { + return &v3.GetUnitStatusReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + obj, err := dh.CreateSystemdObject(conn) + if err != nil { + return &v3.GetUnitStatusReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + state, err := manager.UnitStatus(obj, in.UnitName) + if err != nil { + return &v3.GetUnitStatusReply{ + Success: false, + ErrorMessage: err.Error(), + }, nil + } + + return &v3.GetUnitStatusReply{ + State: state, + Success: true, + }, nil +} diff --git a/pkg/resources/.clang-format b/pkg/resources/.clang-format new file mode 100644 index 0000000..a086b22 --- /dev/null +++ b/pkg/resources/.clang-format @@ -0,0 +1,104 @@ +BasedOnStyle: LLVM +Language: Cpp + +# ------------------------------------------------------------------- +# Indentation & layout +# ------------------------------------------------------------------- +IndentWidth: 2 +TabWidth: 2 +UseTab: Never +ContinuationIndentWidth: 2 + +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: NoIndent + +# ------------------------------------------------------------------- +# Line breaking +# ------------------------------------------------------------------- +ColumnLimit: 0 + +AllowAllParametersOfDeclarationOnNextLine: false +AllowAllArgumentsOnNextLine: false + +AllowShortFunctionsOnASingleLine: false +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false + +AlwaysBreakAfterReturnType: All +AlwaysBreakTemplateDeclarations: No # harmless for C + +BreakBeforeBinaryOperators: None + +# ------------------------------------------------------------------- +# Braces (Allman style) +# ------------------------------------------------------------------- +BreakBeforeBraces: Allman + +BraceWrapping: + AfterCaseLabel: true + BeforeElse: true + BeforeCatch: true + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true + +# NOTE: +# AfterControlStatement / AfterFunction / AfterStruct / AfterEnum / AfterUnion +# are IMPLIED by Allman and must NOT be redundantly specified. + +# ------------------------------------------------------------------- +# Spacing +# ------------------------------------------------------------------- +SpaceBeforeParens: ControlStatements +SpaceBeforeAssignmentOperators: true +SpaceBeforeRangeBasedForLoopColon: true # + +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpacesInAngles: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 + +PointerAlignment: Right +DerivePointerAlignment: false + +# ------------------------------------------------------------------- +# Alignment (explicitly disabled) +# ------------------------------------------------------------------- +AlignAfterOpenBracket: DontAlign +AlignOperands: false +AlignTrailingComments: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: DontAlign + +# ------------------------------------------------------------------- +# Comments +# ------------------------------------------------------------------- +ReflowComments: false +CommentPragmas: '^ dont touch:' +KeepEmptyLinesAtTheStartOfBlocks: false +MaxEmptyLinesToKeep: 1 + +# ------------------------------------------------------------------- +# Includes +# ------------------------------------------------------------------- +SortIncludes: Never +IncludeBlocks: Preserve + +# ------------------------------------------------------------------- +# Macros & preprocessor +# ------------------------------------------------------------------- +MacroBlockBegin: '' +MacroBlockEnd: '' +SpaceAfterCStyleCast: false + +# ------------------------------------------------------------------- +# C-specific +# ------------------------------------------------------------------- +Cpp11BracedListStyle: false +DisableFormat: false diff --git a/pkg/resources/.clangd b/pkg/resources/.clangd new file mode 100644 index 0000000..b8bec03 --- /dev/null +++ b/pkg/resources/.clangd @@ -0,0 +1,68 @@ +CompileFlags: + Add: + - -Iinclude + + Remove: + - -std=* + - -O* + - -march=* + - -mtune=* + +# C configuration +If: + Language: C +Then: + CompileFlags: + Add: + - -std=c99 + - -xc + - -Wall + - -Wextra + - -Wpedantic + - -Wshadow + - -Wconversion + - -Wsign-conversion + - -Wmissing-declarations + - -Wundef + - -Wpointer-arith + - -Wcast-align + - -Wcast-qual + - -Wwrite-strings + - -Wswitch-enum + - -Wformat=2 + - -Wstrict-aliasing=2 + - -Werror=implicit-function-declaration + - -Werror=implicit-int + - -Werror=incompatible-pointer-types + - -Werror=return-type + - -Wformat-security + - -Wnull-dereference + - -Wmisleading-indentation + - -Wunused + - -Wuninitialized + - -Wdouble-promotion + - -Wstrict-overflow=2 + - -D_POSIX_C_SOURCE=200809L + - -ldnf5 + +# C++ configuration +If: + Language: Cpp +Then: + CompileFlags: + Add: + - -std=c++20 + - -Wall + - -Wextra + - -Wpedantic + - -Wshadow + - -Wconversion + - -Wsign-conversion + - -Wundef + - -Wpointer-arith + - -Wcast-align + - -Wcast-qual + - -Wformat=2 + - -Wformat-security + - -Wnull-dereference + - -Wmisleading-indentation diff --git a/pkg/resources/.gitea/workflows/main-build.yaml b/pkg/resources/.gitea/workflows/main-build.yaml new file mode 100644 index 0000000..0da5604 --- /dev/null +++ b/pkg/resources/.gitea/workflows/main-build.yaml @@ -0,0 +1,11 @@ +name: main-build +run-name: compile-build + +on: [push] + +jobs: + main-compile: + runs-on: ubuntu-latest + steps: + - name: Run Build step + - run: echo "Workflow is running" diff --git a/pkg/resources/.github/workflows/main.yml b/pkg/resources/.github/workflows/main.yml new file mode 100644 index 0000000..a712a5f --- /dev/null +++ b/pkg/resources/.github/workflows/main.yml @@ -0,0 +1,24 @@ +name: C CI + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y clang libproc2-dev + + - name: Make build script executable + run: chmod +x build.sh + + - name: Build + run: ./build.sh diff --git a/pkg/resources/.gitignore b/pkg/resources/.gitignore new file mode 100644 index 0000000..2c4c8cd --- /dev/null +++ b/pkg/resources/.gitignore @@ -0,0 +1,67 @@ +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Build files +build/ + +# Config files +.idea +.vscode + +# Junk +.DS_Store +.cache +source/main +tmp +/.vs +*.json diff --git a/pkg/resources/LICENSE b/pkg/resources/LICENSE new file mode 100644 index 0000000..d48e6ad --- /dev/null +++ b/pkg/resources/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Abdellah El Morabit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkg/resources/README.md b/pkg/resources/README.md new file mode 100644 index 0000000..b658d57 --- /dev/null +++ b/pkg/resources/README.md @@ -0,0 +1,15 @@ +# Resources gathering tool + +Tool for gathering system resources on linux + + +### credits + +https://www.gingerbill.org/series/memory-allocation-strategies/ +https://www.youtube.com/watch?v=jgiMagdjA1s +https://www.rfleury.com/p/untangling-lifetimes-the-arena-allocator +https://github.com/EpicGamesExt/raddebugger/blob/master/src/base/base_arena.c +https://www.geeksforgeeks.org/c/inline-function-in-c/ + +https://libdnf.readthedocs.io/en/dnf-5-devel/tutorial/install-build-deps.html +https://man.archlinux.org/man/libalpm.3#Topics diff --git a/pkg/resources/arena.c b/pkg/resources/arena.c new file mode 100644 index 0000000..2104d7d --- /dev/null +++ b/pkg/resources/arena.c @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "arena.h" + +mem_arena * +arena_create(u64 capacity) +{ + mem_arena *arena = mmap(0, capacity, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + if (arena == MAP_FAILED) + { + assert(0); + } + + arena->capacity = capacity; + arena->pos = ARENA_BASE_POS; + + return arena; +} + +// make it a void pointer to allow implicit conversion +void +arena_destroy(mem_arena *arena) +{ + munmap(arena, arena->capacity); +} + +void * +arena_push(mem_arena *arena, u64 size, b32 non_zero) +{ + u64 pos_aligned = ALIGN_UP_POW2(arena->pos, ARENA_ALIGN); + u64 new_pos = pos_aligned + size; + + if (new_pos > arena->capacity) + { + assert(0); + return NULL; + } + + arena->pos = new_pos; + // cast to u8 to be able to do pointer arithemtic + u8 *out = (u8 *)arena + pos_aligned; + + if (!non_zero) + { + memset(out, 0, size); + } + return out; +} +void +arena_pop(mem_arena *arena, u64 size) +{ + size = MIN(size, arena->pos - ARENA_BASE_POS); + arena->pos -= size; +} + +void +arena_pop_to(mem_arena *arena, u64 pos) +{ + u64 size = pos < arena->pos ? arena->pos - pos : 0; + arena_pop(arena, size); +} + +void +arena_clear(mem_arena *arena) +{ + arena_pop_to(arena, ARENA_BASE_POS); +} diff --git a/pkg/resources/arena.h b/pkg/resources/arena.h new file mode 100644 index 0000000..ca39ca0 --- /dev/null +++ b/pkg/resources/arena.h @@ -0,0 +1,75 @@ +#ifndef ARENA_H +#define ARENA_H + +#include "base.h" + +/** + * Arena Helper macro's + * */ + +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) +#define ALIGN_UP_POW2(n, p) (((u64)(n) + ((u64)(p) - 1)) & (~((u64)(p) - 1))) + +#define PUSH_STRUCT(arena, T) (T *)arena_push((arena), sizeof(T), 0) +#define PUSH_STRUCT_NZ(arena, T) (T *)arena_push((arena), sizeof(T), 1) +#define PUSH_ARRAY(arena, T, n) (T *)arena_push((arena), sizeof(T) * (n), 0) +#define PUSH_ARRAY_NZ(arena, T, n) (T *)arena_push((arena), sizeof(T) * (n), 1) + + + + +/* + * Represents a disk partition with major/minor device numbers and block count. + */ + +/** + * replacing malloc/free with arena allocaters + * + * */ + +#define ARENA_BASE_POS (sizeof(mem_arena)) +// void * for the size of a pointer on the machine, 64/32bit comp +#define ARENA_ALIGN (sizeof(void *)) + + +static inline u64 KiB(u64 n) { return n << 10; } +static inline u64 MiB(u64 n) { return n << 20; } +static inline u64 GiB(u64 n) { return n << 30; } + +typedef struct mem_arena mem_arena; + + +struct mem_arena +{ + u64 capacity; + u64 pos; +} ; + +// arena prototypes +mem_arena * +arena_create(u64 capacity); +// make it a void pointer to allow implicit conversion +void +arena_destroy(mem_arena *arena); + +void * +arena_push(mem_arena *arena, u64 size, b32 non_zero); + +void +arena_pop(mem_arena *arena, u64 size); + +void +arena_pop_to(mem_arena *arena, u64 pos); + +void +arena_clear(mem_arena *arena); + + +#define PUSH_STRUCT(arena, T) (T *)arena_push((arena), sizeof(T), 0) +#define PUSH_STRUCT_NZ(arena, T) (T *)arena_push((arena), sizeof(T), 1) +#define PUSH_ARRAY(arena, T, n) (T *)arena_push((arena), sizeof(T) * (n), 0) +#define PUSH_ARRAY_NZ(arena, T, n) (T *)arena_push((arena), sizeof(T) * (n), 1) + + +#endif diff --git a/pkg/resources/base.c b/pkg/resources/base.c new file mode 100644 index 0000000..27cfc7f --- /dev/null +++ b/pkg/resources/base.c @@ -0,0 +1,26 @@ +#include "base.h" + +/** + * Helper function to parse strings to int using ascii codes + * */ +u64 +parse_u64(char *buf, size_t len) +{ + u64 value = 0; + + for ( + size_t buffer_idx = 0; + buffer_idx < len; + ++buffer_idx) + { + char c = buf[buffer_idx]; + if (c < '0' || c > '9') + { + break; + } + u64 digit = (u64)(c - '0'); + value = value * 10 + digit; + } + + return value; +} diff --git a/pkg/resources/base.h b/pkg/resources/base.h new file mode 100644 index 0000000..988d33a --- /dev/null +++ b/pkg/resources/base.h @@ -0,0 +1,48 @@ +#ifndef BASE_H +#define BASE_H + +#include +#include + +#define OK 0 +#define ERR_IO 1 +#define ERR_PARSE 2 +#define ERR_PERM 3 +#define ERR_INVALID 4 + +#define internal static +#define local_persist static +#define global_variable static + +enum +{ + BUFFER_SIZE_SMALL = 128, + BUFFER_SIZE_DEFAULT = 256, + BUFFER_SIZE_LARGE = 512, + PATH_MAX_LEN = 4096 +}; + +typedef uint64_t u64; +typedef uint32_t u32; +typedef uint16_t u16; +typedef uint8_t u8; + +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; + +typedef float f32; +typedef double f64; + +typedef i32 b32; +typedef i16 b16; +typedef u8 b8; + +#define TRUE 1 +#define FALSE 0 + +u64 +parse_u64(char *buf, size_t len); + +#endif diff --git a/pkg/resources/build-pkgm.sh b/pkg/resources/build-pkgm.sh new file mode 100644 index 0000000..ea25767 --- /dev/null +++ b/pkg/resources/build-pkgm.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -eu + +# Compile C files with clang (C compiler) +clang -c arena.c -o arena.o +clang -c resources.c -o resources.o + +# Compile C++ files and link with C objects +clang configure.c arena.o resources.o -ldnf5 -o nob + +./nob diff --git a/pkg/resources/build-pkgmanager.sh b/pkg/resources/build-pkgmanager.sh new file mode 100755 index 0000000..16356db --- /dev/null +++ b/pkg/resources/build-pkgmanager.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -eu + +CXX=c++ +SRC=libdnf_handler.cpp +OUT=build/pkgmanager + +CXXFLAGS=" +-std=c++20 +-Wall -Wextra -Wpedantic +-Wshadow -Wconversion +-Wundef +-Wpointer-arith -Wcast-align -Wcast-qual +-Wformat=2 -Wformat-security +-Wnull-dereference +-Wmisleading-indentation +-Wunused -Wuninitialized +-fexceptions +-fno-rtti +-ldnf5 +-lfmt +" + +mkdir -p build + +echo "Compiling..." +$CXX $CXXFLAGS "$SRC" -o "$OUT" + +echo "Running..." +"$OUT" diff --git a/pkg/resources/build.sh b/pkg/resources/build.sh new file mode 100755 index 0000000..b30970b --- /dev/null +++ b/pkg/resources/build.sh @@ -0,0 +1,46 @@ +#!/bin/sh +set -eu + +CC=cc +AR=ar + +SRC=resources.c +OUT_DIR=build +OUT_OBJ=$OUT_DIR/resources.o +OUT_LIB=$OUT_DIR/libagent_resources.a + +CFLAGS=" +-std=c99 +-Wall +-Wextra +-Wpedantic +-Wshadow +-Wconversion +-Wundef +-Wpointer-arith +-Wcast-align +-Wcast-qual +-Wwrite-strings +-Wformat=2 +-Wformat-security +-Wnull-dereference +-Wmisleading-indentation +-Wunused +-Wuninitialized +-Werror +-Wdouble-promotion +-Wstrict-overflow=2 +-D_POSIX_C_SOURCE=200809L +" + +# TODO(nasr): compile the package manager libs, static bin +mkdir -p "$OUT_DIR" + +echo "Compiling object..." +$CC $CFLAGS -c "$SRC" -o "$OUT_OBJ" + +echo "Creating static library..." +$AR rcs "$OUT_LIB" "$OUT_OBJ" + +echo "Done:" +echo " $OUT_LIB" diff --git a/pkg/resources/build_check_distro.sh b/pkg/resources/build_check_distro.sh new file mode 100755 index 0000000..a952f51 --- /dev/null +++ b/pkg/resources/build_check_distro.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -xeu + +clang configure.cpp -o -Iarena -Ibase check diff --git a/pkg/resources/compile.sh b/pkg/resources/compile.sh new file mode 100755 index 0000000..d3368e4 --- /dev/null +++ b/pkg/resources/compile.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -eu + +CC=clang +SRC=resources_main.c +OUT_DIR=build +OUT_BIN=$OUT_DIR/agent_resources + +CFLAGS=" +-std=c99 +-Wall +-Wextra +-Wpedantic +-Wshadow +-Wconversion +-Wundef +-Wpointer-arith +-Wcast-align +-Wcast-qual +-Wwrite-strings +-Wformat=2 +-D_POSIX_C_SOURCE=200809L +" + +sep() { + printf '%s\n' "============================================================" +} + +header() { + sep + printf ' %s\n' "$1" + sep +} + +info() { + printf ' • %s\n' "$1" +} + +ok() { + printf ' ✓ %s\n' "$1" +} + +mkdir -p "$OUT_DIR" + +header "BUILD" +info "Compiler : $CC" +info "Source : $SRC" +info "Output : $OUT_BIN" + +printf '\n' +info "Compiling…" +$CC $CFLAGS "$SRC" -o "$OUT_BIN" +ok "Compilation finished" + +printf '\n' +header "RUN" +info "Executing binary" +sep +"$OUT_BIN" +sep +ok "Execution finished" diff --git a/pkg/resources/configure.c b/pkg/resources/configure.c new file mode 100644 index 0000000..d45a4e9 --- /dev/null +++ b/pkg/resources/configure.c @@ -0,0 +1,64 @@ +#include "arena.h" +#include "resources.h" +#include "pkgm.hpp" +#include +#include "stdio.h" + +internal enum LinuxDistro +find_lxd_pkgm(mem_arena *arena) +{ + Device *device = device_create(arena); + device_read(device); + enum LinuxDistro lxd; + + i32 len = sizeof(device->os_version); + char *distro_unparsed = (char *)PUSH_ARRAY(arena, char, len); + + memcpy(distro_unparsed, device->os_version, len); + + i32 word_idx = 0; + char *buffer = (char *)arena_push(arena, len, 1); + + while (distro_unparsed[word_idx] != '\0') + { + /* TODO: + * + * Convert character to lowercase using ASCII ordering. + * + * - Check whether the character is an uppercase letter. + * - Uppercase ASCII range: 'A' .. 'Z'. + * - If the character falls within this range: + * - Convert it to lowercase by applying the ASCII offset. + * - Rationale: + * - Lowercase letters appear after uppercase letters in ASCII. + * - Case conversion can be done via direct numeric comparison, + * without locale or library calls. + */ + /* + Calculate the lower-case upper-case differnce + */ + i8 lwc_diff = 'a' - 'A'; + if (distro_unparsed[word_idx] != ' ') + { + if (distro_unparsed[word_idx] < 'a') + { + distro_unparsed[word_idx] += lwc_diff; + } + buffer[word_idx] = distro_unparsed[word_idx]; + } + + ++word_idx; + } + + buffer[word_idx] = '\0'; + + return buffer; +} + +int +main() +{ + mem_arena *arena = arena_create(GiB(1)); + printf("distro name %s", find_lxd_pkgm(arena)); + arena_destroy(arena); +} diff --git a/pkg/resources/nob b/pkg/resources/nob new file mode 100755 index 0000000..eff7a02 Binary files /dev/null and b/pkg/resources/nob differ diff --git a/pkg/resources/pkgm.cpp b/pkg/resources/pkgm.cpp new file mode 100644 index 0000000..3707d11 --- /dev/null +++ b/pkg/resources/pkgm.cpp @@ -0,0 +1,87 @@ +#define DISTRO_FEDORAH +#if defined(DISTRO_ARCH) + +#include + +/** + * TODO(nasr): pacman impelmentation of package manager + * */ + +#elif defined(DISTRO_FEDORAH) + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +libdnf5::Base * +setup() +{ + libdnf5::Base *base = new libdnf5::Base; + base->setup(); + + auto repo_sack = base->get_repo_sack(); + + repo_sack->create_repos_from_system_configuration(); + repo_sack->load_repos(); + + return base; +} + +std::vector +query(libdnf5::Base *base, std::string package_name) +{ + std::vector packages; + + libdnf5::rpm::PackageQuery query(*base); + query.filter_name(package_name); + + for (const auto &pkg : query) + { + packages.push_back(pkg.get_nevra()); + } + + return packages; +} + +void +run_transaction(libdnf5::Base *base, std::string package_name) +{ + libdnf5::Goal goal(*base); + goal.add_rpm_install(package_name); + + auto transaction = goal.resolve(); + + libdnf5::repo::PackageDownloader downloader(*base); + + for (auto &tspkg : transaction.get_transaction_packages()) + { + if (libdnf5::transaction::transaction_item_action_is_inbound(tspkg.get_action())) + { + downloader.add(tspkg.get_package()); + downloader.download(); + } + } + + downloader.download(); + + transaction.set_callbacks(std::unique_ptr()); + transaction.set_description("installing a package for the first time"); + + transaction.run(); + + return; +} + +#elif defined(DISTRO_DEBIAN) + +/** + * TODO(nasr): apt impelmentation of package manager + * */ + +#endif diff --git a/pkg/resources/pkgm.hpp b/pkg/resources/pkgm.hpp new file mode 100644 index 0000000..1fa6e7e --- /dev/null +++ b/pkg/resources/pkgm.hpp @@ -0,0 +1,68 @@ +#ifndef PKGMANAGER_H +#define PKGMANAGER_H + +#include "arena.h" +#include "base.h" + +enum LinuxDistro +{ + DISTRO_UNKNOWN = 0, + + DISTRO_ARCH_LINUX, + DISTRO_MANJARO, + DISTRO_ENDEAVOUROS, + DISTRO_ARCO, + DISTRO_GARUDA, + + DISTRO_DEBIAN, + DISTRO_UBUNTU, + DISTRO_LINUX_MINT, + DISTRO_POP_OS, + + DISTRO_FEDORA, + DISTRO_RHEL, + DISTRO_CENTOS_STREAM, + DISTRO_ROCKY, + DISTRO_ALMA, + + DISTRO_GENTOO, + DISTRO_NIXOS, + DISTRO_OPENSUSE, + + DISTRO_COUNT + +}; + +struct DistroStack +{ + const char *name; + enum LinuxDistro linux_distro; +}; + +internal const struct DistroStack distro_map[] = { + { "arch", DISTRO_ARCH_LINUX }, + { "manjaro", DISTRO_MANJARO }, + { "endeavouros", DISTRO_ENDEAVOUROS }, + { "arco", DISTRO_ARCO }, + { "garuda", DISTRO_GARUDA }, + + { "debian", DISTRO_DEBIAN }, + { "ubuntu", DISTRO_UBUNTU }, + { "mint", DISTRO_LINUX_MINT }, + { "pop", DISTRO_POP_OS }, + + { "fedora", DISTRO_FEDORA }, + { "rhel", DISTRO_RHEL }, + { "centos", DISTRO_CENTOS_STREAM }, + { "rocky", DISTRO_ROCKY }, + { "alma", DISTRO_ALMA }, + + { "gentoo", DISTRO_GENTOO }, + { "nixos", DISTRO_NIXOS }, + { "opensuse", DISTRO_OPENSUSE }, +}; + +internal enum LinuxDistro +find_lxd_pkgm(mem_arena *arena); + +#endif diff --git a/pkg/resources/pkgm_main.cpp b/pkg/resources/pkgm_main.cpp new file mode 100644 index 0000000..a9c3c8c --- /dev/null +++ b/pkg/resources/pkgm_main.cpp @@ -0,0 +1,11 @@ +/* ======================================================================== + $File: pkgm_main.cpp $ + $Date: 2026-01-12 $ + $Revision: $ + $Author: nsrddyn@gmail.com $ + $Notice: (C) Copyright 2026 $ + ======================================================================== */ + +#include "arena.h" +#include "pkgm.hpp" +#include "stdio.h" diff --git a/pkg/resources/resources.c b/pkg/resources/resources.c new file mode 100644 index 0000000..1201437 --- /dev/null +++ b/pkg/resources/resources.c @@ -0,0 +1,890 @@ +/* + * name: Abdellah El Morabit + * organization: Paradigm-Ehb + * year: 2025-2026 + * description: resources gathering library + * + */ + +#include "base.h" +#define _POSIX_C_SOURCE 200809L + +#include "resources.h" +#include "arena.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * disk_push_partition - Add a partition to the disk structure + * @d: Pointer to the Disk structure + * @p: Partition to add + * + * Dynamically grows the partition array if needed. Doubles capacity when + * full. If realloc fails, the partition is not added and function returns + * silently. + */ + +void +disk_push_partition(Disk *d, Partition p, mem_arena *arena) +{ + if (d->part_count == d->part_capacity) + { + size_t new_cap = d->part_capacity ? d->part_capacity * 2 : 8; + + Partition *np = PUSH_ARRAY_NZ(arena, Partition, new_cap); + if (!np) + return; + + if (d->partitions && d->part_count > 0) + { + memcpy(np, d->partitions, d->part_count * sizeof(Partition)); + } + + d->partitions = np; + d->part_capacity = new_cap; + } + + d->partitions[d->part_count++] = p; +} + +/* + * is_numeric - Check if a string contains only digits + * @s: String to check + * + * Return: 1 if string contains only numeric characters, 0 otherwise + */ +int +is_numeric(const char *s) +{ + for (; *s; ++s) + { + if (*s < '0' || *s > '9') + { + return 0; + } + } + return 1; +} + +/* + * cpu_create - Allocate and initialize a new Cpu structure + * + * Return: Pointer to newly allocated Cpu, or NULL on allocation failure + */ +Cpu * +cpu_create(mem_arena *m) +{ + return arena_push(m, sizeof(Cpu), 1); +} + +/* + * cpu_read - Read CPU information from /proc/cpuinfo + * @out: Pointer to Cpu structure to populate + * + * Reads vendor_id, model name, cpu MHz, and cpu cores from /proc/cpuinfo. + * The function reads the first occurrence of each field. + * + * Return: OK on success, AGENT_ERR_INVALID if out is NULL, + * ERR_IO if /proc/cpuinfo cannot be opened + */ + +/** + * get a read of the enabled cpu cores to get a view of the folder structure + * before starting to read every single of of them + * + * call + * + * */ +int +cpu_read_enabled_core_cpu_frequency(Cpu *out, int enabled_cpu_count) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + char path[PATH_MAX_LEN]; + for (i8 i = 0; i <= enabled_cpu_count; i++) + { + snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq", i); + } + FILE *fp = fopen(path, "r"); + // TODO(nasr): do a **cores see each frequency individually together with the utilization + if (!fp) + { + assert(fp); + return ERR_IO; + } + + u64 freq = 0; + if (fscanf(fp, "%lu", &freq) != 1) + { + fclose(fp); + assert(0); + return ERR_PARSE; + } + + fclose(fp); + + snprintf(out->frequency, sizeof(out->frequency), "%lu", freq); + return OK; +} + +int +cpu_read_cpu_model_name_arm64(Cpu *out) +{ + FILE *of = fopen("/proc/device-tree/model", "rb"); + if (!of) + { + assert(0); + return ERR_IO; + } + + u8 buffer[BUFFER_SIZE_DEFAULT]; + + /** + * + * note to self + * fread returns the amount of bytes read + * fwrite returns the amount of bytes written + * + */ + + size_t n = fread(buffer, 1, sizeof(buffer), of); + if (n == 0) + { + assert(0); + return ERR_IO; + } + + size_t len = 0; + while (len < n && buffer[len] != 0) + { + ++len; + } + + memcpy(out->model, buffer, len); + out->model[len] = '\0'; + + fclose(of); + + return OK; +} + +int +cpu_get_cores_enabled_arm(Cpu *out) +{ + assert(out); + + FILE *fp = fopen("/sys/devices/system/cpu/enabled", "r"); + assert(fp); + + char buf[BUFFER_SIZE_DEFAULT]; + assert(fgets(buf, sizeof(buf), fp)); + fclose(fp); + + int max_cpu = 0; + char *p = buf; + + while (*p) + { + if (*p >= '0' && *p <= '9') + { + int v = 0; + while (*p >= '0' && *p <= '9') + { + v = v * 10 + (*p++ - '0'); + } + + if (v > max_cpu) + { + max_cpu = v; + } + } + else + { + ++p; + } + } + + out->cores = (u32)max_cpu; + return OK; +} + +int +cpu_read_arm64(Cpu *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + cpu_get_cores_enabled_arm(out); + cpu_read_cpu_model_name_arm64(out); + cpu_read_enabled_core_cpu_frequency(out, (int)out->cores); + + return OK; +} + +int +cpu_read_amd64(Cpu *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + FILE *f = fopen("/proc/cpuinfo", "r"); + if (!f) + { + assert(0); + return ERR_IO; + } + + char buf[BUFFER_SIZE_LARGE]; + while (fgets(buf, sizeof(buf), f)) + { + char *colon = strchr(buf, ':'); + if (!colon) + continue; + + char *val = colon + 1; + while (*val == ' ') + val++; + + size_t len = strcspn(val, "\n"); + + if (!strncmp(buf, "vendor_id", 9)) + { + memcpy(out->vendor, val, len); + } + if (!strncmp(buf, "model name", 10)) + { + memcpy(out->model, val, len); + } + if (!strncmp(buf, "cpu MHz", 7)) + { + memcpy(out->frequency, val, len); + } + if (!strncmp(buf, "cpu cores", 9)) + { + out->cores = (u32)atoi(buf); + } + } + + fclose(f); + return OK; +} + +int +cpu_read(Cpu *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + +#if defined(__arm__) || defined(__aarch64__) + if (cpu_read_arm64(out) != OK) + { + /** + * Debugging!! + * + * */ + assert(0); + } + +#elif defined(__i386__) || defined(__x86_64__) + if (cpu_read_amd64(out) != OK) + { + /** + * Debugging!! + * + * */ + assert(0); + } + +#else +#error "Unsupported architecture" +#endif + return OK; +} + +int +cpu_read_usage(Cpu *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + FILE *f = fopen("/proc/stat", "r"); + if (!f) + { + assert(0); + return ERR_IO; + } + + char buf[BUFFER_SIZE_LARGE]; + while (fgets(buf, sizeof(buf), f)) + { + if (strncmp(buf, "cpu ", 4) == 0) + { + unsigned long user, nice, system, idle, iowait, irq, softirq, steal; + if (sscanf( + buf + 4, + "%lu %lu %lu %lu %lu %lu %lu %lu", + &user, + &nice, + &system, + &idle, + &iowait, + &irq, + &softirq, + &steal) == 8) + { + u64 idleAll = idle + iowait; + u64 total = user + nice + system + idle + iowait + irq + softirq + steal; + out->idle_time = idleAll; + out->total_time = total; + break; + } + } + } + + fclose(f); + return OK; +} + +/* + * ram_create - Allocate and initialize a new Ram structure + * + * Return: Pointer to newly allocated Ram, or NULL on allocation failure + */ +Ram * +ram_create(mem_arena *m) +{ + return arena_push(m, sizeof(Ram), 1); +} + +/* + * ram_read - Read RAM information from /proc/meminfo + * @out: Pointer to Ram structure to populate + * + * Reads MemTotal and MemFree from /proc/meminfo in kilobytes. + * + * Return: OK on success, AGENT_ERR_INVALID if out is NULL, + * ERR_IO if /proc/meminfo cannot be opened + */ +int +ram_read(Ram *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + FILE *f = fopen("/proc/meminfo", "r"); + if (!f) + { + assert(0); + return ERR_IO; + } + + mem_arena *temp_arena = arena_create(KiB(8)); + + size_t total_len; + size_t free_len; + + char *total_buffer; + char *free_buffer; + + char buf[BUFFER_SIZE_SMALL]; + while (fgets(buf, sizeof(buf), f)) + { + char *colon = strchr(buf, ':'); + if (!colon) + { + continue; + } + + char *val = colon + 1; + while (*val == ' ') + { + val++; + } + + if (!strncmp(buf, "MemTotal", 8)) + { + total_len = strcspn(val, "k\n"); + total_buffer = PUSH_ARRAY(temp_arena, char, total_len); + + memcpy(total_buffer, val, total_len); + } + + if (!strncmp(buf, "MemFree", 7)) + { + free_len = strcspn(val, "k\n"); + free_buffer = PUSH_ARRAY(temp_arena, char, free_len); + + memcpy(free_buffer, val, free_len); + } + } + + out->total = parse_u64(total_buffer, total_len); + out->free = parse_u64(free_buffer, free_len); + + fclose(f); + return OK; +} + +/* + * disk_create - Allocate and initialize a new Disk structure + * + * Return: Pointer to newly allocated Disk, or NULL on allocation failure + */ +Disk * +disk_create(mem_arena *m) +{ + return arena_push(m, sizeof(Disk), 1); +} + +/* + * disk_read - Read disk partition information from /proc/partitions + * @out: Pointer to Disk structure to populate + * + * Reads all partitions from /proc/partitions, storing major/minor device + * numbers, block count, and device name for each partition. Skips the header + * line and any malformed entries. + * + * Return: OK on success, AGENT_ERR_INVALID if out is NULL, + * ERR_IO if /proc/partitions cannot be opened + */ + +int +disk_read(Disk *out, mem_arena *arena) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + FILE *f = fopen("/proc/partitions", "r"); + if (!f) + { + assert(0); + return ERR_IO; + } + + char buf[BUFFER_SIZE_DEFAULT]; + + while (fgets(buf, sizeof(buf), f)) + { + Partition p = { 0 }; + char name[BUFFER_SIZE_DEFAULT]; + + if (sscanf(buf, + "%lu %lu %lu %255s", + &p.major, + &p.minor, + &p.blocks, + name) != 4) + { + continue; + } + + size_t len = strlen(name); + if (len >= sizeof(p.name)) + len = sizeof(p.name) - 1; + + memcpy(p.name, name, len); + p.name[len] = 0; + + disk_push_partition(out, p, arena); + } + + fclose(f); + return OK; +} + +int +fs_usage(char *path, Disk *disk) +{ + struct statfs s; + if (statfs(path, &s) != 0) + { + return ERR_IO; + } + + i64 block_size = s.f_bsize; + + u64 blocks = (u64)s.f_blocks; + u64 bfree = (u64)s.f_bfree; + u64 bavail = (u64)s.f_bavail; + u64 bsize = (u64)block_size; + + disk->disk_usage.total = blocks * bsize; + disk->disk_usage.free = bfree * bsize; + disk->disk_usage.available = bavail * bsize; + disk->disk_usage.used = (blocks - bfree) * bsize; + + return OK; +} + +/* + * device_create - Allocate and initialize a new Device structure + * + * Return: Pointer to newly allocated Device, or NULL on allocation failure + */ +Device * +device_create(mem_arena *m) +{ + return arena_push(m, sizeof(Device), 1); +} + +/* + * collect_processes - Collect all running process IDs from /proc + * @dev: Pointer to Device structure to populate with process IDs + * + * Scans /proc directory for numeric entries (process IDs) and stores them + * as strings in the Device structure. Dynamically grows the process array + * as needed. + */ + +int +process_list_collect(Process_List *list, mem_arena *arena) +{ + DIR *d = opendir("/proc"); + if (!d) + { + assert(0); + return ERR_IO; + } + + struct dirent *e = 0; + + if (!list->items) + { + list->capacity = 8; + list->count = 0; + list->items = PUSH_ARRAY_NZ(arena, Process, list->capacity); + if (!list->items) + { + closedir(d); + assert(0); + return ERR_IO; + } + } + + while ((e = readdir(d))) + { + if (!is_numeric(e->d_name)) + continue; + + if (list->count == list->capacity) + { + size_t new_cap = list->capacity * 2; + Process *np = PUSH_ARRAY_NZ(arena, Process, new_cap); + if (!np) + break; + + memcpy(np, list->items, sizeof(Process) * list->capacity); + list->items = np; + list->capacity = new_cap; + } + + Process *p = &list->items[list->count++]; + + p->pid = atoi(e->d_name); + p->state = PROCESS_UNDEFINED; + p->utime = 0; + p->stime = 0; + p->num_threads = 0; + p->name[0] = 0; + } + + closedir(d); + return OK; +} + +/** + * +struct Proces { + char *pid; + char *name; + char *state; + char *utime; + char *num_threads; +}; + +*/ + +int +process_read(i32 pid, Process *out) +{ + char path[PATH_MAX_LEN]; + snprintf(path, sizeof(path), "/proc/%d/status", pid); + + FILE *fp = fopen(path, "r"); + if (!fp) + { + return ERR_IO; + } + + char buf[BUFFER_SIZE_LARGE]; + + /* initialize */ + out->pid = pid; + + while (fgets( + buf, + sizeof(buf), + fp)) + { + char *colon = strchr(buf, ':'); + if (!colon) + { + continue; + } + + char *val = colon + 1; + while (*val == ' ' || *val == '\t') + { + ++val; + } + + size_t len = strcspn(val, "\n"); + + if (!strncmp(buf, "Name:", 5)) + { + memcpy(out->name, val, len); + out->name[len] = 0; + } + if (!strncmp(buf, "State:", 6)) + { + char state_char = 0; + for (char *p = val; *p; ++p) + { + if ((*p >= 'A' && *p <= 'Z') || *p == 't') + { + state_char = *p; + break; + } + } + + switch (state_char) + { + case 'R': + { + out->state = PROCESS_RUNNING; + break; + } + case 'S': + { + out->state = PROCESS_SLEEPING; + break; + } + case 'D': + { + out->state = PROCESS_DISK_SLEEP; + break; + } + case 'T': + { + out->state = PROCESS_STOPPED; + break; + } + case 't': + { + out->state = PROCESS_TRACING_STOPPED; + break; + } + case 'Z': + { + out->state = PROCESS_ZOMBIE; + break; + } + case 'X': + { + out->state = PROCESS_DEAD; + break; + } + case 'I': + { + out->state = PROCESS_IDLE; + break; + } + default: + { + out->state = PROCESS_UNDEFINED; + break; + } + } + } + + if (!strncmp(buf, "Threads:", 8)) + { + out->num_threads = (u32)strtoul(val, 0, 10); + } + } + + int error = fclose(fp); + if (error != 0) + { + return ERR_IO; + } + + return OK; +} + +int +device_up_time(Device *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + FILE *f = fopen("/proc/uptime", "r"); + if (!f) + { + assert(0); + return ERR_IO; + } + + i64 s; + int result = fscanf(f, "%ld", &s); + fclose(f); + + if (result != 1) + { + assert(0); + return ERR_IO; + } + + i64 day = s / 86400; + i64 hour = s % 86400 / 3600; + i64 min = s % 3600 / 60; + sprintf(out->uptime, "%ldd %ldh %ldm", day, hour, min); + return OK; +} + +/* + * device_read - Read device information including OS version, uptime, and + * processes + * @out: Pointer to Device structure to populate + * + * Reads OS version from /proc/version, uptime from /proc/uptime, and collects + * all running process IDs from /proc directory. + * + * Return: OK on success, AGENT_ERR_INVALID if out is NULL, + * ERR_IO if required files cannot be opened + */ +int +device_read(Device *out) +{ + if (!out) + { + assert(0); + return ERR_INVALID; + } + + FILE *version = fopen("/etc/os-release", "r"); + if (!version) + { + if (version) + { + fclose(version); + } + return ERR_IO; + } + + char buffer[BUFFER_SIZE_DEFAULT]; + while (fgets(buffer, sizeof(out->os_version), version)) + { + if (!strncmp(buffer, "NAME=", 5)) + { + char *start = strchr(buffer, '"'); + if (start) + { + start++; + char *end = strchr(start, '"'); + if (end) + { + i64 len = end - start; + if ((u64)len < sizeof(out->os_version)) + { + memcpy(out->os_version, start, (u64)len); + out->os_version[len] = '\0'; + } + } + } + } + } + + fclose(version); + device_up_time(out); + + return OK; +} + +/* + * process_kill - Send a signal to a process + * @pid: Process ID to signal + * @signal: Signal number to send (e.g., SIGTERM, SIGKILL) + * + * Wrapper around kill(2) system call with error handling. + * + * Return: OK on success, + * ERR_INVALID if pid is invalid or process not found, + * ERR_PERM if permission denied, + * ERR_IO for other errors + */ +int +process_kill(pid_t pid, int signal) +{ + if (pid <= 0) + { + assert(0); + return ERR_INVALID; + } + + if (kill(pid, signal) == -1) + { + if (errno == EPERM) + { + assert(0); + return ERR_PERM; + } + if (errno == ESRCH) + { + assert(0); + return ERR_INVALID; + } + + assert(0); + return ERR_IO; + } + return OK; +} diff --git a/pkg/resources/resources.h b/pkg/resources/resources.h new file mode 100644 index 0000000..d76c4f1 --- /dev/null +++ b/pkg/resources/resources.h @@ -0,0 +1,156 @@ +#ifndef RESOURCES_H +#define RESOURCES_H + +#include +#include +#include "base.h" +#include "arena.h" + +#define RESOURCES_API_VERSION 1 + +typedef struct Cpu Cpu; +typedef struct Ram Ram; +typedef struct Disk Disk; +typedef struct Device Device; + +typedef struct Partition Partition; +typedef struct Process Process; +typedef struct Process_List Process_List; + +/** + * TODO(nasr): + * + * Heh? what are you doing with the types here, + * you mismatched the namings + * check for what you are doing in the cgo wrapper + * this could be a big issue + * + * */ +typedef int32_t ProcessState; + +typedef enum Process_State +{ + PROCESS_UNDEFINED = 0, + PROCESS_RUNNING = 1, + PROCESS_SLEEPING = 2, + PROCESS_DISK_SLEEP = 3, + PROCESS_STOPPED = 4, + PROCESS_TRACING_STOPPED = 5, + PROCESS_ZOMBIE = 6, + PROCESS_DEAD = 7, + PROCESS_IDLE = 8, + +} Process_State; + +struct Process +{ + i32 pid; + Process_State state; + u64 utime; + u64 stime; + u32 num_threads; + char name[BUFFER_SIZE_SMALL]; +}; + +struct Process_List +{ + Process *items; + size_t count; + size_t capacity; +}; + +struct Partition +{ + u64 major; + u64 minor; + u64 blocks; + char name[BUFFER_SIZE_SMALL]; +}; + +struct Cpu +{ + char vendor[BUFFER_SIZE_DEFAULT]; + char model[BUFFER_SIZE_DEFAULT]; + char frequency[BUFFER_SIZE_SMALL]; + u64 total_time; + u64 idle_time; + u32 cores; +}; + +struct Ram +{ + u64 total; + u64 free; +}; + +struct DiskUsage +{ + u64 total; + u64 free; + u64 available; + u64 used; +}; + +struct Disk +{ + Partition *partitions; + + size_t part_count; + size_t part_capacity; + + struct DiskUsage disk_usage; +}; + +struct Device +{ + char os_version[BUFFER_SIZE_DEFAULT]; + char uptime[BUFFER_SIZE_DEFAULT]; + Process_List processes; +}; + +mem_arena * +arena_create(u64 capacity); +/** + * TODO(nasr): add error handling for both the destroy and the clear + * */ +void +arena_destroy(mem_arena *arena); +void +arena_clear(mem_arena *arena); + +Cpu * +cpu_create(mem_arena *arena); +Ram * +ram_create(mem_arena *arena); +Disk * +disk_create(mem_arena *arena); +Device * +device_create(mem_arena *arena); + +int +cpu_read(Cpu *cpu); +int +cpu_read_usage(Cpu *cpu); +int +ram_read(Ram *ram); +int +disk_read(Disk *disk, mem_arena *arena); +int +device_read(Device *device); + +// TODO(nasr): add a function that updates certain values incrementally +// instead of neading to update the entire cpu struct + +int +process_list_collect(Process_List *list, mem_arena *arena); + +int +process_read(i32 pid, Process *out); + +int +process_read2(i32 pid, Process *out); + +int +process_kill(i32 pid, i32 signal); + +#endif /* RESOURCES_H */ diff --git a/pkg/resources/resources_main.c b/pkg/resources/resources_main.c new file mode 100644 index 0000000..c59aca2 --- /dev/null +++ b/pkg/resources/resources_main.c @@ -0,0 +1,47 @@ +#include "base.h" +#include "resources.h" +#include "stdio.h" +#include "arena.h" +#include "arena.c" +#include "resources.c" +#include + +int +main(void) +{ + mem_arena *arena = arena_create(MiB(1)); + + Ram *ram = ram_create(arena); + Device *device = device_create(arena); + + ram_read(ram); + process_list_collect(&device->processes, arena); + + for (size_t proc_idx = 0; + proc_idx < device->processes.count; + ++proc_idx) + { + Process *proc = (Process *)arena_push(arena, + sizeof(Process), + 1); + + i32 error = process_read( + device->processes + .items[proc_idx] + .pid, + proc); + + printf( + "[total] total=%6s free=%3s\n", + ram->total, + ram->free + ); + + if (error != OK) + { + assert(0); + } + } + + return 0; +} diff --git a/proto/resources/v1/device_resources.proto b/proto/resources/v1/deviceresources.proto similarity index 98% rename from proto/resources/v1/device_resources.proto rename to proto/resources/v1/deviceresources.proto index 667c92a..63e6087 100644 --- a/proto/resources/v1/device_resources.proto +++ b/proto/resources/v1/deviceresources.proto @@ -92,6 +92,7 @@ enum ProcessState { PROCESS_STATE_TRACING_STOPPED = 5; PROCESS_STATE_ZOMBIE = 6; PROCESS_STATE_DEAD = 7; + PROCESS_STATE_IDLE = 8; } /** diff --git a/proto/resources/v2/deviceresources.proto b/proto/resources/v2/deviceresources.proto new file mode 100644 index 0000000..13d2712 --- /dev/null +++ b/proto/resources/v2/deviceresources.proto @@ -0,0 +1,124 @@ +syntax = "proto3"; + +package resources.v2; + +option go_package = "paradigm-ehb/agent/gen/resources/v2;resourcespb"; + +service ResourcesService { + rpc GetSystemResources(GetSystemResourcesRequest) + returns (GetSystemResourcesResponse); + rpc ProcessAction(ProcessActionRequest) + returns (ProcessActionReply); + +} + +message GetSystemResourcesRequest {} + +message GetSystemResourcesResponse { + SystemResources resources = 1; +} + +/** + * + * Get a complete system snapshots + * @return Cpu, Memory, Device, Disk ( partitions ) and processes + * + * */ +message SystemResources { + Cpu cpu = 1; + Memory memory = 2; + Device device = 3; + repeated Disk disks = 4; + repeated Process processes = 5; +} + +/** + * Cpu information + * + * @return vendor, model, frequency, cores + * */ +message Cpu { + string vendor = 1; + string model = 2; + string frequency = 3; + uint32 max_core = 4; + uint64 total_time = 5; + uint64 idle_time = 6; +} + +/** + * Memory + * @return total memory on system, available memory on system + * */ +message Memory { + uint64 total = 1; + uint64 free = 2; +} + +/** + * Device specific information + * @return os version ( distro information ), and uptime in string format + * + * */ + +message Device { + string os_version = 1; + string uptime = 2; +} + +/** + * + * Disk information + * @disk a list of all available partitions + * + * */ + +message Disk { + repeated DiskPartition partitions = 1; +} + +/** + * Partition + * @return partition name, major, minor, blocks + * */ +message DiskPartition { + string name = 1; + uint32 major = 2; + uint32 minor = 3; + uint64 blocks = 4; +} + +enum ProcessState { + PROCESS_STATE_UNSPECIFIED = 0; + PROCESS_STATE_RUNNING = 1; + PROCESS_STATE_SLEEPING = 2; + PROCESS_STATE_DISK_SLEEPING = 3; + PROCESS_STATE_STOPPED = 4; + PROCESS_STATE_TRACING_STOPPED = 5; + PROCESS_STATE_ZOMBIE = 6; + PROCESS_STATE_DEAD = 7; + PROCESS_STATE_IDLE = 8; + PROCESS_STATE_UNDEFINED = 9; +} + +/** + * Process information + * @return pid ( process identifier ), process name, the state, todo :) + * */ +message Process { + int32 pid = 1; + string name = 2; + ProcessState state = 3; + uint64 utime = 4; + uint32 num_threads = 5; +} + +message ProcessActionRequest { + int32 pid = 1; + int32 signal = 2; +} + +message ProcessActionReply { + + bool succes = 1; +} diff --git a/proto/services/v3/services.proto b/proto/services/v3/services.proto new file mode 100644 index 0000000..01e9755 --- /dev/null +++ b/proto/services/v3/services.proto @@ -0,0 +1,124 @@ +syntax = "proto3"; + +package services.v3; + +option go_package = "paradigm-ehb/agent/proto/services/v3"; + + +service HandlerService { + rpc PerformUnitAction (UnitActionRequest) returns (UnitActionReply); + rpc PerformUnitFileAction (UnitFileActionRequest) returns (UnitFileActionReply); + rpc GetAllUnits (GetUnitsRequest) returns (GetUnitsReply); + rpc GetLoadedUnits (GetUnitsRequest) returns (GetUnitsReply); + rpc GetFilteredUnits (GetUnitsFilteredRequest) returns (GetUnitsReply); + rpc GetUnitStatus (GetUnitStatusRequest) returns (GetUnitStatusReply); +} + + +message UnitActionRequest { + string unit_name = 1; + + enum UnitAction { + UNIT_ACTION_UNSPECIFIED = 0; + UNIT_ACTION_START = 1; + UNIT_ACTION_STOP = 2; + UNIT_ACTION_RESTART = 3; + } + + UnitAction action = 2; + bool force = 3; +} + +message UnitActionReply { + bytes status = 1; + bool success = 2; + string error_message = 3; +} + + +message UnitFileActionRequest { + string unit_name = 1; + + enum UnitFileAction { + UNIT_FILE_ACTION_UNSPECIFIED = 0; + UNIT_FILE_ACTION_ENABLE = 1; + UNIT_FILE_ACTION_DISABLE = 2; + } + + UnitFileAction action = 2; + bool runtime = 3; + bool force = 4; +} + +message UnitFileActionReply { + bytes status = 1; + bool success = 2; + string error_message = 3; +} + + +message LoadedUnit { + string name = 1; + string description = 2; + string load_state = 3; + string sub_state = 4; + string active_state = 5; + string dep_unit = 6; + + string object_path = 7; + + uint32 queued_job = 8; + string job_type = 9; + string job_path = 10; +} + +message Unit { + string name = 1; + string state = 2; +} + + +message GetUnitsRequest { + + enum UnitState { + UNIT_STATE_UNSPECIFIED = 0; + ENABLED = 1; + DISABLED = 2; + } + + string name = 1; + UnitState state = 2; +} + +message GetUnitsFilteredRequest { + + enum UnitFileState { + + UNIT_FILE_STATE_UNSPECIFIED = 0; + LOADED = 1; + NOT_FOUND = 2; + BAD_SETTING = 3; + ERROR = 4; + MASKED = 5; + } + + repeated UnitFileState filters = 1; + +} + +message GetUnitsReply { + repeated LoadedUnit units = 1; + bool success = 2; + string error_message = 3; +} + + +message GetUnitStatusRequest { + string unit_name = 1; +} + +message GetUnitStatusReply { + string state = 1; + bool success = 2; + string error_message = 3; +} diff --git a/test/grpc_resources_test.sh b/test/grpc_resources_test.sh new file mode 100755 index 0000000..4e7b93b --- /dev/null +++ b/test/grpc_resources_test.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh +set -eu + +ADDR="${ADDR:-localhost:5000}" + +echo "== ResourcesService integration test ==" +echo "Target: $ADDR" +echo + +command -v grpcurl >/dev/null 2>&1 || { + echo "ERROR: grpcurl not found" + exit 1 +} + +echo "== Checking service availability ==" +grpcurl -plaintext "$ADDR" list resources.v2.ResourcesService >/dev/null +echo "OK" +echo + +echo "== Calling GetSystemResources ==" +grpcurl -plaintext \ + -format text \ + "$ADDR" \ + resources.v2.ResourcesService/GetSystemResources +echo + +echo "== Test completed ==" diff --git a/test/grpc_resources_v2_test.sh b/test/grpc_resources_v2_test.sh new file mode 100755 index 0000000..b372ae6 --- /dev/null +++ b/test/grpc_resources_v2_test.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# gRPCurl test script for ResourcesServiceV2 +# Make sure your gRPC server is running before executing these commands + +# Color codes for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default server address +SERVER="localhost:5000" + +# Check if custom server address provided +if [ ! -z "$1" ]; then + SERVER="$1" +fi + +echo -e "${BLUE}Testing ResourcesServiceV2 on ${SERVER}${NC}\n" + +# List available services +echo -e "${GREEN}=== Listing available services ===${NC}" +grpcurl -plaintext ${SERVER} list +echo "" + +# List methods for ResourcesServiceV2 +echo -e "${GREEN}=== Listing methods for resources.v2.ResourcesService ===${NC}" +grpcurl -plaintext ${SERVER} list resources.v2.ResourcesService +echo "" + +# Describe the service +echo -e "${GREEN}=== Describing ResourcesService ===${NC}" +grpcurl -plaintext ${SERVER} describe resources.v2.ResourcesService +echo "" + +# Test GetSystemResources +echo -e "${GREEN}=== Getting System Resources ===${NC}" +grpcurl -plaintext \ + -d '{}' \ + ${SERVER} \ + resources.v2.ResourcesService/GetSystemResources +echo "" + +# Test KillProcess (example with PID 12345) +# WARNING: Uncomment and modify PID carefully - this will kill a process! +# echo -e "${GREEN}=== Killing Process (PID: 12345) ===${NC}" +# grpcurl -plaintext \ +# -d '{"pid": 12345}' \ +# ${SERVER} \ +# resources.v2.ResourcesService/KillProcess +# echo "" + +# Get system resources with formatted output (using jq if available) +echo -e "${GREEN}=== Getting System Resources (formatted) ===${NC}" +if command -v jq &> /dev/null; then + grpcurl -plaintext \ + -d '{}' \ + ${SERVER} \ + resources.v2.ResourcesService/GetSystemResources | jq '.' +else + echo "jq not installed - skipping formatted output" + echo "Install jq for pretty JSON: sudo apt-get install jq" +fi +echo "" + +# Show CPU information only +echo -e "${GREEN}=== CPU Information ===${NC}" +if command -v jq &> /dev/null; then + grpcurl -plaintext \ + -d '{}' \ + ${SERVER} \ + resources.v2.ResourcesService/GetSystemResources | jq '.resources.cpu' +else + echo "jq not installed - install it to filter output" +fi +echo "" + +# Show Memory information only +echo -e "${GREEN}=== Memory Information ===${NC}" +if command -v jq &> /dev/null; then + grpcurl -plaintext \ + -d '{}' \ + ${SERVER} \ + resources.v2.ResourcesService/GetSystemResources | jq '.resources.memory' +else + echo "jq not installed - install it to filter output" +fi +echo "" + +# Show first 5 processes +echo -e "${GREEN}=== First 5 Processes ===${NC}" +if command -v jq &> /dev/null; then + grpcurl -plaintext \ + -d '{}' \ + ${SERVER} \ + resources.v2.ResourcesService/GetSystemResources | jq '.resources.processes[:5]' +else + echo "jq not installed - install it to filter output" +fi +echo "" + +# Count total processes +echo -e "${GREEN}=== Total Process Count ===${NC}" +if command -v jq &> /dev/null; then + grpcurl -plaintext \ + -d '{}' \ + ${SERVER} \ + resources.v2.ResourcesService/GetSystemResources | jq '.resources.processes | length' +else + echo "jq not installed - install it to filter output" +fi +echo "" + +echo -e "${BLUE}Tests completed!${NC}" diff --git a/test/grpc_services_test.sh b/test/grpc_services_test.sh new file mode 100755 index 0000000..c2c13d3 --- /dev/null +++ b/test/grpc_services_test.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env sh +set -euo pipefail + +HOST="${HOST:-localhost}" +PORT="${PORT:-5000}" +ADDR="${HOST}:${PORT}" +SERVICE="services.v2.HandlerService" + +echo "== gRPC integration test ==" +echo "Target: ${ADDR}" +echo + +command -v grpcurl >/dev/null 2>&1 || { + echo "ERROR: grpcurl not found" + exit 1 +} + +echo "== Checking server availability ==" +grpcurl -plaintext "${ADDR}" list >/dev/null +echo "OK" +echo + +echo "== Listing all services ==" +grpcurl -plaintext "${ADDR}" list +echo + +echo "== Checking ${SERVICE} existence ==" +grpcurl -plaintext "${ADDR}" list "${SERVICE}" >/dev/null +echo "OK" +echo + + +echo "== Listing ${SERVICE} methods ==" +grpcurl -plaintext "${ADDR}" list "${SERVICE}" +echo + +echo "== GetAllUnits ==" +grpcurl -plaintext \ + -d '{}' \ + "${ADDR}" \ + "${SERVICE}/GetAllUnits" +echo + +echo "== GetLoadedUnits ==" +grpcurl -plaintext \ + -d '{}' \ + "${ADDR}" \ + "${SERVICE}/GetLoadedUnits" +echo + + echo "== GetUnitStatus (tailscaled.service) ==" + grpcurl -plaintext \ + -d '{ + "unitName": "tailscaled.service" + }' \ + "${ADDR}" \ + "${SERVICE}/GetUnitStatus" + echo + + echo "== PerformUnitAction: START tailscaled.service ==" + grpcurl -plaintext \ + -d '{ + "unitName": "tailscaled.service", + "action": "UNIT_ACTION_START" + }' \ + "${ADDR}" \ + "${SERVICE}/PerformUnitAction" + echo + + echo "== PerformUnitFileAction: ENABLE tailscaled.service (runtime=true, force=true) ==" + grpcurl -plaintext \ + -d '{ + "unitName": "tailscaled.service", + "action": "UNIT_FILE_ACTION_ENABLE", + "runtime": true, + "force": true + }' \ + "${ADDR}" \ + "${SERVICE}/PerformUnitFileAction" + echo + + echo "== PerformUnitAction: STOP tailscaled.service ==" + grpcurl -plaintext \ + -d '{ + "unitName": "tailscaled.service", + "action": "UNIT_ACTION_STOP", + "force": true + }' \ + "${ADDR}" \ + "${SERVICE}/PerformUnitAction" + echo + + + echo "== PerformUnitFileAction: DISABLE tailscaled.service ==" + grpcurl -plaintext \ + -d '{ + "unitName": "tailscaled.service", + "action": "UNIT_FILE_ACTION_DISABLE", + "runtime": true, + "force": true + }' \ + "${ADDR}" \ + "${SERVICE}/PerformUnitFileAction" + echo + +echo "== All gRPC v2 tests passed ==" diff --git a/test/grpc_test_script.sh b/test/grpc_services_v3_test.sh similarity index 85% rename from test/grpc_test_script.sh rename to test/grpc_services_v3_test.sh index 07f19b8..eab5361 100755 --- a/test/grpc_test_script.sh +++ b/test/grpc_services_v3_test.sh @@ -4,10 +4,9 @@ set -euo pipefail HOST="${HOST:-localhost}" PORT="${PORT:-5000}" ADDR="${HOST}:${PORT}" +SERVICE="services.v3.HandlerService" -SERVICE="services.v2.HandlerService" - -echo "== gRPC integration test ==" +echo "== gRPC v3 integration test ==" echo "Target: ${ADDR}" echo @@ -48,6 +47,17 @@ grpcurl -plaintext \ "${SERVICE}/GetLoadedUnits" echo +echo "== GetFilteredUnits (loaded, masked) ==" + +grpcurl -plaintext \ + -d '{ + "filters": ["LOADED", "MASKED"] + }' \ + localhost:5000 \ + services.v3.HandlerService/GetFilteredUnits + +echo + echo "== GetUnitStatus (tailscaled.service) ==" grpcurl -plaintext \ -d '{ @@ -83,14 +93,12 @@ echo "== PerformUnitAction: STOP tailscaled.service ==" grpcurl -plaintext \ -d '{ "unitName": "tailscaled.service", - "action": "UNIT_ACTION_STOP", - "force": true + "action": "UNIT_ACTION_STOP" }' \ "${ADDR}" \ "${SERVICE}/PerformUnitAction" echo - echo "== PerformUnitFileAction: DISABLE tailscaled.service ==" grpcurl -plaintext \ -d '{ @@ -103,4 +111,4 @@ grpcurl -plaintext \ "${SERVICE}/PerformUnitFileAction" echo -echo "== All gRPC v2 tests passed ==" +echo "== All gRPC v3 tests passed ==" diff --git a/test/setup_test.go b/test/integration_setup_test.go similarity index 62% rename from test/setup_test.go rename to test/integration_setup_test.go index e14c012..2b90e4d 100644 --- a/test/setup_test.go +++ b/test/integration_setup_test.go @@ -1,17 +1,23 @@ package server_test +/** + +TODO(nasr): update tests for the new grpc handler services version +*/ import ( "context" "net" "google.golang.org/grpc" "google.golang.org/grpc/test/bufconn" - "paradigm-ehb/agent/pkg/grpc_handler" - respb "paradigm-ehb/agent/gen/resources/v1" + "paradigm-ehb/agent/pkg/grpchandler" + servicesHandlerV1 "paradigm-ehb/agent/pkg/grpchandler/services/v1" + servicesHandlerV2 "paradigm-ehb/agent/pkg/grpchandler/services/v2" + // respb "paradigm-ehb/agent/gen/resources/v1" + serpb_v1 "paradigm-ehb/agent/gen/services/v1" serpb_v2 "paradigm-ehb/agent/gen/services/v2" - greetpb "paradigm-ehb/agent/gen/greet" journalpb "paradigm-ehb/agent/gen/journal/v1" @@ -37,9 +43,9 @@ func init() { grpc_health_v1.RegisterHealthServer(server, healthServer) - respb.RegisterResourcesServiceServer(server, &grpc_handler.ResourcesService{}) - serpb_v1.RegisterHandlerServiceServer(server, &grpc_handler.HandlerService{}) - serpb_v2.RegisterHandlerServiceServer(server, &grpc_handler.HandlerServiceV2{}) + // respb.RegisterResourcesServiceServer(server, &grpc_handler.ResourcesService{}) + serpb_v1.RegisterHandlerServiceServer(server, &servicesHandlerV1.HandlerService{}) + serpb_v2.RegisterHandlerServiceServer(server, &servicesHandlerV2.HandlerServiceV2{}) greetpb.RegisterGreeterServer(server, &grpc_handler.GreeterServer{}) journalpb.RegisterJournalServiceServer(server, &grpc_handler.JournalService{}) diff --git a/test/action_test.go b/test/journal_action_test.go similarity index 99% rename from test/action_test.go rename to test/journal_action_test.go index 635c423..302b846 100644 --- a/test/action_test.go +++ b/test/journal_action_test.go @@ -1,5 +1,6 @@ package server_test +/* import ( "context" "testing" @@ -13,9 +14,8 @@ import ( "google.golang.org/grpc/credentials/insecure" ) -/** TODO(nasr): write test for reboot and shutdown, what would be a proper way of doing this -*/ + func TestActions_all(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() @@ -45,3 +45,4 @@ func TestActions_all(t *testing.T) { t.Fatalf("unexpected response: %q", resp) } } +*/ diff --git a/test/journal_service_test.go b/test/journal_service_test.go new file mode 100644 index 0000000..3dcd0c8 --- /dev/null +++ b/test/journal_service_test.go @@ -0,0 +1,44 @@ +package server_test +// +// import ( +// "context" +// "testing" +// "time" +// +// pb "paradigm-ehb/agent/gen/journal/v1" +// +// "google.golang.org/grpc/resolver" +// +// "google.golang.org/grpc" +// "google.golang.org/grpc/credentials/insecure" +// ) +// +// +// func TestJournal_all(t *testing.T) { +// ctx, cancel := context.WithTimeout(context.Background(), time.Second) +// defer cancel() +// +// resolver.SetDefaultScheme("passthrough") +// +// clientConn, err := grpc.NewClient( +// "bufnet", +// grpc.WithContextDialer(BufDialer), +// grpc.WithTransportCredentials(insecure.NewCredentials()), +// ) +// +// if err != nil { +// t.Fatalf("failed to create client: %v", err) +// } +// defer clientConn.Close() +// +// client := pb.NewJournalServiceClient(clientConn) +// +// resp, err := client.Action(ctx, &pb.JournalRequest{}) +// if err != nil { +// t.Fatalf("rpc failed: %v", err) +// } +// +// if resp == nil { +// t.Fatalf("unexpected response: %q", resp) +// } +// } diff --git a/test/journal_test.go b/test/journal_test.go deleted file mode 100644 index 5db71ba..0000000 --- a/test/journal_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package server_test - -import ( - "context" - "testing" - "time" - - pb "paradigm-ehb/agent/gen/journal/v1" - - "google.golang.org/grpc/resolver" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - - -func TestJournal_all(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - resolver.SetDefaultScheme("passthrough") - - clientConn, err := grpc.NewClient( - "bufnet", - grpc.WithContextDialer(BufDialer), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - - if err != nil { - t.Fatalf("failed to create client: %v", err) - } - defer clientConn.Close() - - client := pb.NewJournalServiceClient(clientConn) - - resp, err := client.Action(ctx, &pb.JournalRequest{}) - if err != nil { - t.Fatalf("rpc failed: %v", err) - } - - if resp == nil { - t.Fatalf("unexpected response: %q", resp) - } -} diff --git a/test/process_action.sh b/test/process_action.sh new file mode 100755 index 0000000..569d500 --- /dev/null +++ b/test/process_action.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +set -eu + +grpcurl -plaintext \ + -d '{ + "pid": 2497, + "signal": 15 + }' \ + localhost:5000 \ + resources.v2.ResourcesService/ProcessAction diff --git a/test/resources_service_test.go b/test/resources_service_test.go new file mode 100644 index 0000000..6bf5cf3 --- /dev/null +++ b/test/resources_service_test.go @@ -0,0 +1,44 @@ +package server_test +// +// import ( +// "context" +// "testing" +// "time" +// +// pb "paradigm-ehb/agent/gen/resources/v1" +// +// "google.golang.org/grpc/resolver" +// +// "google.golang.org/grpc" +// "google.golang.org/grpc/credentials/insecure" +// ) +// +// +// func TestResources_All(t *testing.T) { +// ctx, cancel := context.WithTimeout(context.Background(), time.Second) +// defer cancel() +// +// resolver.SetDefaultScheme("passthrough") +// +// clientConn, err := grpc.NewClient( +// "bufnet", +// grpc.WithContextDialer(BufDialer), +// grpc.WithTransportCredentials(insecure.NewCredentials()), +// ) +// +// if err != nil { +// t.Fatalf("failed to create client: %v", err) +// } +// defer clientConn.Close() +// +// client := pb.NewResourcesServiceClient(clientConn) +// +// resp, err := client.GetSystemResources(ctx, &pb.GetSystemResourcesRequest{}) +// if err != nil { +// t.Fatalf("rpc failed: %v", err) +// } +// +// if resp == nil { +// t.Fatalf("unexpected response: %q", resp) +// } +// } diff --git a/test/resources_test.go b/test/resources_test.go deleted file mode 100644 index 9fc92fe..0000000 --- a/test/resources_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package server_test - -import ( - "context" - "testing" - "time" - - pb "paradigm-ehb/agent/gen/resources/v1" - - "google.golang.org/grpc/resolver" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - - -func TestResources_All(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - resolver.SetDefaultScheme("passthrough") - - clientConn, err := grpc.NewClient( - "bufnet", - grpc.WithContextDialer(BufDialer), - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - - if err != nil { - t.Fatalf("failed to create client: %v", err) - } - defer clientConn.Close() - - client := pb.NewResourcesServiceClient(clientConn) - - resp, err := client.GetSystemResources(ctx, &pb.GetSystemResourcesRequest{}) - if err != nil { - t.Fatalf("rpc failed: %v", err) - } - - if resp == nil { - t.Fatalf("unexpected response: %q", resp) - } -} diff --git a/test/run_all_tests.sh b/test/run_all_tests.sh new file mode 100755 index 0000000..d35c7db --- /dev/null +++ b/test/run_all_tests.sh @@ -0,0 +1,253 @@ +#!/bin/bash +# gRPCurl test commands for all services +# Make sure your gRPC server is running on localhost:5000 (adjust port as needed) +# Install grpcurl: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest + +SERVER="localhost:5000" + +echo "======================================" +echo "ResourcesServiceV2 Tests" +echo "======================================" + +# Get System Resources +echo -e "\n[1] GetSystemResources" +grpcurl -plaintext \ + -d '{}' \ + $SERVER \ + resources.v2.ResourcesService/GetSystemResources + +# Kill Process (replace with actual PID) +echo -e "\n[2] ProcessAction (SIGTERM - signal 15)" +grpcurl -plaintext \ + -d '{ + "pid": 12345, + "signal": 15 + }' \ + $SERVER \ + resources.v2.ResourcesService/ProcessAction + +echo -e "\n[3] ProcessAction (SIGKILL - signal 9)" +grpcurl -plaintext \ + -d '{ + "pid": 12345, + "signal": 9 + }' \ + $SERVER \ + resources.v2.ResourcesService/ProcessAction + +echo "======================================" +echo "HandlerServicev3 Tests (Systemd)" +echo "======================================" + +# Get All Units +echo -e "\n[4] GetAllUnits" +grpcurl -plaintext \ + -d '{}' \ + $SERVER \ + services.v3.HandlerService/GetAllUnits + +# Get Loaded Units +echo -e "\n[5] GetLoadedUnits" +grpcurl -plaintext \ + -d '{}' \ + $SERVER \ + services.v3.HandlerService/GetLoadedUnits + +# Get Filtered Units +echo -e "\n[6] GetFilteredUnits - LOADED" +grpcurl -plaintext \ + -d '{ + "filters": ["LOADED"] + }' \ + $SERVER \ + services.v3.HandlerService/GetFilteredUnits + +echo -e "\n[7] GetFilteredUnits - ERROR and MASKED" +grpcurl -plaintext \ + -d '{ + "filters": ["ERROR", "MASKED"] + }' \ + $SERVER \ + services.v3.HandlerService/GetFilteredUnits + +# Get Unit Status +echo -e "\n[8] GetUnitStatus - nginx.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "nginx.service" + }' \ + $SERVER \ + services.v3.HandlerService/GetUnitStatus + +echo -e "\n[9] GetUnitStatus - sshd.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "sshd.service" + }' \ + $SERVER \ + services.v3.HandlerService/GetUnitStatus + +# Perform Unit Action - Start +echo -e "\n[10] PerformUnitAction - START nginx.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "nginx.service", + "action": "UNIT_ACTION_START" + }' \ + $SERVER \ + services.v3.HandlerService/PerformUnitAction + +# Perform Unit Action - Stop +echo -e "\n[11] PerformUnitAction - STOP nginx.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "nginx.service", + "action": "UNIT_ACTION_STOP" + }' \ + $SERVER \ + services.v3.HandlerService/PerformUnitAction + +# Perform Unit Action - Restart +echo -e "\n[12] PerformUnitAction - RESTART nginx.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "nginx.service", + "action": "UNIT_ACTION_RESTART" + }' \ + $SERVER \ + services.v3.HandlerService/PerformUnitAction + +# Perform Unit File Action - Enable +echo -e "\n[13] PerformUnitFileAction - ENABLE nginx.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "nginx.service", + "action": "UNIT_FILE_ACTION_ENABLE", + "runtime": false, + "force": false + }' \ + $SERVER \ + services.v3.HandlerService/PerformUnitFileAction + +# Perform Unit File Action - Disable +echo -e "\n[14] PerformUnitFileAction - DISABLE nginx.service" +grpcurl -plaintext \ + -d '{ + "unit_name": "nginx.service", + "action": "UNIT_FILE_ACTION_DISABLE", + "runtime": false, + "force": true + }' \ + $SERVER \ + services.v3.HandlerService/PerformUnitFileAction + +echo "======================================" +echo "DeviceActionsService Tests" +echo "======================================" + +# Shutdown (WARNING: Will shutdown the system!) +echo -e "\n[15] Action - SHUTDOWN (commented out for safety)" +# grpcurl -plaintext \ +# -d '{ +# "device_action": "DEVICE_ACTION_SHUTDOWN" +# }' \ +# $SERVER \ +# actions.v1.ActionService/Action + +# Reboot (WARNING: Will reboot the system!) +echo -e "\n[16] Action - REBOOT (commented out for safety)" +# grpcurl -plaintext \ +# -d '{ +# "device_action": "DEVICE_ACTION_REBOOT" +# }' \ +# $SERVER \ +# actions.v1.ActionService/Action + +# Suspend +echo -e "\n[17] Action - SUSPEND" +grpcurl -plaintext \ + -d '{ + "device_action": "DEVICE_ACTION_SUSPEND" + }' \ + $SERVER \ + actions.v1.ActionService/Action + +# Hibernate +echo -e "\n[18] Action - HIBERNATE" +grpcurl -plaintext \ + -d '{ + "device_action": "DEVICE_ACTION_HIBERNATE" + }' \ + $SERVER \ + actions.v1.ActionService/Action + +echo "======================================" +echo "JournalService Tests (Server Streaming)" +echo "======================================" + +# Journal by Systemd Unit +echo -e "\n[19] Action - Get journal for nginx.service" +grpcurl -plaintext \ + -d '{ + "field": 0, + "value": "nginx.service", + "numFromTail": 100, + "cursor": "", + "path": "" + }' \ + $SERVER \ + journal.v1.JournalService/Action + +# Journal by PID +echo -e "\n[20] Action - Get journal for PID 1" +grpcurl -plaintext \ + -d '{ + "field": 1, + "value": "1", + "cursor": "", + "path": "" + }' \ + $SERVER \ + journal.v1.JournalService/Action + +# Journal by UID +echo -e "\n[21] Action - Get journal for UID 0 (root)" +grpcurl -plaintext \ + -d '{ + "field": 2, + "value": "0", + "numFromTail": 100, + "cursor": "", + "path": "" + }' \ + $SERVER \ + journal.v1.JournalService/Action + +# Journal by GID +echo -e "\n[22] Action - Get journal for GID 0 (root)" +grpcurl -plaintext \ + -d '{ + "field": 3, + "value": "0", + "numFromTail": 100, + "cursor": "", + "path": "" + }' \ + $SERVER \ + journal.v1.JournalService/Action + +echo -e "\n======================================" +echo "List Available Services" +echo "======================================" + +grpcurl -plaintext $SERVER list + +echo -e "\n======================================" +echo "Describe a Service" +echo "======================================" + +grpcurl -plaintext $SERVER describe resources.v2.ResourcesService + +echo -e "\n======================================" +echo "Tests Complete!" +echo "======================================" diff --git a/test/services_v1_test.go b/test/services_v1_integration_test.go similarity index 99% rename from test/services_v1_test.go rename to test/services_v1_integration_test.go index 69dc9c8..844c030 100644 --- a/test/services_v1_test.go +++ b/test/services_v1_integration_test.go @@ -1,5 +1,6 @@ package server_test +/* import ( "context" "testing" @@ -49,3 +50,4 @@ func TestService_Test(t *testing.T) { t.Fatalf("unexpected response: %q", resp) } } +*/ diff --git a/test/services_v2_test.go b/test/services_v2_integration_test.go similarity index 100% rename from test/services_v2_test.go rename to test/services_v2_integration_test.go