Added FUSE support #55

Merged
Elara6331 merged 65 commits from yannickulrich/itd:fuse into master 2023-03-25 22:23:52 +00:00
8 changed files with 728 additions and 0 deletions

View File

@ -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
View 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 function should be called startFUSE instead to adhere to Go naming conventions

This function should be called `startFUSE` instead to adhere to Go naming conventions

Thanks, I'm relatively new to Go, sorry for these issues

Done in 673383f

Thanks, I'm relatively new to Go, sorry for these issues Done in 673383f
// This is where we'll mount the FS
err := os.MkdirAll(k.String("fuse.mountpoint"), 0755)
yannickulrich marked this conversation as resolved Outdated

Use os.MkdirAll here instead so that it creates parent directories, and handle the error (just return it).

Use `os.MkdirAll` here instead so that it creates parent directories, and handle the error (just return it).

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. How would you suggest going about doing this?

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?

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 fusefs package, add a file called unmount.go with the following contents:

unmount.go
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

Then, you can simply call that function at the top of startFUSE like so:

// Ignore the error because nothing might be mounted on the mountpoint
_ = fusefs.Unmount(k.String("fuse.mountpoint"))

I'll file an issue with the fuse library to export that function once this is merged.

The best way to solve this should be calling the unmount function.

I agree that this should be done. However, this doesn't solve the problem completely because if ITD panics for whatever reason, it won't get run and then the user will get a confusing message. Let me see what the best way would be to call this function on shutdown.

Yeah, this one is going to be a bit more complicated. The FUSE library does have [a different unmount function](https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_linux.go#L133) 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 `fusefs` package, add a file called `unmount.go` with the following contents: <details> <summary><code>unmount.go</code></summary> ```go 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 ``` </details> Then, you can simply call that function at the top of `startFUSE` like so: ```go // Ignore the error because nothing might be mounted on the mountpoint _ = fusefs.Unmount(k.String("fuse.mountpoint")) ``` I'll file an issue with the fuse library to export that function once this is merged. > The best way to solve this should be calling the unmount function. I agree that this should be done. However, this doesn't solve the problem completely because if ITD panics for whatever reason, it won't get run and then the user will get a confusing message. Let me see what the best way would be to call this function on shutdown.

Cool, thank you! I have implemented this 🙂

Cool, thank you! I have implemented this 🙂
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

err is returned twice here

`err` is returned twice here

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
View File

@ -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
View File

@ -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
View 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

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

Done in a54ca7a

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

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.

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.

Done in 673383f

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
Review

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.

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.
Review

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 getting called. Could you perhaps elaborate on the problem you see?

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?
Review

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.

> 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.
Review

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.
Review

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.

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

You should definitely handle this error

You should definitely handle this error

Done in a54ca7a

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

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

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`

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.

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.

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

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

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
- var _ = (fs.NodeLookuper)((*ITNode)(nil)) 
+ var _ fs.NodeLookuper = (*ITNode)(nil)

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

Done in 2396623

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
- 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()
```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()

Done in b5328ec

Done in b5328ec
var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil)
yannickulrich marked this conversation as resolved Outdated

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.

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`.

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
Review

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.
Review

Done in b5328ec

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

Minor typo, it's "success"

Minor typo, it's "success"

Done in b5328ec

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)
}

View 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
}

View 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

View File

@ -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

FUSE should be started before the socket (socket should always be last)

FUSE should be started before the socket (socket should always be last)

Done in 3b96901

Done in 3b96901
// Start control socket
err = startSocket(ctx, dev)
if err != nil {