Added FUSE support #55

Merged
Elara6331 merged 65 commits from yannickulrich/itd:fuse into master 2023-03-25 22:23:52 +00:00
Contributor

This exposes the watches' file system over FUSE. This way, we can access files on the watch without having to go through itctl or developing 3rd party tools.

Features

  • read/write access to the file system
  • read access to momentary sensor data
  • live access to sensor data (i.e. WatchMotion rather than Motion)
  • configuration of mount point
This exposes the watches' file system over FUSE. This way, we can access files on the watch without having to go through `itctl` or developing 3rd party tools. **Features** - [x] read/write access to the file system - [x] read access to momentary sensor data - [x] live access to sensor data (i.e. WatchMotion rather than Motion) - [x] configuration of mount point
yannickulrich added 49 commits 2023-03-01 16:48:45 +00:00
removed godebug dep
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
cc6fc3e1dc
Elara6331 requested changes 2023-03-01 17:29:01 +00:00
Elara6331 left a comment
Owner

Thanks for your PR. There are some changes I'd like, mostly readability improvements.

Thanks for your PR. There are some changes I'd like, mostly readability improvements.
fuse.go Outdated
@ -0,0 +9,4 @@
"go.arsenm.dev/infinitime"
)
func startFuse(ctx context.Context, dev *infinitime.Device) error {
Owner

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
Author
Contributor

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
yannickulrich marked this conversation as resolved
@ -0,0 +38,4 @@
func BuildRootNode(dev *infinitime.Device) *ITNode {
inodemap = make(map[string]uint64)
myfs, _ = dev.FS()
Owner

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
Author
Contributor

Done in a54ca7a

Done in a54ca7a
yannickulrich marked this conversation as resolved
@ -0,0 +81,4 @@
var myfs *blefs.FS = nil;
Owner

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.
Author
Contributor

Done in 673383f

Done in 673383f
yannickulrich marked this conversation as resolved
@ -0,0 +119,4 @@
case 2:
// on device
files, _ := myfs.ReadDir(n.path)
Owner

You should definitely handle this error

You should definitely handle this error
Author
Contributor

Done in a54ca7a

Done in a54ca7a
yannickulrich marked this conversation as resolved
@ -0,0 +161,4 @@
return fs.NewListDirStream(r), 0
}
var _ = (fs.NodeLookuper)((*ITNode)(nil))
Owner
- 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
Author
Contributor

Done in 2396623

Done in 2396623
yannickulrich marked this conversation as resolved
@ -0,0 +298,4 @@
go func() {
// For every progress event
for sent := range fp.Progress() {
log.Info("Progress").Int("bytes", int(sent)).Int("of", len(fh.content)).Send();
Owner
- 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()
Author
Contributor

Done in b5328ec

Done in b5328ec
yannickulrich marked this conversation as resolved
@ -0,0 +330,4 @@
var _ = (fs.NodeGetattrer)((*ITNode)(nil))
func (bn *ITNode) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
log.Info("getattr").Str("path", bn.path).Send();
Owner

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.
Author
Contributor

Done in b5328ec

Done in b5328ec
yannickulrich marked this conversation as resolved
@ -0,0 +483,4 @@
}
node := f.NewInode(ctx, operations, stable)
log.Info("Mkdir sucess").
Owner

Minor typo, it's "success"

Minor typo, it's "success"
Author
Contributor

Done in b5328ec

Done in b5328ec
yannickulrich marked this conversation as resolved
main.go Outdated
@ -187,2 +187,4 @@
log.Error("Error starting socket").Err(err).Send()
}
// Start fuse socket
if k.Bool("fuse.enabled") {
Owner

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

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

Done in 3b96901

Done in 3b96901
yannickulrich marked this conversation as resolved
yannickulrich added 5 commits 2023-03-01 19:07:56 +00:00
Author
Contributor

I've just realised that the error codes aren't correct. I'll fix this next

I've just realised that the error codes aren't correct. I'll fix this next
yannickulrich added 1 commit 2023-03-01 19:40:50 +00:00
Send better syscall status codes
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
955e1323ce
yannickulrich reviewed 2023-03-01 19:48:49 +00:00
@ -0,0 +126,4 @@
files, err := myfs.ReadDir(n.path)
if err != nil {
log.Error("FUSE ReadDir failed").Str("path", n.path).Err(err).Send()
// TODO we probably should figure out why it failed
Author
Contributor

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

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.
Author
Contributor

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
Owner

That looks good, thanks

That looks good, thanks
Elara6331 marked this conversation as resolved
yannickulrich added 2 commits 2023-03-04 09:19:45 +00:00
Some of these aren't ideal..
Used new error conversion routine
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
b28c386c4e
Owner

Sorry it's taken me a while to respond. I wanted to test this PR but my PineTime wasn't booting. Now that I've gotten it to boot, I'll test this and get back to you.

Sorry it's taken me a while to respond. I wanted to test this PR but my PineTime wasn't booting. Now that I've gotten it to boot, I'll test this and get back to you.
Elara6331 requested changes 2023-03-08 23:48:36 +00:00
fuse.go Outdated
@ -0,0 +11,4 @@
func startFUSE(ctx context.Context, dev *infinitime.Device) error {
// This is where we'll mount the FS
os.Mkdir(k.String("fuse.mountpoint"), 0755)
Owner

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).
Author
Contributor

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

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.
Author
Contributor

Cool, thank you! I have implemented this 🙂

Cool, thank you! I have implemented this 🙂
yannickulrich marked this conversation as resolved
fuse.go Outdated
@ -0,0 +18,4 @@
Err(err).
Send()
return err
return err
Owner

err is returned twice here

`err` is returned twice here
Author
Contributor

Oh, sorry, fixed now

Oh, sorry, fixed now
yannickulrich marked this conversation as resolved
@ -0,0 +97,4 @@
// root folder
r := make([]fuse.DirEntry, 2)
r[0] = fuse.DirEntry{
Name: "device",
Owner

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.
Author
Contributor

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

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.
Author
Contributor

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

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.
yannickulrich marked this conversation as resolved
@ -0,0 +299,4 @@
var _ fs.FileFlusher = (*bytesFileWriteHandle)(nil)
func (fh *bytesFileWriteHandle) Flush(ctx context.Context) (errno syscall.Errno) {
if len(fh.content) == 0 {
return 0
Owner

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

Oh, good point, thank you. Should be fixed now.

Oh, good point, thank you. Should be fixed now.
yannickulrich marked this conversation as resolved
yannickulrich added 2 commits 2023-03-11 14:09:15 +00:00
Fixed 'touch' behaviour
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
dc53ead339
yannickulrich added 3 commits 2023-03-12 12:55:49 +00:00
Designed by Arsen6331
Renamed device folder
All checks were successful
ci/woodpecker/pr/woodpecker Pipeline was successful
1a1bc30df9
yannickulrich added 3 commits 2023-03-25 16:41:53 +00:00
Author
Contributor

Sorry it took me so long to respond but I have now removed the live data thing. This simplifies the sensorFileReadHandle (008f6b3) since it just needs to contain the content of the buffer and send it out in due time

type sensorFileReadHandle struct {
       ch <-chan []byte
       cancel context.CancelFunc
       content []byte
 }

Hope this is what you wanted?

PS: when merging, it's probably a good idea to squash the commits since this has gotten quite messy.. sorry..

Sorry it took me so long to respond but I have now removed the live data thing. This simplifies the `sensorFileReadHandle` (008f6b3) since it just needs to contain the content of the buffer and send it out in due time ```go type sensorFileReadHandle struct { ch <-chan []byte cancel context.CancelFunc content []byte } ``` Hope this is what you wanted? PS: when merging, it's probably a good idea to squash the commits since this has gotten quite messy.. sorry..
Owner

I just tested it, and it looks good to me, merging now. Thanks!

I just tested it, and it looks good to me, merging now. Thanks!
Elara6331 merged commit 9ecd45dadd into master 2023-03-25 22:23:52 +00:00
Elara6331 referenced this issue from a commit 2023-03-25 22:23:52 +00:00
Elara6331 referenced this issue from a commit 2023-04-21 02:08:29 +00:00
Sign in to join this conversation.
No description provided.