From 7e099549a071741d0d3aa4cf50695601f0c79788 Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Fri, 25 Apr 2025 20:28:33 -0600 Subject: [PATCH 1/6] Working implimentation --- discon-client/client.go | 172 ++++++++++++++++++++++++++++++++++- discon-server/server.go | 8 +- discon-server/websocket.go | 177 +++++++++++++++++++++++++++++++++++-- payload.go | 65 +++++++++++++- 4 files changed, 406 insertions(+), 16 deletions(-) diff --git a/discon-client/client.go b/discon-client/client.go index b3d9812..85839b8 100644 --- a/discon-client/client.go +++ b/discon-client/client.go @@ -3,12 +3,17 @@ package main import "C" import ( + "crypto/sha256" dw "discon-wrapper" + "encoding/hex" "fmt" + "io" "log" "net/url" "os" + "path/filepath" "strconv" + "strings" "unsafe" "github.com/gorilla/websocket" @@ -24,6 +29,124 @@ var payload dw.Payload var sentSwapFile *os.File var recvSwapFile *os.File +// GH-Cp gen: Map to store server-side file paths for transferred files +var serverFilePaths = make(map[string]string) + +// GH-Cp gen: Function to check if a file exists +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +// GH-Cp gen: Function to read file contents +func readFileContents(filePath string) ([]byte, error) { + if !fileExists(filePath) { + return nil, fmt.Errorf("file does not exist: %s", filePath) + } + + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + return content, nil +} + +// GH-Cp gen: Function to send file to server and get server path +func sendFileToServer(filePath string) (string, error) { + // Check if we've already sent this file + if serverPath, exists := serverFilePaths[filePath]; exists { + // if debugLevel >= 1 { + // log.Printf("discon-client: Using cached server path for %s: %s", filePath, serverPath) + // } + return serverPath, nil + } + + // Read the file contents + content, err := readFileContents(filePath) + if err != nil { + return "", err + } + + // Create a unique filename using SHA-256 hash of content and original filename + hash := sha256.New() + hash.Write(content) + hash.Write([]byte(filepath.Base(filePath))) + hashString := hex.EncodeToString(hash.Sum(nil)) + + // Generate a server path - use just the filename with a unique prefix + serverPath := fmt.Sprintf("input_%s_%s", hashString[:8], filepath.Base(filePath)) + + // Create a file transfer payload + fileTransferPayload := dw.Payload{ + // Initialize required fields with empty values + Swap: make([]float32, 1), + Fail: 0, + InFile: []byte{0}, + OutName: []byte{0}, + Msg: []byte{0}, + FileContent: content, + ServerFilePath: []byte(serverPath + "\x00"), + } + + if debugLevel >= 1 { + log.Printf("discon-client: Sending file %s to server (size: %d bytes)", filePath, len(content)) + } + + // Send the file transfer payload to the server + b, err := fileTransferPayload.MarshalBinary() + if err != nil { + return "", fmt.Errorf("error marshaling file transfer payload: %w", err) + } + + err = ws.WriteMessage(websocket.BinaryMessage, b) + if err != nil { + return "", fmt.Errorf("error sending file to server: %w", err) + } + + // Wait for server response + _, resp, err := ws.ReadMessage() + if err != nil { + return "", fmt.Errorf("error receiving server response: %w", err) + } + + // Unmarshal the response + var responsePayload dw.Payload + err = responsePayload.UnmarshalBinary(resp) + if err != nil { + return "", fmt.Errorf("error unmarshaling server response: %w", err) + } + + // Check if the file transfer succeeded + if responsePayload.Fail != 0 { + // Get the error message from the Msg field + errMsg := string(responsePayload.Msg) + i0 := strings.IndexByte(errMsg, 0) + if i0 >= 0 { + errMsg = errMsg[:i0] + } + return "", fmt.Errorf("file transfer failed: %s", errMsg) + } + + // Store the server path for future use + serverFilePaths[filePath] = serverPath + + if debugLevel >= 1 { + log.Printf("discon-client: File %s transferred successfully to server at %s", filePath, serverPath) + } + + return serverPath, nil +} + func init() { // Print info @@ -127,12 +250,59 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c log.Printf("discon-client: size of avcMSG: % 5d\n", msgSize) } + // GH-Cp gen: Get the input file path from accInFile + inFilePath := string((*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize-1]) // -1 to exclude null terminator + + // GH-Cp gen: Check if the input file exists locally and transfer it to server if needed + if fileExists(inFilePath) { + // if debugLevel >= 2 { + // log.Printf("discon-client: Input file found locally: %s", inFilePath) + // } + + // Transfer file to server and get the server-side path + serverPath, err := sendFileToServer(inFilePath) + if err != nil { + log.Printf("discon-client: Error transferring file %s: %v", inFilePath, err) + // Set failure flag + *aviFail = C.int(1) + // Set error message + errMsg := fmt.Sprintf("File transfer failed: %v", err) + copy((*[1 << 24]byte)(unsafe.Pointer(avcMsg))[:msgSize], []byte(errMsg)) + return + } + + // GH-Cp gen: Update the InFile field in payload with the server-side path + // First, create a new byte slice with the modified path + serverPathBytes := []byte(serverPath + "\x00") + + // Copy to payload.InFile + payload.InFile = serverPathBytes + + // Update the inFileSize in the swap array + swap[49] = float32(len(serverPathBytes)) + + // if debugLevel >= 1 { + // log.Printf("discon-client: Using server path for input file: %s", serverPath) + // } + } else if debugLevel >= 1 { + // Log if file doesn't exist but continue with normal operation + log.Printf("discon-client: Input file not found locally: %s, continuing with original path", inFilePath) + payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] + } else { + // Normal operation if no debug info + payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] + } + + // Fill the rest of the payload payload.Swap = swap[:swapSize:swapSize] payload.Fail = int32(*aviFail) - payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] payload.OutName = (*[1 << 24]byte)(unsafe.Pointer(avcOutName))[:outNameSize:outNameSize] payload.Msg = (*[1 << 24]byte)(unsafe.Pointer(avcMsg))[:msgSize:msgSize] + // Reset file transfer fields to avoid sending unnecessary data + payload.FileContent = nil + payload.ServerFilePath = nil + // Convert payload to binary and send over websocket b, err := payload.MarshalBinary() if err != nil { diff --git a/discon-server/server.go b/discon-server/server.go index 402ffdf..46da406 100644 --- a/discon-server/server.go +++ b/discon-server/server.go @@ -10,7 +10,8 @@ import ( const program = "discon-server" const version = "v0.1.0" -var debug = false +// GH-Cp gen: Using an int for debug levels instead of boolean +var debugLevel int = 0 var port = 8080 func main() { @@ -19,11 +20,12 @@ func main() { log.Printf("Started %s %s", program, version) flag.IntVar(&port, "port", 8080, "Port to listen on") - flag.BoolVar(&debug, "debug", false, "Enable debug output") + // GH-Cp gen: Updated to use debug levels + flag.IntVar(&debugLevel, "debug", 0, "Debug level: 0=disabled, 1=basic info, 2=verbose with payloads") flag.Parse() http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - ServeWs(w, r, debug) + ServeWs(w, r, debugLevel) }) // Start server diff --git a/discon-server/websocket.go b/discon-server/websocket.go index 511b18c..11aceb8 100644 --- a/discon-server/websocket.go +++ b/discon-server/websocket.go @@ -6,7 +6,9 @@ package main // void unload_shared_library(int connID); import "C" import ( + "crypto/sha256" dw "discon-wrapper" + "encoding/hex" "fmt" "io" "log" @@ -14,6 +16,7 @@ import ( "net/url" "os" "path/filepath" + "strings" "sync" "time" "unsafe" @@ -32,7 +35,110 @@ var connectionIDMutex sync.Mutex // WaitGroup to wait for all goroutines to finish var wg sync.WaitGroup -func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { +// GH-Cp gen: Map of temporary files created for each connection +var tempFiles = make(map[int32][]string) +var tempFilesMutex sync.Mutex + +// GH-Cp gen: Function to check if data is a file transfer +func isFileTransfer(payload *dw.Payload) bool { + return len(payload.FileContent) > 0 && len(payload.ServerFilePath) > 0 +} + +// GH-Cp gen: Function to validate filename for security +func validateFileName(filename string) error { + // Remove null terminators + filename = strings.ReplaceAll(filename, "\x00", "") + + // Check if filename contains suspicious patterns + suspiciousPatterns := []string{"../", "/..", "~", "$", "|", ";", "&", "\\"} + for _, pattern := range suspiciousPatterns { + if strings.Contains(filename, pattern) { + return fmt.Errorf("filename contains invalid pattern: %s", pattern) + } + } + + // Ensure filename is just a basename, not a path + if filepath.Base(filename) != filename { + return fmt.Errorf("filename must not contain path separators") + } + + return nil +} + +// GH-Cp gen: Handle file transfer from client to server +func handleFileTransfer(connID int32, payload *dw.Payload, debugLevel int) (*dw.Payload, error) { + // Initialize response payload + response := dw.Payload{ + Swap: make([]float32, 1), + Fail: 0, + InFile: []byte{0}, + OutName: []byte{0}, + Msg: make([]byte, 256), // Reserve space for error message + } + + // Get server file path from payload + serverFilePath := string(payload.ServerFilePath) + nullIndex := strings.IndexByte(serverFilePath, 0) + if nullIndex >= 0 { + serverFilePath = serverFilePath[:nullIndex] + } + + // Validate filename for security + err := validateFileName(serverFilePath) + if err != nil { + response.Fail = 1 + errMsg := fmt.Sprintf("Security error: %v", err) + copy(response.Msg, []byte(errMsg+"\x00")) + return &response, fmt.Errorf("file validation error: %w", err) + } + + // Verify file contents with hash + hash := sha256.New() + hash.Write(payload.FileContent) + contentHash := hex.EncodeToString(hash.Sum(nil)) + + if debugLevel >= 1 { + log.Printf("Received file transfer request for %s (size: %d bytes, hash: %s)", + serverFilePath, len(payload.FileContent), contentHash[:8]) + } + + // Create the file with the server file path + file, err := os.Create(serverFilePath) + if err != nil { + response.Fail = 1 + errMsg := fmt.Sprintf("Failed to create file: %v", err) + copy(response.Msg, []byte(errMsg+"\x00")) + return &response, fmt.Errorf("failed to create file: %w", err) + } + defer file.Close() + + // Write the file contents + _, err = file.Write(payload.FileContent) + if err != nil { + response.Fail = 1 + errMsg := fmt.Sprintf("Failed to write file: %v", err) + copy(response.Msg, []byte(errMsg+"\x00")) + os.Remove(serverFilePath) // Clean up the partial file + return &response, fmt.Errorf("failed to write file: %w", err) + } + + // Register the file to be cleaned up when the connection closes + tempFilesMutex.Lock() + tempFiles[connID] = append(tempFiles[connID], serverFilePath) + tempFilesMutex.Unlock() + + if debugLevel >= 1 { + log.Printf("File %s created successfully", serverFilePath) + } + + // Set success message + successMsg := fmt.Sprintf("File transferred successfully: %s", serverFilePath) + copy(response.Msg, []byte(successMsg+"\x00")) + + return &response, nil +} + +func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { wg.Add(1) defer wg.Done() @@ -54,7 +160,7 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { path := params.Get("path") proc := params.Get("proc") - if debug { + if debugLevel >= 1 { log.Printf("Received request to load function '%s' from shared controller '%s'\n", proc, path) } @@ -74,10 +180,31 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { } defer os.Remove(tmpPath) - if debug { + if debugLevel >= 1 { log.Printf("Duplicated controller to '%s'\n", tmpPath) } + // GH-Cp gen: Initialize tempFiles entry for this connection + tempFilesMutex.Lock() + tempFiles[connID] = make([]string, 0) + tempFilesMutex.Unlock() + + // GH-Cp gen: Clean up any temporary files when connection closes + defer func() { + tempFilesMutex.Lock() + fileList := tempFiles[connID] + delete(tempFiles, connID) + tempFilesMutex.Unlock() + + // Remove all temporary files for this connection + for _, filePath := range fileList { + if debugLevel >= 1 { + log.Printf("Cleaning up temporary file: %s", filePath) + } + os.Remove(filePath) + } + }() + // Load the shared library libraryPath := C.CString(tmpPath) defer C.free(unsafe.Pointer(libraryPath)) @@ -92,7 +219,7 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { return } - if debug { + if debugLevel >= 1 { log.Printf("Library and function loaded successfully\n") } @@ -110,10 +237,10 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { // Loop while receiving messages over socket for { - // If not debug, set read deadline to 5 seconds + // If not in debug mode, set read deadline to 5 seconds // This will disconnect the client if no message is received in 5 seconds // which allows the controller to be unloaded and deleted - if !debug { + if debugLevel == 0 { ws.SetReadDeadline(time.Now().Add(time.Second * 5)) } @@ -135,8 +262,36 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { break } - if debug { + // GH-Cp gen: Only log full payload at debug level 2 + if debugLevel >= 2 { log.Println("discon-server: received payload:", payload) + // } else if debugLevel == 1 { + // // At level 1, just log basic info without full payload details + // inFilePath := string(payload.InFile) + // nullIndex := strings.IndexByte(inFilePath, 0) + // if nullIndex >= 0 { + // inFilePath = inFilePath[:nullIndex] + // } + // log.Printf("discon-server: received request with InFile: %s", inFilePath) + } + + // GH-Cp gen: Check if the payload is a file transfer + if isFileTransfer(&payload) { + response, err := handleFileTransfer(connID, &payload, debugLevel) + if err != nil { + log.Println("handleFileTransfer:", err) + } + b, err = response.MarshalBinary() + if err != nil { + log.Println("response.MarshalBinary:", err) + break + } + err = ws.WriteMessage(websocket.BinaryMessage, b) + if err != nil { + log.Println("write:", err) + break + } + continue } // Call the function from the shared library with data in payload @@ -159,8 +314,12 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debug bool) { break } - if debug { - fmt.Println("discon-server: sent payload:", payload) + // GH-Cp gen: Only log full response at debug level 2 + if debugLevel >= 2 { + log.Println("discon-server: sent payload:", payload) + // } else if debugLevel == 1 { + // // At level 1, just log that response was sent + // log.Println("discon-server: sent response") } } diff --git a/payload.go b/payload.go index 6687b2d..671ed94 100644 --- a/payload.go +++ b/payload.go @@ -12,7 +12,10 @@ type Payload struct { InFile []byte OutName []byte Msg []byte - buffer bytes.Buffer + // GH-Cp gen: Added fields for file transfer + FileContent []byte // Content of the controller input file + ServerFilePath []byte // Path where the file should be stored on server + buffer bytes.Buffer } func (p *Payload) MarshalBinary() ([]byte, error) { @@ -33,6 +36,15 @@ func (p *Payload) MarshalBinary() ([]byte, error) { if err != nil { return nil, err } + // GH-Cp gen: Added writing FileContent and ServerFilePath lengths + err = binary.Write(&p.buffer, binary.LittleEndian, uint32(len(p.FileContent))) + if err != nil { + return nil, err + } + err = binary.Write(&p.buffer, binary.LittleEndian, uint32(len(p.ServerFilePath))) + if err != nil { + return nil, err + } err = binary.Write(&p.buffer, binary.LittleEndian, p.Swap) if err != nil { return nil, err @@ -53,6 +65,15 @@ func (p *Payload) MarshalBinary() ([]byte, error) { if err != nil { return nil, err } + // GH-Cp gen: Added writing FileContent and ServerFilePath data + err = binary.Write(&p.buffer, binary.LittleEndian, p.FileContent) + if err != nil { + return nil, err + } + err = binary.Write(&p.buffer, binary.LittleEndian, p.ServerFilePath) + if err != nil { + return nil, err + } return p.buffer.Bytes(), nil } @@ -63,6 +84,8 @@ func (p *Payload) UnmarshalBinary(data []byte) error { // Read the lengths of the fields var swapLen, inFileLen, outNameLen, msgLen uint32 + // GH-Cp gen: Added variables for FileContent and ServerFilePath lengths + var fileContentLen, serverFilePathLen uint32 err := binary.Read(r, binary.LittleEndian, &swapLen) if err != nil { @@ -80,6 +103,15 @@ func (p *Payload) UnmarshalBinary(data []byte) error { if err != nil { return err } + // GH-Cp gen: Read lengths of FileContent and ServerFilePath + err = binary.Read(r, binary.LittleEndian, &fileContentLen) + if err != nil { + return err + } + err = binary.Read(r, binary.LittleEndian, &serverFilePathLen) + if err != nil { + return err + } // Allocate slices of the appropriate size if they don't match if len(p.Swap) != int(swapLen) { @@ -94,6 +126,13 @@ func (p *Payload) UnmarshalBinary(data []byte) error { if len(p.Msg) != int(msgLen) { p.Msg = make([]byte, msgLen) } + // GH-Cp gen: Allocate FileContent and ServerFilePath slices + if len(p.FileContent) != int(fileContentLen) { + p.FileContent = make([]byte, fileContentLen) + } + if len(p.ServerFilePath) != int(serverFilePathLen) { + p.ServerFilePath = make([]byte, serverFilePathLen) + } // Read the fields from the buffer err = binary.Read(r, binary.LittleEndian, &p.Swap) @@ -116,6 +155,15 @@ func (p *Payload) UnmarshalBinary(data []byte) error { if err != nil { return err } + // GH-Cp gen: Read FileContent and ServerFilePath data + err = binary.Read(r, binary.LittleEndian, &p.FileContent) + if err != nil { + return err + } + err = binary.Read(r, binary.LittleEndian, &p.ServerFilePath) + if err != nil { + return err + } return nil } @@ -132,14 +180,25 @@ func (p Payload) String() string { if i0Msg < 0 { i0Msg = len(p.Msg) } + // GH-Cp gen: Added handling for ServerFilePath + i0ServerFilePath := bytes.IndexByte(p.ServerFilePath, 0) + if i0ServerFilePath < 0 { + i0ServerFilePath = len(p.ServerFilePath) + } + + // GH-Cp gen: Modified string formatting to include new fields return fmt.Sprintf("avrSWAP: %v\n"+ "aviFAIL: %v\n"+ "accINFILE: '%s'\n"+ "avcOUTNAME: '%s'\n"+ - "avcMSG: '%s'\n", + "avcMSG: '%s'\n"+ + "ServerFilePath: '%s'\n"+ + "FileContent: [%d bytes]\n", p.Swap[:129], p.Fail, p.InFile[:i0InFile], p.OutName[:i0OutName], - p.Msg[:i0Msg]) + p.Msg[:i0Msg], + p.ServerFilePath[:i0ServerFilePath], + len(p.FileContent)) } From 45bb54102576ab83838f92afa7b92536ea1b9aa6 Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Fri, 25 Apr 2025 20:45:30 -0600 Subject: [PATCH 2/6] readme update --- README.md | 26 +++++++++++++++++++++++--- discon-server/server_test.go | 2 +- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e6c6b60..b2a86a9 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,12 @@ There's not a lot to configure, but you're bridging two applications, so things ### Server -Download `discon-server_386.exe` from [Releases](https://github.com/deslaughter/discon-wrapper/releases) and put it in the same directory as the Controller shared library. Start the server from the command line by running `discon-server_amd64.exe --port=8080 --debug`. The `port` argument specifies which port on your computer it will listen to for client connections. This can be any 4-5 digit number that is not already in use by the operating system. If it returns an error, try a different number. The `debug` argument enables debug output which will be helpful for checking that it's working, but should be turned off when running simulations. +Download `discon-server_386.exe` from [Releases](https://github.com/deslaughter/discon-wrapper/releases) and put it in the same directory as the Controller shared library. Start the server from the command line by running `discon-server_amd64.exe --port=8080 --debug=1`. The `port` argument specifies which port on your computer it will listen to for client connections. This can be any 4-5 digit number that is not already in use by the operating system. If it returns an error, try a different number. + +The `debug` argument now accepts a level (0, 1, or 2): +- `0`: No debug output (default) +- `1`: Basic debug information +- `2`: Verbose debug output including full payloads That's it for the server configuration. It doesn't load the controller until the client makes a connection because the client has to tell it what to load, more on that in the next section. @@ -63,12 +68,27 @@ openfast.exe my_turbine.fst You can see that the environment variable settings correspond to the original controller settings that were in the ServoDyn input file. +## Client-Side Input Files + +DISCON-Wrapper now supports input files located on the client side. When a controller asks for an input file (such as DISCON.IN), the client will: + +1. Check if the file exists locally +2. If found, automatically transfer the file to the server before the controller is called +3. Update the file path that's passed to the controller on the server side + +This means you can now keep your input files on the client machine and don't need to manually copy them to the server. The file transfer happens automatically and transparently. + +Benefits: +- No need to copy input files to the server +- Files are transferred only once during a simulation +- Files are automatically cleaned up when the connection closes + ## Running For simplicity, put the OpenFAST input files, controller shared library, `discon-server_386.exe`, and `discon-client_amd64.dll` into a directory. Open two command prompts, one for running the server, and the other for running OpenFAST. Start the server by running the following in one command prompt ``` -discon-server_amd64.exe --port=8080 --debug +discon-server_amd64.exe --port=8080 --debug=1 ``` Switch to the second command prompt, specify the environment variables, and run OpenFAST: @@ -83,4 +103,4 @@ openfast.exe my_turbine.fst If everything is configured properly, the simulation should proceed as though OpenFAST had loaded the controller directly. Remember, that the server must be started before running the simulation because the client will attempt to connect when it is loaded. Once the simulation stops, the client will disconnect. The server is will continue running and wait for new connections so you can run more simulations. To stop the server, switch to the command prompt, and close it or press `CTRL+C` to kill the server. -You may see a temporary copy of the controller while the simulation is running. This is done so the server can load multiple copies of the controller at once, supporting multiple concurrent OpenFAST simulations. If they aren't automatically removed by the server, they can be deleted manually once the server has been stopped. +You may see temporary files created while the simulation is running. These include a copy of the controller and any transferred input files. They are automatically cleaned up when the connection closes, but if they aren't removed for some reason, they can be deleted manually once the server has been stopped. diff --git a/discon-server/server_test.go b/discon-server/server_test.go index af2fa68..9bf58f4 100644 --- a/discon-server/server_test.go +++ b/discon-server/server_test.go @@ -17,7 +17,7 @@ func TestServeWs(t *testing.T) { // connect handler to websocket function http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - ServeWs(w, r, true) + ServeWs(w, r, 1) }) // Start server in separate go routine From 88504263dc30e945259169db3b28cae5dd00eadc Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Fri, 25 Apr 2025 22:58:36 -0600 Subject: [PATCH 3/6] graceful close and docker dependancy libs --- Dockerfile | 7 ++++- discon-client/client.go | 65 +++++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 070d608..2b45ac4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,9 +18,14 @@ RUN go build -o /discon-server discon-wrapper/discon-server # Create the final image FROM ubuntu:24.04 -# Install runtime dependencies +# Install runtime dependencies including C, C++, and Fortran libraries RUN apt-get update && apt-get install -y \ libc6 \ + libstdc++6 \ + libgcc-s1 \ + libgfortran5 \ + liblapack3 \ + libblas3 \ && rm -rf /var/lib/apt/lists/* # Copy the binary from the build stage diff --git a/discon-client/client.go b/discon-client/client.go index 85839b8..517c556 100644 --- a/discon-client/client.go +++ b/discon-client/client.go @@ -34,6 +34,11 @@ var serverFilePaths = make(map[string]string) // GH-Cp gen: Function to check if a file exists func fileExists(filename string) bool { + // GH-Cp gen: Added nil/empty check to prevent segmentation fault + if filename == "" { + return false + } + info, err := os.Stat(filename) if os.IsNotExist(err) { return false @@ -228,7 +233,6 @@ func init() { //export DISCON func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.char) { - // Get first 130 entries of swap array swap := (*[1 << 24]float32)(unsafe.Pointer(avrSwap)) @@ -250,14 +254,24 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c log.Printf("discon-client: size of avcMSG: % 5d\n", msgSize) } - // GH-Cp gen: Get the input file path from accInFile - inFilePath := string((*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize-1]) // -1 to exclude null terminator + // GH-Cp gen: Get the input file path from accInFile with safer handling + var inFilePath string + if accInFile != nil && inFileSize > 0 { + // Safely get the file path - ensure we don't read past the end of the array + safeSize := inFileSize + if safeSize > 1 { + safeSize-- // -1 to exclude null terminator if present + } + inFilePath = string((*[1 << 24]byte)(unsafe.Pointer(accInFile))[:safeSize]) + // Remove any null terminators from the end of the string + inFilePath = strings.TrimRight(inFilePath, "\x00") + } // GH-Cp gen: Check if the input file exists locally and transfer it to server if needed - if fileExists(inFilePath) { - // if debugLevel >= 2 { - // log.Printf("discon-client: Input file found locally: %s", inFilePath) - // } + if inFilePath != "" && fileExists(inFilePath) { + if debugLevel >= 2 { + log.Printf("discon-client: Input file found locally: %s", inFilePath) + } // Transfer file to server and get the server-side path serverPath, err := sendFileToServer(inFilePath) @@ -281,23 +295,38 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c // Update the inFileSize in the swap array swap[49] = float32(len(serverPathBytes)) - // if debugLevel >= 1 { - // log.Printf("discon-client: Using server path for input file: %s", serverPath) - // } - } else if debugLevel >= 1 { - // Log if file doesn't exist but continue with normal operation - log.Printf("discon-client: Input file not found locally: %s, continuing with original path", inFilePath) - payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] + if debugLevel >= 2 { + log.Printf("discon-client: Using server path for input file: %s", serverPath) + } } else { - // Normal operation if no debug info - payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] + // Handle original path - with safety checks + if accInFile != nil && inFileSize > 0 { + if debugLevel >= 1 && inFilePath != "" { + log.Printf("discon-client: Input file not found locally: %s, continuing with original path", inFilePath) + } + payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] + } else { + // Ensure we have at least an empty byte array with a null terminator + payload.InFile = []byte{0} + } } // Fill the rest of the payload payload.Swap = swap[:swapSize:swapSize] payload.Fail = int32(*aviFail) - payload.OutName = (*[1 << 24]byte)(unsafe.Pointer(avcOutName))[:outNameSize:outNameSize] - payload.Msg = (*[1 << 24]byte)(unsafe.Pointer(avcMsg))[:msgSize:msgSize] + + // Safely handle output name and message with null checks + if avcOutName != nil && outNameSize > 0 { + payload.OutName = (*[1 << 24]byte)(unsafe.Pointer(avcOutName))[:outNameSize:outNameSize] + } else { + payload.OutName = []byte{0} + } + + if avcMsg != nil && msgSize > 0 { + payload.Msg = (*[1 << 24]byte)(unsafe.Pointer(avcMsg))[:msgSize:msgSize] + } else { + payload.Msg = []byte{0} + } // Reset file transfer fields to avoid sending unnecessary data payload.FileContent = nil From 5e7984a5c6c408060dcca0b6fb443db07329f8c3 Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Sat, 26 Apr 2025 21:13:37 -0600 Subject: [PATCH 4/6] Now can pass additionall files to server. Needed for ROSCO, Cp_Ct_Cq files --- discon-client/client.go | 118 ++++++++++++++++++++++++++++++++++++++-- discon-server/server.go | 2 +- 2 files changed, 115 insertions(+), 5 deletions(-) diff --git a/discon-client/client.go b/discon-client/client.go index 517c556..efd6c6f 100644 --- a/discon-client/client.go +++ b/discon-client/client.go @@ -20,7 +20,7 @@ import ( ) const program = "discon-client" -const version = "v0.1.0" +const version = "v0.2.0" var debugLevel int = 0 @@ -32,6 +32,18 @@ var recvSwapFile *os.File // GH-Cp gen: Map to store server-side file paths for transferred files var serverFilePaths = make(map[string]string) +// Added map to track if a file is the primary input file +var isPrimaryInputFile = make(map[string]bool) + +// Added map for storing file content replacements +var fileContentReplacements = make(map[string][]struct { + Original string + Replaced string +}) + +// Track if additional files have been processed +var additionalFilesProcessed bool = false + // GH-Cp gen: Function to check if a file exists func fileExists(filename string) bool { // GH-Cp gen: Added nil/empty check to prevent segmentation fault @@ -66,13 +78,81 @@ func readFileContents(filePath string) ([]byte, error) { return content, nil } +// Process the DISCON_ADDITIONAL_FILES environment variable +func processAdditionalFiles() error { + additionalFilesStr, found := os.LookupEnv("DISCON_ADDITIONAL_FILES") + if (!found || additionalFilesStr == "") { + if debugLevel >= 1 { + log.Println("discon-client: No DISCON_ADDITIONAL_FILES specified") + } + return nil + } + + // Split the semicolon-separated list + additionalFiles := strings.Split(additionalFilesStr, ";") + if debugLevel >= 1 { + log.Printf("discon-client: Processing %d additional files", len(additionalFiles)) + } + + // Process all additional files + for _, filePath := range additionalFiles { + filePath = strings.TrimSpace(filePath) + if filePath == "" { + continue + } + + if !fileExists(filePath) { + return fmt.Errorf("additional file does not exist: %s", filePath) + } + + // Send the file but don't track errors - we'll collect and report them later + serverPath, err := sendFileToServer(filePath) + if err != nil { + return fmt.Errorf("failed to send additional file %s: %w", filePath, err) + } + + if debugLevel >= 1 { + log.Printf("discon-client: Additional file %s transferred to server at %s", filePath, serverPath) + } + } + + return nil +} + +// Function to update file references in a content buffer +func updateFileReferences(content []byte) []byte { + contentStr := string(content) + + // Go through each file that might need replacement + for localPath, serverPath := range serverFilePaths { + // Skip the primary input file itself + if isPrimaryInputFile[localPath] { + continue + } + + // Also try to replace just the filename (in case only the filename is referenced) + // but only if it's not already a server path (doesn't start with "input_") + localFilename := filepath.Base(localPath) + serverFilename := filepath.Base(serverPath) + + // Only replace the filename if it's not already a server path (doesn't start with "input_") + if !strings.HasPrefix(localFilename, "input_") { + // Replace only whole words to avoid partial replacements within other words + contentStr = strings.ReplaceAll(contentStr, localFilename, serverFilename) + } + + if debugLevel >= 2 { + log.Printf("discon-client: Replaced references from %s to %s in input file", localFilename, serverFilename) + } + } + + return []byte(contentStr) +} + // GH-Cp gen: Function to send file to server and get server path func sendFileToServer(filePath string) (string, error) { // Check if we've already sent this file if serverPath, exists := serverFilePaths[filePath]; exists { - // if debugLevel >= 1 { - // log.Printf("discon-client: Using cached server path for %s: %s", filePath, serverPath) - // } return serverPath, nil } @@ -81,6 +161,17 @@ func sendFileToServer(filePath string) (string, error) { if err != nil { return "", err } + + // If this is the primary input file and we have additional files transferred, + // update references to those files in the content + if isPrimaryInputFile[filePath] && len(serverFilePaths) > 0 { + + if debugLevel >= 1 { + log.Printf("discon-client: Updating file references in content for %s", filePath) + } + + content = updateFileReferences(content) + } // Create a unique filename using SHA-256 hash of content and original filename hash := sha256.New() @@ -205,6 +296,7 @@ func init() { log.Println("discon-client: DISCON_LIB_PATH=", disconPath) log.Println("discon-client: DISCON_LIB_PROC=", disconFunc) log.Println("discon-client: DISCON_CLIENT_DEBUG=", debugLevel) + log.Println("discon-client: DISCON_ADDITIONAL_FILES=", os.Getenv("DISCON_ADDITIONAL_FILES")) } // Create a URL object @@ -266,12 +358,30 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c // Remove any null terminators from the end of the string inFilePath = strings.TrimRight(inFilePath, "\x00") } + + // Process additional files before handling the main input file (but only once) + if !additionalFilesProcessed { + // Process any additional files specified via environment variable + if err := processAdditionalFiles(); err != nil { + log.Printf("discon-client: Error processing additional files: %v", err) + // Set failure flag + *aviFail = C.int(1) + // Set error message + errMsg := fmt.Sprintf("Additional files transfer failed: %v", err) + copy((*[1 << 24]byte)(unsafe.Pointer(avcMsg))[:msgSize], []byte(errMsg)) + return + } + additionalFilesProcessed = true + } // GH-Cp gen: Check if the input file exists locally and transfer it to server if needed if inFilePath != "" && fileExists(inFilePath) { if debugLevel >= 2 { log.Printf("discon-client: Input file found locally: %s", inFilePath) } + + // Mark this as the primary input file + isPrimaryInputFile[inFilePath] = true // Transfer file to server and get the server-side path serverPath, err := sendFileToServer(inFilePath) diff --git a/discon-server/server.go b/discon-server/server.go index 46da406..12b6b1a 100644 --- a/discon-server/server.go +++ b/discon-server/server.go @@ -8,7 +8,7 @@ import ( ) const program = "discon-server" -const version = "v0.1.0" +const version = "v0.2.0" // GH-Cp gen: Using an int for debug levels instead of boolean var debugLevel int = 0 From c8c4da91a8770787b7e71f014fe5d2082bf25bde Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Sat, 26 Apr 2025 22:14:33 -0600 Subject: [PATCH 5/6] refactoring to remove redundant code --- discon-client/client.go | 183 +++++++++++-------------------------- discon-server/websocket.go | 175 ++++++++--------------------------- shared/utils/file.go | 107 ++++++++++++++++++++++ shared/utils/logging.go | 51 +++++++++++ shared/utils/transfer.go | 123 +++++++++++++++++++++++++ 5 files changed, 372 insertions(+), 267 deletions(-) create mode 100644 shared/utils/file.go create mode 100644 shared/utils/logging.go create mode 100644 shared/utils/transfer.go diff --git a/discon-client/client.go b/discon-client/client.go index efd6c6f..119e725 100644 --- a/discon-client/client.go +++ b/discon-client/client.go @@ -3,11 +3,8 @@ package main import "C" import ( - "crypto/sha256" dw "discon-wrapper" - "encoding/hex" "fmt" - "io" "log" "net/url" "os" @@ -16,6 +13,9 @@ import ( "strings" "unsafe" + // GH-Cp gen: Import shared utilities + "discon-wrapper/shared/utils" + "github.com/gorilla/websocket" ) @@ -44,55 +44,20 @@ var fileContentReplacements = make(map[string][]struct { // Track if additional files have been processed var additionalFilesProcessed bool = false -// GH-Cp gen: Function to check if a file exists -func fileExists(filename string) bool { - // GH-Cp gen: Added nil/empty check to prevent segmentation fault - if filename == "" { - return false - } - - info, err := os.Stat(filename) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -// GH-Cp gen: Function to read file contents -func readFileContents(filePath string) ([]byte, error) { - if !fileExists(filePath) { - return nil, fmt.Errorf("file does not exist: %s", filePath) - } - - file, err := os.Open(filePath) - if err != nil { - return nil, fmt.Errorf("error opening file: %w", err) - } - defer file.Close() - - content, err := io.ReadAll(file) - if err != nil { - return nil, fmt.Errorf("error reading file: %w", err) - } - - return content, nil -} +// GH-Cp gen: Logger for client operations +var logger *utils.DebugLogger // Process the DISCON_ADDITIONAL_FILES environment variable func processAdditionalFiles() error { additionalFilesStr, found := os.LookupEnv("DISCON_ADDITIONAL_FILES") if (!found || additionalFilesStr == "") { - if debugLevel >= 1 { - log.Println("discon-client: No DISCON_ADDITIONAL_FILES specified") - } + logger.Debug("No DISCON_ADDITIONAL_FILES specified") return nil } // Split the semicolon-separated list additionalFiles := strings.Split(additionalFilesStr, ";") - if debugLevel >= 1 { - log.Printf("discon-client: Processing %d additional files", len(additionalFiles)) - } + logger.Debug("Processing %d additional files", len(additionalFiles)) // Process all additional files for _, filePath := range additionalFiles { @@ -101,7 +66,7 @@ func processAdditionalFiles() error { continue } - if !fileExists(filePath) { + if !utils.FileExists(filePath) { return fmt.Errorf("additional file does not exist: %s", filePath) } @@ -111,9 +76,7 @@ func processAdditionalFiles() error { return fmt.Errorf("failed to send additional file %s: %w", filePath, err) } - if debugLevel >= 1 { - log.Printf("discon-client: Additional file %s transferred to server at %s", filePath, serverPath) - } + logger.Debug("Additional file %s transferred to server at %s", filePath, serverPath) } return nil @@ -141,15 +104,13 @@ func updateFileReferences(content []byte) []byte { contentStr = strings.ReplaceAll(contentStr, localFilename, serverFilename) } - if debugLevel >= 2 { - log.Printf("discon-client: Replaced references from %s to %s in input file", localFilename, serverFilename) - } + logger.Verbose("Replaced references from %s to %s in input file", localFilename, serverFilename) } return []byte(contentStr) } -// GH-Cp gen: Function to send file to server and get server path +// GH-Cp gen: Function to send file to server and get server path - refactored to use shared utilities func sendFileToServer(filePath string) (string, error) { // Check if we've already sent this file if serverPath, exists := serverFilePaths[filePath]; exists { @@ -157,7 +118,7 @@ func sendFileToServer(filePath string) (string, error) { } // Read the file contents - content, err := readFileContents(filePath) + content, err := utils.ReadFileContents(filePath) if err != nil { return "", err } @@ -165,86 +126,58 @@ func sendFileToServer(filePath string) (string, error) { // If this is the primary input file and we have additional files transferred, // update references to those files in the content if isPrimaryInputFile[filePath] && len(serverFilePaths) > 0 { - - if debugLevel >= 1 { - log.Printf("discon-client: Updating file references in content for %s", filePath) - } - + logger.Debug("Updating file references in content for %s", filePath) content = updateFileReferences(content) } - // Create a unique filename using SHA-256 hash of content and original filename - hash := sha256.New() - hash.Write(content) - hash.Write([]byte(filepath.Base(filePath))) - hashString := hex.EncodeToString(hash.Sum(nil)) - - // Generate a server path - use just the filename with a unique prefix - serverPath := fmt.Sprintf("input_%s_%s", hashString[:8], filepath.Base(filePath)) - - // Create a file transfer payload - fileTransferPayload := dw.Payload{ - // Initialize required fields with empty values - Swap: make([]float32, 1), - Fail: 0, - InFile: []byte{0}, - OutName: []byte{0}, - Msg: []byte{0}, - FileContent: content, - ServerFilePath: []byte(serverPath + "\x00"), - } + // Generate a server path using shared utility + serverPath := utils.GenerateServerFilePath(content, filePath) - if debugLevel >= 1 { - log.Printf("discon-client: Sending file %s to server (size: %d bytes)", filePath, len(content)) - } + // Create a file transfer payload using shared utility + fileTransferPayload := utils.CreateFileTransferPayload(content, serverPath) + + logger.Debug("Sending file %s to server (size: %d bytes)", filePath, len(content)) // Send the file transfer payload to the server b, err := fileTransferPayload.MarshalBinary() if err != nil { - return "", fmt.Errorf("error marshaling file transfer payload: %w", err) + return "", utils.FormatError("marshaling file transfer payload", err) } err = ws.WriteMessage(websocket.BinaryMessage, b) if err != nil { - return "", fmt.Errorf("error sending file to server: %w", err) + return "", utils.FormatError("sending file to server", err) } // Wait for server response _, resp, err := ws.ReadMessage() if err != nil { - return "", fmt.Errorf("error receiving server response: %w", err) + return "", utils.FormatError("receiving server response", err) } // Unmarshal the response var responsePayload dw.Payload err = responsePayload.UnmarshalBinary(resp) if err != nil { - return "", fmt.Errorf("error unmarshaling server response: %w", err) + return "", utils.FormatError("unmarshaling server response", err) } // Check if the file transfer succeeded if responsePayload.Fail != 0 { - // Get the error message from the Msg field - errMsg := string(responsePayload.Msg) - i0 := strings.IndexByte(errMsg, 0) - if i0 >= 0 { - errMsg = errMsg[:i0] - } + // Get the error message from the Msg field using shared utility + errMsg := utils.GetErrorMessageFromPayload(&responsePayload) return "", fmt.Errorf("file transfer failed: %s", errMsg) } // Store the server path for future use serverFilePaths[filePath] = serverPath - if debugLevel >= 1 { - log.Printf("discon-client: File %s transferred successfully to server at %s", filePath, serverPath) - } + logger.Debug("File %s transferred successfully to server at %s", filePath, serverPath) return serverPath, nil } func init() { - // Print info fmt.Println("Loaded", program, version) @@ -273,6 +206,9 @@ func init() { } } + // GH-Cp gen: Initialize the logger + logger = utils.NewDebugLogger(debugLevel, "discon-client") + // Get discon-server address from environment variable serverAddr, found := os.LookupEnv("DISCON_SERVER_ADDR") if !found { @@ -291,13 +227,11 @@ func init() { log.Fatal("discon-client: environment variable DISCON_LIB_PROC not set (e.g. 'discon')") } - if debugLevel >= 1 { - log.Println("discon-client: DISCON_SERVER_ADDR=", serverAddr) - log.Println("discon-client: DISCON_LIB_PATH=", disconPath) - log.Println("discon-client: DISCON_LIB_PROC=", disconFunc) - log.Println("discon-client: DISCON_CLIENT_DEBUG=", debugLevel) - log.Println("discon-client: DISCON_ADDITIONAL_FILES=", os.Getenv("DISCON_ADDITIONAL_FILES")) - } + logger.Debug("DISCON_SERVER_ADDR= %s", serverAddr) + logger.Debug("DISCON_LIB_PATH= %s", disconPath) + logger.Debug("DISCON_LIB_PROC= %s", disconFunc) + logger.Debug("DISCON_CLIENT_DEBUG= %d", debugLevel) + logger.Debug("DISCON_ADDITIONAL_FILES= %s", os.Getenv("DISCON_ADDITIONAL_FILES")) // Create a URL object u, err := url.Parse(fmt.Sprintf("ws://%s/ws", serverAddr)) @@ -308,9 +242,7 @@ func init() { // Add query parameters for shared library path and proc u.RawQuery = url.Values{"path": {disconPath}, "proc": {disconFunc}}.Encode() - if debugLevel >= 1 { - log.Printf("discon-client: connecting to discon-server at '%s'\n", u.String()) - } + logger.Debug("connecting to discon-server at '%s'", u.String()) // Connect to websocket server ws, _, err = websocket.DefaultDialer.Dial(u.String(), nil) @@ -318,9 +250,7 @@ func init() { log.Fatalf("discon-client: error connecting to discon-server at %s: %s", serverAddr, err) } - if debugLevel >= 1 { - log.Printf("discon-client: Connected to discon-server at '%s'\n", u.String()) - } + logger.Debug("Connected to discon-server at '%s'", u.String()) } //export DISCON @@ -339,12 +269,10 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c payload.Swap = make([]float32, swapSize) } - if debugLevel >= 2 { - log.Printf("discon-client: size of avrSWAP: % 5d\n", swapSize) - log.Printf("discon-client: size of accINFILE: % 5d\n", inFileSize) - log.Printf("discon-client: size of avcOUTNAME: % 5d\n", outNameSize) - log.Printf("discon-client: size of avcMSG: % 5d\n", msgSize) - } + logger.Verbose("size of avrSWAP: % 5d", swapSize) + logger.Verbose("size of accINFILE: % 5d", inFileSize) + logger.Verbose("size of avcOUTNAME: % 5d", outNameSize) + logger.Verbose("size of avcMSG: % 5d", msgSize) // GH-Cp gen: Get the input file path from accInFile with safer handling var inFilePath string @@ -356,14 +284,14 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c } inFilePath = string((*[1 << 24]byte)(unsafe.Pointer(accInFile))[:safeSize]) // Remove any null terminators from the end of the string - inFilePath = strings.TrimRight(inFilePath, "\x00") + inFilePath = utils.SafeTrimString(inFilePath) } // Process additional files before handling the main input file (but only once) if !additionalFilesProcessed { // Process any additional files specified via environment variable if err := processAdditionalFiles(); err != nil { - log.Printf("discon-client: Error processing additional files: %v", err) + logger.Error("Error processing additional files: %v", err) // Set failure flag *aviFail = C.int(1) // Set error message @@ -375,10 +303,8 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c } // GH-Cp gen: Check if the input file exists locally and transfer it to server if needed - if inFilePath != "" && fileExists(inFilePath) { - if debugLevel >= 2 { - log.Printf("discon-client: Input file found locally: %s", inFilePath) - } + if inFilePath != "" && utils.FileExists(inFilePath) { + logger.Verbose("Input file found locally: %s", inFilePath) // Mark this as the primary input file isPrimaryInputFile[inFilePath] = true @@ -386,7 +312,7 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c // Transfer file to server and get the server-side path serverPath, err := sendFileToServer(inFilePath) if err != nil { - log.Printf("discon-client: Error transferring file %s: %v", inFilePath, err) + logger.Error("Error transferring file %s: %v", inFilePath, err) // Set failure flag *aviFail = C.int(1) // Set error message @@ -405,14 +331,12 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c // Update the inFileSize in the swap array swap[49] = float32(len(serverPathBytes)) - if debugLevel >= 2 { - log.Printf("discon-client: Using server path for input file: %s", serverPath) - } + logger.Verbose("Using server path for input file: %s", serverPath) } else { // Handle original path - with safety checks if accInFile != nil && inFileSize > 0 { - if debugLevel >= 1 && inFilePath != "" { - log.Printf("discon-client: Input file not found locally: %s, continuing with original path", inFilePath) + if inFilePath != "" { + logger.Debug("Input file not found locally: %s, continuing with original path", inFilePath) } payload.InFile = (*[1 << 24]byte)(unsafe.Pointer(accInFile))[:inFileSize:inFileSize] } else { @@ -442,6 +366,9 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c payload.FileContent = nil payload.ServerFilePath = nil + // GH-Cp gen: Ensure payload is properly formatted + utils.PreparePayloadForTransmission(&payload) + // Convert payload to binary and send over websocket b, err := payload.MarshalBinary() if err != nil { @@ -449,9 +376,7 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c } ws.WriteMessage(websocket.BinaryMessage, b) - if debugLevel >= 2 { - log.Println("discon-client: sent payload:\n", payload) - } + logger.Verbose("sent payload: %v", payload) if debugLevel >= 1 && sentSwapFile != nil { outSwapSize := min(swapSize, 163) @@ -473,9 +398,7 @@ func DISCON(avrSwap *C.float, aviFail *C.int, accInFile, avcOutName, avcMsg *C.c log.Fatalf("discon-client: %s", err) } - if debugLevel >= 2 { - log.Println("discon-client: received payload:\n", payload) - } + logger.Verbose("received payload: %v", payload) if debugLevel >= 1 && recvSwapFile != nil { outSwapSize := min(swapSize, 163) diff --git a/discon-server/websocket.go b/discon-server/websocket.go index 11aceb8..24b9502 100644 --- a/discon-server/websocket.go +++ b/discon-server/websocket.go @@ -6,21 +6,19 @@ package main // void unload_shared_library(int connID); import "C" import ( - "crypto/sha256" dw "discon-wrapper" - "encoding/hex" "fmt" - "io" "log" "net/http" "net/url" "os" - "path/filepath" - "strings" "sync" "time" "unsafe" + // GH-Cp gen: Use the shared utilities package + "discon-wrapper/shared/utils" + "github.com/gorilla/websocket" ) @@ -39,87 +37,41 @@ var wg sync.WaitGroup var tempFiles = make(map[int32][]string) var tempFilesMutex sync.Mutex -// GH-Cp gen: Function to check if data is a file transfer -func isFileTransfer(payload *dw.Payload) bool { - return len(payload.FileContent) > 0 && len(payload.ServerFilePath) > 0 -} - -// GH-Cp gen: Function to validate filename for security -func validateFileName(filename string) error { - // Remove null terminators - filename = strings.ReplaceAll(filename, "\x00", "") - - // Check if filename contains suspicious patterns - suspiciousPatterns := []string{"../", "/..", "~", "$", "|", ";", "&", "\\"} - for _, pattern := range suspiciousPatterns { - if strings.Contains(filename, pattern) { - return fmt.Errorf("filename contains invalid pattern: %s", pattern) - } - } - - // Ensure filename is just a basename, not a path - if filepath.Base(filename) != filename { - return fmt.Errorf("filename must not contain path separators") - } - - return nil -} - -// GH-Cp gen: Handle file transfer from client to server -func handleFileTransfer(connID int32, payload *dw.Payload, debugLevel int) (*dw.Payload, error) { - // Initialize response payload - response := dw.Payload{ - Swap: make([]float32, 1), - Fail: 0, - InFile: []byte{0}, - OutName: []byte{0}, - Msg: make([]byte, 256), // Reserve space for error message - } - +// GH-Cp gen: Handle file transfer from client to server - updated to use shared utilities +func handleFileTransfer(connID int32, payload *dw.Payload, logger *utils.DebugLogger) (*dw.Payload, error) { // Get server file path from payload - serverFilePath := string(payload.ServerFilePath) - nullIndex := strings.IndexByte(serverFilePath, 0) - if nullIndex >= 0 { - serverFilePath = serverFilePath[:nullIndex] - } + serverFilePath := utils.ExtractStringFromBytes(payload.ServerFilePath) // Validate filename for security - err := validateFileName(serverFilePath) + err := utils.ValidateFileName(serverFilePath) if err != nil { - response.Fail = 1 errMsg := fmt.Sprintf("Security error: %v", err) - copy(response.Msg, []byte(errMsg+"\x00")) - return &response, fmt.Errorf("file validation error: %w", err) + response := utils.CreateFileTransferResponse(false, errMsg) + return response, fmt.Errorf("file validation error: %w", err) } // Verify file contents with hash - hash := sha256.New() - hash.Write(payload.FileContent) - contentHash := hex.EncodeToString(hash.Sum(nil)) - - if debugLevel >= 1 { - log.Printf("Received file transfer request for %s (size: %d bytes, hash: %s)", - serverFilePath, len(payload.FileContent), contentHash[:8]) - } + contentHash := utils.ComputeFileHash(payload.FileContent) + + logger.Debug("Received file transfer request for %s (size: %d bytes, hash: %s)", + serverFilePath, len(payload.FileContent), contentHash[:8]) // Create the file with the server file path file, err := os.Create(serverFilePath) if err != nil { - response.Fail = 1 errMsg := fmt.Sprintf("Failed to create file: %v", err) - copy(response.Msg, []byte(errMsg+"\x00")) - return &response, fmt.Errorf("failed to create file: %w", err) + response := utils.CreateFileTransferResponse(false, errMsg) + return response, fmt.Errorf("failed to create file: %w", err) } defer file.Close() // Write the file contents _, err = file.Write(payload.FileContent) if err != nil { - response.Fail = 1 errMsg := fmt.Sprintf("Failed to write file: %v", err) - copy(response.Msg, []byte(errMsg+"\x00")) + response := utils.CreateFileTransferResponse(false, errMsg) os.Remove(serverFilePath) // Clean up the partial file - return &response, fmt.Errorf("failed to write file: %w", err) + return response, fmt.Errorf("failed to write file: %w", err) } // Register the file to be cleaned up when the connection closes @@ -127,18 +79,17 @@ func handleFileTransfer(connID int32, payload *dw.Payload, debugLevel int) (*dw. tempFiles[connID] = append(tempFiles[connID], serverFilePath) tempFilesMutex.Unlock() - if debugLevel >= 1 { - log.Printf("File %s created successfully", serverFilePath) - } + logger.Debug("File %s created successfully", serverFilePath) - // Set success message + // Create success response successMsg := fmt.Sprintf("File transferred successfully: %s", serverFilePath) - copy(response.Msg, []byte(successMsg+"\x00")) - - return &response, nil + return utils.CreateFileTransferResponse(true, successMsg), nil } func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { + // GH-Cp gen: Create a logger for this connection + logger := utils.NewDebugLogger(debugLevel, "discon-server") + wg.Add(1) defer wg.Done() @@ -160,29 +111,23 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { path := params.Get("path") proc := params.Get("proc") - if debugLevel >= 1 { - log.Printf("Received request to load function '%s' from shared controller '%s'\n", proc, path) - } + logger.Debug("Received request to load function '%s' from shared controller '%s'", proc, path) // Check if controller exists at path - _, err = os.Stat(path) - if os.IsNotExist(err) { + if !utils.FileExists(path) { http.Error(w, "Controller not found at '"+path+"'", http.StatusInternalServerError) return } - // Create a copy of the shared library with a number suffix so multiple instances - // of the same library can be shared at the same time - tmpPath, err := duplicateLibrary(path, connID) + // Create a copy of the shared library with a unique suffix + tmpPath, err := utils.CreateTempFile(path, connID) if err != nil { http.Error(w, "Error duplicating controller: "+err.Error(), http.StatusInternalServerError) return } defer os.Remove(tmpPath) - if debugLevel >= 1 { - log.Printf("Duplicated controller to '%s'\n", tmpPath) - } + logger.Debug("Duplicated controller to '%s'", tmpPath) // GH-Cp gen: Initialize tempFiles entry for this connection tempFilesMutex.Lock() @@ -198,9 +143,7 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { // Remove all temporary files for this connection for _, filePath := range fileList { - if debugLevel >= 1 { - log.Printf("Cleaning up temporary file: %s", filePath) - } + logger.Debug("Cleaning up temporary file: %s", filePath) os.Remove(filePath) } }() @@ -219,9 +162,7 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { return } - if debugLevel >= 1 { - log.Printf("Library and function loaded successfully\n") - } + logger.Debug("Library and function loaded successfully") // Convert connection to a websocket ws, err := upgrader.Upgrade(w, r, nil) @@ -236,7 +177,6 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { // Loop while receiving messages over socket for { - // If not in debug mode, set read deadline to 5 seconds // This will disconnect the client if no message is received in 5 seconds // which allows the controller to be unloaded and deleted @@ -262,24 +202,14 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { break } - // GH-Cp gen: Only log full payload at debug level 2 - if debugLevel >= 2 { - log.Println("discon-server: received payload:", payload) - // } else if debugLevel == 1 { - // // At level 1, just log basic info without full payload details - // inFilePath := string(payload.InFile) - // nullIndex := strings.IndexByte(inFilePath, 0) - // if nullIndex >= 0 { - // inFilePath = inFilePath[:nullIndex] - // } - // log.Printf("discon-server: received request with InFile: %s", inFilePath) - } + // GH-Cp gen: Log received payload using the logger + logger.Verbose("received payload: %v", payload) - // GH-Cp gen: Check if the payload is a file transfer - if isFileTransfer(&payload) { - response, err := handleFileTransfer(connID, &payload, debugLevel) + // GH-Cp gen: Check if the payload is a file transfer using shared utility + if utils.IsFileTransfer(&payload) { + response, err := handleFileTransfer(connID, &payload, logger) if err != nil { - log.Println("handleFileTransfer:", err) + logger.Error("handleFileTransfer: %v", err) } b, err = response.MarshalBinary() if err != nil { @@ -314,39 +244,10 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { break } - // GH-Cp gen: Only log full response at debug level 2 - if debugLevel >= 2 { - log.Println("discon-server: sent payload:", payload) - // } else if debugLevel == 1 { - // // At level 1, just log that response was sent - // log.Println("discon-server: sent response") - } + // GH-Cp gen: Log sent payload using the logger + logger.Verbose("sent payload: %v", payload) } // Unload the shared library C.unload_shared_library(C.int(connID)) } - -func duplicateLibrary(path string, connID int32) (string, error) { - // Create a copy of the shared library with a number suffix so multiple instances - // of the same library can be shared at the same time - outFile, err := os.CreateTemp(".", fmt.Sprintf("%s-%03d-", filepath.Base(path), connID)) - if err != nil { - return "", err - } - defer outFile.Close() - - inFile, err := os.Open(path) - if err != nil { - return "", err - } - defer inFile.Close() - - // Copy the file contents - _, err = io.Copy(outFile, inFile) - if err != nil { - return "", err - } - - return outFile.Name(), nil -} diff --git a/shared/utils/file.go b/shared/utils/file.go new file mode 100644 index 0000000..9c7655c --- /dev/null +++ b/shared/utils/file.go @@ -0,0 +1,107 @@ +// Package utils provides shared utilities for both client and server +package utils + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// GH-Cp gen: FileExists checks if a file exists and is not a directory +func FileExists(filename string) bool { + // GH-Cp gen: Added nil/empty check to prevent segmentation fault + if filename == "" { + return false + } + + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +// GH-Cp gen: ReadFileContents reads the entire content of a file +func ReadFileContents(filePath string) ([]byte, error) { + if !FileExists(filePath) { + return nil, fmt.Errorf("file does not exist: %s", filePath) + } + + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return nil, fmt.Errorf("error reading file: %w", err) + } + + return content, nil +} + +// GH-Cp gen: ValidateFileName checks if a filename is safe and doesn't contain suspicious patterns +func ValidateFileName(filename string) error { + // Remove null terminators + filename = strings.ReplaceAll(filename, "\x00", "") + + // Check if filename contains suspicious patterns + suspiciousPatterns := []string{"../", "/..", "~", "$", "|", ";", "&", "\\"} + for _, pattern := range suspiciousPatterns { + if strings.Contains(filename, pattern) { + return fmt.Errorf("filename contains invalid pattern: %s", pattern) + } + } + + // Ensure filename is just a basename, not a path + if filepath.Base(filename) != filename { + return fmt.Errorf("filename must not contain path separators") + } + + return nil +} + +// GH-Cp gen: GenerateServerFilePath creates a unique server-side path for a file +func GenerateServerFilePath(content []byte, originalFilename string) string { + // Create a unique filename using SHA-256 hash of content and original filename + hash := sha256.New() + hash.Write(content) + hash.Write([]byte(filepath.Base(originalFilename))) + hashString := hex.EncodeToString(hash.Sum(nil)) + + // Generate a server path - use just the filename with a unique prefix + return fmt.Sprintf("input_%s_%s", hashString[:8], filepath.Base(originalFilename)) +} + +// GH-Cp gen: SafeTrimString removes null terminators from the end of a string +func SafeTrimString(s string) string { + return strings.TrimRight(s, "\x00") +} + +// GH-Cp gen: CreateTempFile creates a temp file with a unique name based on original path and connID +func CreateTempFile(originalPath string, connID int32) (string, error) { + outFile, err := os.CreateTemp(".", fmt.Sprintf("%s-%03d-", filepath.Base(originalPath), connID)) + if err != nil { + return "", err + } + defer outFile.Close() + + inFile, err := os.Open(originalPath) + if err != nil { + return "", err + } + defer inFile.Close() + + // Copy the file contents + _, err = io.Copy(outFile, inFile) + if err != nil { + return "", err + } + + return outFile.Name(), nil +} \ No newline at end of file diff --git a/shared/utils/logging.go b/shared/utils/logging.go new file mode 100644 index 0000000..4abd293 --- /dev/null +++ b/shared/utils/logging.go @@ -0,0 +1,51 @@ +// Package utils provides shared utilities for both client and server +package utils + +import ( + "log" +) + +// GH-Cp gen: DebugLogger provides a standardized logging interface +// that respects debug levels for both client and server +type DebugLogger struct { + DebugLevel int + Prefix string +} + +// GH-Cp gen: NewDebugLogger creates a new DebugLogger with the specified debug level and prefix +func NewDebugLogger(debugLevel int, prefix string) *DebugLogger { + return &DebugLogger{ + DebugLevel: debugLevel, + Prefix: prefix, + } +} + +// GH-Cp gen: LogAtLevel logs a message if the current debug level is at least the specified level +func (dl *DebugLogger) LogAtLevel(level int, format string, v ...interface{}) { + if dl.DebugLevel >= level { + if dl.Prefix != "" { + format = dl.Prefix + ": " + format + } + log.Printf(format, v...) + } +} + +// GH-Cp gen: Debug logs at debug level 1 (basic info) +func (dl *DebugLogger) Debug(format string, v ...interface{}) { + dl.LogAtLevel(1, format, v...) +} + +// GH-Cp gen: Verbose logs at debug level 2 (verbose with payloads) +func (dl *DebugLogger) Verbose(format string, v ...interface{}) { + dl.LogAtLevel(2, format, v...) +} + +// GH-Cp gen: Error logs an error regardless of debug level +func (dl *DebugLogger) Error(format string, v ...interface{}) { + if dl.Prefix != "" { + format = dl.Prefix + ": ERROR: " + format + } else { + format = "ERROR: " + format + } + log.Printf(format, v...) +} \ No newline at end of file diff --git a/shared/utils/transfer.go b/shared/utils/transfer.go new file mode 100644 index 0000000..852ea19 --- /dev/null +++ b/shared/utils/transfer.go @@ -0,0 +1,123 @@ +// Package utils provides shared utilities for both client and server +package utils + +import ( + "crypto/sha256" + dw "discon-wrapper" + "encoding/hex" + "fmt" + "strings" +) + +// GH-Cp gen: FileTransferResult holds the result of a file transfer operation +type FileTransferResult struct { + Success bool + ServerPath string + ErrorMessage string +} + +// GH-Cp gen: IsFileTransfer checks if a payload represents a file transfer +func IsFileTransfer(payload *dw.Payload) bool { + return len(payload.FileContent) > 0 && len(payload.ServerFilePath) > 0 +} + +// GH-Cp gen: CreateFileTransferPayload creates a payload for file transfer +func CreateFileTransferPayload(fileContent []byte, serverFilePath string) *dw.Payload { + return &dw.Payload{ + // Initialize required fields with empty values + Swap: make([]float32, 1), + Fail: 0, + InFile: []byte{0}, + OutName: []byte{0}, + Msg: []byte{0}, + FileContent: fileContent, + ServerFilePath: []byte(serverFilePath + "\x00"), + } +} + +// GH-Cp gen: CreateFileTransferResponse creates a response payload for a file transfer +func CreateFileTransferResponse(success bool, message string) *dw.Payload { + response := &dw.Payload{ + Swap: make([]float32, 1), + Fail: 0, + InFile: []byte{0}, + OutName: []byte{0}, + Msg: make([]byte, 256), // Reserve space for message + } + + if !success { + response.Fail = 1 + } + + // Ensure null termination + if !strings.HasSuffix(message, "\x00") { + message += "\x00" + } + + // Copy message to Msg field + copy(response.Msg, []byte(message)) + + return response +} + +// GH-Cp gen: ExtractStringFromBytes extracts a null-terminated string from a byte array +func ExtractStringFromBytes(data []byte) string { + nullIndex := strings.IndexByte(string(data), 0) + if nullIndex >= 0 { + return string(data[:nullIndex]) + } + return string(data) +} + +// GH-Cp gen: ComputeFileHash computes a SHA256 hash of file content +func ComputeFileHash(content []byte) string { + hash := sha256.New() + hash.Write(content) + return hex.EncodeToString(hash.Sum(nil)) +} + +// GH-Cp gen: GetErrorMessageFromPayload extracts an error message from a payload +func GetErrorMessageFromPayload(payload *dw.Payload) string { + if payload == nil { + return "No payload received" + } + + errMsg := string(payload.Msg) + nullIndex := strings.IndexByte(errMsg, 0) + if nullIndex >= 0 { + errMsg = errMsg[:nullIndex] + } + return errMsg +} + +// GH-Cp gen: PreparePayloadForTransmission ensures a payload is properly formatted +func PreparePayloadForTransmission(payload *dw.Payload) error { + // Check required fields + if payload.Swap == nil || len(payload.Swap) == 0 { + payload.Swap = make([]float32, 1) + } + + // Ensure all string fields are null-terminated + if len(payload.InFile) == 0 || payload.InFile[len(payload.InFile)-1] != 0 { + payload.InFile = append(payload.InFile, 0) + } + + if len(payload.OutName) == 0 || payload.OutName[len(payload.OutName)-1] != 0 { + payload.OutName = append(payload.OutName, 0) + } + + if len(payload.Msg) == 0 || payload.Msg[len(payload.Msg)-1] != 0 { + payload.Msg = append(payload.Msg, 0) + } + + if len(payload.ServerFilePath) > 0 && payload.ServerFilePath[len(payload.ServerFilePath)-1] != 0 { + payload.ServerFilePath = append(payload.ServerFilePath, 0) + } + + return nil +} + +// GH-Cp gen: FormatError creates a formatted error message for file transfer errors +func FormatError(operation string, err error) error { + return fmt.Errorf("%s failed: %w", operation, err) +} \ No newline at end of file From c54b966e0820e8c70dba47fc587dd54c3aa03899 Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Sat, 26 Apr 2025 22:55:19 -0600 Subject: [PATCH 6/6] Controller across the internet works nowgit status! --- README.md | 6 +++++- discon-client/client.go | 31 ++++++++++++++++++++++++++++--- discon-server/server.go | 28 +++++++++++++++++++++++++++- discon-server/websocket.go | 25 +++++++++++++++---------- shared/utils/logging.go | 38 ++++++++++++++++++++++++++++++++------ 5 files changed, 107 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b2a86a9..f17e0dc 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,11 @@ set DISCON_CLIENT_DEBUG=1 openfast.exe my_turbine.fst ``` -- `DISCON_SERVER_ADDR` describes the host and port the server is listening on. `localhost` indicates the same machine and the port, `8080`, needs to match the number that was given to the server via the `--port` argument. +- `DISCON_SERVER_ADDR` describes the server address in one of these formats: + - `hostname:port` (e.g. `localhost:8080`) - Standard WebSocket connection + - `domain.name` (e.g. `controller.company.com`) - For use with reverse proxies that handle the port internally + - `http://domain.name` - Explicit HTTP protocol, uses WebSocket (ws://) + - `https://domain.name` - Secure HTTPS protocol, uses secure WebSocket (wss://) - `DISCON_LIB_PATH` is the path from `discon-server_386.exe` to the controller shared library. - `DISCON_LIB_PROC` is the procedure which will be called in the controller shared library. - `DISCON_CLIENT_DEBUG` is used to enable debugging output on the client side, messages will be printed to the terminal. diff --git a/discon-client/client.go b/discon-client/client.go index 119e725..98bc151 100644 --- a/discon-client/client.go +++ b/discon-client/client.go @@ -3,6 +3,7 @@ package main import "C" import ( + "crypto/tls" dw "discon-wrapper" "fmt" "log" @@ -212,7 +213,7 @@ func init() { // Get discon-server address from environment variable serverAddr, found := os.LookupEnv("DISCON_SERVER_ADDR") if !found { - log.Fatal("discon-client: environment variable DISCON_SERVER_ADDR not set (e.g. 'localhost:8080')") + log.Fatal("discon-client: environment variable DISCON_SERVER_ADDR not set (e.g. 'localhost:8080' or 'https://controller.domain.com')") } // Get shared library path from environment variable @@ -233,8 +234,23 @@ func init() { logger.Debug("DISCON_CLIENT_DEBUG= %d", debugLevel) logger.Debug("DISCON_ADDITIONAL_FILES= %s", os.Getenv("DISCON_ADDITIONAL_FILES")) + // Determine if we're using HTTPS/WSS based on the provided server address + var wsURL string + if strings.HasPrefix(strings.ToLower(serverAddr), "http://") { + // HTTP URL provided - use ws:// + serverAddr = strings.TrimPrefix(serverAddr, "http://") + wsURL = fmt.Sprintf("ws://%s/ws", serverAddr) + } else if strings.HasPrefix(strings.ToLower(serverAddr), "https://") { + // HTTPS URL provided - use wss:// + serverAddr = strings.TrimPrefix(serverAddr, "https://") + wsURL = fmt.Sprintf("wss://%s/ws", serverAddr) + } else { + // No protocol provided, assume ws:// (non-secure) + wsURL = fmt.Sprintf("ws://%s/ws", serverAddr) + } + // Create a URL object - u, err := url.Parse(fmt.Sprintf("ws://%s/ws", serverAddr)) + u, err := url.Parse(wsURL) if err != nil { log.Fatal(err) } @@ -245,7 +261,16 @@ func init() { logger.Debug("connecting to discon-server at '%s'", u.String()) // Connect to websocket server - ws, _, err = websocket.DefaultDialer.Dial(u.String(), nil) + dialer := websocket.DefaultDialer + // If using wss (secure WebSocket), we might need to skip certificate verification in some cases + if strings.HasPrefix(u.String(), "wss://") { + dialer.TLSClientConfig = &tls.Config{ + // For production, you should properly handle certificates + // InsecureSkipVerify: true, // Uncomment this line to skip certificate verification (not recommended for production) + } + } + + ws, _, err = dialer.Dial(u.String(), nil) if err != nil { log.Fatalf("discon-client: error connecting to discon-server at %s: %s", serverAddr, err) } diff --git a/discon-server/server.go b/discon-server/server.go index 12b6b1a..a79bfc9 100644 --- a/discon-server/server.go +++ b/discon-server/server.go @@ -1,10 +1,13 @@ package main import ( + "discon-wrapper/shared/utils" "flag" "fmt" "log" "net/http" + "os" + "time" ) const program = "discon-server" @@ -13,8 +16,11 @@ const version = "v0.2.0" // GH-Cp gen: Using an int for debug levels instead of boolean var debugLevel int = 0 var port = 8080 +var serverLogger *utils.DebugLogger func main() { + // Configure logging with timestamp, file and line number + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) // Display program and version log.Printf("Started %s %s", program, version) @@ -23,15 +29,35 @@ func main() { // GH-Cp gen: Updated to use debug levels flag.IntVar(&debugLevel, "debug", 0, "Debug level: 0=disabled, 1=basic info, 2=verbose with payloads") flag.Parse() + + // Create server-wide logger for non-connection-specific logs + serverLogger = utils.NewDebugLogger(debugLevel, "discon-server") + + serverLogger.Debug("Server initialized with debug level %d", debugLevel) + serverLogger.Debug("Hostname: %s", getHostname()) http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + serverLogger.Debug("New connection request from %s", r.RemoteAddr) + start := time.Now() ServeWs(w, r, debugLevel) + connectionDuration := time.Since(start) + serverLogger.Debug("Connection from %s closed after %v", r.RemoteAddr, connectionDuration) }) // Start server - log.Printf("Listening on port %d", port) + serverLogger.Debug("Listening on port %d", port) err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) if err != nil { + serverLogger.Error("ListenAndServe failed: %v", err) log.Fatal("ListenAndServe: ", err) } } + +// Helper function to get hostname for better log context +func getHostname() string { + hostname, err := os.Hostname() + if err != nil { + return "unknown-host" + } + return hostname +} diff --git a/discon-server/websocket.go b/discon-server/websocket.go index 24b9502..260b0f2 100644 --- a/discon-server/websocket.go +++ b/discon-server/websocket.go @@ -87,9 +87,6 @@ func handleFileTransfer(connID int32, payload *dw.Payload, logger *utils.DebugLo } func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { - // GH-Cp gen: Create a logger for this connection - logger := utils.NewDebugLogger(debugLevel, "discon-server") - wg.Add(1) defer wg.Done() @@ -102,6 +99,9 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { } connectionIDMutex.Unlock() + // GH-Cp gen: Create a connection-specific logger with the connection ID + logger := utils.NewConnectionLogger(debugLevel, "discon-server", connID) + // Read controller path and function name from post parameters params, err := url.ParseQuery(r.URL.RawQuery) if err != nil { @@ -172,6 +172,9 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { } defer ws.Close() + // Log client connection info + logger.Debug("New WebSocket connection established from %s", ws.RemoteAddr().String()) + // Create payload structure payload := dw.Payload{} @@ -187,18 +190,18 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { // Read message from websocket messageType, b, err := ws.ReadMessage() if err != nil { - log.Println("read:", err) + logger.Debug("WebSocket read error: %v", err) break } if messageType != websocket.BinaryMessage { - log.Println("message type:", messageType) + logger.Debug("Received non-binary message type: %d", messageType) continue } err = payload.UnmarshalBinary(b) if err != nil { - log.Println("payload.UnmarshalBinary:", err) + logger.Error("Failed to unmarshal payload: %v", err) break } @@ -213,12 +216,12 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { } b, err = response.MarshalBinary() if err != nil { - log.Println("response.MarshalBinary:", err) + logger.Error("Failed to marshal response: %v", err) break } err = ws.WriteMessage(websocket.BinaryMessage, b) if err != nil { - log.Println("write:", err) + logger.Error("Failed to write response: %v", err) break } continue @@ -235,12 +238,12 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { // Convert payload to binary and send over websocket b, err = payload.MarshalBinary() if err != nil { - log.Println("payload.MarshalBinary:", err) + logger.Error("Failed to marshal payload: %v", err) break } err = ws.WriteMessage(websocket.BinaryMessage, b) if err != nil { - log.Println("write:", err) + logger.Error("Failed to write message: %v", err) break } @@ -248,6 +251,8 @@ func ServeWs(w http.ResponseWriter, r *http.Request, debugLevel int) { logger.Verbose("sent payload: %v", payload) } + logger.Debug("WebSocket connection closed") + // Unload the shared library C.unload_shared_library(C.int(connID)) } diff --git a/shared/utils/logging.go b/shared/utils/logging.go index 4abd293..5a89a03 100644 --- a/shared/utils/logging.go +++ b/shared/utils/logging.go @@ -2,14 +2,17 @@ package utils import ( + "fmt" "log" ) // GH-Cp gen: DebugLogger provides a standardized logging interface // that respects debug levels for both client and server type DebugLogger struct { - DebugLevel int - Prefix string + DebugLevel int + Prefix string + ConnectionID int32 // Added field for connection ID + HasConnID bool // Flag to indicate if this logger has a connection ID } // GH-Cp gen: NewDebugLogger creates a new DebugLogger with the specified debug level and prefix @@ -17,14 +20,31 @@ func NewDebugLogger(debugLevel int, prefix string) *DebugLogger { return &DebugLogger{ DebugLevel: debugLevel, Prefix: prefix, + HasConnID: false, + } +} + +// NewConnectionLogger creates a new DebugLogger with connection ID for server-side connection logging +func NewConnectionLogger(debugLevel int, prefix string, connectionID int32) *DebugLogger { + return &DebugLogger{ + DebugLevel: debugLevel, + Prefix: prefix, + ConnectionID: connectionID, + HasConnID: true, } } // GH-Cp gen: LogAtLevel logs a message if the current debug level is at least the specified level func (dl *DebugLogger) LogAtLevel(level int, format string, v ...interface{}) { if dl.DebugLevel >= level { - if dl.Prefix != "" { - format = dl.Prefix + ": " + format + prefix := dl.Prefix + if dl.HasConnID { + // Include connection ID in the log prefix + prefix = fmt.Sprintf("%s[conn-%d]", prefix, dl.ConnectionID) + } + + if prefix != "" { + format = prefix + ": " + format } log.Printf(format, v...) } @@ -42,8 +62,14 @@ func (dl *DebugLogger) Verbose(format string, v ...interface{}) { // GH-Cp gen: Error logs an error regardless of debug level func (dl *DebugLogger) Error(format string, v ...interface{}) { - if dl.Prefix != "" { - format = dl.Prefix + ": ERROR: " + format + prefix := dl.Prefix + if dl.HasConnID { + // Include connection ID in the log prefix for errors too + prefix = fmt.Sprintf("%s[conn-%d]", prefix, dl.ConnectionID) + } + + if prefix != "" { + format = prefix + ": ERROR: " + format } else { format = "ERROR: " + format }