From 5822cfb27906b8cf92bb2b78c2ff6c9c0711587d Mon Sep 17 00:00:00 2001 From: Dokotela Date: Thu, 21 Nov 2024 12:52:12 -0500 Subject: [PATCH] Cleaning updates --- build/build_ios.sh | 31 ++-- go.mod | 1 - go.sum | 2 - main.go | 17 +- pocketfhir/caddy.go | 86 +++++----- pocketfhir/hooks.go | 51 ++++-- pocketfhir/initial_collections.go | 20 --- pocketfhir/load_spec.go | 253 ------------------------------ pocketfhir/server.go | 149 +++++++++++------- pocketfhir/start.go | 22 ++- 10 files changed, 219 insertions(+), 413 deletions(-) delete mode 100644 pocketfhir/load_spec.go diff --git a/build/build_ios.sh b/build/build_ios.sh index 36ce9d3..1e87beb 100755 --- a/build/build_ios.sh +++ b/build/build_ios.sh @@ -1,25 +1,24 @@ #!/bin/bash -# Navigate to the root directory of the project -cd "$(dirname "$0")/.." - -# Clean up previous builds -rm -f pocketfhir_ios - -# Initialize Go modules in the root directory if go.mod does not exist +# Ensure Go modules are initialized if [ ! -f go.mod ]; then go mod init pocketfhir fi -# Tidy up Go modules in the root directory -go mod tidy +# Install necessary tools +go get -u golang.org/x/mobile/bind +go install golang.org/x/mobile/cmd/gomobile@latest +gomobile init -# Build the PocketFHIR server for iOS (arm64 architecture) -echo "Building PocketFHIR for iOS..." -GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o pocketfhir_ios ./src -echo "iOS PocketFHIR build completed." +# Build PocketFHIR .framework for iOS +echo "Building PocketFHIR .framework for iOS..." +gomobile bind -target=ios -o pocketfhir.framework ./pocketfhir +if [ $? -ne 0 ]; then + echo "iOS build failed!" + exit 1 +fi -# Start the PocketFHIR server in the background (for the regular platform) -./pocketfhir serve & +# Move the framework to the appropriate iOS directory +mv pocketfhir.framework ../fhir_ant/ios/libs/pocketfhir.framework -echo "PocketFHIR server started." +echo "PocketFHIR iOS build completed successfully." diff --git a/go.mod b/go.mod index 1b59c5a..ecdb90b 100644 --- a/go.mod +++ b/go.mod @@ -190,7 +190,6 @@ require ( golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/image v0.22.0 // indirect - golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f // indirect golang.org/x/mod v0.22.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.9.0 // indirect diff --git a/go.sum b/go.sum index e662a81..e10451e 100644 --- a/go.sum +++ b/go.sum @@ -1022,8 +1022,6 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f h1:23H/YlmTHfmmvpZ+ajKZL0qLz0+IwFOIqQA0mQbmLeM= -golang.org/x/mobile v0.0.0-20241108191957-fa514ef75a0f/go.mod h1:UbSUP4uu/C9hw9R2CkojhXlAxvayHjBdU9aRvE+c1To= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/main.go b/main.go index 1c3bb2b..65ef1e0 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,16 @@ package main import "github.com/fhir-fli/pocketfhir/pocketfhir" func main() { - // Use the string wrapper for local development - dataDir := "./assets" - ipAddress := "127.0.0.1" - port := "8080" // Changed from 443 to 8080 to avoid permission issues - enableApiLogs := false + // Configuration for local development + dataDir := "./assets" // The directory to use for PocketBase data + pbIpAddress := "127.0.0.1" // PocketBase IP address for local development + caddyIpAddress := "127.0.0.1" // Caddy server loopback IP address to prevent conflict + pbPort := "8090" // PocketBase port set to 8080 + httpPort := "8081" // Caddy HTTP traffic port + httpsPort := "8443" // Caddy HTTPS traffic port + enableApiLogs := true // Enable API logs for detailed local debugging + storagePath := "./storage" // Local storage path for Caddy - pocketfhir.RunServer(dataDir, ipAddress, port, enableApiLogs) + // Start PocketFHIR server with local development configuration + pocketfhir.StartPocketFHIR(pbPort, httpPort, httpsPort, pbIpAddress, caddyIpAddress, dataDir, enableApiLogs, storagePath) } diff --git a/pocketfhir/caddy.go b/pocketfhir/caddy.go index f081e6d..2afba1c 100644 --- a/pocketfhir/caddy.go +++ b/pocketfhir/caddy.go @@ -10,70 +10,58 @@ import ( _ "github.com/caddyserver/caddy/v2/modules/standard" ) +// CaddyConfig is a struct that contains the configuration parameters for starting the Caddy server. +type CaddyConfig struct { + PbPort string + HttpPort string + HttpsPort string + PbUrl string + StoragePath string + IpAddress string +} + // StartCaddy starts the Caddy server with the provided configuration. -func StartCaddy(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress string) { +func StartCaddy(config CaddyConfig) { // Change working directory - if err := os.Chdir(storagePath); err != nil { - log.Fatalf("Failed to change working directory to %s: %v", storagePath, err) + if err := os.Chdir(config.StoragePath); err != nil { + log.Fatalf("Failed to change working directory to %s: %v", config.StoragePath, err) } - // Log configuration for transparency - log.Printf("Starting Caddy server with the following configuration:") - log.Printf("PocketBase Port: %s", pbPort) - log.Printf("HTTP Port: %s", httpPort) - log.Printf("HTTPS Port: %s", httpsPort) - log.Printf("Upstream URL: %s", pbUrl) - log.Printf("Storage Path: %s", storagePath) + // Log consolidated configuration details + log.Printf("Starting Caddy server with configuration: %+v", config) // Generate Caddy config - cfg := createConfig(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress) + caddyCfg := createConfig(config) // Serialize for debugging - configJSON, err := json.MarshalIndent(cfg, "", " ") + configJSON, err := json.MarshalIndent(caddyCfg, "", " ") if err != nil { log.Fatalf("Failed to serialize Caddy config: %v", err) } log.Printf("Generated Caddy config: %s", string(configJSON)) - // Initialize Caddy + // Initialize and run Caddy log.Println("Initializing Caddy...") - if err := caddy.Run(cfg); err != nil { + if err := caddy.Run(caddyCfg); err != nil { log.Fatalf("Error running Caddy: %v", err) } log.Println("Caddy server started successfully.") } -// CreateConfig generates a basic Caddy configuration to run a reverse proxy with HTTP and a static file server. -func createConfig(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress string) *caddy.Config { +// createConfig generates a basic Caddy configuration to run a reverse proxy with HTTP and a static file server. +func createConfig(config CaddyConfig) *caddy.Config { // Define paths for certs and proxy logs - pbProxyPath := fmt.Sprintf("%s/proxy_access.log", storagePath) - - // Generate the JSON configuration - jsonConfig := jsonConfig(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress, pbProxyPath) - log.Printf("Generated JSON Configuration: %s", jsonConfig) - - // Parse the JSON into a caddy.Config struct - var caddyConfig caddy.Config - err := json.Unmarshal([]byte(jsonConfig), &caddyConfig) - if err != nil { - log.Fatalf("Failed to parse JSON configuration: %v", err) - } + pbProxyPath := fmt.Sprintf("%s/proxy_access.log", config.StoragePath) - log.Printf("Generated Caddy Configuration: %s", jsonConfig) - - return &caddyConfig -} - -func jsonConfig(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress, pbProxyPath string) string { - // Construct each part individually and combine them + // Generate the full configuration using helpers loggingConfig := generateLoggingConfig(pbProxyPath) - storageConfig := generateStorageConfig(storagePath) - httpAppConfig := generateHttpAppConfig(pbPort, httpPort, httpsPort, pbUrl, ipAddress) - tlsConfig := generateTlsConfig(ipAddress) + storageConfig := generateStorageConfig(config.StoragePath) + httpAppConfig := generateHttpAppConfig(config) + tlsConfig := generateTlsConfig(config.IpAddress) - // Combine the parts into the final JSON configuration - return fmt.Sprintf(`{ + // Combine all configurations into the final JSON + combinedConfig := fmt.Sprintf(`{ %s, %s, "apps": { @@ -81,6 +69,14 @@ func jsonConfig(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress, pbPr %s } }`, loggingConfig, storageConfig, httpAppConfig, tlsConfig) + + var caddyConfig caddy.Config + err := json.Unmarshal([]byte(combinedConfig), &caddyConfig) + if err != nil { + log.Fatalf("Failed to parse JSON configuration: %v", err) + } + + return &caddyConfig } // Generates the logging configuration section @@ -112,18 +108,18 @@ func generateStorageConfig(storagePath string) string { } // Generates the HTTP application configuration section -func generateHttpAppConfig(pbPort, httpPort, httpsPort, pbUrl, ipAddress string) string { - httpsServerConfig := generateHttpsServerConfig(httpsPort, ipAddress, pbUrl, pbPort) +func generateHttpAppConfig(config CaddyConfig) string { + httpsServerConfig := generateHttpsServerConfig(config) return fmt.Sprintf(`"http": { "http_port": %s, "servers": { %s } - }`, httpPort, httpsServerConfig) + }`, config.HttpPort, httpsServerConfig) } // Generates the HTTP server configuration for reverse proxy with health checks -func generateHttpsServerConfig(httpsPort, ipAddress, pbUrl, pbPort string) string { +func generateHttpsServerConfig(config CaddyConfig) string { return fmt.Sprintf(`"srv_https": { "listen": [":%s"], "routes": [ @@ -152,7 +148,7 @@ func generateHttpsServerConfig(httpsPort, ipAddress, pbUrl, pbPort string) strin ] } ] - }`, httpsPort, ipAddress, pbUrl, pbPort) + }`, config.HttpsPort, config.IpAddress, config.PbUrl, config.PbPort) } // Generates the TLS automation configuration diff --git a/pocketfhir/hooks.go b/pocketfhir/hooks.go index 19d7f4b..64142f4 100644 --- a/pocketfhir/hooks.go +++ b/pocketfhir/hooks.go @@ -3,6 +3,7 @@ package pocketfhir import ( "log" "sync" + "time" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" @@ -34,7 +35,7 @@ func registerRequestLoggingHook(app *pocketbase.PocketBase) { app.OnBeforeServe().Add(func(e *core.ServeEvent) error { e.Router.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { - log.Printf("Request: %s %s", c.Request().Method, c.Request().URL.Path) + log.Printf("[INFO] Request: %s %s", c.Request().Method, c.Request().URL.Path) return next(c) } }) @@ -82,13 +83,31 @@ func registerWebSocketEndpoint(app *pocketbase.PocketBase) { clientsMu.Lock() delete(clients, ws) clientsMu.Unlock() - ws.Close() + _ = ws.Close() + log.Println("[INFO] WebSocket connection closed") }() - // Keep the connection alive + log.Println("[INFO] New WebSocket connection established") + + // Heartbeat mechanism to keep the connection alive + heartbeatTicker := time.NewTicker(30 * time.Second) + defer heartbeatTicker.Stop() + for { - if _, err := ws.Read(nil); err != nil { - break + select { + case <-heartbeatTicker.C: + if err := ws.SetDeadline(time.Now().Add(35 * time.Second)); err != nil { + log.Printf("[ERROR] Failed to set deadline for WebSocket: %v", err) + return + } + if _, err := ws.Write([]byte("ping")); err != nil { + log.Printf("[ERROR] Failed to send heartbeat: %v", err) + return + } + case <-c.Request().Context().Done(): + // Handle client disconnection gracefully + log.Println("[INFO] WebSocket client disconnected") + return } } }).ServeHTTP(c.Response(), c.Request()) @@ -101,7 +120,11 @@ func registerWebSocketEndpoint(app *pocketbase.PocketBase) { // Broadcast a message to all connected WebSocket clients func broadcastWebSocketMessage(eventType string, record *models.Record) { clientsMu.Lock() - defer clientsMu.Unlock() + clientsSnapshot := make([]*websocket.Conn, 0, len(clients)) + for client := range clients { + clientsSnapshot = append(clientsSnapshot, client) + } + clientsMu.Unlock() message := map[string]interface{}{ "type": eventType, @@ -109,11 +132,15 @@ func broadcastWebSocketMessage(eventType string, record *models.Record) { "resource": record.Get("resource"), } - for client := range clients { - if err := websocket.JSON.Send(client, message); err != nil { - log.Printf("Failed to send WebSocket message: %v", err) - client.Close() - delete(clients, client) - } + for _, client := range clientsSnapshot { + go func(client *websocket.Conn) { + if err := websocket.JSON.Send(client, message); err != nil { + log.Printf("[ERROR] Failed to send WebSocket message: %v", err) + clientsMu.Lock() + delete(clients, client) + clientsMu.Unlock() + _ = client.Close() + } + }(client) } } diff --git a/pocketfhir/initial_collections.go b/pocketfhir/initial_collections.go index 138a66f..c8b574b 100644 --- a/pocketfhir/initial_collections.go +++ b/pocketfhir/initial_collections.go @@ -21,26 +21,6 @@ func initializeCollections(app *pocketbase.PocketBase) error { return err } - // Step 2: Check if FHIR spec has been initialized - if !isFhirSpecInitialized(app) { - // Load the FHIR spec into the database - // if err := loadFhirSpecOnce(app); err != nil { - // log.Printf("Failed to load FHIR spec: %v", err) - // return err - // } - - // Set the FHIR spec as initialized - // setFhirSpecInitialized(app) - } else { - log.Println("FHIR spec already initialized.") - } - - // Step 3: Load MIMIC-IV dataset (optional step) - // if err := loadMimicIVData(app); err != nil { - // log.Printf("Failed to load MIMIC-IV dataset: %v", err) - // return err - // } - return nil }) return nil diff --git a/pocketfhir/load_spec.go b/pocketfhir/load_spec.go deleted file mode 100644 index c0c5b2b..0000000 --- a/pocketfhir/load_spec.go +++ /dev/null @@ -1,253 +0,0 @@ -package pocketfhir - -import ( - "bufio" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/pocketbase/pocketbase" - "github.com/pocketbase/pocketbase/models" - "github.com/pocketbase/pocketbase/tools/types" -) - -// Load the FHIR spec (e.g., StructureDefinitions, CapabilityStatements) from JSON files only once -func loadFhirSpecOnce(app *pocketbase.PocketBase) error { - // Check if the FHIR spec has already been loaded - if isFhirSpecInitialized(app) { - log.Println("FHIR resources already initialized, skipping.") - return nil - } - - log.Println("Loading FHIR resources for the first time...") - - // Define paths to the JSON files - jsonFiles := []string{ - "./assets/fhir_spec/conceptmaps.json", - "./assets/fhir_spec/dataelements.json", - "./assets/fhir_spec/extension-definitions.json", - "./assets/fhir_spec/profiles-others.json", - "./assets/fhir_spec/profiles-resources.json", - "./assets/fhir_spec/profiles-types.json", - "./assets/fhir_spec/search-parameters.json", - "./assets/fhir_spec/valuesets.json", - } - - // Iterate over JSON file paths and load resources into the appropriate collections - for _, jsonFilePath := range jsonFiles { - if err := loadFhirResourcesFromJSON(app, jsonFilePath); err != nil { - return fmt.Errorf("failed to load resources from %s: %v", jsonFilePath, err) - } - } - - // Mark FHIR spec as initialized - setFhirSpecInitialized(app) - - log.Println("FHIR resources loaded successfully.") - return nil -} - -// Load FHIR resources from a JSON file and insert them into the corresponding collections -func loadFhirResourcesFromJSON(app *pocketbase.PocketBase, filePath string) error { - // Open the JSON file - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open JSON file '%s': %v", filePath, err) - } - defer file.Close() - - // Parse the JSON file - var fileData map[string]interface{} - if err := json.NewDecoder(file).Decode(&fileData); err != nil { - return fmt.Errorf("failed to decode JSON file '%s': %v", filePath, err) - } - - // Process each resource entry in the Bundle - entries, ok := fileData["entry"].([]interface{}) - if !ok { - return fmt.Errorf("no 'entry' array found in JSON file '%s'", filePath) - } - - for _, entry := range entries { - resource, ok := entry.(map[string]interface{})["resource"].(map[string]interface{}) - if !ok { - log.Printf("Invalid resource format in JSON file '%s', skipping entry.", filePath) - continue - } - - // Determine the collection name based on the resourceType (converted to lowercase) - resourceType, ok := resource["resourceType"].(string) - if !ok || resourceType == "" { - log.Printf("Resource missing 'resourceType', skipping entry.") - continue - } - collectionName := strings.ToLower(resourceType) - - // Ensure the resource contains an "id" field - resourceID, ok := resource["id"].(string) - if !ok || resourceID == "" { - log.Printf("Resource does not contain a valid ID, skipping entry.") - continue - } - - // Check if the collection exists - collection, err := app.Dao().FindCollectionByNameOrId(collectionName) - if err != nil { - log.Printf("Collection '%s' not found for resourceType '%s', skipping entry.", collectionName, resourceType) - continue - } - - // Create a new record for the resource in the corresponding collection - record := models.NewRecord(collection) - record.Set("id", resourceID) // Set the resource ID - resourceJSON, _ := json.Marshal(resource) - record.Set("resource", types.JsonRaw(resourceJSON)) // Set the resource JSON - - // Save the record in the collection - if err := app.Dao().SaveRecord(record); err != nil { - log.Printf("Failed to save resource with ID '%s' to collection '%s': %v", resourceID, collectionName, err) - continue - } - - log.Printf("Successfully loaded resource with ID '%s' into collection '%s' from JSON file '%s'", resourceID, collectionName, filePath) - } - - return nil -} - -// Load MIMIC-IV data from NDJSON files and insert into the appropriate collections -func loadMimicIVData(app *pocketbase.PocketBase) error { - log.Println("Loading MIMIC-IV dataset...") - - // Directory containing the MIMIC-IV NDJSON files - mimicIVDir := "./assets/mimic_iv/" - - // List NDJSON files in the directory - files, err := os.ReadDir(mimicIVDir) - if err != nil { - return fmt.Errorf("failed to read MIMIC-IV directory: %v", err) - } - - // Process each NDJSON file - for _, file := range files { - if !strings.HasSuffix(file.Name(), ".ndjson") { - continue // Skip non-NDJSON files - } - - // Process the NDJSON file and load the resources - ndjsonFilePath := filepath.Join(mimicIVDir, file.Name()) - if err := loadNDJSONFile(app, ndjsonFilePath); err != nil { - return fmt.Errorf("failed to load resources from %s: %v", ndjsonFilePath, err) - } - } - - log.Println("MIMIC-IV dataset loaded successfully.") - return nil -} - -// Load resources from a specific NDJSON file and insert them into the appropriate collection -func loadNDJSONFile(app *pocketbase.PocketBase, filePath string) error { - // Open the NDJSON file - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open NDJSON file '%s': %v", filePath, err) - } - defer file.Close() - - // Initialize scanner to read NDJSON file line by line - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - - // Parse the JSON line into a map - var resource map[string]interface{} - if err := json.Unmarshal([]byte(line), &resource); err != nil { - log.Printf("Failed to parse JSON line in '%s': %v", filePath, err) - continue - } - - // Ensure the resource contains a "resourceType" field - resourceType, ok := resource["resourceType"].(string) - if !ok || resourceType == "" { - log.Printf("Resource does not contain a valid 'resourceType' in '%s', skipping entry.", filePath) - continue - } - - // Convert the resourceType to lowercase to determine the collection name - collectionName := strings.ToLower(resourceType) - - // Check if the collection exists - collection, err := app.Dao().FindCollectionByNameOrId(collectionName) - if err != nil { - log.Printf("Collection '%s' not found for resourceType '%s', skipping entry.", collectionName, resourceType) - continue - } - - // Ensure the resource contains an "id" field (or any other relevant key for MIMIC-IV resources) - resourceID, ok := resource["id"].(string) - if !ok || resourceID == "" { - log.Printf("Resource does not contain a valid 'id' in '%s', skipping entry.", filePath) - continue - } - - // Create a new record for the resource in the specified collection - record := models.NewRecord(collection) - record.Set("id", resourceID) - resourceJSON, _ := json.Marshal(resource) - record.Set("resource", types.JsonRaw(resourceJSON)) - - // Save the record in the collection - if err := app.Dao().SaveRecord(record); err != nil { - log.Printf("Failed to save resource with ID '%s' from file '%s': %v", resourceID, filePath, err) - continue - } - - log.Printf("Successfully loaded resource with ID '%s' into collection '%s' from file '%s'", resourceID, collectionName, filePath) - } - - // Check for errors during scanning - if err := scanner.Err(); err != nil { - return fmt.Errorf("error reading NDJSON file '%s': %v", filePath, err) - } - - return nil -} - -// Check if the FHIR spec is already initialized -func isFhirSpecInitialized(app *pocketbase.PocketBase) bool { - metadataCollection, err := app.Dao().FindCollectionByNameOrId("metadata") - if err != nil { - log.Printf("Metadata collection not found: %v", err) - return false - } - - // Check for a record indicating FHIR spec initialization - record, err := app.Dao().FindFirstRecordByData(metadataCollection.Id, "fhirSpecInitialized", true) - if err != nil || record == nil { - return false - } - - return true -} - -// Mark the FHIR spec as initialized in the "metadata" collection -func setFhirSpecInitialized(app *pocketbase.PocketBase) { - metadataCollection, err := app.Dao().FindCollectionByNameOrId("metadata") - if err != nil { - log.Printf("Metadata collection not found: %v", err) - return - } - - // Create a new record in the metadata collection - record := models.NewRecord(metadataCollection) - record.Set("fhirSpecInitialized", true) - - if err := app.Dao().SaveRecord(record); err != nil { - log.Printf("Failed to save metadata record: %v", err) - } else { - log.Println("FHIR spec initialization marked as complete.") - } -} diff --git a/pocketfhir/server.go b/pocketfhir/server.go index 6a12ca1..55b9d41 100644 --- a/pocketfhir/server.go +++ b/pocketfhir/server.go @@ -5,7 +5,8 @@ import ( "log" "net/http" "os" - "syscall" + "sync" + "time" "github.com/labstack/echo/v5" "github.com/pocketbase/pocketbase" @@ -25,7 +26,7 @@ func RegisterNativeBridgeCallback(c NativeBridge) { func RunServer(dataDir string, ipAddress string, pbPort string, enableApiLogs bool) { // Set CLI-like arguments for PocketBase to specify server address and port log.Printf("[DEBUG] Setting CLI arguments for server address and port: %s:%s\n", ipAddress, pbPort) - os.Args = append(os.Args[:1], "serve", "--http", fmt.Sprintf("%s:%s", ipAddress, pbPort)) + os.Args = []string{os.Args[0], "serve", "--http", fmt.Sprintf("%s:%s", ipAddress, pbPort)} // Create a configuration object with custom settings log.Println("[DEBUG] Creating PocketBase configuration object...") @@ -68,25 +69,27 @@ func RunServer(dataDir string, ipAddress string, pbPort string, enableApiLogs bo // Initialize collections if necessary log.Println("[DEBUG] Initializing collections...") if err := initializeCollections(app); err != nil { - log.Fatalf("Failed to initialize collections: %v", err) - } else { - log.Println("[DEBUG] Collections initialized successfully.") + log.Printf("[ERROR] Failed to initialize collections: %v", err) + return } + log.Println("[DEBUG] Collections initialized successfully.") + + // Start the server in a separate goroutine + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + log.Println("[DEBUG] Calling app.Start() to start the server...") + if err := app.Start(); err != nil { + sendCommand("error", fmt.Sprintf("Error: Failed to start PocketBase server: %v", err)) + log.Printf("[ERROR] Failed to start the app: %v", err) + } else { + log.Println("[DEBUG] PocketFHIR server started successfully.") + } + }() - // Start the server (no need to wait on anything here) - log.Println("[DEBUG] Calling app.Start() to start the server...") - if err := app.Start(); err != nil { - sendCommand("error", fmt.Sprintf("Error: Failed to start PocketBase server: %v", err)) - log.Fatalf("Failed to start the app: %v", err) - } else { - log.Println("[DEBUG] PocketFHIR server started successfully.") - } -} - -// StopServer gracefully stops the running PocketFHIR server -func StopServer() { - log.Println("Stopping PocketFHIR server...") - syscall.Kill(syscall.Getpid(), syscall.SIGINT) + // Wait for server to complete + wg.Wait() } // Helper methods @@ -108,7 +111,13 @@ func sendCommand(command string, data string) string { // setupPocketbaseCallbacks sets up additional callbacks and native routes for PocketBase func setupPocketbaseCallbacks(app *pocketbase.PocketBase, enableApiLogs bool) { - // Setup callbacks + setupOnBeforeServe(app, enableApiLogs) + setupOnBeforeBootstrap(app) + setupOnAfterBootstrap(app) + setupOnTerminate(app) +} + +func setupOnBeforeServe(app *pocketbase.PocketBase, enableApiLogs bool) { log.Println("[DEBUG] Setting up OnBeforeServe callback...") app.OnBeforeServe().Add(func(e *core.ServeEvent) error { log.Println("[DEBUG] OnBeforeServe triggered.") @@ -118,53 +127,62 @@ func setupPocketbaseCallbacks(app *pocketbase.PocketBase, enableApiLogs bool) { e.Router.Use(ApiLogsMiddleWare(app)) } - // Setup a native GET request handler - log.Println("[DEBUG] Adding native GET request handler...") - e.Router.AddRoute(echo.Route{ - Method: http.MethodGet, - Path: "/api/nativeGet", - Handler: func(context echo.Context) error { - log.Println("[DEBUG] Handling native GET request...") - var data = sendCommand("nativeGetRequest", context.QueryParams().Encode()) - return context.JSON(http.StatusOK, map[string]string{ - "success": data, - }) - }, - }) - - // Setup a native POST request handler - log.Println("[DEBUG] Adding native POST request handler...") - e.Router.AddRoute(echo.Route{ - Method: http.MethodPost, - Path: "/api/nativePost", - Handler: func(context echo.Context) error { - log.Println("[DEBUG] Handling native POST request...") - form, err := context.FormValues() - if err != nil { - return context.JSON(http.StatusBadRequest, map[string]string{ - "error": err.Error(), - }) - } - var data = sendCommand("nativePostRequest", form.Encode()) - return context.JSON(http.StatusOK, map[string]string{ - "success": data, - }) - }, - }) - + setupNativeGetHandler(e.Router) + setupNativePostHandler(e.Router) return nil }) +} + +func setupNativeGetHandler(router *echo.Echo) { + log.Println("[DEBUG] Adding native GET request handler...") + router.AddRoute(echo.Route{ + Method: http.MethodGet, + Path: "/api/nativeGet", + Handler: func(context echo.Context) error { + log.Println("[DEBUG] Handling native GET request...") + data := sendCommand("nativeGetRequest", context.QueryParams().Encode()) + return context.JSON(http.StatusOK, map[string]string{"success": data}) + }, + }) +} +func setupNativePostHandler(router *echo.Echo) { + log.Println("[DEBUG] Adding native POST request handler...") + router.AddRoute(echo.Route{ + Method: http.MethodPost, + Path: "/api/nativePost", + Handler: func(context echo.Context) error { + log.Println("[DEBUG] Handling native POST request...") + form, err := context.FormValues() + if err != nil { + return context.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()}) + } + data := sendCommand("nativePostRequest", form.Encode()) + return context.JSON(http.StatusOK, map[string]string{"success": data}) + }, + }) +} + +func setupOnBeforeBootstrap(app *pocketbase.PocketBase) { + log.Println("[DEBUG] Setting up OnBeforeBootstrap callback...") app.OnBeforeBootstrap().Add(func(e *core.BootstrapEvent) error { log.Println("[DEBUG] OnBeforeBootstrap triggered.") sendCommand("OnBeforeBootstrap", "") return nil }) +} + +func setupOnAfterBootstrap(app *pocketbase.PocketBase) { + log.Println("[DEBUG] Setting up OnAfterBootstrap callback...") app.OnAfterBootstrap().Add(func(e *core.BootstrapEvent) error { log.Println("[DEBUG] OnAfterBootstrap triggered.") sendCommand("OnAfterBootstrap", "") return nil }) +} + +func setupOnTerminate(app *pocketbase.PocketBase) { + log.Println("[DEBUG] Setting up OnTerminate callback...") app.OnTerminate().Add(func(e *core.TerminateEvent) error { log.Println("[DEBUG] OnTerminate triggered.") sendCommand("OnTerminate", "") @@ -173,13 +191,32 @@ func setupPocketbaseCallbacks(app *pocketbase.PocketBase, enableApiLogs bool) { } // ApiLogsMiddleWare logs all API calls for PocketBase +var requestLogBuffer []string +var mu sync.Mutex + +func init() { + // Background goroutine to batch log requests + go func() { + for { + time.Sleep(1 * time.Second) + mu.Lock() + if len(requestLogBuffer) > 0 { + log.Println("[DEBUG] Batch API request logs:", requestLogBuffer) + requestLogBuffer = []string{} // Clear the buffer + } + mu.Unlock() + } + }() +} + func ApiLogsMiddleWare(app core.App) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { request := c.Request() fullPath := request.URL.Host + request.URL.Path + "?" + request.URL.RawQuery - log.Printf("[DEBUG] API request made to: %s", fullPath) - sendCommand("apiLogs", fullPath) + mu.Lock() + requestLogBuffer = append(requestLogBuffer, fullPath) + mu.Unlock() return next(c) } } diff --git a/pocketfhir/start.go b/pocketfhir/start.go index 02c7552..79cfcef 100644 --- a/pocketfhir/start.go +++ b/pocketfhir/start.go @@ -32,13 +32,31 @@ func StartPocketFHIR( // Start the Caddy server in a separate goroutine go func() { log.Println("[DEBUG] Starting Caddy server with HTTPS...") - StartCaddy(pbPort, httpPort, httpsPort, pbUrl, storagePath, ipAddress) + caddyConfig := CaddyConfig{ + PbPort: pbPort, + HttpPort: httpPort, + HttpsPort: httpsPort, + PbUrl: pbUrl, + StoragePath: storagePath, + IpAddress: ipAddress, + } + StartCaddy(caddyConfig) }() // Wait for interrupt signal to gracefully shut down the server log.Println("[DEBUG] Waiting for interrupt signal to shut down the servers...") <-stop log.Println("Shutting down PocketFHIR and Caddy servers...") - StopServer() // PocketFHIR shutdown + StopPocketFHIR() // PocketFHIR shutdown log.Println("PocketFHIR and Caddy servers shut down gracefully.") } + +// StopPocketFHIR gracefully stops both PocketFHIR and Caddy servers +func StopPocketFHIR() { + log.Println("Stopping PocketFHIR server and Caddy...") + // Sending SIGTERM to ensure graceful shutdown for both services + signal.Ignore(syscall.SIGTERM) // Avoid receiving the same signal again during shutdown + if err := syscall.Kill(syscall.Getpid(), syscall.SIGTERM); err != nil { + log.Printf("Failed to stop servers: %v", err) + } +}