diff --git a/config.go b/config.go index 49ce23d..a188658 100644 --- a/config.go +++ b/config.go @@ -106,5 +106,8 @@ func setCfgDefaults() { "notifs.ignore.body": []string{}, "music.vol.interval": 5, + + "fuse.enabled": false, + "fuse.mountpoint": "/tmp/itd/mnt", }, "."), nil) } diff --git a/fuse.go b/fuse.go new file mode 100644 index 0000000..a5dea5b --- /dev/null +++ b/fuse.go @@ -0,0 +1,60 @@ +package main +import ( + "go.arsenm.dev/itd/internal/fusefs" + "os" + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "go.arsenm.dev/logger/log" + "context" + "go.arsenm.dev/infinitime" +) + +func startFUSE(ctx context.Context, dev *infinitime.Device) error { + // This is where we'll mount the FS + err := os.MkdirAll(k.String("fuse.mountpoint"), 0755) + if err != nil && !os.IsExist(err) { + return err + } + + // Ignore the error because nothing might be mounted on the mountpoint + _ = fusefs.Unmount(k.String("fuse.mountpoint")) + + + root, err := fusefs.BuildRootNode(dev) + if err != nil { + log.Error("Building root node failed"). + Err(err). + Send() + return err + } + + server, err := fs.Mount(k.String("fuse.mountpoint"), root, &fs.Options{ + MountOptions: fuse.MountOptions{ + // Set to true to see how the file system works. + Debug: false, + SingleThreaded: true, + }, + }) + if err != nil { + log.Error("Mounting failed"). + Str("target", k.String("fuse.mountpoint")). + Err(err). + Send() + return err + } + + log.Info("Mounted on target"). + Str("target", k.String("fuse.mountpoint")). + Send() + + fusefs.BuildProperties(dev) + + if err != nil { + log.Warn("Error getting BLE filesystem").Err(err).Send() + return err + } + + // Wait until unmount before exiting + go server.Serve() + return nil +} diff --git a/go.mod b/go.mod index 80df00c..668793a 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gookit/color v1.5.1 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/hanwen/go-fuse/v2 v2.2.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect diff --git a/go.sum b/go.sum index 0d78863..347e737 100644 --- a/go.sum +++ b/go.sum @@ -257,6 +257,8 @@ github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfI github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hanwen/go-fuse/v2 v2.2.0 h1:jo5QZYmBLNcl9ovypWaQ5yXMSSV+Ch68xoC3rtZvvBM= +github.com/hanwen/go-fuse/v2 v2.2.0/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= diff --git a/internal/fusefs/fuse.go b/internal/fusefs/fuse.go new file mode 100644 index 0000000..9855d3f --- /dev/null +++ b/internal/fusefs/fuse.go @@ -0,0 +1,572 @@ +package fusefs + +import ( + "go.arsenm.dev/infinitime" + "go.arsenm.dev/infinitime/blefs" + "go.arsenm.dev/logger/log" + "context" + "syscall" + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "io" + "bytes" + "strconv" +) + +type ITProperty struct { + name string + Ino uint64 + gen func() ([]byte, error) +} + + +type DirEntry struct { + isDir bool + modtime uint64 + size uint32 + path string +} + +type ITNode struct { + fs.Inode + kind int + Ino uint64 + + lst []DirEntry + self DirEntry + path string +} + +var myfs *blefs.FS = nil +var inodemap map[string]uint64 = nil + +func BuildRootNode(dev *infinitime.Device) (*ITNode, error) { + var err error + inodemap = make(map[string]uint64) + myfs, err = dev.FS() + if err != nil { + log.Error("FUSE Failed to get filesystem").Err(err).Send() + return nil, err + } + + return &ITNode{kind: 0}, nil +} + +var properties = make([]ITProperty, 6) + +func BuildProperties(dev *infinitime.Device) { + properties[0] = ITProperty{"heartrate", 2, + func() ([]byte, error) { + ans, err := dev.HeartRate() + return []byte(strconv.Itoa(int(ans)) + "\n"), err + }} + properties[1] = ITProperty{"battery", 3, + func() ([]byte, error) { + ans, err := dev.BatteryLevel() + return []byte(strconv.Itoa(int(ans)) + "\n"), err + }} + properties[2] = ITProperty{"motion", 4, + func() ([]byte, error) { + ans, err := dev.Motion() + return []byte(strconv.Itoa(int(ans.X)) + " " + strconv.Itoa(int(ans.Y)) + " " + strconv.Itoa(int(ans.Z)) + "\n"), err + }} + properties[3] = ITProperty{"stepcount", 6, + func() ([]byte, error) { + ans, err := dev.StepCount() + return []byte(strconv.Itoa(int(ans)) + "\n"), err + }} + properties[4] = ITProperty{"version", 7, + func() ([]byte, error) { + ans, err := dev.Version() + return []byte(ans + "\n"), err + }} + properties[5] = ITProperty{"address", 8, + func() ([]byte, error) { + ans := dev.Address() + return []byte(ans + "\n"), nil + }} + +} + + +var _ fs.NodeReaddirer = (*ITNode)(nil) + +// Readdir is part of the NodeReaddirer interface +func (n *ITNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { + switch n.kind { + case 0: + // root folder + r := make([]fuse.DirEntry, 2) + r[0] = fuse.DirEntry{ + Name: "info", + Ino: 0, + Mode: fuse.S_IFDIR, + } + r[1] = fuse.DirEntry{ + Name: "fs", + Ino: 1, + Mode: fuse.S_IFDIR, + } + return fs.NewListDirStream(r), 0 + + case 1: + // info folder + r := make([]fuse.DirEntry, 6) + for ind, value := range properties { + r[ind] = fuse.DirEntry{ + Name: value.name, + Ino: value.Ino, + Mode: fuse.S_IFREG, + } + } + + return fs.NewListDirStream(r), 0 + + case 2: + // on info + files, err := myfs.ReadDir(n.path) + if err != nil { + log.Error("FUSE ReadDir failed").Str("path", n.path).Err(err).Send() + return nil, syscallErr(err) + } + + log.Debug("FUSE ReadDir succeeded").Str("path", n.path).Int("objects", len(files)).Send() + r := make([]fuse.DirEntry, len(files)) + n.lst = make([]DirEntry, len(files)) + for ind, entry := range files { + info, err := entry.Info() + if err != nil { + log.Error("FUSE Info failed").Str("path", n.path).Err(err).Send() + return nil, syscallErr(err) + } + name := info.Name() + + file := DirEntry{ + path: n.path + "/" + name, + size: uint32(info.Size()), + modtime: uint64(info.ModTime().Unix()), + isDir: info.IsDir(), + } + n.lst[ind] = file + + ino := inodemap[file.path] + if ino == 0 { + ino = uint64(len(inodemap)) + 1 + inodemap[file.path] = ino + } + + if file.isDir { + r[ind] = fuse.DirEntry{ + Name: name, + Mode: fuse.S_IFDIR, + Ino : ino + 10, + } + } else { + r[ind] = fuse.DirEntry{ + Name: name, + Mode: fuse.S_IFREG, + Ino : ino + 10, + } + } + } + return fs.NewListDirStream(r), 0 + } + r := make([]fuse.DirEntry, 0) + return fs.NewListDirStream(r), 0 +} + +var _ fs.NodeLookuper = (*ITNode)(nil) +func (n *ITNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + switch n.kind { + case 0: + // root folder + if name == "info" { + stable := fs.StableAttr{ + Mode: fuse.S_IFDIR, + Ino: uint64(0), + } + operations := &ITNode{kind: 1, Ino: 0} + child := n.NewInode(ctx, operations, stable) + return child, 0 + } else if name == "fs" { + stable := fs.StableAttr{ + Mode: fuse.S_IFDIR, + Ino: uint64(1), + } + operations := &ITNode{kind: 2, Ino: 1, path : ""} + child := n.NewInode(ctx, operations, stable) + return child, 0 + } + case 1: + // info folder + for _, value := range properties { + if value.name == name { + stable := fs.StableAttr{ + Mode: fuse.S_IFREG, + Ino: uint64(value.Ino), + } + operations := &ITNode{kind: 3, Ino: value.Ino} + child := n.NewInode(ctx, operations, stable) + return child, 0 + } + } + + case 2: + // FS object + if len(n.lst) == 0 { + n.Readdir(ctx) + } + + for _, file := range n.lst { + if file.path != n.path + "/" + name { + continue + } + log.Debug("FUSE Lookup successful").Str("path", file.path).Send() + + if file.isDir { + stable := fs.StableAttr{ + Mode: fuse.S_IFDIR, + Ino: inodemap[file.path], + } + operations := &ITNode{kind: 2, path: file.path} + child := n.NewInode(ctx, operations, stable) + return child, 0 + } else { + stable := fs.StableAttr{ + Mode: fuse.S_IFREG, + Ino: inodemap[file.path], + } + operations := &ITNode{ + kind: 2, path: file.path, + self: file, + } + child := n.NewInode(ctx, operations, stable) + return child, 0 + } + break + } + log.Warn("FUSE Lookup failed").Str("path", n.path + "/" + name).Send() + } + return nil, syscall.ENOENT +} + +type bytesFileReadHandle struct { + content []byte +} + +var _ fs.FileReader = (*bytesFileReadHandle)(nil) +func (fh *bytesFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + log.Debug("FUSE Executing Read").Int("size", len(fh.content)).Send() + end := off + int64(len(dest)) + if end > int64(len(fh.content)) { + end = int64(len(fh.content)) + } + return fuse.ReadResultData(fh.content[off:end]), 0 +} + +type sensorFileReadHandle struct { + content []byte +} +var _ fs.FileReader = (*sensorFileReadHandle)(nil) +func (fh *sensorFileReadHandle) Read(ctx context.Context, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + log.Info("Executing Read").Int("size", len(fh.content)).Send() + end := off + int64(len(dest)) + if end > int64(len(fh.content)) { + end = int64(len(fh.content)) + } + return fuse.ReadResultData(fh.content[off:end]), 0 +} + +var _ fs.FileFlusher = (*sensorFileReadHandle)(nil) +func (fh *sensorFileReadHandle) Flush(ctx context.Context) (errno syscall.Errno) { + return 0 +} + + +type bytesFileWriteHandle struct { + content []byte + path string +} + +var _ fs.FileWriter = (*bytesFileWriteHandle)(nil) +func (fh *bytesFileWriteHandle) Write(ctx context.Context, data []byte, off int64) (written uint32, errno syscall.Errno) { + log.Info("Executing Write").Str("path", fh.path).Int("prev_size", len(fh.content)).Int("next_size", len(data)).Send() + if off != int64(len(fh.content)) { + log.Error("FUSE Write file size changed unexpectedly").Int("expect", int(off)).Int("received", len(fh.content)).Send() + return 0, syscall.ENXIO + } + fh.content = append(fh.content[:], data[:]...) + return uint32(len(data)), 0 +} + +var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil) +func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno) { + + log.Debug("FUSE Attempting flush").Str("path", fh.path).Send() + fp, err := myfs.Create(fh.path, uint32(len(fh.content))) + if err != nil { + log.Error("FUSE Flush failed: create").Str("path", fh.path).Err(err).Send() + return syscallErr(err) + } + + if len(fh.content) == 0 { + log.Debug("FUSE Flush no data to write").Str("path", fh.path).Send() + err = fp.Close() + if err != nil { + log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send() + return syscallErr(err) + } + return 0 + } + + go func() { + // For every progress event + for sent := range fp.Progress() { + log.Debug("FUSE Flush progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send() + } + }() + + r := bytes.NewReader(fh.content) + nread, err := io.Copy(fp, r) + if err != nil { + log.Error("FUSE Flush failed during write").Str("path", fh.path).Err(err).Send() + fp.Close() + return syscallErr(err) + } + if int(nread) != len(fh.content) { + log.Error("FUSE Flush failed during write").Str("path", fh.path).Int("expect", len(fh.content)).Int("got", int(nread)).Send() + fp.Close() + return syscall.EIO + } + err = fp.Close() + if err != nil { + log.Error("FUSE Flush failed during close").Str("path", fh.path).Err(err).Send() + return syscallErr(err) + } + log.Debug("FUSE Flush done").Str("path", fh.path).Int("size", len(fh.content)).Send() + + return 0 +} +var _ fs.FileFsyncer = (*bytesFileWriteHandle)(nil) +func (fh *bytesFileWriteHandle) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) { + return fh.Flush(ctx) +} + +var _ fs.NodeGetattrer = (*ITNode)(nil) +func (bn *ITNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + log.Debug("FUSE getattr").Str("path", bn.path).Send() + out.Ino = bn.Ino + out.Mtime = bn.self.modtime + out.Ctime = bn.self.modtime + out.Atime = bn.self.modtime + out.Size = uint64(bn.self.size) + return 0 +} + +var _ fs.NodeSetattrer = (*ITNode)(nil) +func (bn *ITNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { + log.Debug("FUSE setattr").Str("path", bn.path).Send() + out.Size = 0 + out.Mtime = 0 + return 0 +} + +var _ fs.NodeOpener = (*ITNode)(nil) +func (f *ITNode) Open(ctx context.Context, openFlags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + switch f.kind { + case 2: + // FS file + if openFlags&syscall.O_RDWR != 0 { + log.Error("FUSE Open failed: RDWR").Str("path", f.path).Send() + return nil, 0, syscall.EROFS + } + + if openFlags & syscall.O_WRONLY != 0 { + log.Debug("FUSE Opening for write").Str("path", f.path).Send() + fh = &bytesFileWriteHandle{ + path : f.path, + content : make([]byte, 0), + } + return fh, fuse.FOPEN_DIRECT_IO, 0 + } else { + log.Debug("FUSE Opening for read").Str("path", f.path).Send() + fp, err := myfs.Open(f.path) + if err != nil { + log.Error("FUSE: Opening failed").Str("path", f.path).Err(err).Send() + return nil, 0, syscallErr(err) + } + + defer fp.Close() + + b := &bytes.Buffer{} + + go func() { + // For every progress event + for sent := range fp.Progress() { + log.Debug("FUSE Read progress").Int("bytes", int(sent)).Int("total", int(f.self.size)).Send() + } + }() + + _, err = io.Copy(b, fp) + if err != nil { + log.Error("FUSE Read failed").Str("path", f.path).Err(err).Send() + fp.Close() + return nil, 0, syscallErr(err) + } + + fh = &bytesFileReadHandle{ + content: b.Bytes(), + } + return fh, fuse.FOPEN_DIRECT_IO, 0 + } + + case 3: + // Device file + + // disallow writes + if openFlags&(syscall.O_RDWR|syscall.O_WRONLY) != 0 { + return nil, 0, syscall.EROFS + } + + for _, value := range properties { + if value.Ino == f.Ino { + ans, err := value.gen() + if err != nil { + return nil, 0, syscallErr(err) + } + + fh = &sensorFileReadHandle{ + content : ans, + } + return fh, fuse.FOPEN_DIRECT_IO, 0 + } + } + } + return nil, 0, syscall.EINVAL +} + +var _ fs.NodeCreater = (*ITNode)(nil) +func (f *ITNode) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (node *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + if f.kind != 2 { + return nil, nil, 0, syscall.EROFS + } + + path := f.path + "/" + name + ino := uint64(len(inodemap)) + 11 + inodemap[path] = ino + + stable := fs.StableAttr{ + Mode: fuse.S_IFREG, + Ino: ino, + } + operations := &ITNode{ + kind: 2, Ino: ino, + path : path, + } + node = f.NewInode(ctx, operations, stable) + + fh = &bytesFileWriteHandle{ + path : path, + content : make([]byte, 0), + } + + log.Debug("FUSE Creating file").Str("path", path).Send() + + errno = 0 + return node, fh, fuseFlags, 0 +} + +var _ fs.NodeMkdirer = (*ITNode)(nil) +func (f *ITNode) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + if f.kind != 2 { + return nil, syscall.EROFS + } + + path := f.path + "/" + name + err := myfs.Mkdir(path) + if err != nil { + log.Error("FUSE Mkdir failed"). + Str("path", path). + Err(err). + Send() + return nil, syscallErr(err) + } + + ino := uint64(len(inodemap)) + 11 + inodemap[path] = ino + + stable := fs.StableAttr{ + Mode: fuse.S_IFDIR, + Ino: ino, + } + operations := &ITNode{ + kind: 2, Ino: ino, + path : path, + } + node := f.NewInode(ctx, operations, stable) + + log.Debug("FUSE Mkdir success"). + Str("path", path). + Int("ino", int(ino)). + Send() + return node, 0 +} + +var _ fs.NodeRenamer = (*ITNode)(nil) +func (f *ITNode) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno { + if f.kind != 2 { + return syscall.EROFS + } + + p1 := f.path + "/" + name + p2 := newParent.EmbeddedInode().Path(nil)[2:] + "/" + newName + + err := myfs.Rename(p1, p2) + if err != nil { + log.Error("FUSE Rename failed"). + Str("src", p1). + Str("dest", p2). + Err(err). + Send() + + return syscallErr(err) + } + log.Debug("FUSE Rename sucess"). + Str("src", p1). + Str("dest", p2). + Send() + + ino := inodemap[p1] + delete(inodemap, p1) + inodemap[p2] = ino + + return 0 +} + +var _ fs.NodeUnlinker = (*ITNode)(nil) +func (f *ITNode) Unlink(ctx context.Context, name string) syscall.Errno { + if f.kind != 2 { + return syscall.EROFS + } + + delete(inodemap, f.path + "/" + name) + err := myfs.Remove(f.path + "/" + name) + if err != nil { + log.Error("FUSE Unlink failed"). + Str("file", f.path + "/" + name). + Err(err). + Send() + + return syscallErr(err) + } + + log.Debug("FUSE Unlink success"). + Str("file", f.path + "/" + name). + Send() + return 0 +} + +var _ fs.NodeRmdirer = (*ITNode)(nil) +func (f *ITNode) Rmdir(ctx context.Context, name string) syscall.Errno { + return f.Unlink(ctx, name) +} diff --git a/internal/fusefs/syscallerr.go b/internal/fusefs/syscallerr.go new file mode 100644 index 0000000..e055acb --- /dev/null +++ b/internal/fusefs/syscallerr.go @@ -0,0 +1,67 @@ +package fusefs +import ( + "go.arsenm.dev/infinitime/blefs" + "syscall" +) + +func syscallErr(err error) syscall.Errno { + if err == nil { + return 0 + } + + switch err { + case blefs.FSError{0x02}: // filesystem error + return syscall.EIO // TODO + case blefs.FSError{0x05}: // read-only filesystem + return syscall.EROFS + case blefs.FSError{0x03}: // no such file + return syscall.ENOENT + case blefs.FSError{0x04}: // protocol error + return syscall.EPROTO + case blefs.FSError{-5}: // input/output error + return syscall.EIO + case blefs.FSError{-84}: // filesystem is corrupted + return syscall.ENOTRECOVERABLE // TODO + case blefs.FSError{-2}: // no such directory entry + return syscall.ENOENT + case blefs.FSError{-17}: // entry already exists + return syscall.EEXIST + case blefs.FSError{-20}: // entry is not a directory + return syscall.ENOTDIR + case blefs.FSError{-39}: // directory is not empty + return syscall.ENOTEMPTY + case blefs.FSError{-9}: // bad file number + return syscall.EBADF + case blefs.FSError{-27}: // file is too large + return syscall.EFBIG + case blefs.FSError{-22}: // invalid parameter + return syscall.EINVAL + case blefs.FSError{-28}: // no space left on device + return syscall.ENOSPC + case blefs.FSError{-12}: // no more memory available + return syscall.ENOMEM + case blefs.FSError{-61}: // no attr available + return syscall.ENODATA // TODO + case blefs.FSError{-36}: // file name is too long + return syscall.ENAMETOOLONG + case blefs.ErrFileNotExists: // file does not exist + return syscall.ENOENT + case blefs.ErrFileReadOnly: // file is read only + return syscall.EACCES + case blefs.ErrFileWriteOnly: // file is write only + return syscall.EACCES + case blefs.ErrInvalidOffset: // invalid file offset + return syscall.EFAULT // TODO + case blefs.ErrOffsetChanged: // offset has already been changed + return syscall.ESPIPE + case blefs.ErrReadOpen: // only one file can be opened for reading at a time + return syscall.ENFILE + case blefs.ErrWriteOpen: // only one file can be opened for writing at a time + return syscall.ENFILE + case blefs.ErrNoRemoveRoot: // refusing to remove root directory + return syscall.EPERM + } + + return syscall.EIO // TODO + +} diff --git a/internal/fusefs/unmount.go b/internal/fusefs/unmount.go new file mode 100644 index 0000000..99f9aa6 --- /dev/null +++ b/internal/fusefs/unmount.go @@ -0,0 +1,15 @@ +package fusefs + +import ( + _ "unsafe" + "github.com/hanwen/go-fuse/v2/fuse" +) + +func Unmount(mountPoint string) error { + return unmount(mountPoint, &fuse.MountOptions{DirectMount: false}) +} + +// Unfortunately, the FUSE library does not export its unmount function, +// so this is required until that changes +//go:linkname unmount github.com/hanwen/go-fuse/v2/fuse.unmount +func unmount(mountPoint string, opts *fuse.MountOptions) error diff --git a/main.go b/main.go index 457a16e..0accec8 100644 --- a/main.go +++ b/main.go @@ -181,6 +181,14 @@ func main() { log.Error("Error intializing puremaps integration").Err(err).Send() } + // Start fuse socket + if k.Bool("fuse.enabled") { + err = startFUSE(ctx, dev) + if err != nil { + log.Error("Error starting fuse socket").Err(err).Send() + } + } + // Start control socket err = startSocket(ctx, dev) if err != nil {