-
Notifications
You must be signed in to change notification settings - Fork 22
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
Add basic support for server-side #4
base: master
Are you sure you want to change the base?
Conversation
let keycode = try!(reader.read_u32::<BigEndian>()); | ||
Ok(C2S::ExtendedKeyEvent { down: down, keysym: keysym, keycode: keycode }) | ||
} | ||
_ => Err(Error::Unexpected("server to client QEMU submessage type")) |
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.
is it QEMU specifically?
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, according to http://vncdotool.readthedocs.io/en/latest/rfbproto.html#qemu-extended-key-event-message and some history background here: https://www.berrange.com/tags/key-codes/
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.
My god, what a disaster.
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's worth adding a comment indicating the source for the extended message formats.
This is a very high quality PR, thanks! I think there are a few improvements that could be done to it, though:
|
Yeah, I don't really like that too. I'll check the client side. |
Pushed new proposition. It's just a sketch. Places with XXX have to be finished. If you like the new approach I will finish this and add unit tests to be sure it works because I don't use these encodings in my project. |
And adding ExtendedKeyEvent on the client side is more complex. Client has to send its supported pseudo-encodings. Then server replies if it supports this encoding and extensions may be used only if server confirmed it supports 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.
A few issues with this kind of API, plus there's a bigger one I'll write up separately.
src/server.rs
Outdated
if expected_num_bytes == pixel_data.len() { | ||
Ok(()) | ||
} else { | ||
let msg = format!("Expected data length for rectangle {:?} \ |
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 just be a panic, no need to have a separate error type for a trivially avoidable contract violation.
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.
Slightly disagree. But actually ok.
src/server.rs
Outdated
@@ -101,9 +99,93 @@ pub enum Event { | |||
}, | |||
} | |||
|
|||
/// Data structure containing data to be sent by server in messages containing rectangles. | |||
#[derive(Debug)] | |||
pub enum Rectangle<'a> { |
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 more fitting name for this would be an Update
, since half of the enum variants have nothing to do with rectangles at all.
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.
Agree
src/server.rs
Outdated
} | ||
} | ||
|
||
fn write_rect_to<W: Write>(rect: &Rect, writer: &mut W) -> Result<()> { |
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 definitely be a part of an impl on Rect, not here.
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.
Agree
src/server.rs
Outdated
Ok((Server { stream: stream }, client_init.shared)) | ||
Ok((Server { | ||
stream: stream, | ||
bytes_per_pixel: (pixel_format.bits_per_pixel as u16 + 7) / 8, |
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.
Maybe factor out the conversion into an impl on PixelFormat?
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.
Agree
src/server.rs
Outdated
Ok(()) | ||
} | ||
/// This is all-or-nothing method. Rectangles will be sent only if all rectangles are valid. | ||
pub fn send_framebuffer_update(&mut self, rectangles: &[Rectangle]) -> Result<()> { |
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.
send_update(&mut self, updates: &[Update])
, as per comment above
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.
Agree
src/server.rs
Outdated
pub fn send_raw_data(&mut self, data: &[u8]) -> Result<()> { | ||
try!(self.stream.write_all(data)); | ||
/// Sends `FramebufferUpdate` message without checking validity of its contents. | ||
pub fn send_framebuffer_update_unchecked(&mut self, rectangles: &[Rectangle]) -> Result<()> { |
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 need this? The check is not expensive at all, especially compared to cost of I/O. This just clutters the API.
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, ok. Agree.
src/server.rs
Outdated
encoding: encoding, | ||
}.write_to(&mut self.stream)); | ||
// Send the message | ||
let count = rectangles.len() as u16; |
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.
Use a checked conversion.
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.
Agree
So, this Add a builder for framebuffer updates. This will negate the need for a check (you could keep the API contract by hiding the internals), it will be possible to e.g. just call a method and have rust-vnc decide if raw pixels or ZRLE are the correct way to compress, or do any other kind of state tracking. So the API should be used something like the following: let update = server::FramebufferUpdate::new();
update.add_raw_pixels(rect, pixel_data);
update.add_zrle_pixels(rect, pixel_data, compression_level); // same pixel data! rust-vnc does the compression
update.add_set_cursor(...);
server.send_framebuffer_update(&update); |
Ah, okay then, disregard it for this PR. |
Yes, builder will be much better. But I don't see why rust-vnc would ever want to decide if data should be compressed or track any state. It should be up to high level logic in server to do that. |
I think most servers want to just throw pixels at the underlying library and expect it to do something sensible. But even barring that, right now, the client API is misuse-resistant--you can't violate the wire protocol with it. I think having the server API be misuse-resistant would be great as well--so for example it ought to track which encodings the client understands, and:
This is what you're going to have to do in every server (to a larger or smaller degree), why not write the code once? |
New version pushed. I wanted to add tests for checking interoperability of
Any better solutions? Define |
I don't think it's troublesome in any way. |
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.
Okay, this is almost it! Just a few minor issues left.
src/lib.rs
Outdated
} | ||
} | ||
|
||
/// Writes `Rect` to given stream. |
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.
Also factor out reading a Rect
, perhaps, for consistency.
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.
With Message
trait? This is not a message. Without? That method would be unused generating warning.
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 already use the Message trait for parts of messages as well: https://github.com/whitequark/rust-vnc/blob/master/src/protocol.rs#L486-L514
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.
Ok, I'll move to to protocol and reexport in lib.
src/lib.rs
Outdated
} | ||
|
||
/// Constructs new zero-sized `Rect` placed at (0, 0). | ||
pub fn new_empty() -> Self { |
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.
Why isn't this a constant?
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.
Why should it be? It is jet another way of construction just like new
. Compiler should optimize it so in binary code it will be just in-place initialization.
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.
Because if it's a constant then you can use it in other constants. Why have methods that always return the same value by design?
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.
Ok, both approaches have small pros and cons.
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 the pros of new_empty()
? They should result in exact same machine code...
src/protocol.rs
Outdated
@@ -237,6 +237,40 @@ pub struct PixelFormat { | |||
pub blue_shift: u8, | |||
} | |||
|
|||
impl PixelFormat { | |||
/// Creates RGB pixel format with 4 bytes per pixel and 3 bytes of depth. | |||
pub fn new_rgb8888() -> Self { |
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.
These two also should be constants.
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 same argument as in Rect::new_empty()
.
@@ -513,17 +547,17 @@ impl Message for C2S { | |||
} | |||
|
|||
#[derive(Debug)] | |||
pub struct Rectangle { | |||
pub struct RectangleHeader { |
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 renaming is no longer necessary as we have an Update
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.
But it really is rectangle header. That's how RFC calls this.
src/server.rs
Outdated
zlib_data: &'a [u8], | ||
}, | ||
SetCursor { | ||
size: (u16, u16), |
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 use separate fields for x/y and width/height for consistency.
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.
Ok. I copied it from Client. Maybe it should be changed there too.
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.
Ok, this can be done after merging the PR. No big deal.
src/server.rs
Outdated
/// Checks if all updates are valid. | ||
/// | ||
/// Panics if any of the updates is not valid. | ||
fn check(&self, validation_data: &ValidationData) { |
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 no no, this should not be necessary at all! Pass the PixelFormat into a FramebufferUpdate (e.g. by creating the FramebufferUpdate through a method on the Server) and that gets rid of both the strange ValidationData struct as well as separate 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.
Yes, I thought about creating FramebufferUpdate through a method on the Server. But I'd keep ValidationData. As you said rust-vnc may in future want to prevent sending encodings which client does not support. PixelFormat will be then insufficient and for that I introduced ValidationData.
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 can just take a reference to the Server (you want to prevent Server from being modified anyway since that could desync ValidationData from the actual server state).
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.
Actually I can't borrow anything from server
{
let mut update = server.create_update();
------ immutable borrow occurs here
update.add_raw_pixels(rect, data);
server.send_update(&update);
^^^^^^ mutable borrow occurs here
}
- immutable borrow ends here
To avoid exposing Update
I would have to add another intermediate struct FrambufferUpdateBuilder
and use it like this:
{
let update = {
let mut builder = server.create_update();
builder.add_raw_pixels(rect, data);
builder.done()
};
server.send_update(&update);
}
src/server.rs
Outdated
/// Serializes this structure and sends it using given `writer`. | ||
fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> { | ||
for chunk in self.updates.chunks(u16::max_value() as usize) { | ||
let count = chunk.len() as u16; |
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.
You still aren't checking that there aren't more than 2^16 chunks.
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.
Why would I? Every chunk is now sent in separate message. There's no limit for number of messages. Alternative would be to limit self.update to u16::MAX updates in add_* methods.
Ah. Actually, that wouldn't work because that would still let you to send an update that's not valid in the current state of the server. Let's just use a closure: server.send_update(|update| {
update.add_pixels(...)
}); |
How is the |
It creates a FramebufferUpdate internally and passes a &mut FramebufferUpdate into the closure. This means that a FramebufferUpdate is never desynced with the actual server state. |
fwiw, I have updated the series https://github.com/elmarco/rust-vnc/tree/server |
This is basic support for server-side of the protocol.
I tried to make it similar to client side but could not understand the need for extra thread. At least on server side I think it is not necessary.
Plus: support for ExtendedKeyEvent extension.