-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Support for D-Bus file descriptors #5992
Support for D-Bus file descriptors #5992
Conversation
I think this is ready for a first review. I've moved the internal channel logic into I've also added a small test case which opens a pipe and passes the reader to cockpit via a D-Bus message. I've also documented that cockpit can now receive (but not sent) file descriptors over D-Bus. Do you think this warrants an extra section in the docs? Does anyone have ideas for an actual use case that's small enough in scope to implement before 7.4? |
channel.onmessage = function (event, data) { | ||
assert.equal(data, 'Hello, fd'); | ||
channel.close(); | ||
QUnit.start(); |
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 think you are supposed to call QUint.start()
twice in the same test, the always()
handler already does that. Or is that somehow "refcounted" and the extra QUnit.stop()
above makes that necessary? This could do with a comment.
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.
Yes, start/stop are refcounted. We need this here because the done()
path kicks off another async call to wait for the first message. We still need it in always()
in case the dbus call fails. I'll add a comment.
doc/guide/cockpit-dbus.xml
Outdated
<varlistentry> | ||
<term><code>HANDLE 'h'</code></term> | ||
<listitem><para>A javascript object that describes a cockpit channel which represents the | ||
passed file descriptor. Pass it to <link linkend="cockpit-channels-channel"> |
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.
Could this be more specific about how its members look like?
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 left it intentionally opaque, because the channel is openend via an internal id which is useless anyway. I'll add that this is always a stream channel.
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 point about the opacity, so fine as it is.
@@ -66,6 +66,21 @@ typedef struct { | |||
|
|||
G_DEFINE_TYPE (CockpitPipeChannel, cockpit_pipe_channel, COCKPIT_TYPE_CHANNEL); | |||
|
|||
GHashTable *internal_fds; |
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.
How does this simple hash table keep track of which fds are for reading and which for writing? Or do we only support receiving read fds for now?
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.
Based on the discussion below, this comment is moot.
src/base1/test-dbus-common.js
Outdated
@@ -1130,4 +1130,28 @@ function dbus_track_tests(channel_options, bus_name) { | |||
}); | |||
}); | |||
}); | |||
|
|||
QUnit.asyncTest("receive fd", function() { |
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 tests receiving a read fd. It would be good to also have a test for getting a write fd, unless this shouldn't be supported (for now) -- but then that should be documented.
src/bridge/cockpitpipechannel.c
Outdated
@@ -384,6 +407,10 @@ cockpit_pipe_channel_prepare (CockpitChannel *channel) | |||
else | |||
self->pipe = cockpit_pipe_spawn ((const gchar **)argv, (const gchar **)env, dir, flags); | |||
} | |||
else if (internal && lookup_internal_fd (internal, &fd)) | |||
{ | |||
self->pipe = cockpit_pipe_new (internal, fd, fd); |
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 specifies the same fd for both reading and writing, which is error prone and misleading. Unix pipes are unidirectional, and the sender and receiver need to agree which one gets passed and which one the sender keeps. This is related to my question above how we keep track of reading and writing fds.
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.
Hm, good point. Is there any way to know if the fd we get from D-Bus is read- or write-only? The pipe is only for the test, but I guess this could happen in a practical use case as well.
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 only way known to me is to check if fdopen()
with a particular mode returns EINVAL
or succeeds. But I guess I may have misunderstood this -- so you are saying that cockpit itself should not touch/use the fd at all, just pass it between the service and the client? That would actually make a lot of sense, I just got misled when reviewing this.
Do I understand this alright -- with this, when the client opens a cockpit.channel()
on this handle, it's still the client's responsibility to know whether it can read and/or write into this? If so, then we should really have a test case that ensures that cockpit doesn't touch the fds themselves -- i. e. trying to write to a read fd throws a sensible error, and we should have a test case for the write end of a pipe only to ensure that this direction works as well.
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 I understand this alright
Yep, that's exactly it. I'll add the test cases you suggested.
cockpit_pipe_channel_add_internal_fd (gint fd) | ||
{ | ||
/* We are not multi-threaded. Also don't make this look like normal fd numbers */ | ||
static guint64 unique = 911111; |
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 to me that the keys of that GHashTable are mostly arbitrary and opaque, and we don't actually need them for anything? I must say I don't like these magic numbers. Would it be possible to use it as a set (g_hash_table_add()
and g_hash_table_contains()
and store the fds directly as keys, and use the fd as-is in the ID? Or can it happen that an fd gets closed and reused, but the corresponding cockpit channel doesn't get closed? (That would be an internal inconsistency and most likely lead to errors anyway?)
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 need a unique string to refer to the channel from cockpit. Maybe a random string that doesn't look like it could be a fd is even better?
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.
So an fd isn't unique enough? Once a cockpit channel gets closed because the underlying fd closes, a new one must never use the same name again, even if the next passed fd has the same number than the previously closed one?
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.
No, the fd might be closed and reused before the client calls cockpit.channel()
, which would give it the wrong fd.
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.
s/wrong fd/wrong id/ I suppose. Understood, so let's not dwell on this further -- it's just an opaque ID, so we can change it later on if we want.
src/bridge/cockpitpipechannel.c
Outdated
internal_fds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, internal_fd_free); | ||
|
||
id = g_strdup_printf ("file-descriptor-%" G_GUINT64_FORMAT, unique++); | ||
g_hash_table_replace (internal_fds, id, GINT_TO_POINTER (fd)); |
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 comment would be moot when moving to a set, but I think you should use g_assert (g_hash_table_add ())
here -- like this we don't ever expect an ID to be already present.
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'd actually do the assertion here -- as we've discussed above, we do want the IDs to be unique even if the fds get reused, so this prevents falling into a trap in the future when someone "simplifies" the ID housekeeping.
src/bridge/cockpitpipechannel.c
Outdated
{ | ||
g_hash_table_destroy (internal_fds); | ||
internal_fds = NULL; | ||
} |
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 not sure if that's worth the overhead -- if you keep adding and removing fds, you would always construct and delete the hash table. Seems easier and more efficient to just keep it around?
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.
Indeed :)
src/bridge/cockpitdbusjson.c
Outdated
/* And only keep the last N ready for opening channels */ | ||
while (g_queue_get_length (context->fdids) > MAX_RECEIVED_DBUS_FDS) | ||
{ | ||
old = (const gchar *)g_queue_pop_head (context->fdids); |
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 a bit odd to silently kill the oldest fd instead of rejecting to receive a new one and returning an error instead? This feels much harder to handle from clients than checking for an error on sending. At least this dropping should cause a warning somewhere.
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.
Client code can decide to ignore a passed fd. So we have to keep the fd open for a while, so that the cockpit channel can be created if a client does not ignore it. The problem is when to close fds that were ignored. We decided to just keep the last N fds open.
This is why we can't warn about it, because many of the instances are just normal operation. If the limit turns out too low, the client code will get an error when it establishes the channel.
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.
Makes sense, thanks!
g_signal_handler_disconnect (peer->cache, peer->update_sig); | ||
g_object_run_dispose (G_OBJECT (peer->cache)); | ||
g_object_unref (peer->cache); | ||
} |
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 should be a separate commit. (Not important, though)
Update: implemented @martinpitt's suggestions. Thanks a lot! |
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.
Thanks for the updates! Looks fine to me now, and we can change the ID generation to something nicer in the future if we feel like it, as they are opaque. So, 👍 from me, but I won't merge yet as I want to give @stefwalter a chance to review this too.
src/common/mock-service.c
Outdated
|
||
g_assert (condition == G_IO_IN); | ||
|
||
read (fd, buffer, 100); |
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.
semaphore complains here while checking:
src/common/mock-service.c: In function ‘test_fd_pipe_readable’:
src/common/mock-service.c:469:8: error: ignoring return value of ‘read’, declared with attribute warn_unused_result [-Werror=unused-result]
read (fd, buffer, 100);
^
Even in a mock function it's probably good practice to check this.
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.
Thanks for the catch. I've added a similar check like I have for write
. I think we can safely assume that we always get all data at once since this is a pipe and very little data.
I didn't review anything besides the Semaphore error. |
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 main change I would suggest is transferring ownership of the fd (or duping it, if ownership is shared).
src/bridge/cockpitpipechannel.c
Outdated
@@ -384,6 +407,10 @@ cockpit_pipe_channel_prepare (CockpitChannel *channel) | |||
else | |||
self->pipe = cockpit_pipe_spawn ((const gchar **)argv, (const gchar **)env, dir, flags); | |||
} | |||
else if (internal && lookup_internal_fd (internal, &fd)) |
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.
So the following line claims the fd, however lookup_internal_fd doesn't steal it from the hashtable or otherwise transfer ownership.
I imagine that such fd channels are open-once channels, after which the identifier becomes invalid? Or did you imagine them to be able to open many channels to the same fd? This is obviously a corner case, but either way needs to be made consistent.
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 didn't bother because you wrote this
Yes, in theory multiple channels can be opened with the same unique string. This has similar properties to file descriptors used in process.
in bug #4380. Changed it to remove the channel and added a test.
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.
Well it would have been fine if you had dup()'d it ... then the channel could have been opened more than once. But I think this makes sense, just have it be opened once.
src/bridge/cockpitpipechannel.c
Outdated
GHashTable *internal_fds; | ||
|
||
static gboolean | ||
lookup_internal_fd (const gchar *name, gint *fdp) |
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.
Arguments should be on different lines.
src/bridge/cockpitpipechannel.c
Outdated
if (!internal_fds) | ||
internal_fds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, internal_fd_free); | ||
|
||
id = g_strdup_printf ("file-descriptor-%" G_GUINT64_FORMAT, unique++); |
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 name is a bit misleading. For folks debugging this, they'll think that the file descriptor N is being opened. Worth choosing another name like 'internal-stream-XXX' or something like that? If you don't think so, I'm fine with keeping it as it is. Just consider it.
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.
Changed it.
This works by replacing the file descriptor with a channel identifier and then allowing that channel to be opened as an internal stream channel. There is a maximum number of file descriptors that will be held in this state by a dbus client until those file descriptors are used. Fixes #4380
Don't attempt to free 'peer' member if it is unset.
Update: address issues raised by @stefwalter. Thanks! |
This is based on @stefwalter's initial implementation at #4883. I've rebased it, fixed compile errors, and started to implement the missing bits. Still very much work in progress, but I'm opening this pull request to start the discussion again.
This is basic support for file descriptors passed over DBus. Several APIs like to include stuff like this in their APIs.
Fixes #4380