Added FUSE support #55
@ -106,5 +106,8 @@ func setCfgDefaults() {
|
||||
"notifs.ignore.body": []string{},
|
||||
|
||||
"music.vol.interval": 5,
|
||||
|
||||
"fuse.enabled": false,
|
||||
"fuse.mountpoint": "/tmp/itd/mnt",
|
||||
}, "."), nil)
|
||||
}
|
||||
|
60
fuse.go
Normal file
@ -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 {
|
||||
yannickulrich marked this conversation as resolved
Outdated
|
||||
// This is where we'll mount the FS
|
||||
err := os.MkdirAll(k.String("fuse.mountpoint"), 0755)
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
Use Use `os.MkdirAll` here instead so that it creates parent directories, and handle the error (just return it).
yannickulrich
commented
This is actually a bit more complicated than that. If the mountpoint already exists, fuse will crash. We also can't delete the mountpoint beforehand ( This is actually a bit more complicated than that. If the mountpoint already exists, fuse will crash. We also can't delete the mountpoint beforehand (`rm: cannot remove '/tmp/itd/mnt': Transport endpoint is not connected`). The best way to solve this should be calling the [unmount function](https://pkg.go.dev/github.com/hanwen/go-fuse/v2@v2.2.0/fuse#Server.Unmount). How would you suggest going about doing this?
Elara6331
commented
Yeah, this one is going to be a bit more complicated. The FUSE library does have a different unmount function that you could call before trying to mount the fs, but it's not exported, so we'll need to do a small hack to get access to it anyway. In the
|
||||
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"))
|
||||
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
`err` is returned twice here
yannickulrich
commented
Oh, sorry, fixed now Oh, sorry, fixed now
|
||||
|
||||
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
|
||||
}
|
1
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
|
||||
|
2
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=
|
||||
|
572
internal/fusefs/fuse.go
Normal file
@ -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
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
This error needs to be handled, you can just return it in this case and update the code that calls this function to do the actual handling This error needs to be handled, you can just return it in this case and update the code that calls this function to do the actual handling
yannickulrich
commented
Done in Done in a54ca7a
|
||||
|
||||
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) {
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
These variables should go above These variables should go above `BuildRootNode` because it's using them and it would be more readable that way. Also, Go doesn't require semicolons, you can remove those.
yannickulrich
commented
Done in Done in 673383f
|
||||
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{
|
||||
yannickulrich marked this conversation as resolved
Elara6331
commented
In my opinion, "device" is unclear. I think it should be called "info" instead, that seems clearer. Another thing is that you seem to be providing continuous sensor data by never sending an EOF, which means files will be read forever. Files generally don't work like that, so it leads to consequences. Since the syscall for reading from the file will never return until new information is received, a program that reads from this file will not be able to be killed, even by In my opinion, "device" is unclear. I think it should be called "info" instead, that seems clearer.
Another thing is that you seem to be providing continuous sensor data by never sending an EOF, which means files will be read forever. Files generally don't work like that, so it leads to consequences. Since the syscall for reading from the file will never return until new information is received, a program that reads from this file will not be able to be killed, even by `SIGKILL`. You can see this if you try to `cat` the file. It can also break your system's shell depending on how it's configured. Instead, I think you should just provide single data points, and people will just have to use `itctl` if they want continuous data, because files aren't meant to work this way.
yannickulrich
commented
Happy to change that. Regarding the continuous data stream, isn't this how device files work under Linux? If I run I'm not 100% sure what you mean with the
In the Happy to change that.
Regarding the continuous data stream, isn't this how device files work under Linux? If I run `cat /dev/hidraw0` I get a live stream of data. I was hoping to get the same behaviour. I'm happy to try and fix any bug but think this is probably the best way of doing this. Alternatively, we can have to files, a live stream file (eg. `/tmp/itd/mnt/live/motion`) and a one-shot file (eg. `/tmp/itd/mnt/info/motion`). Would you be opposed to this?
I'm not 100% sure what you mean with the `cat` example. This works perfectly for me:
```
$ cat /tmp/itd/mnt/info/motion
22 -5 -1042
22 -5 -1042
22 -5 -1042
19 -5 -1041
20 -5 -1042
18 -6 -1038
19 0 -1041
24 0 -1043
23 -6 -1041
^C
$
```
In the `itd` log I also see the [Done method of the watcher](https://gitea.arsenm.dev/Arsen6331/infinitime/src/branch/master/infinitime.go#L693-L697) getting called. Could you perhaps elaborate on the problem you see?
Elara6331
commented
Try this with something that returns data less commonly, such as Linux does this kind of stuff with a char device file. Unfortunately, FUSE doesn't provide a way to make a char device. There seems to be a workaround, but it only works for the root user, so it would not work in ITD. I did test that, and I can confirm it doesn't work. > Could you perhaps elaborate on the problem you see?
Try this with something that returns data less commonly, such as `battery`. Every time `cat` tries to read from the file, it executes a syscall. Since data isn't returned until ITD receives it from the watch, the syscall doesn't return until then either. What this means is that whatever program is reading the file will be executing kernel code, which prevents you from killing it.
Linux does this kind of stuff with a char device file. Unfortunately, FUSE doesn't provide a way to make a char device. There seems to be a workaround, but it only works for the root user, so it would not work in ITD. I did test that, and I can confirm it doesn't work.
yannickulrich
commented
Okay, fair enough. Sorry for doubting you. How do you feel about keeping a stream-type file for motion? I'm not 100% sure how much sense the instantaneous motion makes.. Otherwise, I'm going to remove the entire channel stuff. Okay, fair enough. Sorry for doubting you. How do you feel about keeping a stream-type file for motion? I'm not 100% sure how much sense the instantaneous motion makes.. Otherwise, I'm going to remove the entire channel stuff.
Elara6331
commented
Yeah, instantaneous motion data doesn't make much sense, but the continuous motion data file also doesn't work all the time. InfiniTime will stop sending motion data if you have Raise To Wake and ShakeWake disabled and you put the watch to sleep with the button. This will cause programs to once again read forever without the ability to be killed, so I think it would be better to remove the continuous data entirely. I would love to have it, but if it breaks most programs, it's not worth it, especially since Yeah, instantaneous motion data doesn't make much sense, but the continuous motion data file also doesn't work all the time. InfiniTime will stop sending motion data if you have Raise To Wake and ShakeWake disabled and you put the watch to sleep with the button. This will cause programs to once again read forever without the ability to be killed, so I think it would be better to remove the continuous data entirely. I would love to have it, but if it breaks most programs, it's not worth it, especially since `itctl` can be used instead.
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
You should definitely handle this error You should definitely handle this error
yannickulrich
commented
Done in Done in a54ca7a
|
||||
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()
|
||||
Elara6331 marked this conversation as resolved
Outdated
yannickulrich
commented
How would you recommend doing this? I suppose it could fail for all sorts of reasons such as
In other places such as when we open a file, we could have
Again, I'm sorry but I'm not a Go expert and don't really now how to do this properly.. especially when dealing with How would you recommend doing this? I suppose it could fail for all sorts of reasons such as
* no such file or directory `ENOENT`
* generic input/output error `EIO`
* invalid argument `EINVAL`
* connection aborted `ECONNABORTED`
* connection refused `ECONNREFUSED`
* connection reset `ECONNRESET`
* is actually a file `EISNAM`
In other places such as when we open a file, we could have
* is actually a folder `EISDIR`
* file exists `EEXIST`
Again, I'm sorry but I'm not a Go expert and don't really now how to do this properly.. especially when dealing with `FSError`
Elara6331
commented
The In this case, I'd use a Go type switch to check which error type actually occurred and then check the code or do whatever else needs to be done. Maybe there could be a function like If you don't feel comfortable doing that, I can merge this and then implement it myself and send you the commit so you can see how I did it, or you can just try it yourself, whatever you feel would be better. The `err` can be several different kinds of errors, and `FSError` is just one of them. It's actually a type I made. You can see it here: https://gitea.arsenm.dev/Arsen6331/infinitime/src/commit/512d48bc2469/blefs/error.go#L20. It contains an error code you can check to see what went wrong, and you can scroll down to see the meaning of each code.
In this case, I'd use a Go type switch to check which error type actually occurred and then check the code or do whatever else needs to be done. Maybe there could be a function like `syscallErr()` that takes an `error` and returns the proper syscall error?
If you don't feel comfortable doing that, I can merge this and then implement it myself and send you the commit so you can see how I did it, or you can just try it yourself, whatever you feel would be better.
yannickulrich
commented
Something like in 4c59561a99? There are a few Something like in 4c59561a99? There are a few `TODO` where I'm not sure what the correct POSIX error would be and improvised. If you have a better idea, feel free to change them though
Elara6331
commented
That looks good, thanks That looks good, thanks
|
||||
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 {
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
Same for all the other interface checks ```diff
- var _ = (fs.NodeLookuper)((*ITNode)(nil))
+ var _ fs.NodeLookuper = (*ITNode)(nil)
```
Same for all the other interface checks
yannickulrich
commented
Done in Done in 2396623
|
||||
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
|
||||
}
|
||||
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
```diff
- log.Info("Progress").Int("bytes", int(sent)).Int("of", len(fh.content)).Send()
+ log.Info("FUSE Write Progress").Int("bytes", int(sent)).Int("total", len(fh.content)).Send()
yannickulrich
commented
Done in Done in b5328ec
|
||||
var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil)
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
This is not correct. If no content has been written, you should still create the file because the user might want to create an empty file using a command like This is not correct. If no content has been written, you should still create the file because the user might want to create an empty file using a command like `touch`.
yannickulrich
commented
Oh, good point, thank you. Should be fixed now. Oh, good point, thank you. Should be fixed now.
|
||||
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()
|
||||
yannickulrich marked this conversation as resolved
Elara6331
commented
These should be debug logs rather than info logs. Also, the message should be a bit more specific, something like "FUSE getattr". Same for all the similar logs. These should be debug logs rather than info logs. Also, the message should be a bit more specific, something like "FUSE getattr". Same for all the similar logs.
yannickulrich
commented
Done in Done in b5328ec
|
||||
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)
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
Minor typo, it's "success" Minor typo, it's "success"
yannickulrich
commented
Done in Done in b5328ec
|
||||
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)
|
||||
}
|
67
internal/fusefs/syscallerr.go
Normal file
@ -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
|
||||
|
||||
}
|
15
internal/fusefs/unmount.go
Normal file
@ -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
|
8
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()
|
||||
}
|
||||
}
|
||||
yannickulrich marked this conversation as resolved
Outdated
Elara6331
commented
FUSE should be started before the socket (socket should always be last) FUSE should be started before the socket (socket should always be last)
yannickulrich
commented
Done in Done in 3b96901
|
||||
|
||||
// Start control socket
|
||||
err = startSocket(ctx, dev)
|
||||
if err != nil {
|
||||
|
This function should be called
startFUSE
instead to adhere to Go naming conventionsThanks, I'm relatively new to Go, sorry for these issues
Done in
673383f