-
-
Notifications
You must be signed in to change notification settings - Fork 119
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
niri_ipc::NiriSocket
; niri msg version
; version checking on IPC
#278
Conversation
I wonder if we actually want this, at least currently? I'm not sure how much it brings over connecting twice, and it does add all this code to keep the socket open and be able to parse many messages at once, all with extra error conditions that are rare (like, it's easy to get an error when opening a socket and sending one message, but it's harder to get an error in-between messages). Plus, this cannot be used with the actual event streaming, because then there's no way to tell whether a response from a server is the response to the request you just did, or just another event that got streamed in. LSPs pass explicit request ID to get around this problem, but this is even more complexity, all for seemingly little benefit in our case, when the client can just connect an extra time.
This is good.
I am a bit on the fence on this one, because it's adding a whole extra roundtrip to every single invocation of |
In my use, this edge case is not infrequent. I usually do not restart niri often, even though the binary does update on my system. I find it nice to have a heads up, i suppose. I didn't think too much of the impact of adding another request round-trip. One motivation for that specific implementation is to be "backwards compatible" with the previous versions of niri, without radically changing the protocol of those messages. This shouldn't be an issue, really, as we don't actually guarantee any ipc compatibility across commits (to my knowledge), but i personally dislike it quite a lot when a Another way to do this, which would break the current protocol (but who cares? it'd work in the future), is to move the actual response/error into a field of a real "reply" object, where the compositor sends back It is also important to note that the Personally, i find value in having
Sure it can! First of all, we might not want to send events by default, we might have some request to receive them. That makes it backwards compatible by default. A refactor of the API might be needed to make it "nicer" (i.e. separating events from responses. see above suggestion of response trait), but even without radically changing the interface, the "event stream" should simply correspond to a While implementing some request ID could be necessary if we want to make all requests asynchronous; (i.e. handled in parallel and can be fulfilled in any order), currently we do not do that. they are sequential; one is read at a time, and fully handled, and responded to, before we even try reading another one. I believe this is how HTTP works. If we consider a client that does not know every event and response it may receive, the "buffer events until we find a response" approach is less trivial. of course, we should just ignore unknown variants because an unknown response will never be received (because we can't possibly have requested it), but this is slightly incompatible with the current behaviour of "panic on incorrect response". If we distinguish events and request responses (i.e.
I suppose. But these error conditions do not occur if you use it as previously(?). They are also io errors; all io can fail, duh. doing "more" will obviously incur new failure states. io is inherently unreliable, but i don't see these changes creating any "unexpected" failures. any error between messages also applies to errors during messages. previously, a client can open a socket and never write a newline or close it, for example. then, the socket would stay open in compositor forever. yes, a client can connect twice. but this too adds extra error conditions: the socket may, in an extreme case, actually be connected to a different instance of niri between consecutive calls. |
But what was the last case that using
I was planning to have a new
What I was thinking is something like a
It's one case if you have short-lived one-off requests where every request creates the connection from scratch, and another case when you have a long-lived connection that can fail, because now you need to potentially handle re-establishing the connection in case of a (comparatively rare) error. |
Adding
I guess?
Yeah, so for the
I suppose? But how is it better to force you to re-establish the connection every time? If you want to make a new socket for each connection, sending just one request then closing it, the compositor will handle that identically to before. |
That should not have broken any other functionality though, right? So if you do a version check only on error, then that will work.
Yeah, that was a breaking change, that I intend to try not to do going forward.
I consider both
It's better because it preserves the error surface, whereas multiplexing introduces additional error surface that will also not be very well tested just because the conditions are rarer. |
I've pushed some more changes:
|
src/ipc/client.rs
Outdated
|
||
let mut stream = | ||
UnixStream::connect(socket_path).context("error connecting to {socket_path}")?; | ||
// Default SIGPIPE so that our prints don't panic on stdout closing. |
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 intentionally put this after all IPC had been done and before any prints because I'm not sure how this will affect the IPC writes and reads. I suspect it might prevent read/write errors from going through.
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. How should best to do it then? Handle all IPC logic first, encapsulate the results (including errors) in some variable, then set the signal handler, then do all error printing?
Why do we need it? Is it problematic if the niri msg
process panics upon stdout hangup? If yes, wouldn't it be better to lock stdio and actually handle the err case in our code (rather than relying on an unsafe call to libc)?
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 it because otherwise things like niri msg -j focused-window | jq aaaa
(malformed jq command) will spew a Rust backtrace from niri msg in addition to the jq error.
I am not sure what happens to socket IO when it is set though.
Sidenote, there are discussions in Rust to make this more easily settable (e.g. rust-lang/rust#120832), but they are considering CLI apps that don't do socket IO, from my understanding.
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 think it should be possible in this case to do all the requests before any of the printing, right?
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 is possible, but somewhat annoying, particularly because of the edge case with version checking. In #294, where i've mainly focused on a more "correct" API (which is easier for e.g. scripting use cases that don't need to handle all possible requests and responses like niri msg does), i've also more clearly separated functionality for sending request, handling version checking, and "outputting" the results of these operations. and then, it's quite trivial to insert a call to ignore SIGPIPE
at the appropriate point.
Lines 56 to 63 in f1bfef9
let reply = request.send().context("a communication error occurred")?; | |
// Default SIGPIPE so that our prints don't panic on stdout closing. | |
unsafe { | |
libc::signal(libc::SIGPIPE, libc::SIG_DFL); | |
} | |
match reply { |
src/ipc/server.rs
Outdated
|
||
let buf = serde_json::to_vec(&reply).context("error formatting reply")?; | ||
write.write_all(&buf).await.context("error writing reply")?; | ||
while let Some(line) = lines.next().await { |
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 you mind splitting out the "multiple requests per connection" into a separate PR? I'm not convinced that we need it right now, but I would like the rest of this PR in (version, return-error, version checks).
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 made it no longer handle multiple requests on a connection; most of the inner workings to do so are still present, they're just unused.
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. I think the NiriSocket
struct is good, but I'd further roll back some logic complexity that will only be necessary for multiple requests per connection. I can do it myself if you'd prefer to save on the comments back and forth.
niri_ipc::NiriSocket
; niri msg version
; version checking on IPCniri_ipc::NiriSocket
; niri msg version
; version checking on IPC
niri_ipc::NiriSocket
; niri msg version
; version checking on IPCniri_ipc::NiriSocket
; niri msg version
; version checking on IPC
Adjusted this PR to my liking. |
/// | ||
/// This struct is used to communicate with the niri IPC server. It handles the socket connection | ||
/// and serialization/deserialization of messages. | ||
pub struct NiriSocket { | ||
pub struct Socket { |
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 personally don't like this generic name as much. When use
d in a file, it's easier to confuse with more general I/O operations when skimming (compare to naming of UnixStream
, TcpStream
, UdpSocket
in the std library). It's an unimportant point though; this was an exhaustive list of all the downsides that such a name brings.
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 get that, but I decided to do this because in other niri code I'm already using fully-qualified names for some types like niri_config::Animation
and niri_ipc::Output
(leaving non-qualified names for more frequently occurring types of the same name). So it's consistent this way.
} | ||
} | ||
|
||
return Ok(()); | ||
Some(_) => { |
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 block now handles the Reply::Ok
case for non-Response::Version
. Is that desirable? This wouldn't be caused by an outdated compositor (it would cause a Reply::Err("error parsing request")
), and as such i made it not print any kind of error response. In reality, it should be unreachable since the compositor server Does Not Behave Like 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.
Yeah, it is intentional for the reason that you mentioned. If it ever happens then something weird is going on; doesn't hurt to show the warning.
eprintln!("Running niri compositor has a different version from the niri CLI:"); | ||
eprintln!("Compositor version: {compositor_version}"); | ||
eprintln!("CLI version: {cli_version}"); | ||
eprintln!("Did you forget to restart niri after an update?"); |
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 prefer your phrasing to my original version. "Running niri compositor" is better than "the compositor".
|
||
if let Err(err) = &reply { | ||
warn!("error processing IPC request: {err:?}"); | ||
if !requested_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.
Do we want to handle this error flow differently? It's not really problematic to print it; nobody's gonna spam niri msg request-error
and then complain that the log output contains the same message; conversely, it's reasonable that someone may want to see what it looks like on the compositor side handling an error in which case this check somewhat obscures 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.
Well, no hard opinions here, but in my head this request is aimed much more at the client side, so as far as the server is concerned it shouldn't print a warning.
stream | ||
.read_to_end(&mut buf) | ||
.context("error reading IPC response")?; | ||
let socket = Socket::connect().context("error connecting to the niri socket")?; |
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.
re: naming of niri_ipc::Socket
(what? is there a better mechanism to add suggestions as replies to existing review comments?)
in that case, given that we use qualified niri_ipc::Window
(among others), it should be more consistent across all niri code.
let socket = Socket::connect().context("error connecting to the niri socket")?; | |
let socket = niri_ipc::Socket::connect().context("error connecting to the niri socket")?; |
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.
In this file specifically, since it's under src/ipc/
and doesn't import any of the conflicting types, I am importing and using all niri_ipc
types directly. The only exception is Transform
which seems like an oversight; I'll also import it. (In src/ipc/server.rs
it's clearer to fully qualify, because it's filling info from a Smithay Window
).
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.
Although in general, for other types, i agree with that justification; Qualify them to indicate it's the niri_ipc
version of that type when not in the context of IPC. But, see point about preferring NiriSocket
over Socket
(and i stand by that as it does avoid the need to qualify the name at all): In this context, while it's obvious that we're working with niri_ipc, it's not obvious what Socket
means without viewing the imports or having seen the definition. Qualifying the name here is to disambiguate (for the reader) the concept of a "niri IPC socket" from "a generic type of socket with arbitrary data flowing in each direction" (e.g. they are different kinds of objects in the semantic sense); not to disambiguate the specific variant of "a transform but with serde attributes" from "a transform but the one Smithay likes" (i.e. they are the same thing semantically but distinct only in type systems and specific trait impls that both could just as well do)
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 guess we can always change it later if we need
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.
Interestingly i cannot mark my approval for others' changes on my own pull request? (focused on your commit; not the whole PR overview)
but yeah, looks good to me. concerns are pretty much exclusively with naming of some items or exact behaviour of error printing flow. they don't matter too much and i don't see a problem with merging as-is
implement version checking; streamed IPC streamed IPC will allow multiple requests per connection add nonsense request change inline struct to json macro only check version if request actually fails fix usage of inspect_err (MSRV 1.72.0; stabilized 1.76.0) "nonsense request" -> "return error" oneshot connections
Thanks! |
This does not implement any sort of event stream, but rather implements basic support for the IPC client and server to handle multiple messages on each stream.Previously, the server would ignore any additional events sent to it. This means every IPC request must start a new socket connection.Now, with my changes, single IPC connection can handle arbitrarily many requests, separated by newlines.The client can additionally handle them with arbitrary separation, as it relies on serde_json'sStreamDeserializer
. It cannot be used on the server-side, because it does not support async readers.(no longer part of this PR)
A new API is made available in
niri_ipc
:NiriSocket
. It handles the underlyingUnixStream
and allows a consumer to send multiple requests without closing it inbetween.Request
is a trait, with an associated typeResponse
? That way, the request method could statically return the correct type (rather than an enum).New command
niri msg version
to check the running compositor's versionAll
niri msg
commands will now check the running compositor's version.If it matches, output is identical to before.
If it does not match, additional output is printed upon each
niri msg
invocation, informing the user of the discrepancy so they can understand any potential problem more easily.This does not break existing scripts.
The "Did you forget?" hint is also available for compositors prior to this commit
The human-readable form of
niri msg version
does not duplicate the version information."Did you forget?" can be filtered out just as with json output
JSON output of
niri msg version
does duplicate the information, with rationale going that you may be piping its output into some other command that eventually discards the version numbers or puts it somewhere other than the terminal, but stderr should still be visible to the humanThe exact wording of human-readable output in the screenshots above may vary from the real product.
Additionally, no attempt is made to do anything more than tell the user that the version string mismatches. This is because packagers can1 and will2 and do3 overwrite the
version()
function with their own implementation, under the assumption that the result will only be shown to the user. This PR does not try to fundamentally change this assumption.1: COPR at
yalter/niri/niri
2: Nix at
github:sodiboo/niri-flake
3: AUR at
niri-bin
depends on 1