From cca18ac6d3cfda60df6443ecd7f7785280851755 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Mon, 6 Apr 2026 14:47:25 +0000 Subject: [PATCH 1/9] db: uprev godbus and new packege file A simple commit that uprevs godbus to latest version compatible with the go version openxt-go is pinned to in go.mod. It also adds db.go that provides a location to hold the top level package documentation. Place the package global variable PathDelimiter in db.go and document it. Signed-off-by: Daniel P. Smith --- db/db.go | 13 +++++++++++++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 db/db.go diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..74091b1 --- /dev/null +++ b/db/db.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +/* +Package db implements an client and server for OpenXT db. +*/ +package db + +// PathDelimiter allows specifying the delimiter used for path element +// separation. +var PathDelimiter string = "/" diff --git a/go.mod b/go.mod index 9de977f..19788df 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/openxt/openxt-go go 1.12 require ( - github.com/godbus/dbus/v5 v5.0.3 + github.com/godbus/dbus/v5 v5.1.0 github.com/spf13/pflag v1.0.5 ) diff --git a/go.sum b/go.sum index 9ae06e1..74d827f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= From 050ac2bdf9dcc5e0849d7448f079fd20a9b54e27 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Sun, 8 Mar 2026 15:43:56 -0400 Subject: [PATCH 2/9] db: replace with generated dbus bindings This replaces the existing db client code with the generated dbus bindings. In the process rename the file to client.go to reflect this is the client code. Update dbdcmd to work with the new db client code. Signed-off-by: Daniel P. Smith --- cmd/dbdcmd/main.go | 9 ++- db/client.go | 77 +++++++++++++++++++++++++ db/dbd.go | 137 --------------------------------------------- 3 files changed, 83 insertions(+), 140 deletions(-) create mode 100644 db/client.go delete mode 100644 db/dbd.go diff --git a/cmd/dbdcmd/main.go b/cmd/dbdcmd/main.go index 5f206e7..dda4b28 100644 --- a/cmd/dbdcmd/main.go +++ b/cmd/dbdcmd/main.go @@ -9,6 +9,7 @@ import ( "fmt" "os" + "github.com/godbus/dbus/v5" "github.com/openxt/openxt-go/db" ) @@ -36,11 +37,13 @@ func main() { usage() } - db, err := dbd.NewClient() - + conn, err := dbus.SystemBus() if err != nil { - die("DB connection error: %v", err) + die("Error connecting to system bus: %v\n", err) } + defer conn.Close() + + db := db.NewDbClient(conn, db.DbServiceName, "/") operation := os.Args[1] diff --git a/db/client.go b/db/client.go new file mode 100644 index 0000000..c173a83 --- /dev/null +++ b/db/client.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package db + +import ( + "github.com/godbus/dbus/v5" +) + +const DbServiceName = "com.citrix.xenclient.db" + +type DbClient struct { + dbus.BusObject +} + +func NewDbClient(conn *dbus.Conn, dest, path string) *DbClient { + return &DbClient{conn.Object(dest, dbus.ObjectPath(path))} +} + +/* Interface com.citrix.xenclient.db */ +func (d *DbClient) Dump(path string) (value string, err error) { + + err = d.Call("com.citrix.xenclient.db.dump", 0, path).Store(&value) + + return +} + +func (d *DbClient) Exists(path string) (ex bool, err error) { + + err = d.Call("com.citrix.xenclient.db.exists", 0, path).Store(&ex) + + return +} + +func (d *DbClient) Inject(path string, value string) error { + + call := d.Call("com.citrix.xenclient.db.inject", 0, path, value) + + return call.Err +} + +func (d *DbClient) List(path string) (value []string, err error) { + + err = d.Call("com.citrix.xenclient.db.list", 0, path).Store(&value) + + return +} + +func (d *DbClient) Read(path string) (value string, err error) { + + err = d.Call("com.citrix.xenclient.db.read", 0, path).Store(&value) + + return +} + +func (d *DbClient) ReadBinary(path string) (value []byte, err error) { + + err = d.Call("com.citrix.xenclient.db.read_binary", 0, path).Store(&value) + + return +} + +func (d *DbClient) Rm(path string) error { + + call := d.Call("com.citrix.xenclient.db.rm", 0, path) + + return call.Err +} + +func (d *DbClient) Write(path string, value string) error { + + call := d.Call("com.citrix.xenclient.db.write", 0, path, value) + + return call.Err +} diff --git a/db/dbd.go b/db/dbd.go deleted file mode 100644 index c0bdb46..0000000 --- a/db/dbd.go +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// -// Copyright 2026 Apertus Soutions, LLC -// - - -package dbd - -import ( - "github.com/godbus/dbus/v5" -) - -type Client interface { - Read(path string) (string, error) - ReadBinary(path string) ([]byte, error) - Write(path string, value string) error - Dump(path string) (string, error) - Inject(path string, value string) error - List(path string) ([]string, error) - Rm(path string) error - Exists(path string) (bool, error) -} - -type Dbd struct { - conn *dbus.Conn -} - -func NewClient() (Client, error) { - conn, err := dbus.SystemBus() - - if err != nil { - return nil, err - } - return &Dbd{ - conn: conn, - }, nil -} - -// -// -// -// -func (c *Dbd) Read(path string) (string, error) { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - var s string - err := obj.Call("com.citrix.xenclient.db.read", 0, path).Store(&s) - - return s, err -} - -// -// -// -// -func (c *Dbd) ReadBinary(path string) ([]byte, error) { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - var b []byte - err := obj.Call("com.citrix.xenclient.db.read_binary", 0, path).Store(&b) - - return b, err -} - -// -// -// -// -func (c *Dbd) Write(path string, value string) error { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - call := obj.Call("com.citrix.xenclient.db.write", 0, path, value) - - return call.Err -} - -// -// -// -// -func (c *Dbd) Dump(path string) (string, error) { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - var s string - err := obj.Call("com.citrix.xenclient.db.dump", 0, path).Store(&s) - - return s, err -} - -// -// -// -// -func (c *Dbd) Inject(path string, value string) error { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - call := obj.Call("com.citrix.xenclient.db.inject", 0, path, value) - - return call.Err -} - -// -// -// -// -func (c *Dbd) List(path string) ([]string, error) { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - var s []string - err := obj.Call("com.citrix.xenclient.db.list", 0, path).Store(&s) - - return s, err -} - -// -// -// -func (c *Dbd) Rm(path string) error { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - call := obj.Call("com.citrix.xenclient.db.rm", 0, path) - - return call.Err -} - -// -// -// -// -func (c *Dbd) Exists(path string) (bool, error) { - obj := c.conn.Object("com.citrix.xenclient.db", "/") - - var b bool - err := obj.Call("com.citrix.xenclient.db.read", 0, path).Store(&b) - - return b, err -} From e8550a2fac47e76bcd68dbff0e2360a997e834a3 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Sun, 8 Mar 2026 17:06:34 -0400 Subject: [PATCH 3/9] db-cmd: renaming dbdcmd to db-cmd This aligns the the existing Ocaml db-cmd. While renaming, expand the command to be subcommand compatible with Ocaml db-cmd. Signed-off-by: Daniel P. Smith --- Makefile | 2 +- cmd/db-cmd/main.go | 199 +++++++++++++++++++++++++++++++++++++++++++++ cmd/dbdcmd/main.go | 96 ---------------------- 3 files changed, 200 insertions(+), 97 deletions(-) create mode 100644 cmd/db-cmd/main.go delete mode 100644 cmd/dbdcmd/main.go diff --git a/Makefile b/Makefile index 9eda553..e8c547a 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ bindir = $(exec_prefix)/bin PKGBASE := github.com/openxt/openxt-go PKGS := argo db ioctl -CMDS := argo-nc dbdcmd dbus-send +CMDS := argo-nc db-cmd dbus-send VERSION := 0.1.0 # FIPS is not available until Go 1.24 diff --git a/cmd/db-cmd/main.go b/cmd/db-cmd/main.go new file mode 100644 index 0000000..7f238f4 --- /dev/null +++ b/cmd/db-cmd/main.go @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package main + +import ( + "encoding/binary" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/godbus/dbus/v5" + argoDbus "github.com/openxt/openxt-go/argo/dbus" + "github.com/openxt/openxt-go/db" + flag "github.com/spf13/pflag" +) + +func die(format string, a ...interface{}) { + fmt.Fprintf(os.Stderr, format, a...) + fmt.Fprintln(os.Stderr) + os.Exit(1) +} + +func usage() { + fmt.Fprintf(os.Stderr, "Usage of %s []:\n", os.Args[0]) + flag.PrintDefaults() + die(` +Available commands are: + cat Dump raw value for + exists Check if exist + ls List tree start at + nodes List immediate childtren of + read Retrieve string value for + rm Delete + write Store for `) +} + +func list(c *db.DbClient, fullPath bool, indent int, path string) (string, error) { + path = strings.TrimRight(path, db.PathDelimiter) + result, err := c.List(path) + if err != nil { + return "", err + } + + var key string + if fullPath { + key = path + } else { + key = strings.Repeat(" ", indent) + if path != "" { + key += filepath.Base(path) + } + } + + if len(result) == 0 { + value, err := c.Read(path) + if err != nil { + return "", fmt.Errorf("failed reading %s: %v\n", path, err) + } + return fmt.Sprintf("%s = \"%s\"", key, value), nil + } + + out := key + " =" + for _, elem := range result { + r, err := list(c, fullPath, indent+1, path+"/"+elem) + if err != nil { + return "", err + } + + out += "\n" + r + } + + return out, nil +} + +func main() { + var conn *dbus.Conn + + helpFlag := flag.BoolP("help", "h", false, "Print help") + fullPathFlag := flag.BoolP("full", "f", false, "Full path") + platBusFlag := flag.BoolP("platform", "p", false, "Connect to the platform bus") + flag.CommandLine.MarkHidden("full") + flag.Parse() + + if *helpFlag { + usage() + } + + if *platBusFlag { + var err error + conn, err = argoDbus.ConnectPlatformBus() + if err != nil { + die("Error connecting to platform bus: %v\n", err) + } + } else { + var err error + conn, err = dbus.SystemBus() + if err != nil { + die("Error connecting to system bus: %v\n", err) + } + } + defer conn.Close() + + args := flag.Args() + if len(args) < 1 { + usage() + } + + client := db.NewDbClient(conn, db.DbServiceName, "/") + + operation := args[0] + + args = args[1:] + arglen := len(args) + + switch operation { + case "cat": + if arglen != 1 { + fmt.Fprintf(os.Stderr, + "Error: incorrect number of arguments.\n") + usage() + } + result, err := client.ReadBinary(args[1]) + if err != nil { + die("DB read binary error: %v", err) + } + binary.Write(os.Stdout, binary.LittleEndian, result) + + case "exists": + if arglen != 1 { + fmt.Fprintf(os.Stderr, + "Error: incorrect number of arguments.\n") + usage() + } + result, err := client.Exists(os.Args[0]) + if err != nil { + die("DB exists error: %v", err) + } + fmt.Printf("%t", result) + case "ls": + path := "/" + if len(args) != 0 { + path = args[0] + } + result, err := list(client, *fullPathFlag, 0, path) + if err != nil { + die("DB list error: %v", err) + } + fmt.Printf("%s\n", result) + + case "nodes": + if arglen != 1 { + fmt.Fprintf(os.Stderr, + "Error: incorrect number of arguments.\n") + usage() + } + result, err := client.List(args[0]) + if err != nil { + die("DB read error: %v", err) + } + fmt.Printf("%s\n", strings.Join(result, " ")) + case "read": + if arglen != 1 { + fmt.Fprintf(os.Stderr, + "Error: incorrect number of arguments.\n") + usage() + } + result, err := client.Read(args[0]) + if err != nil { + die("DB read error: %v", err) + } + fmt.Printf("%s\n", result) + case "rm": + if arglen != 1 { + fmt.Fprintf(os.Stderr, + "Error: incorrect number of arguments.\n") + usage() + } + err := client.Rm(args[0]) + if err != nil { + die("DB rm error: %v", err) + } + case "write": + if arglen != 2 { + fmt.Fprintf(os.Stderr, + "Error: incorrect number of arguments.\n") + usage() + } + err := client.Write(args[0], args[1]) + if err != nil { + die("DB write error: %v", err) + } + default: + usage() + } +} diff --git a/cmd/dbdcmd/main.go b/cmd/dbdcmd/main.go deleted file mode 100644 index dda4b28..0000000 --- a/cmd/dbdcmd/main.go +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// -// Copyright 2026 Apertus Soutions, LLC -// - -package main - -import ( - "fmt" - "os" - - "github.com/godbus/dbus/v5" - "github.com/openxt/openxt-go/db" -) - -func die(format string, a ...interface{}) { - fmt.Fprintf(os.Stderr, format, a...) - fmt.Fprintln(os.Stderr) - os.Exit(1) -} - -func usage() { - die( - `Usage: dbcmd [] - -Available commands are: - read Retrieve from db - write Store for in the db - rm Delete from db - exists Check if exist in the db - help Print this help`) -} - -func main() { - arglen := len(os.Args) - if arglen < 2 { - usage() - } - - conn, err := dbus.SystemBus() - if err != nil { - die("Error connecting to system bus: %v\n", err) - } - defer conn.Close() - - db := db.NewDbClient(conn, db.DbServiceName, "/") - - operation := os.Args[1] - - switch operation { - case "read": - if arglen != 3 { - fmt.Fprintf(os.Stderr, - "Error: incorrect number of arguments.\n") - usage() - } - result, err := db.Read(os.Args[2]) - if err != nil { - die("DB read error: %v", err) - } - fmt.Println(os.Stdout, "%s", result) - case "write": - if arglen != 4 { - fmt.Fprintf(os.Stderr, - "Error: incorrect number of arguments.\n") - usage() - } - err := db.Write(os.Args[2], os.Args[3]) - if err != nil { - die("DB write error: %v", err) - } - case "rm": - if arglen != 3 { - fmt.Fprintf(os.Stderr, - "Error: incorrect number of arguments.\n") - usage() - } - err := db.Rm(os.Args[2]) - if err != nil { - die("DB rm error: %v", err) - } - case "exists": - if arglen != 3 { - fmt.Fprintf(os.Stderr, - "Error: incorrect number of arguments.\n") - usage() - } - result, err := db.Exists(os.Args[2]) - if err != nil { - die("DB exists error: %v", err) - } - fmt.Println(os.Stdout, "%t", result) - default: - usage() - } -} From 7d9e53bf8d05baba9e8cf75c84e36d80d4f7c431 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Mon, 21 Jul 2025 20:12:09 -0400 Subject: [PATCH 4/9] logging: introduce a logging library around syslog This adds a convenience logging library for logging to syslog. Signed-off-by: Daniel P. Smith --- logging/logging.go | 101 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 logging/logging.go diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000..ac1cb12 --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package logging + +import ( + "fmt" + "log" + "log/syslog" + "os" +) + +var ( + DefaultLogLevel syslog.Priority = syslog.LOG_INFO +) + +type SystemLogger struct { + handle *syslog.Writer +} + +func NewSystemLogger(name string) *SystemLogger { + l := SystemLogger{} + + w, e := syslog.New(syslog.LOG_DAEMON|DefaultLogLevel, name) + if e != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to connect to syslog!\n") + return &l + } + + // Any library that uses Go's log library will also get logged to + // syslog at the debug level + log.SetOutput(w) + + l.handle = w + + return &l +} + +func (l *SystemLogger) Close() { + if l.handle != nil { + l.handle.Close() + } +} + +func (l *SystemLogger) Emerg(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Emerg(m) + } +} + +func (l *SystemLogger) Alert(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Alert(m) + } +} + +func (l *SystemLogger) Crit(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Crit(m) + } +} + +func (l *SystemLogger) Err(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Err(m) + } +} + +func (l *SystemLogger) Warning(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Warning(m) + } +} + +func (l *SystemLogger) Notice(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Notice(m) + } +} + +func (l *SystemLogger) Info(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Info(m) + } +} + +func (l *SystemLogger) Debug(format string, a ...interface{}) { + if l.handle != nil { + m := fmt.Sprintf(format, a...) + l.handle.Debug(m) + } +} From f49dd7a4b6cfe064440064e81bdcedb34be000e8 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Mon, 21 Jul 2025 19:33:22 -0400 Subject: [PATCH 5/9] utils: add json error reporting helper Introduce a new utils library with the initial utility for json logging. This utility provides allows the generation of a contextual error from the error returned json.Unmarshal. Signed-off-by: Daniel P. Smith --- utils/json.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 utils/json.go diff --git a/utils/json.go b/utils/json.go new file mode 100644 index 0000000..a53d08a --- /dev/null +++ b/utils/json.go @@ -0,0 +1,34 @@ +package utils + +import ( + "encoding/json" + "fmt" + "strings" +) + +/* Takes the error from json.Unmarshal and provides a contextual error */ +func FormatJsonError(contents []byte, err error) error { + jsonErr, ok := err.(*json.SyntaxError) + if !ok { + return err + } + + if strings.Count(string(contents), "\n") > 1 { + offset := 0 + problemPart := "" + for _, line := range strings.Split(string(contents), "\n") { + if jsonErr.Offset < int64(offset+len(line)) { + problemPart = strings.TrimRight(line, "\n") + break + } + offset += len(line) + } + err = fmt.Errorf("%w: error near (offset %d):\n\t'%s'", err, jsonErr.Offset-int64(offset), problemPart) + } else { + offset := 10 + problemPart := contents[jsonErr.Offset-int64(offset) : jsonErr.Offset+int64(offset)] + err = fmt.Errorf("%w: error near (offset %d):\n\t'%s'", err, offset, problemPart) + } + + return err +} From 62db265f18c6b3f74832e64a77d74ab65788fff2 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Mon, 21 Jul 2025 19:42:27 -0400 Subject: [PATCH 6/9] db: introduce arbitrary tree from json library This introduces the core of the dbd library, the Node object. A Node object can hold the parsed results of an arbitrary JSON object. It is then able to present the results as a tree structure that can be descended into to manipulate arbitrary leaves in the tree. Signed-off-by: Daniel P. Smith --- db/node.go | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 db/node.go diff --git a/db/node.go b/db/node.go new file mode 100644 index 0000000..78e21a1 --- /dev/null +++ b/db/node.go @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package db + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + + "github.com/openxt/openxt-go/utils" +) + +type NodeType int + +const ( + UnknownNode NodeType = iota + MapNode + ListNode + StringNode + NumberNode + BooleanNode +) + +func (nt *NodeType) String() string { + switch *nt { + case UnknownNode: + return "Unknown" + case MapNode: + return "Map" + case ListNode: + return "List" + case StringNode: + return "String" + case NumberNode: + return "Number" + case BooleanNode: + return "Boolean" + } + return "Unknown" +} + +type NodePath []string + +type Node struct { + data interface{} +} + +func NewNode(jsonBytes []byte) (*Node, error) { + var node Node + + if err := json.Unmarshal(jsonBytes, &node.data); err != nil { + return nil, utils.FormatJsonError(jsonBytes, err) + } + + return &node, nil +} + +func (n *Node) IsMap() bool { + if _, ok := (n.data).(map[string]interface{}); ok { + return true + } + return false +} + +func (n *Node) IsList() bool { + if _, ok := (n.data).([]interface{}); ok { + return true + } + return false +} + +func (n *Node) IsString() bool { + if _, ok := (n.data).(string); ok { + return true + } + return false +} + +func (n *Node) IsNumber() bool { + switch n.data.(type) { + case float32, float64: + return true + case int, int8, int16, int32, int64: + return true + case uint, uint8, uint16, uint32, uint64: + return true + } + return false +} + +func (n *Node) IsBoolean() bool { + if _, ok := (n.data).(bool); ok { + return true + } + return false +} + +func (n *Node) String() string { + + switch n.data.(type) { + case string: + return n.data.(string) + case bool: + case float32, float64: + return strconv.FormatFloat(n.data.(float64), 'e', -1, 64) + case int, int8, int16, int32, int64: + return strconv.FormatInt(n.data.(int64), 10) + case uint, uint8, uint16, uint32, uint64: + return strconv.FormatUint(n.data.(uint64), 10) + } + + return "" +} + +func (n *Node) Type() NodeType { + switch { + case n.IsMap(): + return MapNode + case n.IsList(): + return ListNode + case n.IsString(): + return StringNode + case n.IsNumber(): + return NumberNode + case n.IsBoolean(): + return BooleanNode + } + return UnknownNode +} + +var ( + ErrNoSuchChild = errors.New("no such child") + ErrNotAMap = errors.New("node is not a map") +) + +func (n *Node) Children() []string { + if n.IsMap() { + m := (n.data).(map[string]interface{}) + + children := []string{} + for k := range m { + children = append(children, k) + } + return children + } + + return []string{} +} + +func (n *Node) nextChild(key string) (*Node, error) { + if n.IsMap() { + m := (n.data).(map[string]interface{}) + if _, ok := m[key]; !ok { + return nil, fmt.Errorf("%w: invalid map key (%s)", + ErrNoSuchChild, key) + } + + return &Node{data: m[key]}, nil + } + + return nil, ErrNotAMap +} + +func (n *Node) Child(path NodePath) (*Node, error) { + if len(path) == 0 { + return n, nil + } + + next, err := n.nextChild(path[0]) + if err != nil { + return nil, err + } + + return next.Child(path[1:]) +} + +func (n *Node) NewChild(key string) (*Node, error) { + if n.IsMap() { + node := Node{data: map[string]interface{}{}} + m := (n.data).(map[string]interface{}) + m[key] = node.data + + return &node, nil + } + + return nil, ErrNotAMap +} + +func (n *Node) DelChild(key string) error { + if n.IsMap() { + m := (n.data).(map[string]interface{}) + delete(m, key) + + return nil + } + + return ErrNotAMap +} + +func (n *Node) AddNode(path NodePath, node *Node, replace bool) error { + if len(path) == 0 { + return fmt.Errorf("path must have at least one element") + } + + if !n.IsMap() { + return ErrNotAMap + } + + next, err := n.nextChild(path[0]) + if !errors.Is(err, ErrNoSuchChild) { + return err + } + + if len(path) == 1 { + if next != nil { + if replace { + next.data = node.data + return nil + } + return fmt.Errorf("node exists") + } + + m := (n.data).(map[string]interface{}) + m[path[0]] = node.data + + return nil + } + + if next != nil { + return next.AddNode(path[1:], node, replace) + } + + child, err := n.NewChild(path[0]) + if err != nil { + return err + } + if err := child.AddNode(path[1:], node, replace); err != nil { + n.DelChild(path[0]) + return err + } + + return nil +} + +func (n *Node) DelNode(path NodePath) error { + plen := len(path) + key := path[plen-1] + parent, err := n.Child(path[:plen-1]) + if err != nil { + return err + } + + m := (parent.data).(map[string]interface{}) + + if _, ok := m[key]; ok { + delete(m, key) + return nil + } + + return ErrNoSuchChild +} + +func (n *Node) Exists(path NodePath) bool { + _, err := n.Child(path) + if err == nil { + return true + } + + return false +} + +func (n *Node) List(indent string) [][]string { + paths := [][]string{} + + if n.IsMap() { + m := (n.data).(map[string]interface{}) + + for k, v := range m { + child := Node{data: v} + paths = append(paths, []string{indent, k, child.String()}) + + if child.IsMap() { + childPaths := child.List(indent + " ") + paths = append(paths, childPaths...) + } + } + } + + return paths +} From fe47fde4555bf918d7844168573b0f348cad7a8a Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Mon, 21 Jul 2025 20:02:14 -0400 Subject: [PATCH 7/9] db: introduce json store around node This introduces the JsonStore type that provides a locking wrapper around the Node type. In addition, it provides the abstraction of moving between one or more json blobs and a single node tree. Signed-off-by: Daniel P. Smith --- db/json-store.go | 189 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 db/json-store.go diff --git a/db/json-store.go b/db/json-store.go new file mode 100644 index 0000000..a088d23 --- /dev/null +++ b/db/json-store.go @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package db + +import ( + "encoding/json" + "fmt" + "strings" + "sync" +) + +func StringToNodePath(s string) NodePath { + s = strings.Trim(s, PathDelimiter) + if len(s) == 0 { + return NodePath{} + } + elems := strings.Split(s, PathDelimiter) + + return NodePath(elems) +} + +func NodePathToString(np NodePath) string { + path := PathDelimiter + + for _, p := range np { + path += PathDelimiter + p + } + + return path +} + +type JsonStore struct { + mutex sync.RWMutex + root *Node +} + +func NewJsonStore(base []byte) (*JsonStore, error) { + node, err := NewNode(base) + if err != nil { + return nil, err + } + + return &JsonStore{root: node}, nil +} + +func (js *JsonStore) Read(path string) (string, error) { + np := StringToNodePath(path) + + js.mutex.RLock() + defer js.mutex.RUnlock() + + if !js.root.Exists(np) { + return "", nil + } + + node, err := js.root.Child(np) + if err != nil { + return "", err + } + + return node.String(), nil +} + +func (js *JsonStore) Write(path, value string) error { + np := StringToNodePath(path) + pp := np[:len(np)-1] + key := np[len(np)-1] + + js.mutex.Lock() + defer js.mutex.Unlock() + + if !js.root.Exists(pp) { + node := Node{data: map[string]interface{}{key: value}} + return js.root.AddNode(pp, &node, false) + } + + node, err := js.root.Child(pp) + if err != nil { + return err + } + + if !node.IsMap() { + return fmt.Errorf("%s is not a path element", NodePathToString(pp)) + } + + m := (node.data).(map[string]interface{}) + m[key] = value + + return nil +} + +func (js *JsonStore) Dump(path string) ([]byte, error) { + np := StringToNodePath(path) + + js.mutex.RLock() + defer js.mutex.RUnlock() + + node, err := js.root.Child(np) + if err != nil { + return nil, err + } + + return json.Marshal(node.data) +} + +func (js *JsonStore) Inject(path string, contents []byte) error { + np := StringToNodePath(path) + + js.mutex.Lock() + defer js.mutex.Unlock() + + node, err := NewNode(contents) + if err != nil { + return err + } + + if err := js.root.AddNode(np, node, false); err != nil { + return err + } + + return nil +} + +func (js *JsonStore) RList(path string) ([]string, error) { + np := StringToNodePath(path) + + js.mutex.RLock() + defer js.mutex.RUnlock() + + node, err := js.root.Child(np) + if err != nil { + return nil, err + } + + paths := [][]string{ + []string{"", np[len(np)-1], node.String()}, + } + + if node.IsMap() { + childPaths := node.List(" ") + paths = append(paths, childPaths...) + } + + var entries []string + for _, e := range paths { + if len(e) != 3 { + return nil, fmt.Errorf("invalid path entry") + } + entry := e[0] + e[1] + " = " + e[2] + entries = append(entries, entry) + } + + return entries, nil +} + +func (js *JsonStore) List(path string) ([]string, error) { + np := StringToNodePath(path) + + js.mutex.RLock() + defer js.mutex.RUnlock() + + node, err := js.root.Child(np) + if err != nil { + return nil, err + } + + return node.Children(), nil +} + +func (js *JsonStore) Remove(path string) error { + np := StringToNodePath(path) + + js.mutex.Lock() + defer js.mutex.Unlock() + + return js.root.DelNode(np) +} + +func (js *JsonStore) Exist(path string) bool { + np := StringToNodePath(path) + + js.mutex.RLock() + defer js.mutex.RUnlock() + + return js.root.Exists(np) +} From 3a1cf49aa8264824fd7f6453424df2101d6af918 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Mon, 21 Jul 2025 20:38:27 -0400 Subject: [PATCH 8/9] db: add server type This adds the Server type to expose a JsonStore as an OpenXT dbd dbus server instance in compliance with the OpenXT dbd IDL. The Ocaml instance of dbd does not support the write_binary interface, but is defined in the IDL. This implementation adds support for this interface by storing the binary bytes as a base64 encoded string in the JSON field. Signed-off-by: Daniel P. Smith --- db/introspection.go | 54 ++++++ db/server.go | 430 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 db/introspection.go create mode 100644 db/server.go diff --git a/db/introspection.go b/db/introspection.go new file mode 100644 index 0000000..77d5b90 --- /dev/null +++ b/db/introspection.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package db + +import ( + "github.com/godbus/dbus/v5/introspect" +) + +const DbdIntrospection = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + introspect.IntrospectDataString + ` +` diff --git a/db/server.go b/db/server.go new file mode 100644 index 0000000..58536a5 --- /dev/null +++ b/db/server.go @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package db + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "github.com/openxt/openxt-go/logging" +) + +var ( + ConfigPath = "/config" + + methodMap = map[string]string{ + "Dump": "dump", + "Exists": "exists", + "Inject": "inject", + "List": "list", + "Read": "read", + "ReadBinary": "read_binary", + "Remove": "rm", + "Write": "write", + "WriteBinary": "write_binary", + } + + logger *logging.SystemLogger +) + +const ( + coreDbFile = "db" + vmDir = "vms" + domstoreDir = "dom-store" + + dbdInterface = "com.citrix.xenclient.db" + introspectInterface = "org.freedesktop.DBus.Introspectable" +) + +type Server struct { + js *JsonStore + conn *dbus.Conn + mutex sync.Mutex + dirty bool +} + +func NewServer() (*Server, error) { + if logger == nil { + logger = logging.NewSystemLogger("dbd") + } + logger.Info("starting new dbd server using config directory %s", ConfigPath) + + s := &Server{} + + if err := s.initJsonStore(); err != nil { + return nil, err + } + + return s, nil +} + +func (s *Server) initJsonStore() error { + jsonBytes, err := ioutil.ReadFile(filepath.Join(ConfigPath, coreDbFile)) + if err != nil { + logger.Crit("Failed reading db file (%s): %s", coreDbFile, err) + return err + } + + js, err := NewJsonStore(jsonBytes) + if err != nil { + logger.Crit("Failed parsing db file (%s): %s", coreDbFile, err) + return err + } + + s.js = js + + if err := s.loadConfigDir(vmDir, "/vm/"); err != nil { + logger.Crit("Failed parsing VM db file(s): %s", err) + return err + } + if err := s.loadConfigDir(domstoreDir, "/dom-store/"); err != nil { + logger.Crit("Failed parsing domstore db file(s): %s", err) + return err + } + + return nil +} + +func (s *Server) flushJsonStore() error { + if err := s.storeConfigDir(vmDir, "/vm"); err != nil { + logger.Crit("Failed to persist VM configs to disk: %s", err) + return err + } + + if err := s.js.Remove("/vm"); err != nil { + logger.Crit("Failed to clear VM configs from store: %s", err) + return err + } + + if err := s.storeConfigDir(domstoreDir, "/dom-store"); err != nil { + logger.Crit("Failed to persist domstore configs to disk: %s", err) + return err + } + + if err := s.js.Remove("/dom-store"); err != nil { + logger.Crit("Failed to clear dom-store configs from store: %s", err) + return err + } + + f, err := os.OpenFile(filepath.Join(ConfigPath, coreDbFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + logger.Crit("Unable to open db file, unable to write to disk: %s", err) + return err + } + defer f.Close() + + contents, err := s.js.Dump("/") + if err != nil { + logger.Crit("Unable to export db contents, unable to write to disk: %s", err) + return err + } + + var buf bytes.Buffer + + if err := json.Indent(&buf, contents, "", " "); err != nil { + logger.Crit("Failed to format db contents, unable to write to disk: %s", err) + return err + } + + if _, err := f.Write(buf.Bytes()); err != nil { + logger.Crit("Failed to write db contents to disk: %s", err) + return err + } + + return nil +} + +func (s *Server) loadConfigDir(dir, path string) error { + baseDir := filepath.Join(ConfigPath, dir) + entries, err := ioutil.ReadDir(baseDir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, entry := range entries { + node := strings.TrimSuffix(entry.Name(), ".db") + if node == entry.Name() { + continue + } + + contents, err := ioutil.ReadFile(baseDir + "/" + entry.Name()) + if err != nil { + logger.Err("Failed reading VM db file (%s), skipping: %s", entry.Name(), err) + } + + if err := s.js.Inject(path+node, contents); err != nil { + logger.Err("Failed inserting VM (%s) config, skipping: %s", node, err) + } + } + + return nil +} + +func checkDir(dirName string) bool { + dir, err := os.Stat(dirName) + if os.IsNotExist(err) { + if err := os.MkdirAll(dirName, 0755); err != nil { + return false + } + return true + } + + return dir.Mode().IsDir() +} + +func (s *Server) storeConfigDir(dir, path string) error { + dir = filepath.Join(ConfigPath, dir) + path = strings.TrimRight(path, PathDelimiter) + + if !s.js.Exist(path) { + logger.Info("Store config: No entries for path %s", path) + return nil + } + + if !checkDir(dir) { + return fmt.Errorf("Unable to create directory: %s", dir) + } + + entries, err := s.js.List(path) + if err != nil { + return err + } + + for _, entry := range entries { + fpath := fmt.Sprintf("%s/%s.db", dir, entry) + f, err := os.OpenFile(fpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + logger.Crit("Unable to open db file for %s, skipping writing to disks: %s", entry, err) + continue + } + + contents, err := s.js.Dump(path + PathDelimiter + entry) + if err != nil { + logger.Crit("Unable to export contents for %s, skipping writing to disk: %s", entry, err) + continue + } + + var buf bytes.Buffer + + if err := json.Indent(&buf, contents, "", " "); err != nil { + logger.Crit("Failed to format contents for %s, skipping writing to disk: %s", entry, err) + continue + } + + f.Write(buf.Bytes()) + f.Close() + } + + return nil +} + +func (s *Server) DBusListen() error { + conn, err := dbus.SystemBus() + if err != nil { + logger.Crit("Failed connecting to dbus system bus: %s", err) + return err + } + + err = conn.ExportWithMap(s, methodMap, "/", dbdInterface) + if err != nil { + conn.Close() + logger.Crit("Failed exporting dbus interface: %s", err) + return err + } + err = conn.Export(introspect.Introspectable(DbdIntrospection), "/", + introspectInterface) + if err != nil { + conn.Close() + logger.Crit("Failed exporting introspection interface: %s", err) + return err + } + + reply, err := conn.RequestName(dbdInterface, dbus.NameFlagDoNotQueue) + if err != nil { + conn.Close() + logger.Crit("Failed requesting dbus name: %s", err) + return err + } + if reply != dbus.RequestNameReplyPrimaryOwner { + conn.Close() + logger.Crit("Failed requesting dbus primary owner: %s", err) + return fmt.Errorf("name already taken") + } + + s.conn = conn + return nil +} + +func (s *Server) Sync() error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if s.dirty { + if err := s.flushJsonStore(); err != nil { + return err + } + if err := s.initJsonStore(); err != nil { + return err + } + + s.dirty = false + } + + return nil +} + +func (s *Server) Reload() error { + s.mutex.Lock() + defer s.mutex.Unlock() + + if err := s.initJsonStore(); err != nil { + return err + } + + s.dirty = false + return nil +} + +func (s *Server) Shutdown(flush bool) { + s.conn.Close() + if flush { + s.flushJsonStore() + } +} + +func (s *Server) Dump(path string) (string, *dbus.Error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + jsb, err := s.js.Dump(path) + if err != nil { + logger.Err("dump: failed dumping path %s: %s", path, err) + return "", dbus.MakeFailedError(err) + } + + var buf bytes.Buffer + + if err := json.Indent(&buf, jsb, "", " "); err != nil { + logger.Err("dump: failed formating for path %s: %s", path, err) + return "", dbus.MakeFailedError(err) + } + + return buf.String(), nil +} + +func (s *Server) Exists(path string) (bool, *dbus.Error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + return s.js.Exist(path), nil +} + +func (s *Server) Inject(path, value string) *dbus.Error { + s.mutex.Lock() + defer s.mutex.Unlock() + + err := s.js.Inject(path, []byte(value)) + if err != nil { + logger.Err("inject: failed inserting at path %s: %s", path, err) + return dbus.MakeFailedError(err) + } + s.dirty = true + return nil +} + +func (s *Server) List(path string) ([]string, *dbus.Error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + str, err := s.js.List(path) + if err != nil { + logger.Err("list: failed list of path %s: %s", path, err) + return nil, dbus.MakeFailedError(err) + } + return str, nil +} + +func (s *Server) Read(path string) (string, *dbus.Error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + str, err := s.js.Read(path) + if err != nil { + logger.Err("read: failed reading path %s: %s", path, err) + return "", dbus.MakeFailedError(err) + } + return str, nil +} + +func (s *Server) ReadBinary(path string) ([]byte, *dbus.Error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + str, err := s.js.Read(path) + if err != nil { + logger.Err("read binary: failed reading path %s: %s", path, err) + return nil, dbus.MakeFailedError(err) + } + + data, err := base64.StdEncoding.DecodeString(str) + if err != nil { + logger.Err("read binary: failed decoding %s: %s", path, err) + return nil, dbus.MakeFailedError(err) + } + + return data, nil +} + +func (s *Server) Remove(path string) *dbus.Error { + s.mutex.Lock() + defer s.mutex.Unlock() + + err := s.js.Remove(path) + if err != nil { + logger.Err("remove: failed deleting %s: %s", path, err) + return dbus.MakeFailedError(err) + } + s.dirty = true + return nil +} + +func (s *Server) Write(path, value string) *dbus.Error { + s.mutex.Lock() + defer s.mutex.Unlock() + + err := s.js.Write(path, value) + if err != nil { + logger.Err("write: failed writing to %s: %s", path, err) + return dbus.MakeFailedError(err) + } + s.dirty = true + return nil +} + +func (s *Server) WriteBinary(path string, value []byte) *dbus.Error { + s.mutex.Lock() + defer s.mutex.Unlock() + + data := base64.StdEncoding.EncodeToString(value) + err := s.js.Write(path, data) + if err != nil { + logger.Err("write binary: failed writing to %s: %s", path, err) + return dbus.MakeFailedError(err) + } + s.dirty = true + return nil +} From d70074be39f40ae48522340a03de78e168af6da8 Mon Sep 17 00:00:00 2001 From: "Daniel P. Smith" Date: Sat, 7 Mar 2026 16:31:33 -0500 Subject: [PATCH 9/9] dbd: introduce dbd command Signed-off-by: Daniel P. Smith --- Makefile | 2 +- cmd/dbd/main.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 cmd/dbd/main.go diff --git a/Makefile b/Makefile index e8c547a..0a6b470 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ bindir = $(exec_prefix)/bin PKGBASE := github.com/openxt/openxt-go PKGS := argo db ioctl -CMDS := argo-nc db-cmd dbus-send +CMDS := argo-nc db-cmd dbus-send dbd VERSION := 0.1.0 # FIPS is not available until Go 1.24 diff --git a/cmd/dbd/main.go b/cmd/dbd/main.go new file mode 100644 index 0000000..902f299 --- /dev/null +++ b/cmd/dbd/main.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BSD-3-Clause +// +// Copyright 2026 Apertus Soutions, LLC +// + +package main + +import ( + "fmt" + "log/syslog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/openxt/openxt-go/db" + "github.com/openxt/openxt-go/logging" + flag "github.com/spf13/pflag" +) + +const ( + RefreshUnit = time.Minute + RefreshInterval = 30 +) + +func usage() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + flag.PrintDefaults() + os.Exit(0) +} + +func main() { + helpFlag := flag.Bool("help", false, "Print help") + debugFlag := flag.Bool("debug", false, "Enable debug") + configDirFlag := flag.String("config", "", "Set the config directory") + flag.Parse() + + if *helpFlag { + usage() + } + + if *debugFlag { + logging.DefaultLogLevel = syslog.LOG_DEBUG + } + + if *configDirFlag != "" { + db.ConfigPath = *configDirFlag + } + + sigs := make(chan os.Signal, 1) + exit := make(chan int, 1) + signal.Notify(sigs, + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGQUIT, + syscall.SIGTERM) + + s, err := db.NewServer() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + err = s.DBusListen() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + + go func() { + for { + select { + case sig := <-sigs: + switch sig { + case syscall.SIGHUP: + if err := s.Reload(); err != nil { + exit <- 1 + return + } + default: + s.Shutdown(true) + exit <- 0 + return + } + case <-time.After(RefreshInterval * RefreshUnit): + if err := s.Sync(); err != nil { + exit <- 1 + return + } + } + } + }() + + code := <-exit + os.Exit(code) +}