Scille / parsec-cloud

Open source Dropbox-like file sharing with full client encryption !
https://parsec.cloud
Other
270 stars 40 forks source link

Implement mountpoint for macOS #6144

Closed mmmarcos closed 3 weeks ago

mmmarcos commented 10 months ago

Parsec packages are already available for macOS (although not yet signed). The only thing missing seems to be file system integration (mountpoints).

Implementation should probably be based on macFUSE because already used for V2, although it may be useful to do some research about other APIs (see comments below).

FirelightFlagboy commented 10 months ago

Alternative to macFuse ? https://developer.apple.com/documentation/fileprovider

FirelightFlagboy commented 8 months ago

Possible other alternative: https://www.fuse-t.org/

mmmarcos commented 8 months ago

Yet another alternative to macFUSE: https://www.fuse-t.org/

Our thoughts about it:

@touilleMan 
It's based on a local NFS server that macOS connects to, so there's no driver problem 
like with macFUSE on the other hand, in terms of security, this means that data can 
more easily remain in macOS cache even after Parsec has been shut down (NFS is used 
to expose hard disks across the network, so the OS tends to do quite a lot of caching to
limit latency...) the downside is that this thing isn't open-source (the source code isn't 
available on GitHub), so I imagine that if you take out a license for commercial use, you'll 
need access to the source code...
@mmmarcos 
IMO, not sure that's the right approach: from what I've read, Apple is increasingly 
discouraging kernel extensions, but they don't yet have alternative APIs to do it all. 
That's probably why extensions still exist. 
MacFUSE seems to me to be a good example, because it's a stable product that doesn't seem
ready to make the switch until the OS offers something that holds up.
Fuse-T is probably a good project but, in addition to the problems you highlight, we're not sure 
about its stability and robustness. Incidentally, here's some not-so-positive feedback: 
https://github.com/osxfuse/osxfuse/issues/987#issuecomment-1932795467
Better the devil you know than the devil you don’t ^^

Anyway, it may be interesting to make some test. We could also ask them about the cache.

See also: https://github.com/macos-fuse-t/fuse-t/wiki#unsupported-features

FirelightFlagboy commented 4 months ago

Current state of the investigation

I will describe the current state of the investigation to add support of mounpoint on MacOS.

Where do we start ?

The work to add the support is located in the Draft #7420. It is based on the previous work provided by @TimeEngineer in the Draft #7016.

Current observation

Working on the Draft #7420 and on cberner/fuser#286 allow me to make the following observation:

What if we just run the application with mountpoint enabled ?

We can run the application with the mountpoint enabled it "work" but with some caveats:

I'm currently working on implementing a MemFS using the fuser crate, for me it as the following benefit:

mmmarcos commented 4 months ago
* We cannot run mountpoint's tests on macOS, since it require installing the kernel extension which is not possible on the provided github runner (it require to restart the VM at minimum)

Why this is not the case on V2?


* `cp` will display error because we don't support `chmod` operations.
  But the file are still copied and their content are okish.

How is the error displayed? (console? GUI Dialog?). Depending on that, this may be acceptable, since it seems to be the case in Parsec V2:

https://github.com/Scille/parsec-cloud/blob/d397c035d1369e498a48dea26837d93b7291dfe4/parsec/core/mountpoint/fuse_operations.py#L241-L243


* `mv` don't work at all.

Can you elaborate a little more the tests you performed? (mv a file? mv a dir? mv into the same dir? move into another dir?). Does the rename operation work?

There is a comment in the V2 that might be related: https://github.com/Scille/parsec-cloud/blob/d397c035d1369e498a48dea26837d93b7291dfe4/parsec/core/mountpoint/fuse_operations.py#L305-L317


* `mount` command to list mountpoint don't seems to list the mountpoint prior a handcheck of somesort (juste create a `fuser::Mountpoint` and not running it won't work)

What exactly is "handcheck" in this case? Does the mount command it work after it? Eventually, we can consider it as temporary hack on macOS platform while waiting for a full fix.

FirelightFlagboy commented 4 months ago

Why this is not the case on V2?

Yes, it's not the case on v2 for macOS

How is the error displayed?

Console

What exactly is "handcheck" in this case? Does the mount command it work after it?

The "handcheck" is the operation that append just after you create the mountpoint and start processing event from the kernel, it correspond to the FUSE INIT event. So if you don't "run" the filesystem, the init event is never processed and causing the mountpoint to not be displayed on macOS

Can you elaborate a little more the tests you performed?

cd $PARSEC_MNT
touch foo
mv foo bar

The last command don't work

vxgmichel commented 4 months ago

There is a comment in the V2 that might be related: [...]

