diff --git a/Makefile b/Makefile index 9eda553..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 dbdcmd 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/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/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) +} diff --git a/cmd/dbdcmd/main.go b/cmd/dbdcmd/main.go deleted file mode 100644 index 5f206e7..0000000 --- a/cmd/dbdcmd/main.go +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// -// Copyright 2026 Apertus Soutions, LLC -// - -package main - -import ( - "fmt" - "os" - - "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() - } - - db, err := dbd.NewClient() - - if err != nil { - die("DB connection error: %v", err) - } - - 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() - } -} 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/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/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 -} 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/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) +} 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 +} 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 +} 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= 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) + } +} 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 +}