-
Notifications
You must be signed in to change notification settings - Fork 203
Add filesystem notification (FSNotify) support #294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
f89a058
to
128421e
Compare
128421e
to
816c698
Compare
@realrajaryan Can you take a peek at the CI fail, it's for the new test added |
b33caa9
to
07a7db3
Compare
LINK = 2; | ||
UNLINK = 3; | ||
MODIFY = 4; | ||
UNDEFINED = 99; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does an UNDEFINED event represent? When would such an event be propagated through the gRPC API?
message NotifyFileSystemEventRequest { | ||
string path = 1; | ||
FileSystemEventType event_type = 2; | ||
string container_id = 3; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear why, but it seems we use snake case for just about everything in this file, except a container ID is containerID
🤷
} | ||
|
||
/// Send a filesystem event notification to the guest. | ||
public func notifyFileSystemEvent( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're using streaming, the API here should be some sort of collection of Com_Apple_Containerization_Sandbox_V3_NotifyFileSystemEventRequest (or a typealias of it).
Sources/Integration/Suite.swift
Outdated
|
||
let fs: Containerization.Mount = try await { | ||
let fsPath = Self.testDir.appending(component: "rootfs.ext4") | ||
let imageHash = Self.hashImageReference(reference) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just use image.digest
in place of manually hashing the image reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, I'd suggested doing this verbally but I forgot we have the image object already at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's good about just using the digest is in the (no idea why we'd do this but) odd scenario where it's the same image, just a different reference/tag, we still wouldn't unpack for no reason if we already had.
Sources/Integration/VMTests.swift
Outdated
} | ||
print("MODIFY event succeeded") | ||
|
||
// Wait for events to be processed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove all the debug prints before merging.
import NIOCore | ||
import NIOPosix | ||
|
||
extension Application { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this command work? What commands should I use to test it locally?
Sources/Integration/VMTests.swift
Outdated
print("All FSNotify events tested successfully") | ||
} | ||
|
||
private func createFSNotifyTestDirectory() throws -> URL { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this, or could we instead use createMountDirectory since we're only working with one file? If we do need to create a subdirectory we could do it as part of the test.
Sources/Integration/VMTests.swift
Outdated
print("=== End Container Output ===") | ||
|
||
// Verify that inotify detected the modify event | ||
guard inotifyOutput.contains("change") && inotifyOutput.contains("existing.txt") else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that this should below the delete event, and rather than checking for the presence of any change
and any existing.txt
, we should be ensuring that the actual output exactly matches the expected output.
private func readResponseFromChild(socket: Int32) throws -> (UInt32, UInt8)? { | ||
// Read event_id:4 | ||
var eventID: UInt32 = 0 | ||
guard read(socket, &eventID, 4) == 4 else { return nil } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a mechanism to get notified of when we have events to read so it seems we're just doing blocking read(2)'s on a swift concurrency thread. For low core count guests this will likely cause us to hang 😅. You're not supposed to block any of the swift concurrency threads, as they're always supposed to be making forward progress or hitting some suspension point. We could use nio for these socket reads and that'd at least get us into shape
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get the concurrency working so that it's not @unchecked
. See Danny's suggestion about using NIO for the socket communication.
private var namespaceWorker: NamespaceWorker? | ||
|
||
/// Worker child process that runs in container's namespace for filesystem operations | ||
private final class NamespaceWorker: @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few things here:
- Is there a reason for nesting this class? It's adding about 350 lines to ManagedContainer, which makes it harder for someone unfamiliar with the code to reason about either. I can move it outside of ManagedContainer and still compile it, so let's move it into its own file.
- I could see this being called NamespaceWorker if it facilitated any kind of work taking place between the guest agent and namespace, which could be a useful abstraction later on. For now let's change the name to describe specifically the work being done.
- We need to manage the concurrency between guest agent, worker parent, and worker child such that
@unchecked
isn't needed for Sendable. In particular, we can't do blocking socketread()
calls in Task context.
throw ContainerizationError(.internalError, message: "Child process failed to start") | ||
} | ||
|
||
if signal == 0xFF { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extract constants for these and then you can get rid of the comments.
Also, let's not call it a "signal" as that has a specific meaning that is not this.
} | ||
|
||
// Start response reader task | ||
self.responseReaderTask = Task { [weak self] in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is no different from:
self.responseReaderTask = Task {
await self.readChildResponses()
}
…reading restrictions
11e1720
to
eb651c5
Compare
import Glibc | ||
#endif | ||
|
||
final class FilesystemEventWorker: @unchecked Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should try and make this actually sendable. Just shove all the mutable state in a Mutex (we can reuse the one we have for pendingEvents, but just make a struct with all of this data on it, and call the field state or something similar.
let parentSocket = sockets[0] | ||
let childSocket = sockets[1] | ||
|
||
let pid = fork() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm skeptical this will work alright. Here's a decent article on some of the pitfalls https://thorstenball.com/blog/2014/10/13/why-threads-cant-fork/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd ideally want to exec a new process that we pass the other half of the socket pair to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(the new process does not physically need to be a new binary, we can just edit vminitd to have a fs-notify subcommand and fork+exec ourselves)
Addresses apple/container#141, where containers don't receive filesystem events on mounted volumes, preventing incremental rebuilds and other file-watching features. This PR implements the guest-side components for FSNotify. Host-side implementation in the container repo will complete the pipeline.
Summary:
cctl fsnotify
) and integration test infrastructure