To give a bit of context about this workaround: in parsec v2, renaming a file from a directory to another one (typically when moving a file) was not supported. Instead, trying this renaming would raise a FSCrossDeviceError to indicate to the program performing this file operation that it is not supported. In this case, the program would typically fall back to a copy-then-delete kind of move. This is typically what happens when you try to move a file from one drive to another one (since it's obviously not possible to simply rename the file without copying it).

This workaround of delegating the copy-then-delete to the program accessing the mountpoint works fine for most cases but still failed for some specific cases, including the macos temporary files mentioned in the comment. In this case, parsec performs the copy-then-delete even though it is not an atomic operation.

In parsec v3, renaming a file from a directory to another is supported (@touilleMan can you confirm this?), so this workaround should not be necessary anymore.

mmmarcos commented 4 months ago

Why this is not the case on V2?

Yes, it's not the case on v2 for macOS

The point was : do you have any ideas why it worked before and not now? (fuser issue? changes in our ci? etc.)

What exactly is "handcheck" in this case? Does the mount command it work after it?

The "handcheck" is the operation that append just after you create the mountpoint and start processing event from the kernel, it correspond to the FUSE INIT event. So if you don't "run" the filesystem, the init event is never processed and causing the mountpoint to not be displayed on macOS

All in all, we might accept the problems you listed and consider them bugs to fix in later releases.

I suggest we merge your PR to enable macOS so our colleagues using Mac can test it.

FirelightFlagboy commented 4 months ago

Why this is not the case on V2?

Yes, it's not the case on v2 for macOS

The point was : do you have any ideas why it worked before and not now? (fuser issue? changes in our ci? etc.)

What worked in the CI in V2? We never had mountpoint tests for macOS in v2

touilleMan commented 4 months ago

For debugging the issue about mv not working:

Run cargo run -p libparsec_platform_mountpoint --example minimal $HOME/parsec-test this will mount a workspace in path $HOME/parsec-test/wksp1. Note the mountpoint correspond to minimal_client_ready testbed template, and the console ouput DEBUG log for each operation involving the mountpoint

After that, you can move into $HOME/parsec-test/wksp1/ and do the touch a (not we are not creating a foo file here, this is given there is already a foo folder existing in the workspace).

Once this is done, repetedly smash enter on the console that is running cargo run -p libparsec_platform_mountpoint --example minimal $HOME/parsec-test. This way is very easy to see where start the logs that will occur for the next operation. Next operation that, of course, is the mv a b.

Once this is done you can provide here the DEBUG logs you got ;-)

For instance on Linux here is what we have when doing mv a b:

[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(518) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(520) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(522) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(524) ino 0x0000000000000001 LOOKUP name "a"
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] lookup(parent: 0x1, name: "a")
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(526) ino 0x0000000000000001 LOOKUP name "b"
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] lookup(parent: 0x1, name: "b")
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(528) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(530) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(532) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(534) ino 0x0000000000000001 LOOKUP name "b"
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] lookup(parent: 0x1, name: "b")
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(536) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(538) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(540) ino 0x0000000000000001 LOOKUP name "a"
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] lookup(parent: 0x1, name: "a")
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(542) ino 0x0000000000000001 LOOKUP name "b"
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] lookup(parent: 0x1, name: "b")
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(544) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(546) ino 0x0000000000000001 GETATTR
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] getattr(ino: 0x1)
[2024-06-26T07:58:54Z DEBUG fuser::request] FUSE(548) ino 0x0000000000000001 RENAME src FilenameInDir { dir: INodeNo(1), name: "a" }, dest FilenameInDir { dir: INodeNo(1), name: "b" }
[2024-06-26T07:58:54Z DEBUG libparsec_platform_mountpoint::unix::filesystem] [FUSE] rename(src_parent: 0x1, src_name: "a", dst_parent: 0x1, \
                std_name: "b", flags: 0)
[2024-06-26T07:58:54Z DEBUG sqlx::query] summary="INSERT INTO vlobs(vlob_id, need_sync, …" db.statement="\n\nINSERT INTO\n  vlobs(\n    vlob_id,\n    need_sync,\n    blob,\n    base_version,\n    remote_version\n  )\nVALUES\n  (?1, ?2, ?3, ?4, ?5) ON CONFLICT DO\nUPDATE\nSET\n  need_sync = excluded.need_sync,\n  blob = excluded.blob,\n  base_version = excluded.base_version,\n  remote_version = MAX(excluded.remote_version, remote_version)\n" rows_affected=1 rows_returned=0 elapsed=36.97µs
fabienSvtr commented 4 months ago

What I noticed when I tested the mountpoint on macOS.

Feature wanted By finder By app
Create folder “no-name”
Rename file/folder
Move file/folder
Delete file/folder
Copy/paste
Add new file/folder
FirelightFlagboy commented 4 months ago

I performed some additional tests on my side using a Custom MemFS using fuser. Here is the following capabilities I was able to achieved:

That's a nice set of capabilities, but something important is missing:

The abilities to rename files!

Currently when doing a rename operation:

fn rename(src_dir: Inode, src_name: &OsStr, dst_dir: Inode, dst_name: &OsStr) {
  // ...
}

macFUSE do not provide src_name & dst_name (the value are blank) making it impossible to perform the operation.

rename(1, "", 2, "")

[!NOTE] I say macFUSE but I haven't checked who is at fault between:

  • The kernel extension macFUSE
  • The interation between macFUSE and fuser
FirelightFlagboy commented 4 months ago

I've tested Parsec using the same tests script than MemFS. It result in the same results: The rename do not work!

I've also tried some basic operation on the Finder:

Creating a folder don't work because finder first create a folder named untitled folder then tries to rename it.

You still obtain a folder named untitled folder tho

Creating a file using TextEdit don't work for the same reason as for the folder:

  1. First the file is copied
  2. Then it tries to rename

Since the rename fail here, it also do a unlink step meaning the file is removed.

FirelightFlagboy commented 4 months ago

What need to do first is to investigate macFUSE:

Does macFUSE allow to rename a file?

Here we would need to test macFUSE without using fuser. first we could try using the fuse high-level function and depending if it's possible to perform a rename operation try the low-level stuff

[!NOTE] If the high-level test fail that mean game-over for macFUSE but since we where able to provide the mountpoint for parsec-v2. I expect that should work and the problem lies on the low-level stuff where it differ from linux and macos.

FirelightFlagboy commented 3 weeks ago

After some investigation, I've found out that fuser was doing a bad parsing of the data provided by macFUSE on the rename operation.

I've provided a fix for that (cberner/fuser#307), in the mean time we will use that fix early (#8766)