commit 4063fe6bd8463cdf8dbfd43e5a78f199a22e1ea0 Author: Jacob Schmidt Date: Thu Apr 10 21:49:46 2025 -0500 Initial Repo Setup diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..0db94aa --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,48 @@ +name: Build + +on: + push: + branches: [ master ] + tags: + - 'v*' + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '9.0.x' + + - name: Restore dependencies + run: dotnet restore FireflyClient.sln + + - name: Build + run: | + chmod +x build-all.sh + ./build-all.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: firefly-artifacts + path: | + artifacts/exe/* + artifacts/native/* + + - name: Create Release + if: startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v1 + with: + files: | + artifacts/exe/* + artifacts/native/* + generate_release_notes: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef2e450 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.editorconfig +nuget.config +*.7z +bin +obj +.venv +.vs +.vscode \ No newline at end of file diff --git a/FireflyClient.csproj b/FireflyClient.csproj new file mode 100644 index 0000000..e213262 --- /dev/null +++ b/FireflyClient.csproj @@ -0,0 +1,67 @@ + + + + + net9.0 + enable + enable + Speed + icon.ico + icon.png + true + win-x64;linux-x64;osx-x64 + true + true + embedded + + + FireflyClient + FireflyClient + Firefly Client + A client library for interacting with Firefly Redis-compatible server + FireflyClient + 1.0.0 + IDSolutions + true + false + true + + + + + Exe + true + true + true + true + true + Speed + true + true + + + + + Library + true + Shared + true + true + true + Speed + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FireflyClient.csproj.user b/FireflyClient.csproj.user new file mode 100644 index 0000000..4dd6d0e --- /dev/null +++ b/FireflyClient.csproj.user @@ -0,0 +1,6 @@ + + + + <_LastSelectedProfileId>G:\forge\firefly\extension\FireflyClient\Properties\PublishProfiles\FolderProfile.pubxml + + \ No newline at end of file diff --git a/FireflyClient.sln b/FireflyClient.sln new file mode 100644 index 0000000..429b142 --- /dev/null +++ b/FireflyClient.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35919.96 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FireflyClient", "FireflyClient.csproj", "{2AC8541A-E7A5-9E29-C21E-529DB87DC28F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2AC8541A-E7A5-9E29-C21E-529DB87DC28F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AC8541A-E7A5-9E29-C21E-529DB87DC28F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AC8541A-E7A5-9E29-C21E-529DB87DC28F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AC8541A-E7A5-9E29-C21E-529DB87DC28F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BD4E2CA4-BE5E-4D45-B35E-09A818648961} + EndGlobalSection +EndGlobal diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..b30dac5 --- /dev/null +++ b/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,17 @@ + + + + + Release + Any CPU + bin\Release\net9.0\publish\ + FileSystem + <_TargetId>Folder + net9.0 + win-x64 + true + true + true + true + + \ No newline at end of file diff --git a/Properties/PublishProfiles/FolderProfile.pubxml.user b/Properties/PublishProfiles/FolderProfile.pubxml.user new file mode 100644 index 0000000..b636215 --- /dev/null +++ b/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -0,0 +1,8 @@ + + + + + True|2025-04-07T00:26:35.2561447Z||;True|2025-04-06T18:20:46.8599175-05:00||;True|2025-04-06T17:37:25.8290243-05:00||;False|2025-04-06T17:37:09.9452190-05:00||;True|2025-04-06T17:30:00.9176527-05:00||;True|2025-04-06T17:23:09.5889784-05:00||;True|2025-04-06T17:00:26.8780282-05:00||;True|2025-04-06T16:53:46.4057024-05:00||;True|2025-04-06T16:29:10.3736900-05:00||;True|2025-04-06T16:24:55.0062926-05:00||;True|2025-04-06T16:18:23.5491794-05:00||;True|2025-04-06T16:06:28.3996665-05:00||;True|2025-04-06T15:28:33.7325402-05:00||;True|2025-04-06T13:14:44.0043568-05:00||;True|2025-04-06T12:35:46.4550298-05:00||; + + + \ No newline at end of file diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 0000000..f998483 --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,83 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace FireflyClient.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FireflyClient.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] icon { + get { + object obj = ResourceManager.GetObject("icon", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] icon1 { + get { + object obj = ResourceManager.GetObject("icon1", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 0000000..3979b4f --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\resources\icon.ico;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\resources\icon.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c2ef1b --- /dev/null +++ b/README.md @@ -0,0 +1,622 @@ +# Firefly Client Library + +A C# client library for interacting with the Firefly Redis-compatible server. This library provides a clean API for storing and retrieving data from Firefly servers, handling all the protocol details for you. + +## Features + +- Simple API for common Redis/Firefly operations (strings, lists, hashes) +- Automatic handling of quotes and special characters in commands +- Built-in connection management and authentication +- Optional Native Interop library for use from C, C++, Python, etc. +- Designed with thread-safety in mind (see Thread Safety section) + +## Usage + +### As a Console Application + +Simply run the FireflyClient executable (if built as an executable) to connect to a Firefly server and execute commands interactively: + +```bash +# Example (replace FireflyClient.exe with actual executable name/path) +./FireflyClient --host 127.0.0.1 --port 6379 --password yourpassword +``` + +### As a C# Library + +#### Installation + +Reference the FireflyClient library in your .NET project: + +1. Add a reference to the compiled `FireflyClient.dll`. +2. Or add a project reference if working in the same solution. + +#### Basic Usage + +```csharp +using FireflyClient; // Namespace +using System; +using System.Collections.Generic; + +// Create a client within a using statement for proper disposal +// Connects to localhost:6379 by default +using (var client = new FireflyClient("127.0.0.1", 6379)) +{ + try + { + // Authenticate if needed + // bool authResult = client.Authenticate("yourpassword"); + // if (!authResult) { + // Console.WriteLine("Authentication failed."); + // return; + // } + + // Store a string + client.StringSet("mykey", "Hello C# world!"); + + // Retrieve a string + string value = client.StringGet("mykey"); + Console.WriteLine($"GET mykey: {value}"); + + // Work with lists + client.ListRightPush("mylist", "item1"); // Can take multiple values + client.ListRightPush("mylist", "item2", "item3"); + List items = client.ListRange("mylist", 0, -1); + Console.WriteLine($"LRANGE mylist: {string.Join(", ", items)}"); + + // Work with hashes + client.HashSet("user:1", "name", "Alice"); + client.HashSet("user:1", "email", "alice@example.com"); + Dictionary userData = client.HashGetAll("user:1"); + Console.WriteLine("HGETALL user:1:"); + foreach(var kvp in userData) + { + Console.WriteLine($" {kvp.Key}: {kvp.Value}"); + } + + // Use HMSET + var moreData = new Dictionary { { "city", "Csharpville" }, { "zip", "98765" } }; + client.HashMultiSet("user:1", moreData); + Console.WriteLine($"HMSET city/zip for user:1"); + + string city = client.HashGet("user:1", "city"); + Console.WriteLine($"HGET user:1 city: {city}"); + + // Delete keys + client.Delete("mykey"); + client.Delete("mylist"); + client.Delete("user:1"); + Console.WriteLine("Deleted keys."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } +} // client.Dispose() is automatically called here +``` + +#### Error Handling + +Operations can throw exceptions (e.g., `SocketException` for connection issues, `InvalidOperationException` for protocol errors). Always use try-catch blocks for robust applications. + +## C# API Reference (Partial) + +This assumes the `FireflyClient` object is named `client`. + +### Client Management +- `new FireflyClient(host, port)`: Constructor to create and connect. +- `new FireflyClient(host, port, password)`: Constructor to create, connect, and authenticate. +- `client.Dispose()`: Disconnects and releases resources (called automatically by `using`). +- `client.Authenticate(password)`: Authenticates an existing connection. +- `client.IsConnected`: Property (bool) to check connection status. +- `client.IsAuthenticated`: Property (bool) to check authentication status. + +### String Operations +- `client.StringSet(key, value)`: Sets a string value. +- `client.StringGet(key)`: Gets a string value. +- `client.Delete(key)`: Deletes a key (works for any type). + +### List Operations +- `client.ListLeftPush(key, value, ...)`: Inserts value(s) at the head. +- `client.ListRightPush(key, value, ...)`: Appends value(s) to the tail. +- `client.ListLeftPop(key)`: Removes and returns the first element. +- `client.ListRightPop(key)`: Removes and returns the last element. +- `client.ListRange(key, start, stop)`: Gets a range of elements. +- `client.ListIndex(key, index)`: Gets element at index. +- `client.ListSet(key, index, value)`: Sets element at index. +- `client.ListPosition(key, element, rank, maxlen)`: Finds position of element. +- `client.ListTrim(key, start, stop)`: Trims list to specified range. +- `client.ListRemove(key, count, element)`: Removes elements from list. + +### Hash Operations +- `client.HashSet(key, field, value)`: Sets a field in a hash. +- `client.HashGet(key, field)`: Gets a field from a hash. +- `client.HashDelete(key, field)`: Deletes a field from a hash. +- `client.HashFieldExists(key, field)`: Checks if a field exists. +- `client.HashMultiSet(key, dictionary)`: Sets multiple fields from a Dictionary. +- `client.HashGetAll(key)`: Gets all fields and values as a Dictionary. + +### Pipeline Operations +- `client.SetPipelineMode(enabled)`: Enables/disables pipeline mode. +- `client.SetBatchSize(size)`: Sets the command batch size for pipelining. +- `client.FlushPipeline()`: Sends all queued commands. +- `client.QueuedCommandCount`: Property (int) to get queued command count. +- `client.IsPipelineMode`: Property (bool) to check pipeline mode. +- `client.MaxBatchSize`: Property (int) to get max batch size. + +### Server Operations (Example - verify actual methods) +- `client.ExecuteCommand(command, args)`: Executes a raw command. +- `client.Ping()`: Checks server responsiveness. +- `client.Save()`: Requests a synchronous save. +- `client.BackgroundSave()`: Requests an asynchronous save. + +*(Note: This API list is based on common patterns and native functions. Verify actual C# method names and signatures.)* + +## Native Interop Library (`firefly.h` API) + +A native C API is provided for using the client from non-.NET languages. + +### Prerequisites + +- **Library Build:** You need the compiled shared library (`libFireflyClient.dll` on Windows, `libFireflyClient.so` on Linux/macOS). +- **Header:** You need the `firefly.h` header file. +- **Runtime:** The target machine needs the appropriate .NET Runtime installed (e.g., .NET 9.0 as specified during build). + +### Building the Native Library + +Use `dotnet publish` with the appropriate Runtime Identifier (RID) and configuration. Example publish profiles might be provided (`NativeLinux`, `NativeWin`). + +```bash +# Example Linux Build +dotnet publish -c Release -r linux-x64 --self-contained false # Requires .NET Runtime +# Or self-contained (larger size): +# dotnet publish -c Release -r linux-x64 --self-contained true + +# Example Windows Build +dotnet publish -c Release -r win-x64 --self-contained false +``` +*(Adjust RID and configuration as needed. Output will be in `bin/Release/net9.0//publish`)* + +### Usage (C/C++) + +```c +#include "firefly.h" // Make sure this path is correct +#include +#include // For exit +#include // For string processing + +// Simple helper to split string by newline +void process_multiline_string(const char* str, const char* prefix) { + if (!str) return; + char* buffer = strdup(str); + if (!buffer) return; + char* line = strtok(buffer, "\n"); + while (line != NULL) { + printf("%s%s\n", prefix, line); + line = strtok(NULL, "\n"); + } + free(buffer); +} + +// Simple helper to parse field=value string +void process_hash_string(const char* str, const char* prefix) { + if (!str) return; + char* buffer = strdup(str); + if (!buffer) return; + char* line = strtok(buffer, "\n"); + while (line != NULL) { + char* eq = strchr(line, '='); + if (eq) { + *eq = '\0'; // Split string at '=' + printf("%s%s: %s\n", prefix, line, eq + 1); + } + line = strtok(NULL, "\n"); + } + free(buffer); +} + +int main() { + // Initialize client + void* client = CreateClient("127.0.0.1", 6379); + if (!client) { + fprintf(stderr, "Failed to create client\n"); + return 1; + } + printf("Client created.\n"); + + // Authenticate (optional) + // if (!Authenticate(client, "your_password")) { ... } + + // String operations + StringSet(client, "mykey", "Hello C world!"); + char* value = StringGet(client, "mykey"); + if (value) { + printf("StringGet: %s\n", value); + FreeString(value); // Free the returned string + } + + // List operations + ListRightPush(client, "mylist", "item1"); + ListRightPush(client, "mylist", "item2"); + + // Get list range (char* return) + char* range = ListRange(client, "mylist", 0, -1); + if (range) { + printf("ListRange:\n"); + process_multiline_string(range, " - "); + FreeString(range); // Free the returned string + } + + // Use other list ops... + ListSet(client, "mylist", 0, "item1_updated"); + ListRemove(client, "mylist", 1, "item2"); + + // Hash operations + HashSet(client, "user:1", "name", "Alice C"); + HashMultiSet(client, "user:1", "email alice.c@example.com city C-Town"); + + char* name = HashGet(client, "user:1", "name"); + if (name) { + printf("HashGet Name: %s\n", name); + FreeString(name); // Free the returned string + } + + // Get all hash fields (char* return) + char* hashData = HashGetAll(client, "user:1"); + if (hashData) { + printf("HashGetAll:\n"); + process_hash_string(hashData, " "); + FreeString(hashData); // Free the returned string + } + + // Cleanup keys + Delete(client, "mykey"); + Delete(client, "mylist"); + Delete(client, "user:1"); + + // Cleanup client + printf("Destroying client.\n"); + DestroyClient(client); + return 0; +} +``` + +### Usage (Python) + +```python +import ctypes +import os +import platform +from ctypes import c_char_p, c_int, c_void_p, c_bool # Use c_bool + +# --- Define FireflyClient Python Wrapper Class --- +class FireflyClientPy: + def __init__(self, library_path=None, host="127.0.0.1", port=6379, password=None): + if library_path is None: + # Simple auto-detect based on OS (adjust path as needed) + script_dir = os.path.dirname(os.path.abspath(__file__)) + if platform.system() == "Windows": + library_path = os.path.join(script_dir, "libFireflyClient.dll") # Assumes DLL is here + else: + library_path = os.path.join(script_dir, "libFireflyClient.so") # Assumes SO is here + + if not os.path.exists(library_path): + raise FileNotFoundError(f"Shared library not found: {library_path}") + + self.lib = ctypes.CDLL(library_path) + self._define_signatures() + + self.client_handle = self.lib.CreateClient(host.encode('utf-8'), port) + if not self.client_handle: + raise ConnectionError(f"Failed to create client for {host}:{port}") + + if password: + if not self.lib.Authenticate(self.client_handle, password.encode('utf-8')): + self.lib.DestroyClient(self.client_handle) + raise ConnectionError("Authentication failed") + + def _define_signatures(self): + # Client Management + self.lib.CreateClient.argtypes = [c_char_p, c_int] + self.lib.CreateClient.restype = c_void_p + self.lib.DestroyClient.argtypes = [c_void_p] + self.lib.DestroyClient.restype = None + self.lib.Authenticate.argtypes = [c_void_p, c_char_p] + self.lib.Authenticate.restype = c_bool + + # String Ops + self.lib.StringSet.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.StringSet.restype = c_bool + self.lib.StringGet.argtypes = [c_void_p, c_char_p] + self.lib.StringGet.restype = c_char_p + self.lib.Delete.argtypes = [c_void_p, c_char_p] + self.lib.Delete.restype = c_int + self.lib.FreeString.argtypes = [c_char_p] # Keep c_char_p, ctypes handles IntPtr + self.lib.FreeString.restype = None + + # List Ops + self.lib.ListLeftPush.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.ListLeftPush.restype = c_int + self.lib.ListRightPush.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.ListRightPush.restype = c_int + self.lib.ListLeftPop.argtypes = [c_void_p, c_char_p] + self.lib.ListLeftPop.restype = c_char_p + self.lib.ListRightPop.argtypes = [c_void_p, c_char_p] + self.lib.ListRightPop.restype = c_char_p + self.lib.ListRange.argtypes = [c_void_p, c_char_p, c_int, c_int] + self.lib.ListRange.restype = c_char_p + self.lib.ListIndex.argtypes = [c_void_p, c_char_p, c_int] + self.lib.ListIndex.restype = c_char_p + self.lib.ListSet.argtypes = [c_void_p, c_char_p, c_int, c_char_p] + self.lib.ListSet.restype = c_bool + self.lib.ListPosition.argtypes = [c_void_p, c_char_p, c_char_p, c_int, c_int] + self.lib.ListPosition.restype = c_int + self.lib.ListTrim.argtypes = [c_void_p, c_char_p, c_int, c_int] + self.lib.ListTrim.restype = c_bool + self.lib.ListRemove.argtypes = [c_void_p, c_char_p, c_int, c_char_p] + self.lib.ListRemove.restype = c_int + + # Hash Ops + self.lib.HashSet.argtypes = [c_void_p, c_char_p, c_char_p, c_char_p] + self.lib.HashSet.restype = c_bool + self.lib.HashGet.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashGet.restype = c_char_p + self.lib.HashDelete.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashDelete.restype = c_bool + self.lib.HashFieldExists.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashFieldExists.restype = c_bool + self.lib.HashMultiSet.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashMultiSet.restype = c_bool + self.lib.HashGetAll.argtypes = [c_void_p, c_char_p] + self.lib.HashGetAll.restype = c_char_p + + # Raw Command + self.lib.ExecuteCommand.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.ExecuteCommand.restype = c_char_p + + # Pipeline Ops + self.lib.SetPipelineMode.argtypes = [c_void_p, c_bool] + self.lib.SetPipelineMode.restype = c_bool + self.lib.SetBatchSize.argtypes = [c_void_p, c_int] + self.lib.SetBatchSize.restype = c_bool + self.lib.FlushPipeline.argtypes = [c_void_p] + self.lib.FlushPipeline.restype = c_char_p + self.lib.GetQueuedCommandCount.argtypes = [c_void_p] + self.lib.GetQueuedCommandCount.restype = c_int + self.lib.IsPipelineMode.argtypes = [c_void_p] + self.lib.IsPipelineMode.restype = c_bool + self.lib.GetBatchSize.argtypes = [c_void_p] + self.lib.GetBatchSize.restype = c_int + + def close(self): + if self.client_handle: + self.lib.DestroyClient(self.client_handle) + self.client_handle = None + + def _call_get_string(self, func, key): + key_b = key.encode('utf-8') + result_ptr = func(self.client_handle, key_b) + if result_ptr: + value = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') + self.lib.FreeString(result_ptr) + return value + return None + + def _parse_and_free_list(self, result_ptr): + if result_ptr: + full_string = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') + self.lib.FreeString(result_ptr) + return full_string.split('\n') if full_string else [] + return [] + + def _parse_and_free_hash(self, result_ptr): + data = {} + if result_ptr: + full_string = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') + self.lib.FreeString(result_ptr) + if full_string: + pairs = full_string.split('\n') + for pair in pairs: + if '=' in pair: + field, value = pair.split('=', 1) + data[field] = value + return data + + # --- Public Methods --- + def string_set(self, key, value): + return self.lib.StringSet(self.client_handle, key.encode('utf-8'), value.encode('utf-8')) + + def string_get(self, key): + return self._call_get_string(self.lib.StringGet, key) + + def list_right_push(self, key, value): + return self.lib.ListRightPush(self.client_handle, key.encode('utf-8'), value.encode('utf-8')) + + def list_range(self, key, start, stop): + result_ptr = self.lib.ListRange(self.client_handle, key.encode('utf-8'), start, stop) + return self._parse_and_free_list(result_ptr) + + def hash_set(self, key, field, value): + return self.lib.HashSet(self.client_handle, key.encode('utf-8'), field.encode('utf-8'), value.encode('utf-8')) + + def hash_get_all(self, key): + result_ptr = self.lib.HashGetAll(self.client_handle, key.encode('utf-8')) + return self._parse_and_free_hash(result_ptr) + + # Add other methods here following the pattern... + def list_left_pop(self, key): + return self._call_get_string(self.lib.ListLeftPop, key) + + def hash_get(self, key, field): + key_b = key.encode('utf-8') + field_b = field.encode('utf-8') + result_ptr = self.lib.HashGet(self.client_handle, key_b, field_b) + if result_ptr: + value = ctypes.cast(result_ptr, c_char_p).value.decode('utf-8') + self.lib.FreeString(result_ptr) + return value + return None + + def hash_multi_set(self, key, field_value_dict): + pairs_str = " ".join([f"{k} {v}" for k, v in field_value_dict.items()]) + return self.lib.HashMultiSet(self.client_handle, key.encode('utf-8'), pairs_str.encode('utf-8')) + + def delete(self, key): + return self.lib.Delete(self.client_handle, key.encode('utf-8')) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + +# --- Example Usage --- +if __name__ == "__main__": + try: + # Assumes library is in the same directory or accessible via path + # Use context manager for cleanup + with FireflyClientPy(host="127.0.0.1", port=6379) as client: + print("Python client created.") + # Authenticate if needed: client.lib.Authenticate(...) after creation + + # String operations + client.string_set("py:mykey", "Hello Python world!") + value = client.string_get("py:mykey") + print(f"StringGet: {value}") + + # List operations + client.list_right_push("py:mylist", "item1") + client.list_right_push("py:mylist", "item2") + + # Get list range (updated parsing) + items = client.list_range("py:mylist", 0, -1) + print(f"ListRange: {items}") + + # Use other list ops... + left_popped = client.list_left_pop("py:mylist") + print(f"ListLeftPop: {left_popped}") + + # Hash operations + client.hash_set("py:user:1", "language", "Python") + client.hash_multi_set("py:user:1", {"version": "3.10", "framework": "None"}) + + lang = client.hash_get("py:user:1", "language") + print(f"HashGet language: {lang}") + + # Get all hash fields (updated parsing) + hash_data = client.hash_get_all("py:user:1") + print(f"HashGetAll: {hash_data}") + + # Cleanup + client.delete("py:mykey") + client.delete("py:mylist") + client.delete("py:user:1") + print("Cleaned up keys.") + + except (FileNotFoundError, ConnectionError, Exception) as e: + print(f"Error: {e}") + + print("Python example finished.") + +``` + +## Native API Reference + +This lists the functions exported by the native library (defined in `firefly.h`). + +### Client Management +- `void* CreateClient(const char* host, int port)` +- `void DestroyClient(void* handle)` +- `bool Authenticate(void* handle, const char* password)` + +### String Operations +- `bool StringSet(void* handle, const char* key, const char* value)` +- `char* StringGet(void* handle, const char* key)` +- `int Delete(void* handle, const char* key)` + +### List Operations +- `int ListLeftPush(void* handle, const char* key, const char* value)` +- `int ListRightPush(void* handle, const char* key, const char* value)` +- `char* ListLeftPop(void* handle, const char* key)` +- `char* ListRightPop(void* handle, const char* key)` +- `char* ListRange(void* handle, const char* key, int start, int stop)` +- `char* ListIndex(void* handle, const char* key, int index)` +- `bool ListSet(void* handle, const char* key, int index, const char* value)` +- `int ListPosition(void* handle, const char* key, const char* element, int rank, int maxlen)` +- `bool ListTrim(void* handle, const char* key, int start, int stop)` +- `int ListRemove(void* handle, const char* key, int count, const char* element)` + +### Hash Operations +- `bool HashSet(void* handle, const char* key, const char* field, const char* value)` +- `char* HashGet(void* handle, const char* key, const char* field)` +- `bool HashDelete(void* handle, const char* key, const char* field)` +- `bool HashFieldExists(void* handle, const char* key, const char* field)` +- `bool HashMultiSet(void* handle, const char* key, const char* fieldValuePairs)` +- `char* HashGetAll(void* handle, const char* key)` + +### Raw Command Execution +- `char* ExecuteCommand(void* handle, const char* command, const char* args)` + +### Pipeline Operations +- `bool SetPipelineMode(void* handle, bool enabled)` +- `bool SetBatchSize(void* handle, int size)` +- `char* FlushPipeline(void* handle)` +- `int GetQueuedCommandCount(void* handle)` +- `bool IsPipelineMode(void* handle)` +- `int GetBatchSize(void* handle)` + +### Memory Management +- `void FreeString(char* str)`: **Crucial** - Must be called by the user to free the memory for any `char*` returned by the library functions (StringGet, ListLeftPop, ListRightPop, ListRange, ListIndex, HashGet, HashGetAll, ExecuteCommand, FlushPipeline). + +### Error Handling +Most functions return `NULL` (for `char*`) or `false` / `0` / `-1` (for `bool`/`int`) on failure. The native caller should check these return values. + +## Thread Safety + +The library's native API is designed with thread safety in mind, relying on the underlying thread-safe mechanisms implemented in the C# `FireflyClient`. + +- **Instance Handles (`void*`)**: Each handle returned by `CreateClient` represents an independent connection and state. Operations on different handles do not interfere. +- **Internal Locking**: The C# implementation is assumed to use appropriate locking (e.g., per-key locks for hashes/lists, connection locks) to allow safe concurrent calls on the *same* handle from multiple native threads. +- **Caller Responsibility**: The caller must ensure that a single handle is not concurrently used in conflicting ways if the underlying operation requires it (though many operations like reads on different keys should be safe). + +*(Verify the specific locking strategies implemented in the C# partial classes for detailed guarantees.)* + +## Performance Considerations + +- **Interop Overhead**: There is inherent overhead in calling between native code and the .NET runtime. +- **Pipelining**: Use the pipeline operations (`SetPipelineMode`, queue commands, `FlushPipeline`) for significantly better performance when sending multiple commands. +- **Memory Management**: Frequent allocation/deallocation of strings across the interop boundary can impact performance. Ensure `FreeString` is called promptly. + +## Best Practices + +1. **Memory Management**: Always free returned strings. + ```c + char* value = StringGet(client, "mykey"); + if (value) { + // Use value + printf("Value: %s\n", value); + FreeString(value); // Essential! + } + ``` + +2. **Error Handling**: Check return values. + ```c + if (!ListSet(client, key, index, value)) { // Check boolean return + fprintf(stderr, "Error setting list value!\n"); + } + char* item = ListIndex(client, key, index); + if (!item) { // Check pointer return + fprintf(stderr, "Error getting list item or index out of range!\n"); + } else { + // Use item + FreeString(item); + } + ``` + +3. **Resource Cleanup**: Always destroy the client. + ```c + void* client = CreateClient(...); + // ... Use client ... + DestroyClient(client); // Ensure cleanup + ``` + +4. **Encoding**: Assume all `char*` parameters and return values use UTF-8 encoding. \ No newline at end of file diff --git a/artifacts/exe/FireflyClient b/artifacts/exe/FireflyClient new file mode 100644 index 0000000..a1bf1eb Binary files /dev/null and b/artifacts/exe/FireflyClient differ diff --git a/artifacts/exe/FireflyClient.dbg b/artifacts/exe/FireflyClient.dbg new file mode 100644 index 0000000..25c0e5c Binary files /dev/null and b/artifacts/exe/FireflyClient.dbg differ diff --git a/artifacts/exe/FireflyClient.exe b/artifacts/exe/FireflyClient.exe new file mode 100644 index 0000000..d280d11 Binary files /dev/null and b/artifacts/exe/FireflyClient.exe differ diff --git a/artifacts/exe/FireflyClient.pdb b/artifacts/exe/FireflyClient.pdb new file mode 100644 index 0000000..c349a4b Binary files /dev/null and b/artifacts/exe/FireflyClient.pdb differ diff --git a/artifacts/exe/FireflyClient.xml b/artifacts/exe/FireflyClient.xml new file mode 100644 index 0000000..5f487b8 --- /dev/null +++ b/artifacts/exe/FireflyClient.xml @@ -0,0 +1,471 @@ + + + + FireflyClient + + + + + A strongly-typed resource class, for looking up localized strings, etc. + + + + + Returns the cached ResourceManager instance used by this class. + + + + + Overrides the current thread's CurrentUICulture property for all + resource lookups using this strongly typed resource class. + + + + + Looks up a localized resource of type System.Byte[]. + + + + + Looks up a localized resource of type System.Byte[]. + + + + + FireflyClient provides a clean API for interacting with the Firefly server. + This can be used as a library for integrating with other applications. + + + + + Creates a new FireflyClient and connects to the specified server + + + + + Creates a new FireflyClient, connects to the server and authenticates + + + + + Authenticates with the server using the provided password + + + + + Executes a raw command with any number of arguments + + + + + Enables or disables pipeline mode + + + + + Flushes any queued commands in pipeline mode + + + + + Sets the maximum number of commands to batch before sending + + + + + Gets the current number of queued commands + + + + + Gets whether pipeline mode is enabled + + + + + Gets the maximum batch size + + + + + Gets whether the client is connected to the server + + + + + Gets whether the client is authenticated + + + + + Gets all keys matching the specified pattern + + Pattern to match against keys. Use * for wildcard matches. + List of matching keys, or empty list if none found or on error + + + + Parses an array response from the server + + + + + + + + Sets a field in a hash + + + + + Gets a field from a hash + + + + + Deletes a field from a hash + + + + + Checks if a field exists in a hash + + + + + Gets all fields and values from a hash + + + + + Sets multiple fields in a hash at once + + + + + Adds values to the beginning of a list + + + + + Adds values to the end of a list + + + + + Removes and returns the first element of a list + + + + + Removes and returns the last element of a list + + + + + Gets a range of elements from a list + + + + + Gets the element at the specified index in a list + + + + + Sets the element at the specified index in a list + + + + + Returns the index of the first occurrence of an element in a list + + + + + Trims a list to the specified range + + + + + Removes elements equal to the given value from a list + + + + + Creates a new FireflyClient instance for native interop + + + + + Destroys a FireflyClient instance created for native interop + + + + + Authenticates with the server using the provided password + + + + + Executes a raw command. Note: Argument parsing is basic. + Consider using specific functions instead for reliability. + + + + + Frees a string allocated by the native interop methods + + + + + Sets a string value for a given key (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the key. + Pointer to a null-terminated UTF-8 string representing the value. + True if the command was successful (e.g., server replied OK), false otherwise. + + + + Gets a string value for a given key (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the key. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if key not found. + + + + Deletes one or more keys (Native Interop). + Note: Currently only supports deleting a single key via the C# Delete method. + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the key to delete. + The number of keys that were removed (typically 1 or 0), or 0 on error. + + + + Adds a value to the beginning of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string representing the value to add. + The length of the list after the push operation, or 0 on error. + + + + Adds a value to the end of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string representing the value to add. + The length of the list after the push operation, or 0 on error. + + + + Removes and returns the first element of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if list is empty. + + + + Removes and returns the last element of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if list is empty. + + + + Gets a range of elements from a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The start index (0-based). + The stop index (inclusive, use -1 for end). + Pointer to a null-terminated UTF-8 string containing newline-delimited list elements, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error. + The returned IntPtr points to a single string. The native caller must parse this string (e.g., split by '\n') and free the pointer using NativeFreeString. + + + + Gets an element from a list by its index (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The index of the element (0-based). + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if index is out of range. + + + + Sets the value of an element in a list by its index (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The index of the element to set (0-based). + Pointer to a null-terminated UTF-8 string representing the new value. + True if the command was successful, false otherwise (e.g., index out of range). + + + + Returns the index of the first occurrence of an element in a list (Native Interop). + Rank and MaxLen parameters are currently ignored. + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string representing the element to find. + Optional rank (ignored). + Optional max length (ignored). + The 0-based index of the element, or -1 if not found or on error. + + + + Trims a list to contain only the specified range of elements (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The start index (0-based). + The stop index (inclusive, use -1 for end). + True if the command was successful, false otherwise. + + + + Removes occurrences of elements from a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Number of occurrences to remove (see C# ListRemove docs). + Pointer to a null-terminated UTF-8 string representing the element to remove. + The number of elements removed, or 0 on error. + + + + Sets the value of a field within a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field name. + Pointer to a null-terminated UTF-8 string representing the value to set. + True if the field was new or updated successfully, false on error. + + + + Gets the value of a field within a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field name. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if field/key not found. + + + + Deletes a field from a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field to delete. + True if the field was deleted, false otherwise (e.g., field/key not found). + + + + Checks if a field exists within a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field name. + True if the field exists, false otherwise. + + + + Sets multiple fields and values in a hash (Native Interop). + Parses a space-separated string of field-value pairs. + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string of space-separated field-value pairs. + True if successful, false on error (e.g., odd number of pairs). + + + + Gets all fields and values from a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string containing newline-delimited "field=value" pairs, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if hash not found. + The returned IntPtr points to a single string. The native caller must parse this string (e.g., split by '\n', then by '=') and free the pointer using NativeFreeString. + + + + Enables or disables pipeline mode for the client (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + True to enable pipeline mode, false to disable. + True if the mode was set successfully, false on error. + + + + Sets the maximum number of commands to batch in pipeline mode (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + The maximum number of commands to queue before sending. + True if the batch size was set successfully, false on error (e.g., invalid size). + + + + Sends all queued commands to the server immediately (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string containing the server's combined response, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if queue was empty. + + + + Gets the number of commands currently waiting in the pipeline queue (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + The number of queued commands, or 0 on error. + + + + Checks if pipeline mode is currently enabled for the client (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + True if pipeline mode is enabled, false otherwise or on error. + + + + Gets the current maximum batch size configured for pipeline mode (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + The maximum batch size, or 0 on error. + + + + Gets all keys matching the specified pattern (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the pattern to match against keys. + Pointer to a null-terminated UTF-8 string containing newline-delimited keys, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error. + + + + Sets a key-value pair + + + + + Gets a value by key + + + + + Deletes a key from all stores (string, list, hash) + + + + + Command-line interface for Firefly + + + + diff --git a/artifacts/native/FireflyClient.pdb b/artifacts/native/FireflyClient.pdb new file mode 100644 index 0000000..c3bcbbf Binary files /dev/null and b/artifacts/native/FireflyClient.pdb differ diff --git a/artifacts/native/FireflyClient.so.dbg b/artifacts/native/FireflyClient.so.dbg new file mode 100644 index 0000000..d8c3807 Binary files /dev/null and b/artifacts/native/FireflyClient.so.dbg differ diff --git a/artifacts/native/FireflyClient.xml b/artifacts/native/FireflyClient.xml new file mode 100644 index 0000000..5f487b8 --- /dev/null +++ b/artifacts/native/FireflyClient.xml @@ -0,0 +1,471 @@ + + + + FireflyClient + + + + + A strongly-typed resource class, for looking up localized strings, etc. + + + + + Returns the cached ResourceManager instance used by this class. + + + + + Overrides the current thread's CurrentUICulture property for all + resource lookups using this strongly typed resource class. + + + + + Looks up a localized resource of type System.Byte[]. + + + + + Looks up a localized resource of type System.Byte[]. + + + + + FireflyClient provides a clean API for interacting with the Firefly server. + This can be used as a library for integrating with other applications. + + + + + Creates a new FireflyClient and connects to the specified server + + + + + Creates a new FireflyClient, connects to the server and authenticates + + + + + Authenticates with the server using the provided password + + + + + Executes a raw command with any number of arguments + + + + + Enables or disables pipeline mode + + + + + Flushes any queued commands in pipeline mode + + + + + Sets the maximum number of commands to batch before sending + + + + + Gets the current number of queued commands + + + + + Gets whether pipeline mode is enabled + + + + + Gets the maximum batch size + + + + + Gets whether the client is connected to the server + + + + + Gets whether the client is authenticated + + + + + Gets all keys matching the specified pattern + + Pattern to match against keys. Use * for wildcard matches. + List of matching keys, or empty list if none found or on error + + + + Parses an array response from the server + + + + + + + + Sets a field in a hash + + + + + Gets a field from a hash + + + + + Deletes a field from a hash + + + + + Checks if a field exists in a hash + + + + + Gets all fields and values from a hash + + + + + Sets multiple fields in a hash at once + + + + + Adds values to the beginning of a list + + + + + Adds values to the end of a list + + + + + Removes and returns the first element of a list + + + + + Removes and returns the last element of a list + + + + + Gets a range of elements from a list + + + + + Gets the element at the specified index in a list + + + + + Sets the element at the specified index in a list + + + + + Returns the index of the first occurrence of an element in a list + + + + + Trims a list to the specified range + + + + + Removes elements equal to the given value from a list + + + + + Creates a new FireflyClient instance for native interop + + + + + Destroys a FireflyClient instance created for native interop + + + + + Authenticates with the server using the provided password + + + + + Executes a raw command. Note: Argument parsing is basic. + Consider using specific functions instead for reliability. + + + + + Frees a string allocated by the native interop methods + + + + + Sets a string value for a given key (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the key. + Pointer to a null-terminated UTF-8 string representing the value. + True if the command was successful (e.g., server replied OK), false otherwise. + + + + Gets a string value for a given key (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the key. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if key not found. + + + + Deletes one or more keys (Native Interop). + Note: Currently only supports deleting a single key via the C# Delete method. + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the key to delete. + The number of keys that were removed (typically 1 or 0), or 0 on error. + + + + Adds a value to the beginning of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string representing the value to add. + The length of the list after the push operation, or 0 on error. + + + + Adds a value to the end of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string representing the value to add. + The length of the list after the push operation, or 0 on error. + + + + Removes and returns the first element of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if list is empty. + + + + Removes and returns the last element of a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if list is empty. + + + + Gets a range of elements from a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The start index (0-based). + The stop index (inclusive, use -1 for end). + Pointer to a null-terminated UTF-8 string containing newline-delimited list elements, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error. + The returned IntPtr points to a single string. The native caller must parse this string (e.g., split by '\n') and free the pointer using NativeFreeString. + + + + Gets an element from a list by its index (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The index of the element (0-based). + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if index is out of range. + + + + Sets the value of an element in a list by its index (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The index of the element to set (0-based). + Pointer to a null-terminated UTF-8 string representing the new value. + True if the command was successful, false otherwise (e.g., index out of range). + + + + Returns the index of the first occurrence of an element in a list (Native Interop). + Rank and MaxLen parameters are currently ignored. + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Pointer to a null-terminated UTF-8 string representing the element to find. + Optional rank (ignored). + Optional max length (ignored). + The 0-based index of the element, or -1 if not found or on error. + + + + Trims a list to contain only the specified range of elements (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + The start index (0-based). + The stop index (inclusive, use -1 for end). + True if the command was successful, false otherwise. + + + + Removes occurrences of elements from a list (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the list key. + Number of occurrences to remove (see C# ListRemove docs). + Pointer to a null-terminated UTF-8 string representing the element to remove. + The number of elements removed, or 0 on error. + + + + Sets the value of a field within a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field name. + Pointer to a null-terminated UTF-8 string representing the value to set. + True if the field was new or updated successfully, false on error. + + + + Gets the value of a field within a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field name. + Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if field/key not found. + + + + Deletes a field from a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field to delete. + True if the field was deleted, false otherwise (e.g., field/key not found). + + + + Checks if a field exists within a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string representing the field name. + True if the field exists, false otherwise. + + + + Sets multiple fields and values in a hash (Native Interop). + Parses a space-separated string of field-value pairs. + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string of space-separated field-value pairs. + True if successful, false on error (e.g., odd number of pairs). + + + + Gets all fields and values from a hash (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the hash key. + Pointer to a null-terminated UTF-8 string containing newline-delimited "field=value" pairs, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if hash not found. + The returned IntPtr points to a single string. The native caller must parse this string (e.g., split by '\n', then by '=') and free the pointer using NativeFreeString. + + + + Enables or disables pipeline mode for the client (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + True to enable pipeline mode, false to disable. + True if the mode was set successfully, false on error. + + + + Sets the maximum number of commands to batch in pipeline mode (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + The maximum number of commands to queue before sending. + True if the batch size was set successfully, false on error (e.g., invalid size). + + + + Sends all queued commands to the server immediately (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string containing the server's combined response, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if queue was empty. + + + + Gets the number of commands currently waiting in the pipeline queue (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + The number of queued commands, or 0 on error. + + + + Checks if pipeline mode is currently enabled for the client (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + True if pipeline mode is enabled, false otherwise or on error. + + + + Gets the current maximum batch size configured for pipeline mode (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + The maximum batch size, or 0 on error. + + + + Gets all keys matching the specified pattern (Native Interop). + + The GCHandle (as IntPtr) representing the client instance. + Pointer to a null-terminated UTF-8 string representing the pattern to match against keys. + Pointer to a null-terminated UTF-8 string containing newline-delimited keys, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error. + + + + Sets a key-value pair + + + + + Gets a value by key + + + + + Deletes a key from all stores (string, list, hash) + + + + + Command-line interface for Firefly + + + + diff --git a/artifacts/native/libFireflyClient.dll b/artifacts/native/libFireflyClient.dll new file mode 100644 index 0000000..6364094 Binary files /dev/null and b/artifacts/native/libFireflyClient.dll differ diff --git a/artifacts/native/libFireflyClient.so b/artifacts/native/libFireflyClient.so new file mode 100644 index 0000000..4f8273b Binary files /dev/null and b/artifacts/native/libFireflyClient.so differ diff --git a/build-all.ps1 b/build-all.ps1 new file mode 100644 index 0000000..1195632 --- /dev/null +++ b/build-all.ps1 @@ -0,0 +1,52 @@ +# Build configuration +$configuration = "Release" +$baseOutputPath = ".\artifacts" + +# Determine current OS and platform +$currentPlatform = "win-x64" # Since we're running PowerShell, we're on Windows +Write-Host "Building for current platform: $currentPlatform" + +# Create output directories +$exeOutputPath = "$baseOutputPath\exe" +$nativeOutputPath = "$baseOutputPath\native" +New-Item -ItemType Directory -Force -Path $exeOutputPath | Out-Null +New-Item -ItemType Directory -Force -Path $nativeOutputPath | Out-Null + +# Build executable +Write-Host "Building executable..." +dotnet publish ` + -c $configuration ` + -r $currentPlatform ` + --self-contained true ` + -p:PublishDir=$exeOutputPath + +if ($LASTEXITCODE -ne 0) { + Write-Host "Executable build failed. Check the error messages above." + exit 1 +} + +# Build native shared library +Write-Host "Building native shared library..." +dotnet publish ` + -c $configuration ` + -r $currentPlatform ` + --self-contained true ` + -p:BuildType=lib ` + -p:PublishDir=$nativeOutputPath + +if ($LASTEXITCODE -ne 0) { + Write-Host "Library build failed. Check the error messages above." + exit 1 +} + +# Rename the native library to the expected name +$dllPath = Join-Path $nativeOutputPath "FireflyClient.dll" +$libDllPath = Join-Path $nativeOutputPath "libFireflyClient.dll" +if (Test-Path $dllPath) { + Write-Host "Renaming native library to libFireflyClient.dll..." + Move-Item -Path $dllPath -Destination $libDllPath -Force +} + +Write-Host "Build complete. Outputs available at:" +Write-Host "Executable: $exeOutputPath" +Write-Host "Native library: $nativeOutputPath" \ No newline at end of file diff --git a/build-all.sh b/build-all.sh new file mode 100644 index 0000000..7bea521 --- /dev/null +++ b/build-all.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +configuration="Release" +baseOutputPath="./artifacts" + +# Detect platform +if [[ "$OSTYPE" == "darwin"* ]]; then + currentPlatform="osx-x64" +else + currentPlatform="linux-x64" +fi + +echo "Building for current platform: $currentPlatform" + +# Create output directories +exeOutputPath="$baseOutputPath/exe" +nativeOutputPath="$baseOutputPath/native" +mkdir -p "$exeOutputPath" +mkdir -p "$nativeOutputPath" + +# Build executable +echo "Building executable..." +dotnet publish \ + -c $configuration \ + -r $currentPlatform \ + --self-contained true \ + -p:PublishDir=$exeOutputPath + +if [ $? -ne 0 ]; then + echo "Executable build failed. Check the error messages above." + exit 1 +fi + +# Build native shared library +echo "Building native shared library..." +dotnet publish \ + -c $configuration \ + -r $currentPlatform \ + --self-contained true \ + -p:BuildType=lib \ + -p:PublishDir=$nativeOutputPath + +if [ $? -ne 0 ]; then + echo "Library build failed. Check the error messages above." + exit 1 +fi + +# Rename the native library to the expected name +dllPath="$nativeOutputPath/FireflyClient.so" +libDllPath="$nativeOutputPath/libFireflyClient.so" +if [ -f "$dllPath" ]; then + echo "Renaming native library to libFireflyClient.so..." + mv "$dllPath" "$libDllPath" +fi + +echo "Build complete. Outputs available at:" +echo "Executable: $exeOutputPath" +echo "Native library: $nativeOutputPath" + +# Make the outputs executable +if [[ "$currentPlatform" == "linux-x64" ]]; then + chmod +x "$exeOutputPath/FireflyClient" + if [ -f "$libDllPath" ]; then + chmod +x "$libDllPath" + else + echo "Warning: Native library not found at $libDllPath" + fi +elif [[ "$currentPlatform" == "osx-x64" ]]; then + chmod +x "$exeOutputPath/FireflyClient" + chmod +x "$nativeOutputPath/libFireflyClient.dylib" +fi \ No newline at end of file diff --git a/examples/example.cpp b/examples/example.cpp new file mode 100644 index 0000000..07e7742 --- /dev/null +++ b/examples/example.cpp @@ -0,0 +1,176 @@ +#include "../src/firefly.h" // Adjusted path to header in src directory +#include +#include +#include +#include + +// Helper function to split string (basic) +std::vector split(const std::string& s, char delimiter) { + std::vector tokens; + std::string token; + std::istringstream tokenStream(s); + while (std::getline(tokenStream, token, delimiter)) { + tokens.push_back(token); + } + return tokens; +} + +int main() { + // Create client + // Assumes the compiled library is accessible (e.g., in PATH or same dir) + void* client = CreateClient("localhost", 6379); + if (!client) { + std::cerr << "Failed to create client" << std::endl; + return 1; + } + std::cout << "Client created." << std::endl; + + // Authenticate if needed (replace with your password or remove if none) + // if (!Authenticate(client, "your_password")) { + // std::cerr << "Authentication failed" << std::endl; + // DestroyClient(client); + // return 1; + // } + // std::cout << "Authentication successful." << std::endl; + + // String operations + std::cout << "\n--- String Ops ---" << std::endl; + if (StringSet(client, "cpp:test", "hello from c++")) { + std::cout << "StringSet OK" << std::endl; + } + + char* value = StringGet(client, "cpp:test"); + if (value) { + std::cout << "StringGet: " << value << std::endl; + FreeString(value); // IMPORTANT: Free the returned string + } + + // List operations + std::cout << "\n--- List Ops ---" << std::endl; + ListLeftPush(client, "cpp:mylist", "item1"); + ListLeftPush(client, "cpp:mylist", "item2"); + ListRightPush(client, "cpp:mylist", "item3"); + std::cout << "LPUSH/RPUSH OK" << std::endl; + + char* popped = ListLeftPop(client, "cpp:mylist"); + if (popped) { + std::cout << "ListLeftPop: " << popped << std::endl; + FreeString(popped); // IMPORTANT: Free + } + + // Get a range of elements (Updated for char* return) + std::cout << "ListRange:" << std::endl; + char* rangeStr = ListRange(client, "cpp:mylist", 0, -1); + if (rangeStr) { + std::string rangeResult(rangeStr); + FreeString(rangeStr); // IMPORTANT: Free + + // Parse the newline-delimited string + std::vector range = split(rangeResult, '\n'); + for (size_t i = 0; i < range.size(); ++i) { + std::cout << " Element " << i << ": " << range[i] << std::endl; + } + } else { + std::cout << " (empty or error)" << std::endl; + } + + // ListPosition + int pos = ListPosition(client, "cpp:mylist", "item1", 0, 0); + std::cout << "ListPosition of 'item1': " << pos << std::endl; + + // ListSet + if (ListSet(client, "cpp:mylist", 0, "item1_updated")) { + std::cout << "ListSet OK" << std::endl; + } + + // ListRemove + int removed = ListRemove(client, "cpp:mylist", 1, "item3"); + std::cout << "ListRemove ('item3', count 1): Removed " << removed << " elements" << std::endl; + + // ListTrim + if (ListTrim(client, "cpp:mylist", 0, 0)) { // Trim to keep only the first element + std::cout << "ListTrim OK" << std::endl; + } + + // Verify trim + char* finalRangeStr = ListRange(client, "cpp:mylist", 0, -1); + if (finalRangeStr) { + std::cout << "List after Trim: " << finalRangeStr << std::endl; + FreeString(finalRangeStr); + } + + // Hash operations + std::cout << "\n--- Hash Ops ---" << std::endl; + HashSet(client, "cpp:user:1", "name", "John C."); + HashSet(client, "cpp:user:1", "email", "john.c@example.com"); + std::cout << "HashSet OK" << std::endl; + + char* name = HashGet(client, "cpp:user:1", "name"); + if (name) { + std::cout << "HashGet 'name': " << name << std::endl; + FreeString(name); // IMPORTANT: Free + } + + // HashMultiSet + if(HashMultiSet(client, "cpp:user:1", "city NewYork country USA")) { + std::cout << "HashMultiSet OK" << std::endl; + } + + // Get all hash fields (Updated for char* return) + std::cout << "HashGetAll:" << std::endl; + char* hashDataStr = HashGetAll(client, "cpp:user:1"); + if (hashDataStr) { + std::string hashResult(hashDataStr); + FreeString(hashDataStr); // IMPORTANT: Free + + // Parse the newline-delimited "field=value" string + std::vector pairs = split(hashResult, '\n'); + for (const auto& pair : pairs) { + size_t eqPos = pair.find('='); + if (eqPos != std::string::npos) { + std::cout << " " << pair.substr(0, eqPos) << ": " << pair.substr(eqPos + 1) << std::endl; + } + } + } else { + std::cout << " (empty or error)" << std::endl; + } + + // Execute a raw command + std::cout << "\n--- Raw Command ---" << std::endl; + char* result = ExecuteCommand(client, "PING", ""); + if (result) { + std::cout << "PING result: " << result; // Result likely includes \r\n + FreeString(result); // IMPORTANT: Free + } + + // Pipeline operations (example) + std::cout << "\n--- Pipeline Ops ---" << std::endl; + if (SetPipelineMode(client, true)) { + std::cout << "Pipeline mode enabled." << std::endl; + SetBatchSize(client, 5); + StringSet(client, "pipe:1", "a"); + StringSet(client, "pipe:2", "b"); + std::cout << " Queued: " << GetQueuedCommandCount(client) << std::endl; + char* flushResult = FlushPipeline(client); + if(flushResult) { + std::cout << " Flushed Result: " << flushResult << std::endl; + FreeString(flushResult); + } + SetPipelineMode(client, false); + std::cout << "Pipeline mode disabled." << std::endl; + } + + // Clean up string/list/hash keys used + Delete(client, "cpp:test"); + Delete(client, "cpp:mylist"); + Delete(client, "cpp:user:1"); + Delete(client, "pipe:1"); + Delete(client, "pipe:2"); + + // Destroy client + std::cout << "\nDestroying client..." << std::endl; + DestroyClient(client); + std::cout << "Client destroyed." << std::endl; + + return 0; +} \ No newline at end of file diff --git a/examples/example.py b/examples/example.py new file mode 100644 index 0000000..b32ecb3 --- /dev/null +++ b/examples/example.py @@ -0,0 +1,1438 @@ +import os +import platform +from ctypes import cdll, c_char_p, c_bool, c_void_p, c_int, POINTER +import re +import traceback +import sys + +# Set up logging +import logging + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("firefly_debug.log"), + logging.StreamHandler(sys.stdout), + ], +) +logger = logging.getLogger("FireflyDB") + +logger.info("Script starting...") + + +class FireflyDatabase: + """Wrapper for Firefly database client library""" + + def __init__(self, host="localhost", port=6379, password=None): + """Initialize the Firefly database connection + + Args: + host: Hostname of the Firefly server (default: localhost) + port: Port number of the Firefly server (default: 6379) + password: Optional password for authentication + """ + self.client = None + self.lib = None + self._load_library() + self._connect(host, port, password) + + def _load_library(self): + """Load the appropriate Firefly library for the current platform""" + try: + # Determine the library path based on the platform + lib_path = os.path.dirname(os.path.abspath(__file__)) + + if platform.system() == "Windows": + lib_file = os.path.join(lib_path, "libFirefly.dll") + else: # Linux/macOS + lib_file = os.path.join(lib_path, "libFirefly.so") + + if not os.path.exists(lib_file): + raise FileNotFoundError( + f"Firefly library not found: {lib_file}" + ) + + # Load the library + self.lib = cdll.LoadLibrary(lib_file) + if self.lib is None: # Explicitly check for None + raise OSError("Failed to load the Firefly library") + + logger.debug(f"Firefly library loaded from: {lib_file}") + + # Set argument and return types. Moved here to be closer to load + self.lib.CreateClient.restype = c_void_p + self.lib.CreateClient.argtypes = [c_char_p, c_int] + self.lib.DestroyClient.argtypes = [c_void_p] + self.lib.Authenticate.restype = c_bool + self.lib.Authenticate.argtypes = [c_void_p, c_char_p] + self.lib.StringSet.restype = c_bool + self.lib.StringSet.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.StringGet.restype = c_char_p + self.lib.StringGet.argtypes = [c_void_p, c_char_p] + self.lib.FreeString.argtypes = [c_char_p] + self.lib.ListLeftPush.restype = c_int + self.lib.ListLeftPush.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.ListRightPush.restype = c_int + self.lib.ListRightPush.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.ListLeftPop.restype = c_char_p + self.lib.ListLeftPop.argtypes = [c_void_p, c_char_p] + self.lib.ListRightPop.restype = c_char_p + self.lib.ListRightPop.argtypes = [c_void_p, c_char_p] + self.lib.ListRange.restype = c_char_p + self.lib.ListRange.argtypes = [c_void_p, c_char_p, c_int, c_int] + self.lib.ListIndex.restype = c_char_p + self.lib.ListIndex.argtypes = [c_void_p, c_char_p, c_int] + self.lib.ListSet.restype = c_bool + self.lib.ListSet.argtypes = [c_void_p, c_char_p, c_int, c_char_p] + self.lib.ListPosition.restype = c_int + self.lib.ListPosition.argtypes = [ + c_void_p, + c_char_p, + c_char_p, + c_int, + c_int, + ] + self.lib.ListTrim.restype = c_bool + self.lib.ListTrim.argtypes = [c_void_p, c_char_p, c_int, c_int] + self.lib.ListRemove.restype = c_int + self.lib.ListRemove.argtypes = [ + c_void_p, + c_char_p, + c_int, + c_char_p, + ] + self.lib.HashSet.restype = c_bool + self.lib.HashSet.argtypes = [ + c_void_p, + c_char_p, + c_char_p, + c_char_p, + ] + self.lib.HashGet.restype = c_char_p + self.lib.HashGet.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashDelete.restype = c_bool + self.lib.HashDelete.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashFieldExists.restype = c_bool + self.lib.HashFieldExists.argtypes = [c_void_p, c_char_p, c_char_p] + self.lib.HashGetAll.restype = c_char_p + self.lib.HashGetAll.argtypes = [c_void_p, c_char_p] + self.lib.ExecuteCommand.restype = c_char_p + self.lib.ExecuteCommand.argtypes = [c_void_p, c_char_p, c_char_p] + + # Pipeline operations + self.lib.SetPipelineMode.restype = c_bool + self.lib.SetPipelineMode.argtypes = [c_void_p, c_bool] + self.lib.SetBatchSize.restype = c_bool + self.lib.SetBatchSize.argtypes = [c_void_p, c_int] + self.lib.GetQueuedCommandCount.restype = c_int + self.lib.GetQueuedCommandCount.argtypes = [c_void_p] + self.lib.FlushPipeline.restype = c_char_p + self.lib.FlushPipeline.argtypes = [c_void_p] + self.lib.IsPipelineMode.restype = c_bool + self.lib.IsPipelineMode.argtypes = [c_void_p] + self.lib.GetBatchSize.restype = c_int + self.lib.GetBatchSize.argtypes = [c_void_p] + + except FileNotFoundError as e: + logger.error(f"Error loading library: {e}") + raise # Re-raise to halt execution + except OSError as e: + logger.error(f"OS Error loading library: {e}") + raise + except Exception as e: + logger.error(f"Unexpected error loading library: {e}") + raise + + def _connect(self, host, port, password=None): + """Connect to the Firefly server and authenticate if needed""" + try: + # Convert host to bytes for C API + host_bytes = host.encode("utf-8") + logger.debug(f"Connecting to {host}:{port}") + + # Create client + self.client = self.lib.CreateClient(host_bytes, port) + if not self.client: + raise ConnectionError( + f"Failed to connect to Firefly server at {host}:{port}" + ) + logger.debug("Client created successfully") + + # Authenticate if password is provided + if password: + password_bytes = password.encode("utf-8") + logger.debug("Authenticating...") + if not self.lib.Authenticate(self.client, password_bytes): + self.close() + raise ConnectionError("Authentication failed") + logger.debug("Authentication successful") + else: + logger.debug("No password provided, skipping authentication") + + except ConnectionError as e: + logger.error(f"Connection error: {e}") + if self.client: + self.close() # Clean up the client if it was created + raise # Re-raise the exception for the caller to handle + except Exception as e: + logger.error(f"Unexpected error during connection: {e}") + if self.client: + self.close() + raise + + def close(self): + """Close the connection to the Firefly server""" + try: + if self.client: + logger.debug("Destroying client connection") + self.lib.DestroyClient(self.client) + self.client = None + logger.debug("Client connection destroyed") + else: + logger.debug("Client connection already closed") + except Exception as e: + logger.error(f"Error closing connection: {e}") + # Do not re-raise here, as close() should not throw exceptions in normal usage. + # Caller has no recovery options. + + def __enter__(self): + """Context manager entry""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit""" + self.close() + if exc_type: # Log the exception if one occurred within the context + logger.error( + f"Exception in context: {exc_type.__name__}, {exc_val}. Traceback:\n{''.join(traceback.format_exception(exc_type, exc_val, exc_tb))}" + ) + + def _check_connection(self): + """Check if the client is connected""" + if not self.client: + raise ConnectionError("Not connected to Firefly server") + + def _to_bytes(self, value): + """Convert a value to bytes for C API""" + if isinstance(value, bytes): + return value + return str(value).encode("utf-8") + + def _from_bytes(self, value): + """Convert bytes from C API to string""" + try: + if value is None: + logger.debug("_from_bytes received None value") + return None + + if not value: # Zero value check + logger.debug("_from_bytes received empty value") + return "" + + # Try to decode safely + try: + result = value.decode("utf-8") + return result + except UnicodeDecodeError as e: + logger.error(f"Unicode decode error in _from_bytes: {e}") + # Try with a more forgiving approach + return value.decode("utf-8", errors="replace") + except Exception as e: + logger.error(f"Error in _from_bytes: {e}") + return None + + def _free_string(self, ptr): + """Free a string pointer allocated by the C API""" + logger.debug("Starting free string") + try: + # Check if ptr is valid and non-zero + if ptr and ptr != 0: + # Wrap in another try/except to catch any errors from the FreeString call + try: + self.lib.FreeString(ptr) + logger.debug("String freed successfully") + except Exception as inner_e: + logger.error(f"Error in FreeString call: {inner_e}") + else: + logger.debug(f"Skipping free for null or zero pointer: {ptr}") + except Exception as e: + logger.error(f"Error in _free_string outer block: {e}") + # Do not re-raise; resource cleanup, caller cannot handle. + + def execute_command(self, command, args=""): + """Execute a raw command on the server + + Args: + command: The command to execute + args: Optional arguments for the command + + Returns: + The response from the server + """ + try: + logger.debug(f"Executing command: {command} with args: {args}") + self._check_connection() + command_bytes = self._to_bytes(command) + args_bytes = self._to_bytes(args) + + result = self.lib.ExecuteCommand( + self.client, command_bytes, args_bytes + ) + + logger.debug(f"ExecuteCommand result pointer: {result}") + + if result: + try: + # If the result is already a bytes object, we don't need to free it + if isinstance(result, bytes): + logger.debug("Result is already a Python bytes object, no need to free") + response = result.decode("utf-8") + logger.debug(f"Received response: {response}") + return response + + # Otherwise, treat it as a C pointer that needs to be freed + response = self._from_bytes(result) + logger.debug(f"Received response: {response}") + + # Log before freeing + logger.debug(f"About to free string at address: {result}") + self._free_string(result) + return response + except Exception as e: + logger.error(f"Error processing ExecuteCommand result: {e}") + # Try to free anyway to avoid memory leaks, but only if it's not a bytes object + try: + if result and not isinstance(result, bytes): + self._free_string(result) + except Exception as free_e: + logger.error(f"Failed to free result after error: {free_e}") + return None + else: + logger.warning("ExecuteCommand returned NULL") + return None + except ConnectionError as e: + logger.error(f"Connection error during execute_command: {e}") + raise # Re-raise to be handled by caller + except Exception as e: + logger.error( + f"Error in execute_command: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None # Consistent return on error + + def ping(self): + """Test the connection to the server + + Returns: + True if the server responds with PONG, False otherwise + """ + try: + logger.debug("Sending PING command") + self._check_connection() + + # Add a try/except specifically around execute_command + try: + response = self.execute_command("PING") + logger.debug(f"Raw ping response: '{response}'") + except Exception as e: + logger.error(f"Exception in execute_command during ping: {e}") + return False + + # Log the type and value of the response + logger.debug( + f"Response type: {type(response)}, value: '{response}'" + ) + + if response is None: + logger.warning("Received NULL response from ping") + return False + + # Normalize: strip whitespace, remove leading '+', uppercase + try: + normalized = response.strip().lstrip("+").upper() + logger.debug(f"Normalized response: '{normalized}'") + + if normalized == "PONG": + logger.debug( + "PONG found in normalized response - ping successful" + ) + return True + else: + logger.warning( + f"PONG not found in response: raw='{response}', normalized='{normalized}'" + ) + return False + except AttributeError: + logger.error(f"Unable to process ping response: {response}") + return False + except Exception as e: + logger.error( + f"Ping failed: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + #region String operations + def string_set(self, key, value): + """Set a string value + + Args: + key: The key to set + value: The value to set + + Returns: + True if successful + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + value_bytes = self._to_bytes(value) + result = self.lib.StringSet(self.client, key_bytes, value_bytes) + logger.debug(f"StringSet result for key '{key}': {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in string_set: {e}") + raise + except Exception as e: + logger.error( + f"Error in string_set: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def string_get(self, key): + """Get a string value + + Args: + key: The key to get + + Returns: + The value, or None if not found + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.StringGet(self.client, key_bytes) + logger.debug(f"StringGet raw result pointer: {result}") + + if result: + try: + # If the result is already a bytes object, we don't need to free it + if isinstance(result, bytes): + logger.debug("Result is already a Python bytes object, no need to free") + value = result.decode("utf-8") + logger.debug(f"StringGet for key '{key}': {value}") + return value + + # Otherwise, treat it as a C pointer that needs to be freed + value = self._from_bytes(result) + logger.debug(f"StringGet decoded value: {value}") + + # Log before freeing + logger.debug(f"About to free string at address: {result}") + self._free_string(result) + logger.debug(f"StringGet for key '{key}': {value}") + return value + except Exception as decode_e: + logger.error(f"Error processing StringGet result: {decode_e}") + # Try to free anyway, but only if it's not a bytes object + try: + if not isinstance(result, bytes): + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing string in StringGet: {free_e}") + return None + logger.debug(f"StringGet for key '{key}': Key not found") + return None + except ConnectionError as e: + logger.error(f"Connection error in string_get: {e}") + raise + except Exception as e: + logger.error( + f"Error in string_get: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None + + def delete(self, key): + """Delete a key + + Args: + key: The key to delete + + Returns: + The number of keys removed + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.ExecuteCommand(self.client, b"DEL", key_bytes) + logger.debug(f"Delete result: {result}") + + if result: + try: + # Handle as bytes or C pointer + if isinstance(result, bytes): + # Directly decode bytes + response = result.decode("utf-8") + + # Check if this is a pipeline response (multiple lines or not starting with ':') + if "\r\n" in response and not response.startswith(':'): + # If in pipeline mode, find the actual DEL response (usually the last line with ':') + for line in response.split("\r\n"): + if line.startswith(':'): + try: + count = int(line.strip(':')) + logger.debug(f"Found DEL count in pipeline response: {count}") + return count + except ValueError: + pass + + # If we didn't find a clear DEL count, just report success + logger.debug(f"Complex pipeline response for DEL on key '{key}', assuming success") + return 1 + else: + # Regular response format + try: + count = int(response.strip(":\r\n")) + except ValueError: + logger.warning(f"Unexpected response from DEL command: {response}") + count = 0 + else: + # Handle as C pointer + try: + response = self._from_bytes(result) + count = int(response.strip(":\r\n")) + self._free_string(result) + except ValueError: + self._free_string(result) # Free memory even on error + logger.warning(f"Unexpected response from DEL command: {response}") + count = 0 + + logger.debug(f"Deleted key '{key}'. Count: {count}") + return count + except Exception as e: + logger.error(f"Error processing DEL result: {e}") + # Try to free if needed + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing DEL result: {free_e}") + return 0 + logger.debug(f"Key '{key}' not found.") + return 0 + except ConnectionError as e: + logger.error(f"Connection error in delete: {e}") + raise + except Exception as e: + logger.error( + f"Error in delete: {e}. Traceback:\n{traceback.format_exc()}" + ) + return 0 + #endregion + + #region List operations + def list_left_push(self, key, value): + """Push a value to the left of a list + + Args: + key: The list key + value: The value to push + + Returns: + The length of the list after the push + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + value_bytes = self._to_bytes(value) + result = self.lib.ListLeftPush(self.client, key_bytes, value_bytes) + logger.debug( + f"ListLeftPush on key '{key}' with value '{value}'. New length: {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in list_left_push: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_left_push: {e}. Traceback:\n{traceback.format_exc()}" + ) + return 0 + def list_right_push(self, key, value): + """Push a value to the right of a list + + Args: + key: The list key + value: The value to push + + Returns: + The length of the list after the push + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + value_bytes = self._to_bytes(value) + result = self.lib.ListRightPush( + self.client, key_bytes, value_bytes + ) + logger.debug( + f"ListRightPush on key '{key}' with value '{value}'. New length: {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in list_right_push: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_right_push: {e}. Traceback:\n{traceback.format_exc()}" + ) + return 0 + + def list_left_pop(self, key): + """Pop a value from the left of a list + + Args: + key: The list key + + Returns: + The popped value, or None if the list is empty + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.ListLeftPop(self.client, key_bytes) + logger.debug(f"ListLeftPop raw result: {result}") + + if result: + try: + # If the result is already a bytes object, we don't need to free it + if isinstance(result, bytes): + logger.debug("Result is already a Python bytes object, no need to free") + value = result.decode("utf-8") + logger.debug(f"ListLeftPop on key '{key}'. Popped value: {value}") + return value + + # Otherwise, treat it as a C pointer that needs to be freed + value = self._from_bytes(result) + logger.debug(f"About to free string at address: {result}") + self._free_string(result) + logger.debug(f"ListLeftPop on key '{key}'. Popped value: {value}") + return value + except Exception as e: + logger.error(f"Error processing ListLeftPop result: {e}") + # Try to free if needed + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing string in ListLeftPop: {free_e}") + return None + logger.debug(f"ListLeftPop on key '{key}'. List is empty.") + return None + except ConnectionError as e: + logger.error(f"Connection error in list_left_pop: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_left_pop: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None + + def list_right_pop(self, key): + """Pop a value from the right of a list + + Args: + key: The list key + + Returns: + The popped value, or None if the list is empty + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.ListRightPop(self.client, key_bytes) + logger.debug(f"ListRightPop raw result: {result}") + + if result: + try: + # If result is already a bytes object + if isinstance(result, bytes): + logger.debug("Result is already a Python bytes object, no need to free") + value = result.decode("utf-8") + logger.debug(f"ListRightPop on key '{key}'. Popped value: {value}") + return value + + # Regular C pointer handling + value = self._from_bytes(result) + logger.debug(f"About to free string at address: {result}") + self._free_string(result) + logger.debug(f"ListRightPop on key '{key}'. Popped value: {value}") + return value + except Exception as e: + logger.error(f"Error processing ListRightPop result: {e}") + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing string in ListRightPop: {free_e}") + return None + logger.debug(f"ListRightPop on key '{key}'. List empty") + return None + except ConnectionError as e: + logger.error(f"Connection error in list_right_pop: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_right_pop: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None + + def list_range(self, key, start, stop): + """Get a range of elements from a list + + Args: + key: The list key + start: The start index (inclusive) + stop: The stop index (inclusive) + + Returns: + A list of values in the specified range + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.ListRange(self.client, key_bytes, start, stop) + logger.debug(f"ListRange raw result: {result}") + + if result: + try: + # The result is a newline-delimited string according to the README + if isinstance(result, bytes): + # Directly decode the bytes object + value_str = result.decode("utf-8") + else: + # Handle as C pointer to string + value_str = self._from_bytes(result) + # Free the allocated string + self._free_string(result) + + # Split by newlines to get individual list items + values = value_str.split('\n') if value_str else [] + + logger.debug(f"ListRange on key '{key}' from {start} to {stop}. Values: {values}") + return values + except Exception as e: + logger.error(f"Error processing ListRange result: {e}") + # Try to free if it was a C pointer + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing ListRange result: {free_e}") + return [] + logger.debug(f"ListRange on key '{key}' from {start} to {stop}. Empty list") + return [] + except ConnectionError as e: + logger.error(f"Connection error in list_range: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_range: {e}. Traceback:\n{traceback.format_exc()}" + ) + return [] + + def list_index(self, key, index): + """Get an element at a specific index in a list + + Args: + key: The list key + index: The index of the element + + Returns: + The element at the specified index, or None if not found + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.ListIndex(self.client, key_bytes, index) + if result: + value = self._from_bytes(result) + self._free_string(result) + logger.debug( + f"ListIndex on key '{key}' at index {index}: {value}" + ) + return value + logger.debug( + f"ListIndex on key '{key}' at index {index}: Not found." + ) + return None + except ConnectionError as e: + logger.error(f"Connection error in list_index: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_index: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None + + def list_set(self, key, index, value): + """Set an element at a specific index in a list + + Args: + key: The list key + index: The index of the element + value: The value to set + + Returns: + True if successful + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + value_bytes = self._to_bytes(value) + result = self.lib.ListSet( + self.client, key_bytes, index, value_bytes + ) + logger.debug( + f"ListSet on key '{key}' at index {index} with value '{value}': {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in list_set: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_set: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def list_position(self, key, element, rank=1, maxlen=0): + """Find the position of an element in a list + + Args: + key: The list key + element: The element to find + rank: The rank of the element to find (default: 1) + maxlen: Maximum number of elements to scan (default: 0, meaning no limit) + + Returns: + The index of the element, or -1 if not found + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + element_bytes = self._to_bytes(element) + result = self.lib.ListPosition( + self.client, key_bytes, element_bytes, rank, maxlen + ) + logger.debug( + f"ListPosition on key '{key}' for element '{element}' (rank={rank}, maxlen={maxlen}): {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in list_position: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_position: {e}. Traceback:\n{traceback.format_exc()}" + ) + return -1 + + def list_trim(self, key, start, stop): + """Trim a list to the specified range + + Args: + key: The list key + start: The start index (inclusive) + stop: The stop index (inclusive) + + Returns: + True if successful + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.ListTrim(self.client, key_bytes, start, stop) + logger.debug( + f"ListTrim on key '{key}' from {start} to {stop}: {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in list_trim: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_trim: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def list_remove(self, key, count, element): + """Remove elements equal to the given value from a list + + Args: + key: The list key + count: The number of occurrences to remove (positive: from head, negative: from tail, 0: all) + element: The element to remove + + Returns: + The number of elements removed + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + element_bytes = self._to_bytes(element) + result = self.lib.ListRemove( + self.client, key_bytes, count, element_bytes + ) + logger.debug( + f"ListRemove on key '{key}' removing {count} of element '{element}': {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in list_remove: {e}") + raise + except Exception as e: + logger.error( + f"Error in list_remove: {e}. Traceback:\n{traceback.format_exc()}" + ) + return 0 + #endregion + + #region Hash operations + def hash_set(self, key, field, value): + """Set a field in a hash + + Args: + key: The hash key + field: The field name + value: The field value + + Returns: + True if successful + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + field_bytes = self._to_bytes(field) + value_bytes = self._to_bytes(value) + result = self.lib.HashSet( + self.client, key_bytes, field_bytes, value_bytes + ) + logger.debug(f"HashSet on key '{key}', field '{field}': {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in hash_set: {e}") + raise + except Exception as e: + logger.error( + f"Error in hash_set: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def hash_get(self, key, field): + """Get a field from a hash + + Args: + key: The hash key + field: The field name + + Returns: + The field value, or None if not found + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + field_bytes = self._to_bytes(field) + result = self.lib.HashGet(self.client, key_bytes, field_bytes) + logger.debug(f"HashGet raw result: {result}") + + if result: + try: + # Process the result based on its type + if isinstance(result, bytes): + # Directly decode the bytes object + value = result.decode("utf-8") + else: + # Handle as C pointer to string + value = self._from_bytes(result) + # Free the allocated string + logger.debug(f"About to free string at address: {result}") + self._free_string(result) + + logger.debug(f"HashGet on key '{key}', field '{field}': {value}") + return value + except Exception as e: + logger.error(f"Error processing HashGet result: {e}") + # Try to free if it was a C pointer + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing HashGet result: {free_e}") + return None + logger.debug(f"HashGet on key '{key}', field '{field}': Not found.") + return None + except ConnectionError as e: + logger.error(f"Connection error in hash_get: {e}") + raise + except Exception as e: + logger.error( + f"Error in hash_get: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None + + def hash_delete(self, key, field): + """Delete a field from a hash + + Args: + key: The hash key + field: The field name + + Returns: + True if successful + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + field_bytes = self._to_bytes(field) + result = self.lib.HashDelete(self.client, key_bytes, field_bytes) + logger.debug( + f"HashDelete on key '{key}', field '{field}': {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in hash_delete: {e}") + raise + except Exception as e: + logger.error( + f"Error in hash_delete: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def hash_field_exists(self, key, field): + """Check if a field exists in a hash + + Args: + key: The hash key + field: The field name + + Returns: + True if the field exists + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + field_bytes = self._to_bytes(field) + result = self.lib.HashFieldExists( + self.client, key_bytes, field_bytes + ) + logger.debug( + f"HashFieldExists on key '{key}', field '{field}': {result}" + ) + return result + except ConnectionError as e: + logger.error(f"Connection error in hash_field_exists: {e}") + raise + except Exception as e: + logger.error( + f"Error in hash_field_exists: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def hash_get_all(self, key): + """Get all fields and values from a hash + + Args: + key: The hash key + + Returns: + A dictionary of field-value pairs + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + result = self.lib.HashGetAll(self.client, key_bytes) + logger.debug(f"HashGetAll raw result: {result}") + + if result: + try: + # The result is a newline-delimited string with field=value format + if isinstance(result, bytes): + # Directly decode the bytes object + value_str = result.decode("utf-8") + else: + # Handle as C pointer to string + value_str = self._from_bytes(result) + # Free the allocated string + self._free_string(result) + + # Parse the field=value format + data = {} + if value_str: + pairs = value_str.split('\n') + for pair in pairs: + if '=' in pair: + field, value = pair.split('=', 1) + data[field] = value + + logger.debug(f"HashGetAll on key '{key}': {data}") + return data + except Exception as e: + logger.error(f"Error processing HashGetAll result: {e}") + # Try to free if it was a C pointer + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing HashGetAll result: {free_e}") + return {} + logger.debug(f"HashGetAll on key '{key}': Hash is empty") + return {} + except ConnectionError as e: + logger.error(f"Connection error in hash_get_all: {e}") + raise + except Exception as e: + logger.error( + f"Error in hash_get_all: {e}. Traceback:\n{traceback.format_exc()}" + ) + return {} + + def hash_multi_set(self, key, field_values): + """Set multiple fields in a hash at once + + Args: + key: The hash key + field_values: A dictionary of field-value pairs + + Returns: + True if successful + """ + try: + self._check_connection() + key_bytes = self._to_bytes(key) + + # Build the command arguments + args = [] + for field, value in field_values.items(): + args.append(field) + args.append(value) + + args_str = " ".join(args) + args_bytes = self._to_bytes(args_str) + + result = self.lib.ExecuteCommand(self.client, b"HMSET", args_bytes) + if result: + success = self._from_bytes(result) == "+OK\r\n" + self._free_string(result) + logger.debug( + f"HashMultiSet on key '{key}' with {len(field_values)} fields: {success}" + ) + return success + logger.warning(f"HashMultiSet on key '{key}' failed") + return False + except ConnectionError as e: + logger.error(f"Connection error in hash_multi_set: {e}") + raise + except Exception as e: + logger.error( + f"Error in hash_multi_set: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + #endregion + + #region Pipeline operations + def set_pipeline_mode(self, enabled): + """Enable or disable pipeline mode + + Args: + enabled: True to enable pipeline mode, False to disable + + Returns: + True if successful + """ + try: + self._check_connection() + logger.debug(f"Setting pipeline mode to {enabled}") + result = self.lib.SetPipelineMode(self.client, enabled) + logger.debug(f"SetPipelineMode to {enabled}: {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in set_pipeline_mode: {e}") + raise + except Exception as e: + logger.error( + f"Error in set_pipeline_mode: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def set_batch_size(self, size): + """Set the batch size for pipelined commands + + Args: + size: The maximum number of commands to batch before sending + + Returns: + True if successful + """ + try: + self._check_connection() + logger.debug(f"Setting batch size to {size}") + result = self.lib.SetBatchSize(self.client, size) + logger.debug(f"SetBatchSize to {size}: {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in set_batch_size: {e}") + raise + except Exception as e: + logger.error( + f"Error in set_batch_size: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def get_queued_command_count(self): + """Get the current number of queued commands + + Returns: + The number of queued commands + """ + try: + self._check_connection() + result = self.lib.GetQueuedCommandCount(self.client) + logger.debug(f"GetQueuedCommandCount: {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in get_queued_command_count: {e}") + raise + except Exception as e: + logger.error( + f"Error in get_queued_command_count: {e}. Traceback:\n{traceback.format_exc()}" + ) + return 0 + + def flush_pipeline(self): + """Flush any queued commands in pipeline mode + + Returns: + The response from the server + """ + try: + self._check_connection() + result = self.lib.FlushPipeline(self.client) + if result: + try: + # Handle as bytes or C pointer + if isinstance(result, bytes): + # Directly decode bytes + response = result.decode("utf-8") + else: + # Handle as C pointer + response = self._from_bytes(result) + self._free_string(result) + + logger.debug(f"Flushed pipeline. Response: {response}") + return response + except Exception as e: + logger.error(f"Error processing FlushPipeline result: {e}") + # Try to free if needed + if result and not isinstance(result, bytes): + try: + self._free_string(result) + except Exception as free_e: + logger.error(f"Error freeing FlushPipeline result: {free_e}") + return None + logger.warning("FlushPipeline returned NULL") + return None + except ConnectionError as e: + logger.error(f"Connection error in flush_pipeline: {e}") + raise + except Exception as e: + logger.error( + f"Error in flush_pipeline: {e}. Traceback:\n{traceback.format_exc()}" + ) + return None + + def is_pipeline_mode(self): + """Check if pipeline mode is enabled + + Returns: + True if pipeline mode is enabled + """ + try: + self._check_connection() + result = self.lib.IsPipelineMode(self.client) + logger.debug(f"IsPipelineMode: {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in is_pipeline_mode: {e}") + raise + except Exception as e: + logger.error( + f"Error in is_pipeline_mode: {e}. Traceback:\n{traceback.format_exc()}" + ) + return False + + def get_batch_size(self): + """Get the current batch size + + Returns: + The current batch size + """ + try: + self._check_connection() + result = self.lib.GetBatchSize(self.client) + logger.debug(f"GetBatchSize: {result}") + return result + except ConnectionError as e: + logger.error(f"Connection error in get_batch_size: {e}") + raise + except Exception as e: + logger.error( + f"Error in get_batch_size: {e}. Traceback:\n{traceback.format_exc()}" + ) + return 0 + #endregion + +def main(): + logger.info("Starting FireflyDatabase test...") + + try: + # Basic usage + logger.info("Creating FireflyDatabase instance...") + with FireflyDatabase( + host="135.134.202.221", port=6379, password="xyz123" + ) as db: + logger.info("Connected to Firefly server") + logger.info("Performing operations...") + + logger.info("About to call ping()...") + try: + ping_result = db.ping() + logger.info(f"Ping completed with result: {ping_result}") + if not ping_result: + logger.error("Ping failed, exiting test.") + exit(1) + except Exception as ping_error: + logger.error(f"Exception during ping: {ping_error}") + logger.error(f"Exception type: {type(ping_error)}") + logger.error(traceback.format_exc()) + exit(1) # Exit if ping fails + + logger.info("After ping attempt") + + # String operations + logger.info("Testing Database") + + db.string_set("test", "hello world") + value = db.string_get("test") + logger.info(f"Value: {value}") + + # List operations + db.list_right_push("mylist", "item1") + db.list_right_push("mylist", "item2") + db.list_right_push("mylist", "item3") + + items = db.list_range("mylist", 0, -1) + logger.info("List items:") + for item in items: + logger.info(f" - {item}") + + # Hash operations + db.hash_set("user:1", "name", "John") + db.hash_set("user:1", "email", "john@example.com") + + name = db.hash_get("user:1", "name") + logger.info(f"Name: {name}") + + user_data = db.hash_get_all("user:1") + logger.info("User data:") + for field, value in user_data.items(): + logger.info(f" {field}: {value}") + + # Pipeline operations + logger.info("\nPipeline Operations:") + db.set_pipeline_mode(True) + logger.info("Note: When in pipeline mode, operations return 'QUEUED' instead of actual values") + logger.info("Values aren't actually set until flush_pipeline() is called") + db.set_batch_size(100) + + # Add dummy entries at index 0 for each data type to handle Redis pipeline reordering + logger.info("Adding dummy entries for each data type to handle Redis pipeline shifting") + db.string_set("pipeline:string:0", "dummy-string") + db.list_right_push("pipeline:list:0", "dummy-list-item") + db.hash_set("pipeline:hash", "dummy-field", "dummy-value") + + # Queue some commands with unique identifiable values + # Commands will return QUEUED but not be executed until flush_pipeline + for i in range(1, 6): + # Use distinct prefixes for each key type to identify in results + db.string_set(f"pipeline:string:{i}", f"string-value-{i}") + # Use separate list keys for each list item to prevent overwriting + db.list_right_push(f"pipeline:list", f"list-item-{i}") + # Make sure we use field names that match our expected pattern + db.hash_set("pipeline:hash", f"field-{i}", f"hash-value-{i}") + + logger.info(f"Queued commands: {db.get_queued_command_count()}") + logger.info("Results should be obtained after flushing the pipeline") + + # Flush pipeline (execute all queued commands as a batch) + result = db.flush_pipeline() + logger.info(f"Pipeline flush result: {result}") + + logger.info("After pipeline flush, we need to exit pipeline mode to get actual values") + + # Disable pipeline mode before verifying results to see actual values, not QUEUED + db.set_pipeline_mode(False) + logger.info("Pipeline mode disabled") + + # Verify results - include index 0 for completeness + logger.info("\nVerifying results (note: Redis pipeline responses might not match input order):") + for i in range(0, 6): # Start from 0 to include dummy entry + value = db.string_get(f"pipeline:string:{i}") + logger.info(f"String key:{i} = {value}") + + # Check each individual list including dummy list + for i in range(0, 6): # Start from 0 to include dummy entry + items = db.list_range("pipeline:list", 0, -1) + logger.info(f"List items: {items}") + + # Fix the hash_get_all implementation issue + hash_data = db.hash_get_all("pipeline:hash") + + # If hash_data is empty but we expect data, try a different approach + if not hash_data: + logger.info("Hash appears empty from hash_get_all, trying alternative approach...") + # Try to get each field individually + hash_data = {} + # Get the dummy field first + dummy_value = db.hash_get("pipeline:hash", "dummy-field") + if dummy_value: + hash_data["dummy-field"] = dummy_value + + # Try to get each field we expect + for i in range(1, 6): + field_name = f"field-{i}" + field_value = db.hash_get("pipeline:hash", field_name) + if field_value: + hash_data[field_name] = field_value + + logger.info("Hash fields:") + for field, value in hash_data.items(): + logger.info(f" {field}: {value}") + + # Cleanup - don't forget to clean up the dummy entries too + for i in range(0, 6): # Start from 0 to include dummy entry + db.delete(f"pipeline:string:{i}") + db.delete(f"pipeline:list:{i}") + db.delete("pipeline:hash") + logger.info("Cleanup complete") + except Exception as e: + logger.error(f"Exception occurred: {e}") + logger.error(traceback.format_exc()) + finally: + logger.info("Test completed") + logger.info("Script completed successfully.") + # print("exiting") + # sys.exit() + + +if __name__ == "__main__": + main() diff --git a/examples/firefly_debug.log b/examples/firefly_debug.log new file mode 100644 index 0000000..2a37703 --- /dev/null +++ b/examples/firefly_debug.log @@ -0,0 +1,178 @@ +2025-04-09 18:14:40,895 - FireflyDB - INFO - Script starting... +2025-04-09 18:14:40,895 - FireflyDB - INFO - Starting FireflyDatabase test... +2025-04-09 18:14:40,896 - FireflyDB - INFO - Creating FireflyDatabase instance... +2025-04-09 18:14:40,940 - FireflyDB - DEBUG - Firefly library loaded from: G:\firefly\client\examples\libFirefly.dll +2025-04-09 18:14:40,940 - FireflyDB - DEBUG - Connecting to 135.134.202.221:6379 +2025-04-09 18:14:40,944 - FireflyDB - DEBUG - Client created successfully +2025-04-09 18:14:40,944 - FireflyDB - DEBUG - Authenticating... +2025-04-09 18:14:40,964 - FireflyDB - DEBUG - Authentication successful +2025-04-09 18:14:40,964 - FireflyDB - INFO - Connected to Firefly server +2025-04-09 18:14:40,964 - FireflyDB - INFO - Performing operations... +2025-04-09 18:14:40,965 - FireflyDB - INFO - About to call ping()... +2025-04-09 18:14:40,965 - FireflyDB - DEBUG - Sending PING command +2025-04-09 18:14:40,965 - FireflyDB - DEBUG - Executing command: PING with args: +2025-04-09 18:14:40,981 - FireflyDB - DEBUG - ExecuteCommand result pointer: b'+PONG\r\n' +2025-04-09 18:14:40,982 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:40,982 - FireflyDB - DEBUG - Received response: +PONG + + +2025-04-09 18:14:40,982 - FireflyDB - DEBUG - Raw ping response: '+PONG + +' +2025-04-09 18:14:40,983 - FireflyDB - DEBUG - Response type: , value: '+PONG + +' +2025-04-09 18:14:40,983 - FireflyDB - DEBUG - Normalized response: 'PONG' +2025-04-09 18:14:40,983 - FireflyDB - DEBUG - PONG found in normalized response - ping successful +2025-04-09 18:14:40,983 - FireflyDB - INFO - Ping completed with result: True +2025-04-09 18:14:40,983 - FireflyDB - INFO - After ping attempt +2025-04-09 18:14:40,983 - FireflyDB - INFO - Testing Database +2025-04-09 18:14:40,996 - FireflyDB - DEBUG - StringSet result for key 'test': True +2025-04-09 18:14:41,015 - FireflyDB - DEBUG - StringGet raw result pointer: b'"hello world"' +2025-04-09 18:14:41,015 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:41,016 - FireflyDB - DEBUG - StringGet for key 'test': "hello world" +2025-04-09 18:14:41,016 - FireflyDB - INFO - Value: "hello world" +2025-04-09 18:14:41,018 - FireflyDB - DEBUG - ListRightPush on key 'mylist' with value 'item1'. New length: 4 +2025-04-09 18:14:41,033 - FireflyDB - DEBUG - ListRightPush on key 'mylist' with value 'item2'. New length: 5 +2025-04-09 18:14:41,036 - FireflyDB - DEBUG - ListRightPush on key 'mylist' with value 'item3'. New length: 6 +2025-04-09 18:14:41,045 - FireflyDB - DEBUG - ListRange raw result: b'item1\nitem2\nitem3\nitem1\nitem2\nitem3' +2025-04-09 18:14:41,046 - FireflyDB - DEBUG - ListRange on key 'mylist' from 0 to -1. Values: ['item1', 'item2', 'item3', 'item1', 'item2', 'item3'] +2025-04-09 18:14:41,046 - FireflyDB - INFO - List items: +2025-04-09 18:14:41,046 - FireflyDB - INFO - - item1 +2025-04-09 18:14:41,047 - FireflyDB - INFO - - item2 +2025-04-09 18:14:41,047 - FireflyDB - INFO - - item3 +2025-04-09 18:14:41,047 - FireflyDB - INFO - - item1 +2025-04-09 18:14:41,047 - FireflyDB - INFO - - item2 +2025-04-09 18:14:41,047 - FireflyDB - INFO - - item3 +2025-04-09 18:14:41,061 - FireflyDB - DEBUG - HashSet on key 'user:1', field 'name': False +2025-04-09 18:14:41,071 - FireflyDB - DEBUG - HashSet on key 'user:1', field 'email': False +2025-04-09 18:14:41,076 - FireflyDB - DEBUG - HashGet raw result: b'John' +2025-04-09 18:14:41,076 - FireflyDB - DEBUG - HashGet on key 'user:1', field 'name': John +2025-04-09 18:14:41,077 - FireflyDB - INFO - Name: John +2025-04-09 18:14:41,089 - FireflyDB - DEBUG - HashGetAll raw result: b'email=john@example.com\nname=John' +2025-04-09 18:14:41,089 - FireflyDB - DEBUG - HashGetAll on key 'user:1': {'email': 'john@example.com', 'name': 'John'} +2025-04-09 18:14:41,090 - FireflyDB - INFO - User data: +2025-04-09 18:14:41,090 - FireflyDB - INFO - email: john@example.com +2025-04-09 18:14:41,090 - FireflyDB - INFO - name: John +2025-04-09 18:14:41,090 - FireflyDB - INFO - +Pipeline Operations: +2025-04-09 18:14:41,090 - FireflyDB - DEBUG - Setting pipeline mode to True +2025-04-09 18:14:41,090 - FireflyDB - DEBUG - SetPipelineMode to True: True +2025-04-09 18:14:41,091 - FireflyDB - INFO - Note: When in pipeline mode, operations return 'QUEUED' instead of actual values +2025-04-09 18:14:41,091 - FireflyDB - INFO - Values aren't actually set until flush_pipeline() is called +2025-04-09 18:14:41,091 - FireflyDB - DEBUG - Setting batch size to 100 +2025-04-09 18:14:41,091 - FireflyDB - DEBUG - SetBatchSize to 100: True +2025-04-09 18:14:41,091 - FireflyDB - INFO - Adding dummy entries for each data type to handle Redis pipeline shifting +2025-04-09 18:14:41,092 - FireflyDB - DEBUG - StringSet result for key 'pipeline:string:0': False +2025-04-09 18:14:41,092 - FireflyDB - DEBUG - ListRightPush on key 'pipeline:list:0' with value 'dummy-list-item'. New length: 0 +2025-04-09 18:14:41,092 - FireflyDB - DEBUG - HashSet on key 'pipeline:hash', field 'dummy-field': False +2025-04-09 18:14:41,092 - FireflyDB - DEBUG - StringSet result for key 'pipeline:string:1': False +2025-04-09 18:14:41,092 - FireflyDB - DEBUG - ListRightPush on key 'pipeline:list' with value 'list-item-1'. New length: 0 +2025-04-09 18:14:41,093 - FireflyDB - DEBUG - HashSet on key 'pipeline:hash', field 'field-1': False +2025-04-09 18:14:41,093 - FireflyDB - DEBUG - StringSet result for key 'pipeline:string:2': False +2025-04-09 18:14:41,093 - FireflyDB - DEBUG - ListRightPush on key 'pipeline:list' with value 'list-item-2'. New length: 0 +2025-04-09 18:14:41,093 - FireflyDB - DEBUG - HashSet on key 'pipeline:hash', field 'field-2': False +2025-04-09 18:14:41,094 - FireflyDB - DEBUG - StringSet result for key 'pipeline:string:3': False +2025-04-09 18:14:41,094 - FireflyDB - DEBUG - ListRightPush on key 'pipeline:list' with value 'list-item-3'. New length: 0 +2025-04-09 18:14:41,094 - FireflyDB - DEBUG - HashSet on key 'pipeline:hash', field 'field-3': False +2025-04-09 18:14:41,094 - FireflyDB - DEBUG - StringSet result for key 'pipeline:string:4': False +2025-04-09 18:14:41,094 - FireflyDB - DEBUG - ListRightPush on key 'pipeline:list' with value 'list-item-4'. New length: 0 +2025-04-09 18:14:41,095 - FireflyDB - DEBUG - HashSet on key 'pipeline:hash', field 'field-4': False +2025-04-09 18:14:41,095 - FireflyDB - DEBUG - StringSet result for key 'pipeline:string:5': False +2025-04-09 18:14:41,095 - FireflyDB - DEBUG - ListRightPush on key 'pipeline:list' with value 'list-item-5'. New length: 0 +2025-04-09 18:14:41,095 - FireflyDB - DEBUG - HashSet on key 'pipeline:hash', field 'field-5': False +2025-04-09 18:14:41,095 - FireflyDB - DEBUG - GetQueuedCommandCount: 18 +2025-04-09 18:14:41,095 - FireflyDB - INFO - Queued commands: 18 +2025-04-09 18:14:41,096 - FireflyDB - INFO - Results should be obtained after flushing the pipeline +2025-04-09 18:14:41,103 - FireflyDB - DEBUG - Flushed pipeline. Response: +OK + + +2025-04-09 18:14:41,103 - FireflyDB - INFO - Pipeline flush result: +OK + + +2025-04-09 18:14:41,103 - FireflyDB - INFO - After pipeline flush, we need to exit pipeline mode to get actual values +2025-04-09 18:14:41,103 - FireflyDB - DEBUG - Setting pipeline mode to False +2025-04-09 18:14:41,103 - FireflyDB - DEBUG - SetPipelineMode to False: True +2025-04-09 18:14:41,103 - FireflyDB - INFO - Pipeline mode disabled +2025-04-09 18:14:41,103 - FireflyDB - INFO - +Verifying results (note: Redis pipeline responses might not match input order): +2025-04-09 18:14:41,105 - FireflyDB - DEBUG - StringGet raw result pointer: b'' +2025-04-09 18:14:41,105 - FireflyDB - DEBUG - StringGet for key 'pipeline:string:0': Key not found +2025-04-09 18:14:41,106 - FireflyDB - INFO - String key:0 = None +2025-04-09 18:14:41,107 - FireflyDB - DEBUG - StringGet raw result pointer: b'dummy-string' +2025-04-09 18:14:41,107 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:41,108 - FireflyDB - DEBUG - StringGet for key 'pipeline:string:1': dummy-string +2025-04-09 18:14:41,108 - FireflyDB - INFO - String key:1 = dummy-string +2025-04-09 18:14:41,116 - FireflyDB - DEBUG - StringGet raw result pointer: b'string-value-1' +2025-04-09 18:14:41,116 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:41,116 - FireflyDB - DEBUG - StringGet for key 'pipeline:string:2': string-value-1 +2025-04-09 18:14:41,116 - FireflyDB - INFO - String key:2 = string-value-1 +2025-04-09 18:14:41,118 - FireflyDB - DEBUG - StringGet raw result pointer: b'string-value-2' +2025-04-09 18:14:41,118 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:41,118 - FireflyDB - DEBUG - StringGet for key 'pipeline:string:3': string-value-2 +2025-04-09 18:14:41,118 - FireflyDB - INFO - String key:3 = string-value-2 +2025-04-09 18:14:41,129 - FireflyDB - DEBUG - StringGet raw result pointer: b'string-value-3' +2025-04-09 18:14:41,129 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:41,129 - FireflyDB - DEBUG - StringGet for key 'pipeline:string:4': string-value-3 +2025-04-09 18:14:41,129 - FireflyDB - INFO - String key:4 = string-value-3 +2025-04-09 18:14:41,131 - FireflyDB - DEBUG - StringGet raw result pointer: b'string-value-4' +2025-04-09 18:14:41,131 - FireflyDB - DEBUG - Result is already a Python bytes object, no need to free +2025-04-09 18:14:41,132 - FireflyDB - DEBUG - StringGet for key 'pipeline:string:5': string-value-4 +2025-04-09 18:14:41,132 - FireflyDB - INFO - String key:5 = string-value-4 +2025-04-09 18:14:41,151 - FireflyDB - DEBUG - ListRange raw result: None +2025-04-09 18:14:41,151 - FireflyDB - DEBUG - ListRange on key 'pipeline:list' from 0 to -1. Empty list +2025-04-09 18:14:41,152 - FireflyDB - INFO - List items: [] +2025-04-09 18:14:41,153 - FireflyDB - DEBUG - ListRange raw result: b'list-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5\nlist-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5' +2025-04-09 18:14:41,153 - FireflyDB - DEBUG - ListRange on key 'pipeline:list' from 0 to -1. Values: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,154 - FireflyDB - INFO - List items: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,168 - FireflyDB - DEBUG - ListRange raw result: b'list-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5\nlist-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5' +2025-04-09 18:14:41,168 - FireflyDB - DEBUG - ListRange on key 'pipeline:list' from 0 to -1. Values: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,168 - FireflyDB - INFO - List items: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,170 - FireflyDB - DEBUG - ListRange raw result: b'list-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5\nlist-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5' +2025-04-09 18:14:41,170 - FireflyDB - DEBUG - ListRange on key 'pipeline:list' from 0 to -1. Values: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,170 - FireflyDB - INFO - List items: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,183 - FireflyDB - DEBUG - ListRange raw result: b'list-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5\nlist-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5' +2025-04-09 18:14:41,183 - FireflyDB - DEBUG - ListRange on key 'pipeline:list' from 0 to -1. Values: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,183 - FireflyDB - INFO - List items: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,185 - FireflyDB - DEBUG - ListRange raw result: b'list-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5\nlist-item-1\nlist-item-2\nlist-item-3\nlist-item-4\nlist-item-5' +2025-04-09 18:14:41,185 - FireflyDB - DEBUG - ListRange on key 'pipeline:list' from 0 to -1. Values: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,185 - FireflyDB - INFO - List items: ['list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5', 'list-item-1', 'list-item-2', 'list-item-3', 'list-item-4', 'list-item-5'] +2025-04-09 18:14:41,196 - FireflyDB - DEBUG - HashGetAll raw result: b'list-item-1=list-item-2\nlist-item-3=list-item-4\nlist-item-5=list-item-1\nlist-item-2=list-item-3\nlist-item-4=list-item-5' +2025-04-09 18:14:41,196 - FireflyDB - DEBUG - HashGetAll on key 'pipeline:hash': {'list-item-1': 'list-item-2', 'list-item-3': 'list-item-4', 'list-item-5': 'list-item-1', 'list-item-2': 'list-item-3', 'list-item-4': 'list-item-5'} +2025-04-09 18:14:41,196 - FireflyDB - INFO - Hash fields: +2025-04-09 18:14:41,196 - FireflyDB - INFO - list-item-1: list-item-2 +2025-04-09 18:14:41,197 - FireflyDB - INFO - list-item-3: list-item-4 +2025-04-09 18:14:41,197 - FireflyDB - INFO - list-item-5: list-item-1 +2025-04-09 18:14:41,197 - FireflyDB - INFO - list-item-2: list-item-3 +2025-04-09 18:14:41,197 - FireflyDB - INFO - list-item-4: list-item-5 +2025-04-09 18:14:41,199 - FireflyDB - DEBUG - Delete result: b'*12\r\n+field-5\r\n+hash-value-5\r\n+field-4\r\n+hash-value-4\r\n+field-3\r\n+hash-value-3\r\n+field-2\r\n+hash-value-2\r\n+field-1\r\n+hash-value-1\r\n+dummy-field\r\n+dummy-value\r\n' +2025-04-09 18:14:41,199 - FireflyDB - DEBUG - Complex pipeline response for DEL on key 'pipeline:string:0', assuming success +2025-04-09 18:14:41,209 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,209 - FireflyDB - DEBUG - Deleted key 'pipeline:list:0'. Count: 1 +2025-04-09 18:14:41,211 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,211 - FireflyDB - DEBUG - Deleted key 'pipeline:string:1'. Count: 1 +2025-04-09 18:14:41,222 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,223 - FireflyDB - DEBUG - Deleted key 'pipeline:list:1'. Count: 1 +2025-04-09 18:14:41,225 - FireflyDB - DEBUG - Delete result: b':0\r\n' +2025-04-09 18:14:41,225 - FireflyDB - DEBUG - Deleted key 'pipeline:string:2'. Count: 0 +2025-04-09 18:14:41,231 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,231 - FireflyDB - DEBUG - Deleted key 'pipeline:list:2'. Count: 1 +2025-04-09 18:14:41,233 - FireflyDB - DEBUG - Delete result: b':0\r\n' +2025-04-09 18:14:41,233 - FireflyDB - DEBUG - Deleted key 'pipeline:string:3'. Count: 0 +2025-04-09 18:14:41,246 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,247 - FireflyDB - DEBUG - Deleted key 'pipeline:list:3'. Count: 1 +2025-04-09 18:14:41,248 - FireflyDB - DEBUG - Delete result: b':0\r\n' +2025-04-09 18:14:41,248 - FireflyDB - DEBUG - Deleted key 'pipeline:string:4'. Count: 0 +2025-04-09 18:14:41,258 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,258 - FireflyDB - DEBUG - Deleted key 'pipeline:list:4'. Count: 1 +2025-04-09 18:14:41,260 - FireflyDB - DEBUG - Delete result: b':0\r\n' +2025-04-09 18:14:41,260 - FireflyDB - DEBUG - Deleted key 'pipeline:string:5'. Count: 0 +2025-04-09 18:14:41,273 - FireflyDB - DEBUG - Delete result: b':1\r\n' +2025-04-09 18:14:41,273 - FireflyDB - DEBUG - Deleted key 'pipeline:list:5'. Count: 1 +2025-04-09 18:14:41,275 - FireflyDB - DEBUG - Delete result: b':0\r\n' +2025-04-09 18:14:41,275 - FireflyDB - DEBUG - Deleted key 'pipeline:hash'. Count: 0 +2025-04-09 18:14:41,276 - FireflyDB - INFO - Cleanup complete +2025-04-09 18:14:41,276 - FireflyDB - DEBUG - Destroying client connection +2025-04-09 18:14:41,284 - FireflyDB - DEBUG - Client connection destroyed +2025-04-09 18:14:41,285 - FireflyDB - INFO - Test completed +2025-04-09 18:14:41,285 - FireflyDB - INFO - Script completed successfully. diff --git a/examples/libfirefly.dll b/examples/libfirefly.dll new file mode 100644 index 0000000..d092122 Binary files /dev/null and b/examples/libfirefly.dll differ diff --git a/firefly.h b/firefly.h new file mode 100644 index 0000000..f221e30 --- /dev/null +++ b/firefly.h @@ -0,0 +1,318 @@ +#pragma once + +#include // For bool type +#include // For size_t (though not directly used, common practice) + +#ifdef __cplusplus +extern "C" { +#endif + +// --- Client Management --- + +/** + * @brief Creates a new Firefly client instance and connects to the server. + * @param host Null-terminated UTF-8 string for the server hostname or IP. + * @param port The server port number. + * @return A handle (void*) representing the client instance, or NULL on failure. + * This handle must be passed to other functions and eventually freed with DestroyClient. + */ +void* CreateClient(const char* host, int port); + +/** + * @brief Disposes of the Firefly client instance and frees associated resources. + * @param handle The client handle obtained from CreateClient. + */ +void DestroyClient(void* handle); + +/** + * @brief Authenticates the client connection with the server. + * @param handle The client handle. + * @param password Null-terminated UTF-8 string for the password. + * @return true if authentication was successful, false otherwise. + */ +bool Authenticate(void* handle, const char* password); + +// --- String Operations --- + +/** + * @brief Sets a string value for a given key. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the key. + * @param value Null-terminated UTF-8 string for the value. + * @return true if the operation was successful, false otherwise. + */ +bool StringSet(void* handle, const char* key, const char* value); + +/** + * @brief Gets the string value for a given key. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the key. + * @return A pointer to a null-terminated UTF-8 string containing the value. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the key does not exist or an error occurs. + */ +char* StringGet(void* handle, const char* key); + +/** + * @brief Deletes a key. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the key to delete. + * @return The number of keys deleted (usually 1 if the key existed, 0 otherwise), or 0 on error. + */ +int Delete(void* handle, const char* key); + +/** + * @brief Frees memory allocated by the library for returned strings (e.g., from StringGet). + * @param str Pointer to the string allocated by the library. + */ +void FreeString(char* str); + +// --- List Operations --- + +/** + * @brief Adds a value to the beginning (left side) of a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param value Null-terminated UTF-8 string for the value to add. + * @return The length of the list after the push operation, or 0 on error. + */ +int ListLeftPush(void* handle, const char* key, const char* value); + +/** + * @brief Adds a value to the end (right side) of a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param value Null-terminated UTF-8 string for the value to add. + * @return The length of the list after the push operation, or 0 on error. + */ +int ListRightPush(void* handle, const char* key, const char* value); + +/** + * @brief Removes and returns the first element (left side) of a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @return A pointer to a null-terminated UTF-8 string containing the popped value. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the list is empty or an error occurs. + */ +char* ListLeftPop(void* handle, const char* key); + +/** + * @brief Removes and returns the last element (right side) of a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @return A pointer to a null-terminated UTF-8 string containing the popped value. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the list is empty or an error occurs. + */ +char* ListRightPop(void* handle, const char* key); + +/** + * @brief Gets a range of elements from a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param start The start index (0-based). + * @param stop The stop index (0-based, inclusive). Use -1 to specify the end of the list. + * @return A pointer to a null-terminated UTF-8 string containing the list elements, separated by newline characters (\n). + * NOTE: This is a single string containing all elements. The caller is responsible for parsing this string (e.g., splitting by '\n'). + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the key does not exist or an error occurs. + */ +char* ListRange(void* handle, const char* key, int start, int stop); + +/** + * @brief Gets the element at the specified index in a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param index The 0-based index of the element. + * @return A pointer to a null-terminated UTF-8 string containing the element. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the index is out of range or an error occurs. + */ +char* ListIndex(void* handle, const char* key, int index); + +/** + * @brief Sets the list element at the specified index. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param index The 0-based index of the element to set. + * @param value Null-terminated UTF-8 string for the new value. + * @return true if the operation was successful, false if the index is out of range or an error occurs. + */ +bool ListSet(void* handle, const char* key, int index, const char* value); + +/** + * @brief Returns the index of the first occurrence of an element in a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param element Null-terminated UTF-8 string for the element to find. + * @param rank Optional rank for finding duplicates (not typically used/supported). + * @param maxlen Optional max length to search (not typically used/supported). + * @return The 0-based index of the element, or -1 if not found or on error. + */ +int ListPosition(void* handle, const char* key, const char* element, int rank, int maxlen); + +/** + * @brief Trims a list to contain only the specified range of elements. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param start The start index (0-based). + * @param stop The stop index (0-based, inclusive). Use -1 for end. + * @return true if the operation was successful, false otherwise. + */ +bool ListTrim(void* handle, const char* key, int start, int stop); + +/** + * @brief Removes occurrences of elements from a list. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the list key. + * @param count Number of occurrences to remove: + * count > 0: Remove elements equal to 'element' moving from head to tail. + * count < 0: Remove elements equal to 'element' moving from tail to head. + * count = 0: Remove all elements equal to 'element'. + * @param element Null-terminated UTF-8 string for the element to remove. + * @return The number of elements removed, or 0 on error. + */ +int ListRemove(void* handle, const char* key, int count, const char* element); + +// --- Hash Operations --- + +/** + * @brief Sets the value of a field within a hash. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the hash key. + * @param field Null-terminated UTF-8 string for the field name. + * @param value Null-terminated UTF-8 string for the value. + * @return true if the operation was successful (field created or updated), false on error. + */ +bool HashSet(void* handle, const char* key, const char* field, const char* value); + +/** + * @brief Gets the value of a field within a hash. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the hash key. + * @param field Null-terminated UTF-8 string for the field name. + * @return A pointer to a null-terminated UTF-8 string containing the field's value. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the key or field does not exist or an error occurs. + */ +char* HashGet(void* handle, const char* key, const char* field); + +/** + * @brief Deletes a field from a hash. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the hash key. + * @param field Null-terminated UTF-8 string for the field to delete. + * @return true if the field was deleted, false if the field or key did not exist or on error. + */ +bool HashDelete(void* handle, const char* key, const char* field); + +/** + * @brief Checks if a field exists within a hash. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the hash key. + * @param field Null-terminated UTF-8 string for the field name. + * @return true if the field exists, false otherwise or on error. + */ +bool HashFieldExists(void* handle, const char* key, const char* field); + +/** + * @brief Gets all fields and values from a hash. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the hash key. + * @return A pointer to a null-terminated UTF-8 string containing the hash fields and values. + * Each field/value pair is represented as "field=value", separated by newline characters (\n). + * NOTE: This is a single string containing all field-value pairs. The caller is responsible for parsing this string (e.g., splitting by '\n' then by '='). + * Example: "field1=value1\nfield2=value2\nfield3=value3" + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the key does not exist or an error occurs. + */ +char* HashGetAll(void* handle, const char* key); + +/** + * @brief Sets multiple fields and values in a hash. + * @param handle The client handle. + * @param key Null-terminated UTF-8 string for the hash key. + * @param fieldValuePairs Null-terminated UTF-8 string containing space-separated field-value pairs. + * Example: "field1 value1 field2 value2 field3 value3" + * @warning Values containing spaces are not properly handled by the basic parsing. + * @return true if the operation was successful, false on error (e.g., odd number of items in pairs string). + */ +bool HashMultiSet(void* handle, const char* key, const char* fieldValuePairs); + +// --- Raw Command Execution --- + +/** + * @brief Executes a raw command string with optional arguments. + * @warning The argument parsing is basic (simple space split) and may not handle arguments with spaces correctly. + * Prefer using the specific typed functions (StringSet, ListLeftPush, etc.) for reliability. + * @param handle The client handle. + * @param command Null-terminated UTF-8 string for the command (e.g., "PING"). + * @param args Null-terminated UTF-8 string containing space-separated arguments (e.g., "mykey myvalue"). + * @return A pointer to a null-terminated UTF-8 string containing the raw server response. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL on error. + */ +char* ExecuteCommand(void* handle, const char* command, const char* args); + +/** + * @brief Gets all keys matching the specified pattern. + * @param handle The client handle. + * @param pattern Null-terminated UTF-8 string for the pattern to match against keys (e.g., "*" for all keys). + * @return A pointer to a null-terminated UTF-8 string containing newline-delimited keys. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if no keys are found or on error. + */ +char* Keys(void* handle, const char* pattern); + +// --- Pipeline Operations --- + +/** + * @brief Enables or disables pipeline mode for the client. + * @param handle The client handle. + * @param enabled true to enable pipeline mode, false to disable. + * @return true if the mode was set successfully, false on error. + */ +bool SetPipelineMode(void* handle, bool enabled); + +/** + * @brief Sets the maximum number of commands to batch in pipeline mode. + * @param handle The client handle. + * @param size The maximum number of commands to queue before sending. + * @return true if the batch size was set successfully, false on error (e.g., invalid size). + */ +bool SetBatchSize(void* handle, int size); + +/** + * @brief Sends all queued commands to the server immediately. + * @param handle The client handle. + * @return A pointer to a null-terminated UTF-8 string containing the raw combined server response for all flushed commands. + * This string is allocated by the library and MUST be freed by the caller using FreeString(). + * Returns NULL if the queue was empty or an error occurs. + */ +char* FlushPipeline(void* handle); + +/** + * @brief Gets the number of commands currently waiting in the pipeline queue. + * @param handle The client handle. + * @return The number of queued commands, or 0 on error. + */ +int GetQueuedCommandCount(void* handle); + +/** + * @brief Checks if pipeline mode is currently enabled for the client. + * @param handle The client handle. + * @return true if pipeline mode is enabled, false otherwise or on error. + */ +bool IsPipelineMode(void* handle); + +/** + * @brief Gets the current maximum batch size configured for pipeline mode. + * @param handle The client handle. + * @return The maximum batch size, or 0 on error. + */ +int GetBatchSize(void* handle); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..accd97b Binary files /dev/null and b/icon.ico differ diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..1d5a019 Binary files /dev/null and b/icon.png differ diff --git a/resources/icon.ico b/resources/icon.ico new file mode 100644 index 0000000..accd97b Binary files /dev/null and b/resources/icon.ico differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 0000000..1d5a019 Binary files /dev/null and b/resources/icon.png differ diff --git a/src/FireflyClient.cs b/src/FireflyClient.cs new file mode 100644 index 0000000..ee387d3 --- /dev/null +++ b/src/FireflyClient.cs @@ -0,0 +1,278 @@ +using System.Net.Sockets; +using System.Text; + +namespace FireflyClient +{ + /// + /// FireflyClient provides a clean API for interacting with the Firefly server. + /// This can be used as a library for integrating with other applications. + /// + public partial class FireflyClient : IDisposable + { + private readonly TcpClient _client; + private readonly NetworkStream _stream; + private readonly string _host; + private readonly int _port; + private readonly string _password; + private readonly byte[] _responseBuffer = new byte[1024 * 1024]; // 1MB buffer for responses + private int _maxBatchSize = 1000; + private readonly int _maxPipelineSize = 10000; + private readonly Queue _commandQueue = new(); + private bool _isPipelineMode = false; + private bool _isAuthenticated = false; + + // Static instance for native interop + // private static FireflyClient? _instance; + + /// + /// Creates a new FireflyClient and connects to the specified server + /// + public FireflyClient(string host = "127.0.0.1", int port = 6379) + { + _host = host; + _port = port; + _password = string.Empty; + _client = new TcpClient(); + _client.Connect(_host, _port); + _stream = _client.GetStream(); + } + + /// + /// Creates a new FireflyClient, connects to the server and authenticates + /// + public FireflyClient(string host, int port, string password) : this(host, port) + { + _password = password; + if (!string.IsNullOrEmpty(_password)) + { + Authenticate(_password); + } + } + + /// + /// Authenticates with the server using the provided password + /// + public bool Authenticate(string password) + { + string response = ExecuteCommand("AUTH", password); + _isAuthenticated = response.StartsWith("+OK"); + return _isAuthenticated; + } + + /// + /// Executes a raw command with any number of arguments + /// + public string ExecuteCommand(string command, params string[] args) + { + if (!_client.Connected) + { + throw new InvalidOperationException("Not connected to server"); + } + + // Build the command string + string fullCommand = $"{command.ToUpperInvariant()}{string.Join("", args.Select(arg => $" {QuoteIfNeeded(arg)}"))}"; + + // Handle special commands that should not be pipelined + if (command.Equals("AUTH", StringComparison.OrdinalIgnoreCase) || + command.Equals("PING", StringComparison.OrdinalIgnoreCase) || + command.Equals("QUIT", StringComparison.OrdinalIgnoreCase)) + { + return SendCommandInternal(fullCommand); + } + + // Add command to queue + _commandQueue.Enqueue(fullCommand); + + // Process queue if we've reached batch size or if pipeline mode is disabled + if (_commandQueue.Count >= _maxBatchSize || !_isPipelineMode) + { + return ProcessCommandQueue(); + } + + return "+QUEUED\r\n"; + } + + // Helper method to quote a string if needed + private static string QuoteIfNeeded(string input) + { + if (input.Contains(' ') || input.Contains('\'')) + { + // If the input already has quotes, don't add more + if (input.StartsWith('"') && input.EndsWith('"')) + { + return input; + } + + // Escape any quotes in the input + return $"\"{input.Replace("\"", "\\\"")}\""; + } + return input; + } + + // Private method to send a command to the server + private string SendCommandInternal(string command) + { + // Add CRLF if not present + if (!command.EndsWith("\r\n")) + { + command += "\r\n"; + } + + byte[] commandBytes = Encoding.UTF8.GetBytes(command); + _stream.Write(commandBytes, 0, commandBytes.Length); + + // Read response + int bytesRead = _stream.Read(_responseBuffer, 0, _responseBuffer.Length); + return bytesRead > 0 ? Encoding.UTF8.GetString(_responseBuffer, 0, bytesRead) : string.Empty; + } + + private string ProcessCommandQueue() + { + if (_commandQueue.Count == 0) return string.Empty; + + // Build the pipeline command string + string pipelineCommand = string.Join("\r\n", _commandQueue) + "\r\n"; + _commandQueue.Clear(); + + return SendCommandInternal(pipelineCommand); + } + + /// + /// Enables or disables pipeline mode + /// + public void SetPipelineMode(bool enabled) + { + _isPipelineMode = enabled; + if (!enabled) + { + ProcessCommandQueue(); + } + } + + /// + /// Flushes any queued commands in pipeline mode + /// + public string FlushPipeline() + { + return ProcessCommandQueue(); + } + + /// + /// Sets the maximum number of commands to batch before sending + /// + public void SetBatchSize(int size) + { + if (size <= 0) + { + throw new ArgumentException("Batch size must be greater than 0"); + } + _maxBatchSize = Math.Min(size, _maxPipelineSize); + } + + /// + /// Gets the current number of queued commands + /// + public int QueuedCommandCount => _commandQueue.Count; + + /// + /// Gets whether pipeline mode is enabled + /// + public bool IsPipelineMode => _isPipelineMode; + + /// + /// Gets the maximum batch size + /// + public int MaxBatchSize => _maxBatchSize; + + /// + /// Gets whether the client is connected to the server + /// + public bool IsConnected => _client.Connected; + + /// + /// Gets whether the client is authenticated + /// + public bool IsAuthenticated => _isAuthenticated; + + /// + /// Gets all keys matching the specified pattern + /// + /// Pattern to match against keys. Use * for wildcard matches. + /// List of matching keys, or empty list if none found or on error + public List Keys(string pattern = "*") + { + try + { + string response = ExecuteCommand("KEYS", pattern); + if (string.IsNullOrEmpty(response) || !response.StartsWith('+')) + { + return []; + } + + // Remove the '+' prefix and split by newlines + string keysStr = response[1..].Trim(); + return string.IsNullOrEmpty(keysStr) + ? [] + : [.. keysStr.Split('\n')]; + } + catch + { + return []; + } + } + + /// + /// Parses an array response from the server + /// + protected static List ParseArrayResponse(string response) + { + var result = new List(); + + // If not an array response, return empty list + if (!response.StartsWith('*')) + { + return result; + } + + string[] parts = response.Split("\r\n"); + + // Skip the first line which just contains the * prefix + for (int i = 1; i < parts.Length; i++) + { + if (!string.IsNullOrEmpty(parts[i]) && (parts[i].StartsWith('+') || parts[i].StartsWith('$'))) + { + // Extract the value part (after the + or $ prefix) + result.Add(parts[i][1..]); + } + } + + return result; + } + + /// + public void Dispose() + { + try + { + // Try to gracefully disconnect by sending QUIT command + if (_client.Connected) + { + try + { + ExecuteCommand("QUIT"); + } + catch + { + // Ignore exceptions during QUIT command + } + } + } + finally + { + _stream.Dispose(); + _client.Dispose(); + GC.SuppressFinalize(this); + } + } + } +} \ No newline at end of file diff --git a/src/HashOperations.cs b/src/HashOperations.cs new file mode 100644 index 0000000..e551b47 --- /dev/null +++ b/src/HashOperations.cs @@ -0,0 +1,86 @@ +namespace FireflyClient +{ + public partial class FireflyClient + { + #region Hash Operations + + /// + /// Sets a field in a hash + /// + public bool HashSet(string key, string field, string value) + { + string response = ExecuteCommand("HSET", key, field, value); + return response.StartsWith(":1"); + } + + /// + /// Gets a field from a hash + /// + public string HashGet(string key, string field) + { + string response = ExecuteCommand("HGET", key, field); + if (response.StartsWith('+')) + { + return response[1..].TrimEnd('\r', '\n'); + } + return string.Empty; + } + + /// + /// Deletes a field from a hash + /// + public bool HashDelete(string key, string field) + { + string response = ExecuteCommand("HDEL", key, field); + return response.StartsWith(":1"); + } + + /// + /// Checks if a field exists in a hash + /// + public bool HashFieldExists(string key, string field) + { + string response = ExecuteCommand("HEXISTS", key, field); + return response.StartsWith(":1"); + } + + /// + /// Gets all fields and values from a hash + /// + public Dictionary HashGetAll(string key) + { + string response = ExecuteCommand("HGETALL", key); + List items = ParseArrayResponse(response); + + var result = new Dictionary(); + for (int i = 0; i < items.Count; i += 2) + { + if (i + 1 < items.Count) + { + result[items[i]] = items[i + 1]; + } + } + + return result; + } + + /// + /// Sets multiple fields in a hash at once + /// + public bool HashMultiSet(string key, Dictionary fieldValues) + { + List args = [key]; + + foreach (var kvp in fieldValues) + { + args.Add(kvp.Key); + args.Add(kvp.Value); + } + + string response = ExecuteCommand("HMSET", [.. args]); + return response.StartsWith("+OK"); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/ListOperations.cs b/src/ListOperations.cs new file mode 100644 index 0000000..41a8dd3 --- /dev/null +++ b/src/ListOperations.cs @@ -0,0 +1,159 @@ +namespace FireflyClient +{ + public partial class FireflyClient + { + #region List Operations + + /// + /// Adds values to the beginning of a list + /// + public int ListLeftPush(string key, params string[] values) + { + var args = new List { key }; + args.AddRange(values); + + string response = ExecuteCommand("LPUSH", [.. args]); + if (response.StartsWith(':')) + { + if (int.TryParse(response[1..].TrimEnd('\r', '\n'), out int result)) + { + return result; + } + } + return 0; + } + + /// + /// Adds values to the end of a list + /// + public int ListRightPush(string key, params string[] values) + { + var args = new List { key }; + args.AddRange(values); + + string response = ExecuteCommand("RPUSH", [.. args]); + if (response.StartsWith(':')) + { + if (int.TryParse(response[1..].TrimEnd('\r', '\n'), out int result)) + { + return result; + } + } + return 0; + } + + /// + /// Removes and returns the first element of a list + /// + public string ListLeftPop(string key) + { + string response = ExecuteCommand("LPOP", key); + if (response.StartsWith('+')) + { + return response[1..].TrimEnd('\r', '\n'); + } + return string.Empty; + } + + /// + /// Removes and returns the last element of a list + /// + public string ListRightPop(string key) + { + string response = ExecuteCommand("RPOP", key); + if (response.StartsWith('+')) + { + return response[1..].TrimEnd('\r', '\n'); + } + return string.Empty; + } + + /// + /// Gets a range of elements from a list + /// + public List ListRange(string key, int start, int stop) + { + string response = ExecuteCommand("LRANGE", key, start.ToString(), stop.ToString()); + return ParseArrayResponse(response); + } + + /// + /// Gets the element at the specified index in a list + /// + public string ListIndex(string key, int index) + { + string response = ExecuteCommand("LINDEX", key, index.ToString()); + if (response.StartsWith('+')) + { + return response[1..].TrimEnd('\r', '\n'); + } + return string.Empty; + } + + /// + /// Sets the element at the specified index in a list + /// + public bool ListSet(string key, int index, string value) + { + string response = ExecuteCommand("LSET", key, index.ToString(), value); + return response.StartsWith("+OK"); + } + + /// + /// Returns the index of the first occurrence of an element in a list + /// + public int ListPosition(string key, string element, int rank = 1, int maxlen = 0) + { + var args = new List { key, element }; + + if (rank != 1) + { + args.Add("RANK"); + args.Add(rank.ToString()); + } + + if (maxlen > 0) + { + args.Add("MAXLEN"); + args.Add(maxlen.ToString()); + } + + string response = ExecuteCommand("LPOS", [.. args]); + if (response.StartsWith(':')) + { + if (int.TryParse(response[1..].TrimEnd('\r', '\n'), out int result)) + { + return result; + } + } + return -1; + } + + /// + /// Trims a list to the specified range + /// + public bool ListTrim(string key, int start, int stop) + { + string response = ExecuteCommand("LTRIM", key, start.ToString(), stop.ToString()); + return response.StartsWith("+OK"); + } + + /// + /// Removes elements equal to the given value from a list + /// + public int ListRemove(string key, int count, string element) + { + string response = ExecuteCommand("LREM", key, count.ToString(), element); + if (response.StartsWith(':')) + { + if (int.TryParse(response[1..].TrimEnd('\r', '\n'), out int result)) + { + return result; + } + } + return 0; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/NativeInterop.cs b/src/NativeInterop.cs new file mode 100644 index 0000000..6f8c2a5 --- /dev/null +++ b/src/NativeInterop.cs @@ -0,0 +1,718 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace FireflyClient +{ + public partial class FireflyClient + { + // Helper to safely get client from handle + private static FireflyClient? GetClientFromHandle(IntPtr handle) + { + if (handle == IntPtr.Zero) return null; + try + { + GCHandle gch = (GCHandle)handle; + return gch.Target as FireflyClient; + } + catch + { + return null; // Invalid handle + } + } + + // Helper to marshal string result to IntPtr + private static IntPtr MarshalStringResult(string? result) + { + if (result == null) return IntPtr.Zero; + + byte[] bytes = Encoding.UTF8.GetBytes(result); + IntPtr ptr = Marshal.AllocHGlobal(bytes.Length + 1); + Marshal.Copy(bytes, 0, ptr, bytes.Length); + Marshal.WriteByte(ptr + bytes.Length, 0); // Null terminator + return ptr; + } + + // Helper to marshal List result to IntPtr (newline-delimited) + private static IntPtr MarshalStringListResult(List? result) + { + if (result == null || result.Count == 0) return IntPtr.Zero; + string joinedResult = string.Join("\n", result); // Using \n as delimiter + return MarshalStringResult(joinedResult); + } + + // Helper to marshal Dictionary result to IntPtr (newline-delimited field=value) + private static IntPtr MarshalStringDictionaryResult(Dictionary? result) + { + if (result == null || result.Count == 0) return IntPtr.Zero; + string joinedResult = string.Join("\n", result.Select(kvp => $"{kvp.Key}={kvp.Value}")); + return MarshalStringResult(joinedResult); + } + + #region Native Interop Methods + + /// + /// Creates a new FireflyClient instance for native interop + /// + [UnmanagedCallersOnly(EntryPoint = "CreateClient")] + public static IntPtr CreateClient(IntPtr hostPtr, int port) + { + try + { + // Use UTF8 for host string + string host = Marshal.PtrToStringUTF8(hostPtr) ?? "127.0.0.1"; + var client = new FireflyClient(host, port); + var handle = GCHandle.Alloc(client); + return GCHandle.ToIntPtr(handle); + } + catch + { + return IntPtr.Zero; + } + } + + /// + /// Destroys a FireflyClient instance created for native interop + /// + [UnmanagedCallersOnly(EntryPoint = "DestroyClient")] + public static void DestroyClient(IntPtr handle) + { + if (handle != IntPtr.Zero) + { + try + { + GCHandle gch = GCHandle.FromIntPtr(handle); + if (gch.Target is FireflyClient client) + { + client.Dispose(); + } + // Check if handle is still allocated before freeing + if (gch.IsAllocated) + { + gch.Free(); + } + } + catch { /* Handle potential errors with invalid handles */ } + } + } + + /// + /// Authenticates with the server using the provided password + /// + [UnmanagedCallersOnly(EntryPoint = "Authenticate")] + public static bool NativeAuthenticate(IntPtr handle, IntPtr passwordPtr) + { + try + { + // Use UTF8 for password string + string password = Marshal.PtrToStringUTF8(passwordPtr) ?? string.Empty; + var client = GetClientFromHandle(handle); + return client?.Authenticate(password) ?? false; + } + catch + { + return false; + } + } + + /// + /// Executes a raw command. Note: Argument parsing is basic. + /// Consider using specific functions instead for reliability. + /// + [UnmanagedCallersOnly(EntryPoint = "ExecuteCommand")] + public static IntPtr NativeExecuteCommand(IntPtr handle, IntPtr commandPtr, IntPtr argsPtr) + { + try + { + // Use UTF8 for command and args strings + string command = Marshal.PtrToStringUTF8(commandPtr) ?? string.Empty; + string args = Marshal.PtrToStringUTF8(argsPtr) ?? string.Empty; + var client = GetClientFromHandle(handle); + // WARNING: Basic split, unreliable if args have spaces + string[] argArray = args.Split(' ', StringSplitOptions.RemoveEmptyEntries); + string? result = client?.ExecuteCommand(command, argArray); + return MarshalStringResult(result); + } + catch + { + return IntPtr.Zero; + } + } + + /// + /// Frees a string allocated by the native interop methods + /// + [UnmanagedCallersOnly(EntryPoint = "FreeString")] + public static void NativeFreeString(IntPtr ptr) + { + if (ptr != IntPtr.Zero) + { + Marshal.FreeHGlobal(ptr); + } + } + + // --- String Operations --- + + /// + /// Sets a string value for a given key (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the key. + /// Pointer to a null-terminated UTF-8 string representing the value. + /// True if the command was successful (e.g., server replied OK), false otherwise. + [UnmanagedCallersOnly(EntryPoint = "StringSet")] + public static bool NativeStringSet(IntPtr handle, IntPtr keyPtr, IntPtr valuePtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string value = Marshal.PtrToStringUTF8(valuePtr) ?? string.Empty; + return client?.StringSet(key, value) ?? false; + } + catch { return false; } + } + + /// + /// Gets a string value for a given key (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the key. + /// Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if key not found. + [UnmanagedCallersOnly(EntryPoint = "StringGet")] + public static IntPtr NativeStringGet(IntPtr handle, IntPtr keyPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string? result = client?.StringGet(key); + return MarshalStringResult(result); + } + catch { return IntPtr.Zero; } + } + + /// + /// Deletes one or more keys (Native Interop). + /// Note: Currently only supports deleting a single key via the C# Delete method. + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the key to delete. + /// The number of keys that were removed (typically 1 or 0), or 0 on error. + [UnmanagedCallersOnly(EntryPoint = "Delete")] + public static int NativeDelete(IntPtr handle, IntPtr keyPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + // Assuming Delete returns the number of keys deleted (usually 1 or 0) + return client?.Delete(key) ?? 0; + } + catch { return 0; } + } + + // --- List Operations --- + + /// + /// Adds a value to the beginning of a list (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// Pointer to a null-terminated UTF-8 string representing the value to add. + /// The length of the list after the push operation, or 0 on error. + [UnmanagedCallersOnly(EntryPoint = "ListLeftPush")] + public static int NativeListLeftPush(IntPtr handle, IntPtr keyPtr, IntPtr valuePtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string value = Marshal.PtrToStringUTF8(valuePtr) ?? string.Empty; + // Assuming LPUSH returns the new length of the list + return client?.ListLeftPush(key, value) ?? 0; + } + catch { return 0; } + } + + /// + /// Adds a value to the end of a list (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// Pointer to a null-terminated UTF-8 string representing the value to add. + /// The length of the list after the push operation, or 0 on error. + [UnmanagedCallersOnly(EntryPoint = "ListRightPush")] + public static int NativeListRightPush(IntPtr handle, IntPtr keyPtr, IntPtr valuePtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string value = Marshal.PtrToStringUTF8(valuePtr) ?? string.Empty; + // Assuming RPUSH returns the new length of the list + return client?.ListRightPush(key, value) ?? 0; + } + catch { return 0; } + } + + /// + /// Removes and returns the first element of a list (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if list is empty. + [UnmanagedCallersOnly(EntryPoint = "ListLeftPop")] + public static IntPtr NativeListLeftPop(IntPtr handle, IntPtr keyPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string? result = client?.ListLeftPop(key); + return MarshalStringResult(result); + } + catch { return IntPtr.Zero; } + } + + /// + /// Removes and returns the last element of a list (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if list is empty. + [UnmanagedCallersOnly(EntryPoint = "ListRightPop")] + public static IntPtr NativeListRightPop(IntPtr handle, IntPtr keyPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string? result = client?.ListRightPop(key); + return MarshalStringResult(result); + } + catch { return IntPtr.Zero; } + } + + /// + /// Gets a range of elements from a list (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// The start index (0-based). + /// The stop index (inclusive, use -1 for end). + /// Pointer to a null-terminated UTF-8 string containing newline-delimited list elements, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error. + /// The returned IntPtr points to a single string. The native caller must parse this string (e.g., split by '\n') and free the pointer using NativeFreeString. + [UnmanagedCallersOnly(EntryPoint = "ListRange")] + public static IntPtr NativeListRange(IntPtr handle, IntPtr keyPtr, int start, int stop) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + List? result = client?.ListRange(key, start, stop); + // Marshal List as a single newline-delimited string + return MarshalStringListResult(result); + } + catch { return IntPtr.Zero; } + } + + /// + /// Gets an element from a list by its index (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// The index of the element (0-based). + /// Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if index is out of range. + [UnmanagedCallersOnly(EntryPoint = "ListIndex")] + public static IntPtr NativeListIndex(IntPtr handle, IntPtr keyPtr, int index) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string? result = client?.ListIndex(key, index); + return MarshalStringResult(result); + } + catch { return IntPtr.Zero; } + } + + /// + /// Sets the value of an element in a list by its index (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// The index of the element to set (0-based). + /// Pointer to a null-terminated UTF-8 string representing the new value. + /// True if the command was successful, false otherwise (e.g., index out of range). + [UnmanagedCallersOnly(EntryPoint = "ListSet")] + public static bool NativeListSet(IntPtr handle, IntPtr keyPtr, int index, IntPtr valuePtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string value = Marshal.PtrToStringUTF8(valuePtr) ?? string.Empty; + return client?.ListSet(key, index, value) ?? false; + } + catch { return false; } + } + + /// + /// Returns the index of the first occurrence of an element in a list (Native Interop). + /// Rank and MaxLen parameters are currently ignored. + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// Pointer to a null-terminated UTF-8 string representing the element to find. + /// Optional rank (ignored). + /// Optional max length (ignored). + /// The 0-based index of the element, or -1 if not found or on error. + [UnmanagedCallersOnly(EntryPoint = "ListPosition")] + public static int NativeListPosition(IntPtr handle, IntPtr keyPtr, IntPtr elementPtr, int rank, int maxlen) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string element = Marshal.PtrToStringUTF8(elementPtr) ?? string.Empty; + // Note: C# ListPosition returns the first index or -1 + var position = client?.ListPosition(key, element, rank, maxlen); + return position ?? -1; + } + catch { return -1; } + } + + /// + /// Trims a list to contain only the specified range of elements (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// The start index (0-based). + /// The stop index (inclusive, use -1 for end). + /// True if the command was successful, false otherwise. + [UnmanagedCallersOnly(EntryPoint = "ListTrim")] + public static bool NativeListTrim(IntPtr handle, IntPtr keyPtr, int start, int stop) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + return client?.ListTrim(key, start, stop) ?? false; + } + catch { return false; } + } + + /// + /// Removes occurrences of elements from a list (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the list key. + /// Number of occurrences to remove (see C# ListRemove docs). + /// Pointer to a null-terminated UTF-8 string representing the element to remove. + /// The number of elements removed, or 0 on error. + [UnmanagedCallersOnly(EntryPoint = "ListRemove")] + public static int NativeListRemove(IntPtr handle, IntPtr keyPtr, int count, IntPtr elementPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string element = Marshal.PtrToStringUTF8(elementPtr) ?? string.Empty; + return client?.ListRemove(key, count, element) ?? 0; + } + catch { return 0; } + } + + // --- Hash Operations --- + + /// + /// Sets the value of a field within a hash (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the hash key. + /// Pointer to a null-terminated UTF-8 string representing the field name. + /// Pointer to a null-terminated UTF-8 string representing the value to set. + /// True if the field was new or updated successfully, false on error. + [UnmanagedCallersOnly(EntryPoint = "HashSet")] + public static bool NativeHashSet(IntPtr handle, IntPtr keyPtr, IntPtr fieldPtr, IntPtr valuePtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string field = Marshal.PtrToStringUTF8(fieldPtr) ?? string.Empty; + string value = Marshal.PtrToStringUTF8(valuePtr) ?? string.Empty; + // Assuming HSET returns 1 if field is new, 0 if updated + return client?.HashSet(key, field, value) ?? false; // Adapt if C# returns differently + } + catch { return false; } + } + + /// + /// Gets the value of a field within a hash (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the hash key. + /// Pointer to a null-terminated UTF-8 string representing the field name. + /// Pointer to a null-terminated UTF-8 string result allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if field/key not found. + [UnmanagedCallersOnly(EntryPoint = "HashGet")] + public static IntPtr NativeHashGet(IntPtr handle, IntPtr keyPtr, IntPtr fieldPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string field = Marshal.PtrToStringUTF8(fieldPtr) ?? string.Empty; + string? result = client?.HashGet(key, field); + return MarshalStringResult(result); + } + catch { return IntPtr.Zero; } + } + + /// + /// Deletes a field from a hash (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the hash key. + /// Pointer to a null-terminated UTF-8 string representing the field to delete. + /// True if the field was deleted, false otherwise (e.g., field/key not found). + [UnmanagedCallersOnly(EntryPoint = "HashDelete")] + public static bool NativeHashDelete(IntPtr handle, IntPtr keyPtr, IntPtr fieldPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string field = Marshal.PtrToStringUTF8(fieldPtr) ?? string.Empty; + // Assuming HDEL returns true if field was deleted, false otherwise + return client?.HashDelete(key, field) ?? false; + } + catch { return false; } + } + + /// + /// Checks if a field exists within a hash (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the hash key. + /// Pointer to a null-terminated UTF-8 string representing the field name. + /// True if the field exists, false otherwise. + [UnmanagedCallersOnly(EntryPoint = "HashFieldExists")] + public static bool NativeHashFieldExists(IntPtr handle, IntPtr keyPtr, IntPtr fieldPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string field = Marshal.PtrToStringUTF8(fieldPtr) ?? string.Empty; + return client?.HashFieldExists(key, field) ?? false; + } + catch { return false; } + } + + /// + /// Sets multiple fields and values in a hash (Native Interop). + /// Parses a space-separated string of field-value pairs. + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the hash key. + /// Pointer to a null-terminated UTF-8 string of space-separated field-value pairs. + /// True if successful, false on error (e.g., odd number of pairs). + [UnmanagedCallersOnly(EntryPoint = "HashMultiSet")] + public static bool NativeHashMultiSet(IntPtr handle, IntPtr keyPtr, IntPtr fieldValuePairsPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + string fieldValuePairs = Marshal.PtrToStringUTF8(fieldValuePairsPtr) ?? string.Empty; + + string[] pairs = fieldValuePairs.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (pairs.Length % 2 != 0) + { + // Invalid input: odd number of elements + return false; + } + + var dict = new Dictionary(); + for (int i = 0; i < pairs.Length; i += 2) + { + dict[pairs[i]] = pairs[i + 1]; + } + + return client?.HashMultiSet(key, dict) ?? false; + } + catch { return false; } + } + + /// + /// Gets all fields and values from a hash (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the hash key. + /// Pointer to a null-terminated UTF-8 string containing newline-delimited "field=value" pairs, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if hash not found. + /// The returned IntPtr points to a single string. The native caller must parse this string (e.g., split by '\n', then by '=') and free the pointer using NativeFreeString. + [UnmanagedCallersOnly(EntryPoint = "HashGetAll")] + public static IntPtr NativeHashGetAll(IntPtr handle, IntPtr keyPtr) + { + try + { + var client = GetClientFromHandle(handle); + string key = Marshal.PtrToStringUTF8(keyPtr) ?? string.Empty; + Dictionary? result = client?.HashGetAll(key); + // Marshal Dictionary as a single newline-delimited string + return MarshalStringDictionaryResult(result); + } + catch { return IntPtr.Zero; } + } + + // --- Pipeline Operations (Already Implemented) --- + + /// + /// Enables or disables pipeline mode for the client (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// True to enable pipeline mode, false to disable. + /// True if the mode was set successfully, false on error. + [UnmanagedCallersOnly(EntryPoint = "SetPipelineMode")] + public static bool NativeSetPipelineMode(IntPtr handle, bool enabled) + { + try + { + var client = GetClientFromHandle(handle); + if (client == null) return false; + + client.SetPipelineMode(enabled); + return true; + } + catch + { + return false; + } + } + + /// + /// Sets the maximum number of commands to batch in pipeline mode (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// The maximum number of commands to queue before sending. + /// True if the batch size was set successfully, false on error (e.g., invalid size). + [UnmanagedCallersOnly(EntryPoint = "SetBatchSize")] + public static bool NativeSetBatchSize(IntPtr handle, int size) + { + try + { + var client = GetClientFromHandle(handle); + if (client == null) return false; + + client.SetBatchSize(size); + return true; + } + catch + { + return false; + } + } + + /// + /// Sends all queued commands to the server immediately (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string containing the server's combined response, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error or if queue was empty. + [UnmanagedCallersOnly(EntryPoint = "FlushPipeline")] + public static IntPtr NativeFlushPipeline(IntPtr handle) + { + try + { + var client = GetClientFromHandle(handle); + string? response = client?.FlushPipeline(); + return MarshalStringResult(response); + } + catch + { + return IntPtr.Zero; + } + } + + /// + /// Gets the number of commands currently waiting in the pipeline queue (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// The number of queued commands, or 0 on error. + [UnmanagedCallersOnly(EntryPoint = "GetQueuedCommandCount")] + public static int NativeGetQueuedCommandCount(IntPtr handle) + { + try + { + var client = GetClientFromHandle(handle); + return client?.QueuedCommandCount ?? 0; + } + catch + { + return 0; + } + } + + /// + /// Checks if pipeline mode is currently enabled for the client (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// True if pipeline mode is enabled, false otherwise or on error. + [UnmanagedCallersOnly(EntryPoint = "IsPipelineMode")] + public static bool NativeIsPipelineMode(IntPtr handle) + { + try + { + var client = GetClientFromHandle(handle); + return client?.IsPipelineMode ?? false; + } + catch + { + return false; + } + } + + /// + /// Gets the current maximum batch size configured for pipeline mode (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// The maximum batch size, or 0 on error. + [UnmanagedCallersOnly(EntryPoint = "GetBatchSize")] + public static int NativeGetBatchSize(IntPtr handle) + { + try + { + var client = GetClientFromHandle(handle); + return client?.MaxBatchSize ?? 0; + } + catch + { + return 0; + } + } + + /// + /// Gets all keys matching the specified pattern (Native Interop). + /// + /// The GCHandle (as IntPtr) representing the client instance. + /// Pointer to a null-terminated UTF-8 string representing the pattern to match against keys. + /// Pointer to a null-terminated UTF-8 string containing newline-delimited keys, allocated via AllocHGlobal (caller must free with FreeString), or IntPtr.Zero on error. + [UnmanagedCallersOnly(EntryPoint = "Keys")] + public static IntPtr NativeKeys(IntPtr handle, IntPtr patternPtr) + { + try + { + var client = GetClientFromHandle(handle); + string pattern = Marshal.PtrToStringUTF8(patternPtr) ?? "*"; + List? result = client?.Keys(pattern); + return MarshalStringListResult(result); + } + catch + { + return IntPtr.Zero; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs new file mode 100644 index 0000000..e8c0534 --- /dev/null +++ b/src/Program.cs @@ -0,0 +1,269 @@ +using System.Text; + +#pragma warning disable IDE0130 // Namespace does not match folder structure +namespace FireflyClient +#pragma warning restore IDE0130 // Namespace does not match folder structure +{ + /// + /// Command-line interface for Firefly + /// + public class Program + { + static async Task Main(string[] args) + { + Console.WriteLine("Firefly Client"); + Console.WriteLine("Enter commands (type EXIT to quit)"); + Console.WriteLine("Use double quotes for values with spaces: HSET email:1 subject \"Hello World\""); + Console.WriteLine("Type HELP for basic commands or HELP EMAIL for email examples"); + + // Parse command line arguments + string host = "127.0.0.1"; + int port = 6379; + string password = string.Empty; + + for (int i = 0; i < args.Length; i++) + { + if ((args[i] == "--host" || args[i] == "-h") && i + 1 < args.Length) + { + host = args[++i]; + } + else if ((args[i] == "--port" || args[i] == "-p") && i + 1 < args.Length && int.TryParse(args[i + 1], out int parsedPort)) + { + port = parsedPort; + i++; + } + else if ((args[i] == "--password" || args[i] == "--pass") && i + 1 < args.Length) + { + password = args[++i]; + } + else if (args[i] == "--help" || args[i] == "-?") + { + PrintClientHelp(); + return; + } + } + + try + { + await RunInteractiveClientAsync(host, port, password); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + static void PrintClientHelp() + { + Console.WriteLine("\nFirefly Client Usage:"); + Console.WriteLine(" --host, -h Server hostname or IP (default: 127.0.0.1)"); + Console.WriteLine(" --port, -p Server port (default: 6379)"); + Console.WriteLine(" --password, --pass Server password"); + Console.WriteLine(" --help, -? Show this help message\n"); + Console.WriteLine("Example: FireflyClient --host localhost --port 6380 --password secret123\n"); + } + + static Task RunInteractiveClientAsync(string host, int port, string password) + { + using var client = new FireflyClient(host, port); + Console.WriteLine($"Connecting to server at {host}:{port}..."); + Console.WriteLine("Connected to server!"); + + // If password wasn't provided as a command line argument, prompt for it + if (string.IsNullOrEmpty(password)) + { + Console.Write("Enter password (leave empty if no password required): "); + password = Console.ReadLine() ?? string.Empty; + } + + // Authenticate if password was provided + if (!string.IsNullOrEmpty(password)) + { + bool success = client.Authenticate(password); + + // Check if authentication failed + if (!success) + { + Console.WriteLine("Authentication failed. Disconnecting."); + return Task.CompletedTask; + } + } + + while (true) + { + // Get command from user + Console.Write("> "); + string input = Console.ReadLine() ?? string.Empty; + + if (string.IsNullOrEmpty(input) || input.Equals("EXIT", StringComparison.OrdinalIgnoreCase)) + { + // Send QUIT command before exiting + try + { + string response = client.ExecuteCommand("QUIT"); + Console.WriteLine($"Server: {response.TrimEnd('\r', '\n')}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error while quitting: {ex.Message}"); + } + break; + } + + // Check for help commands + if (input.Equals("HELP", StringComparison.OrdinalIgnoreCase)) + { + PrintBasicCommands(); + continue; + } + // Handle clear screen command + else if (input.Equals("CLS", StringComparison.OrdinalIgnoreCase) || + input.Equals("CLEAR", StringComparison.OrdinalIgnoreCase)) + { + Console.Clear(); + continue; + } + + try + { + // Parse the command + string[] parts = SplitCommandLine(input); + if (parts.Length == 0) continue; + + string command = parts[0]; + string[] args = [.. parts.Skip(1)]; + + // Execute the command + string response = client.ExecuteCommand(command, args); + FormatAndPrintResponse(response); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } + + Console.WriteLine("Disconnected from server."); + return Task.CompletedTask; + } + + // Helper method to split command line respecting quotes + static string[] SplitCommandLine(string commandLine) + { + var result = new List(); + bool inQuotes = false; + StringBuilder currentArg = new(); + + for (int i = 0; i < commandLine.Length; i++) + { + char c = commandLine[i]; + + if (c == '"') + { + inQuotes = !inQuotes; + // Don't include the quote character + } + else if (c == ' ' && !inQuotes) + { + // End of argument + if (currentArg.Length > 0) + { + result.Add(currentArg.ToString()); + currentArg.Clear(); + } + } + else + { + currentArg.Append(c); + } + } + + // Add the last argument if any + if (currentArg.Length > 0) + { + result.Add(currentArg.ToString()); + } + + return [.. result]; + } + + static void FormatAndPrintResponse(string response) + { + // Remove trailing whitespace + response = response.TrimEnd(); + + if (response.StartsWith('*') && response.Contains("\r\n")) + { + // Format array responses for better readability + string[] parts = response.Split("\r\n"); + if (parts.Length > 1) + { + Console.WriteLine("Server: Array response:"); + for (int i = 1; i < parts.Length; i++) + { + if (!string.IsNullOrEmpty(parts[i])) + { + // For simple string and bulk string responses in an array + if (parts[i].StartsWith('+') || parts[i].StartsWith('$')) + { + Console.WriteLine($"{i}) {parts[i][1..]}"); + } + else if (!parts[i].StartsWith('*')) // Skip the array count line + { + Console.WriteLine($"{i}) {parts[i]}"); + } + } + } + return; + } + } + + // Default formatting + Console.WriteLine($"Server: {response}"); + } + + // Helper method: Print basic commands + static void PrintBasicCommands() + { + Console.WriteLine("\n==== Basic Firefly Commands ===="); + + Console.WriteLine("\n## String Operations ##"); + Console.WriteLine("SET key value # Set a string value"); + Console.WriteLine("GET key # Get a string value"); + Console.WriteLine("DEL key # Delete a key"); + Console.WriteLine("SEXISTS key # Check if string key exists"); + + Console.WriteLine("\n## List Operations ##"); + Console.WriteLine("LPUSH key value # Add value to the beginning of a list"); + Console.WriteLine("RPUSH key value # Add value to the end of a list"); + Console.WriteLine("LPOP key # Remove and return the first element"); + Console.WriteLine("RPOP key # Remove and return the last element"); + Console.WriteLine("LRANGE key start end # Get a range of elements (0 -1 for all)"); + Console.WriteLine("LINDEX key index # Get element at specific position"); + Console.WriteLine("LGET key [index] # Get entire list or element at index"); + Console.WriteLine("LEXISTS key # Check if list key exists"); + + Console.WriteLine("\n## Hash Operations ##"); + Console.WriteLine("HSET key field value # Set a field in a hash"); + Console.WriteLine("HGET key field # Get a field from a hash"); + Console.WriteLine("HDEL key field # Delete a field from a hash"); + Console.WriteLine("HEXISTS key field # Check if field exists in hash"); + Console.WriteLine("HGETALL key # Get all fields and values in a hash"); + Console.WriteLine("HMSET key field1 val1... # Set multiple fields at once"); + Console.WriteLine("HASKEY key # Check if hash key exists"); + + Console.WriteLine("\n## Server Operations ##"); + Console.WriteLine("PING # Test connection"); + Console.WriteLine("ECHO message # Echo a message"); + Console.WriteLine("SAVE # Save database to disk"); + Console.WriteLine("BGSAVE # Save database in background"); + Console.WriteLine("QUIT # Gracefully disconnect from server"); + + Console.WriteLine("\n## Help Commands ##"); + Console.WriteLine("HELP # Show this help message"); + Console.WriteLine("HELP EMAIL # Show email storage examples"); + Console.WriteLine("CLS, CLEAR # Clear the console screen"); + Console.WriteLine("EXIT # Exit the client"); + } + } +} \ No newline at end of file diff --git a/src/StringOperations.cs b/src/StringOperations.cs new file mode 100644 index 0000000..de56e3c --- /dev/null +++ b/src/StringOperations.cs @@ -0,0 +1,47 @@ +namespace FireflyClient +{ + public partial class FireflyClient + { + #region String Operations + + /// + /// Sets a key-value pair + /// + public bool StringSet(string key, string value) + { + string response = ExecuteCommand("SET", key, value); + return response.StartsWith("+OK"); + } + + /// + /// Gets a value by key + /// + public string StringGet(string key) + { + string response = ExecuteCommand("GET", key); + if (response.StartsWith('+')) + { + return response[1..].TrimEnd('\r', '\n'); + } + return string.Empty; + } + + /// + /// Deletes a key from all stores (string, list, hash) + /// + public int Delete(string key) + { + string response = ExecuteCommand("DEL", key); + if (response.StartsWith(':')) + { + if (int.TryParse(response[1..].TrimEnd('\r', '\n'), out int result)) + { + return result; + } + } + return 0; + } + + #endregion + } +} \ No newline at end of file