make getty work

Signed-off-by: Anhad Singh <andypython@protonmail.com>
This commit is contained in:
Anhad Singh
2023-07-07 16:51:27 +10:00
parent 2cfd62d194
commit 5c4e7b36fa
10 changed files with 375 additions and 111 deletions
Generated
+4
View File
@@ -680,6 +680,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"log",
"orbclient",
"redox-daemon",
"redox-log",
"redox_syscall 0.3.5",
@@ -1921,6 +1922,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
name = "vesad"
version = "0.1.0"
dependencies = [
"inputd",
"orbclient",
"ransid",
"redox-daemon",
@@ -1968,12 +1970,14 @@ version = "0.1.0"
dependencies = [
"anyhow",
"futures",
"inputd",
"log",
"orbclient",
"paste",
"pcid",
"redox-daemon",
"redox_syscall 0.3.5",
"spin",
"static_assertions",
"virtio-core",
]
+1
View File
@@ -10,3 +10,4 @@ log = "0.4.19"
redox-daemon = "0.1.0"
redox-log = "0.1.1"
redox_syscall = "0.3.5"
orbclient = "0.3.27"
+25
View File
@@ -0,0 +1,25 @@
use std::fs::File;
use std::io::{Error, Read};
pub struct Handle(File);
impl Handle {
pub fn new<S: Into<String>>(device_name: S) -> Result<Self, Error> {
let path = format!("input:handle/{}", device_name.into());
Ok(Self(File::open(path)?))
}
// The return value is the display identifier. It will be used to uniquely
// identify the display on activation events.
pub fn register(&mut self) -> Result<usize, Error> {
Ok(dbg!(self.0.read(&mut [])?))
}
}
#[repr(packed)]
pub struct Damage {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
+106 -10
View File
@@ -15,10 +15,11 @@ use std::collections::BTreeMap;
use std::fs::File;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use orbclient::{Event, EventOption};
use syscall::{Error as SysError, EventFlags, Packet, SchemeMut, EINVAL};
#[derive(Debug)]
enum Handle {
Producer,
Consumer {
@@ -26,6 +27,7 @@ enum Handle {
pending: Vec<u8>,
notified: bool,
},
Device(Arc<String>),
}
impl Handle {
@@ -36,27 +38,46 @@ impl Handle {
struct InputScheme {
handles: BTreeMap<usize, Handle>,
next_id: AtomicUsize,
next_vt_id: AtomicUsize,
vts: BTreeMap<usize, Arc<String>>,
super_key: bool,
active_vt: Option<(Arc<String>, File)>,
}
impl InputScheme {
pub fn new() -> Self {
Self {
next_id: AtomicUsize::new(0),
next_vt_id: AtomicUsize::new(1),
handles: BTreeMap::new(),
vts: BTreeMap::new(),
super_key: false,
active_vt: None,
}
}
}
impl SchemeMut for InputScheme {
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> syscall::Result<usize> {
let handle_ty = match path {
let mut path_parts = path.split('/');
let command = path_parts.next().ok_or(SysError::new(EINVAL))?;
let handle_ty = match command {
"producer" => Handle::Producer,
"consumer" => Handle::Consumer {
events: EventFlags::empty(),
pending: Vec::new(),
notified: false,
},
"handle" => {
let value = path_parts.next().ok_or(SysError::new(EINVAL))?;
Handle::Device(Arc::new(value.to_string()))
}
_ => unreachable!("inputd: invalid path {path}"),
};
@@ -72,17 +93,24 @@ impl SchemeMut for InputScheme {
fn read(&mut self, id: usize, buf: &mut [u8]) -> syscall::Result<usize> {
let handle = self.handles.get_mut(&id).ok_or(SysError::new(EINVAL))?;
if let Handle::Consumer { pending, .. } = handle {
let copy = core::cmp::min(pending.len(), buf.len());
match handle {
Handle::Consumer { pending, .. } => {
let copy = core::cmp::min(pending.len(), buf.len());
for (i, byte) in pending.drain(..copy).enumerate() {
buf[i] = byte;
for (i, byte) in pending.drain(..copy).enumerate() {
buf[i] = byte;
}
Ok(copy)
}
Ok(copy)
} else {
// A producer cannot read from the channel.
unreachable!()
Handle::Device(device) => {
let vt = self.next_vt_id.fetch_add(1, Ordering::SeqCst);
self.vts.insert(vt, device.clone());
Ok(vt)
}
_ => unreachable!(),
}
}
@@ -91,6 +119,74 @@ impl SchemeMut for InputScheme {
return Ok(1);
}
let events = unsafe {
core::slice::from_raw_parts(
buf.as_ptr() as *const Event,
buf.len() / core::mem::size_of::<Event>(),
)
};
for event in events.iter() {
let mut new_active_opt = None;
match event.to_option() {
EventOption::Key(key_event) => match key_event.scancode {
f @ 0x3B..=0x44 if self.super_key => {
// F1 through F10
new_active_opt = Some((f - 0x3A) as usize);
}
0x57 if self.super_key => {
// F11
new_active_opt = Some(11);
}
0x58 if self.super_key => {
// F12
new_active_opt = Some(12);
}
0x5B => {
// Super
self.super_key = key_event.pressed;
}
_ => (),
},
_ => continue,
}
if let Some(new_active) = new_active_opt {
if let Some(vt) = self.vts.get(&new_active) {
// Result: display/virtio-gpu:3/acvtivate
let activate = format!("display/{vt}:{new_active}/activate");
log::info!("inputd: switching to VT #{new_active} ({activate})");
let file = File::open(&activate).unwrap();
// Drop the old active VT first.
//
// TODO(andypython): This can be drastically improved by introducting something
// like ioctl() to the display scheme.
for (vt, device) in self.vts.iter() {
if vt == &new_active {
continue;
}
let deactivate = format!("display/{device}:{}/deactivate", vt);
let _ = File::open(&deactivate);
if device.contains("virtio") {
break;
}
}
self.active_vt = Some((vt.clone(), file));
} else {
log::warn!("inputd: switch to non-existent VT #{new_active} was requested");
}
}
}
let handle = self.handles.get_mut(&id).ok_or(SysError::new(EINVAL))?;
assert!(handle.is_producer());
+1
View File
@@ -9,6 +9,7 @@ ransid = "0.4"
rusttype = { version = "0.2", optional = true }
redox_syscall = "0.3"
redox-daemon = "0.1"
inputd = { path = "../inputd" }
[features]
default = []
+11 -5
View File
@@ -38,10 +38,13 @@ pub struct DisplayScheme {
next_id: usize,
pub handles: BTreeMap<usize, Handle>,
super_key: bool,
inputd_handle: inputd::Handle,
}
impl DisplayScheme {
pub fn new(mut framebuffers: Vec<FrameBuffer>, spec: &[bool]) -> DisplayScheme {
let mut inputd_handle = inputd::Handle::new("vesa").unwrap();
let mut onscreens = Vec::new();
for fb in framebuffers.iter_mut() {
onscreens.push(unsafe {
@@ -51,7 +54,6 @@ impl DisplayScheme {
let mut vts = BTreeMap::<VtIndex, BTreeMap<ScreenIndex, Box<dyn Screen>>>::new();
let mut vt_i = 1;
for &vt_type in spec.iter() {
let mut screens = BTreeMap::<ScreenIndex, Box<dyn Screen>>::new();
for fb_i in 0..framebuffers.len() {
@@ -62,8 +64,7 @@ impl DisplayScheme {
Box::new(TextScreen::new(Display::new(fb.width, fb.height)))
});
}
vts.insert(VtIndex(vt_i), screens);
vt_i += 1;
vts.insert(VtIndex(inputd_handle.register().unwrap()), screens);
}
DisplayScheme {
@@ -74,6 +75,7 @@ impl DisplayScheme {
next_id: 0,
handles: BTreeMap::new(),
super_key: false,
inputd_handle
}
}
@@ -168,11 +170,15 @@ impl SchemeMut for DisplayScheme {
let screen_i = ScreenIndex(
vt_screen.next().unwrap_or("").parse::<usize>().unwrap_or(0)
);
if let Some(screens) = self.vts.get(&vt_i) {
if screens.contains_key(&screen_i) {
if let Some(screens) = self.vts.get_mut(&vt_i) {
if let Some(screen) = screens.get_mut(&screen_i) {
for cmd in parts {
if cmd == "activate" {
self.active = vt_i;
screen.redraw(
self.onscreens[screen_i.0],
self.framebuffers[screen_i.0].stride
);
}
}
+5
View File
@@ -380,6 +380,11 @@ impl<'a> StandardTransport<'a> {
})
}
pub fn reset(&self) {
let mut common = self.common.lock().unwrap();
common.device_status.set(DeviceStatusFlags::empty());
}
pub fn check_device_feature(&self, feature: u32) -> bool {
let mut common = self.common.lock().unwrap();
+2
View File
@@ -13,7 +13,9 @@ paste = "1.0.13"
virtio-core = { path = "../virtio-core" }
pcid = { path = "../pcid" }
inputd = { path = "../inputd" }
redox-daemon = "0.1"
redox_syscall = "0.3"
orbclient = "0.3.27"
spin = "0.9.8"
+71 -30
View File
@@ -22,6 +22,7 @@
#![feature(int_roundings)]
use std::cell::UnsafeCell;
use std::fs::File;
use std::io::{Read, Write};
@@ -144,6 +145,15 @@ pub struct ControlHeader {
padding: [u8; 3],
}
impl ControlHeader {
pub fn with_ty(ty: CommandTy) -> Self {
Self {
ty: VolatileCell::new(ty),
..Default::default()
}
}
}
impl Default for ControlHeader {
fn default() -> Self {
Self {
@@ -157,44 +167,41 @@ impl Default for ControlHeader {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
#[repr(C)]
pub struct GpuRect {
pub x: VolatileCell<u32>,
pub y: VolatileCell<u32>,
pub width: VolatileCell<u32>,
pub height: VolatileCell<u32>,
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl GpuRect {
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x: VolatileCell::new(x),
y: VolatileCell::new(y),
width: VolatileCell::new(width),
height: VolatileCell::new(height),
x,
y,
width,
height,
}
}
#[inline]
pub fn width(&self) -> u32 {
self.width.get()
}
#[inline]
pub fn height(&self) -> u32 {
self.height.get()
}
}
#[derive(Debug)]
#[repr(C)]
pub struct DisplayInfo {
pub rect: GpuRect,
rect: UnsafeCell<GpuRect>,
pub enabled: VolatileCell<u32>,
pub flags: VolatileCell<u32>,
}
impl DisplayInfo {
pub fn rect(&self) -> &GpuRect {
// SAFETY: We never give out mutable references to `self.rect`.
unsafe { &*self.rect.get() }
}
}
#[derive(Debug)]
#[repr(C)]
pub struct GetDisplayInfo {
@@ -274,16 +281,31 @@ pub struct AttachBacking {
impl AttachBacking {
pub fn new(resource_id: u32, num_entries: u32) -> Self {
Self {
header: ControlHeader {
ty: VolatileCell::new(CommandTy::ResourceAttachBacking),
..Default::default()
},
header: ControlHeader::with_ty(CommandTy::ResourceAttachBacking),
resource_id,
num_entries,
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct DetachBacking {
pub header: ControlHeader,
pub resource_id: u32,
pub padding: u32,
}
impl DetachBacking {
pub fn new(resource_id: u32) -> Self {
Self {
header: ControlHeader::with_ty(CommandTy::ResourceDetachBacking),
resource_id,
padding: 0,
}
}
}
#[derive(Debug)]
#[repr(C)]
pub struct ResourceFlush {
@@ -296,11 +318,7 @@ pub struct ResourceFlush {
impl ResourceFlush {
pub fn new(resource_id: u32, rect: GpuRect) -> Self {
Self {
header: ControlHeader {
ty: VolatileCell::new(CommandTy::ResourceFlush),
..Default::default()
},
header: ControlHeader::with_ty(CommandTy::ResourceFlush),
rect,
resource_id,
padding: 0,
@@ -308,6 +326,24 @@ impl ResourceFlush {
}
}
#[derive(Debug)]
#[repr(C)]
pub struct ResourceUnref {
pub header: ControlHeader,
pub resource_id: u32,
pub padding: u32,
}
impl ResourceUnref {
pub fn new(resource_id: u32) -> Self {
Self {
header: ControlHeader::with_ty(CommandTy::ResourceUnref),
resource_id,
padding: 0,
}
}
}
#[repr(C)]
#[derive(Debug)]
pub struct SetScanout {
@@ -389,7 +425,12 @@ fn deamon(deamon: redox_daemon::Daemon) -> anyhow::Result<()> {
deamon.ready().unwrap();
let mut socket_file = File::create(":display/virtio-gpu")?;
let mut scheme = scheme::Scheme::new(config, control_queue.clone(), cursor_queue.clone())?;
let mut scheme = futures::executor::block_on(scheme::Scheme::new(
config,
control_queue.clone(),
cursor_queue.clone(),
device.transport.clone(),
))?;
loop {
let mut packet = Packet::default();
+149 -66
View File
@@ -1,23 +1,38 @@
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use inputd::Damage;
use syscall::{Dma, Error as SysError, SchemeMut, EINVAL};
use virtio_core::spec::{Buffer, ChainBuilder, DescriptorFlags};
use virtio_core::transport::{Error, Queue};
use virtio_core::transport::{Error, Queue, StandardTransport};
use virtio_core::utils::VolatileCell;
use crate::*;
static RESOURCE_ALLOC: AtomicU32 = AtomicU32::new(1); // XXX: 0 is reserved for whatever that takes `resource_id`.
impl Into<GpuRect> for &Damage {
fn into(self) -> GpuRect {
GpuRect {
x: self.x as u32,
y: self.y as u32,
width: self.width as u32,
height: self.height as u32,
}
}
}
pub struct Display<'a> {
control_queue: Arc<Queue<'a>>,
cursor_queue: Arc<Queue<'a>>,
transport: Arc<StandardTransport<'a>>,
mapped: Option<usize>,
// TODO(andypython): Remove the need for the spin crate after the `once_cell`
// API is stabilized.
mapped: spin::Once<usize>,
width: u32,
height: u32,
@@ -30,24 +45,25 @@ impl<'a> Display<'a> {
pub fn new(
control_queue: Arc<Queue<'a>>,
cursor_queue: Arc<Queue<'a>>,
_display_info: &mut DisplayInfo,
transport: Arc<StandardTransport<'a>>,
id: usize,
) -> Self {
Self {
control_queue,
cursor_queue,
mapped: None,
mapped: spin::Once::new(),
width: 1920,
height: 1080,
transport,
id,
resource_id: RESOURCE_ALLOC.fetch_add(1, Ordering::SeqCst),
}
}
async fn get_fpath(&mut self, buffer: &mut [u8]) -> Result<usize, Error> {
async fn get_fpath(&self, buffer: &mut [u8]) -> Result<usize, Error> {
let path = format!("display/virtio-gpu:3.0/{}/{}", self.width, self.height);
// Copy the path into the target buffer.
@@ -55,7 +71,7 @@ impl<'a> Display<'a> {
Ok(path.len())
}
async fn send_request<T>(&mut self, request: Dma<T>) -> Result<Dma<ControlHeader>, Error> {
async fn send_request<T>(&self, request: Dma<T>) -> Result<Dma<ControlHeader>, Error> {
let header = Dma::new(ControlHeader::default())?;
let command = ChainBuilder::new()
.chain(Buffer::new(&request))
@@ -66,15 +82,15 @@ impl<'a> Display<'a> {
Ok(header)
}
async fn flush_resource(&mut self, flush: ResourceFlush) -> Result<(), Error> {
async fn flush_resource(&self, flush: ResourceFlush) -> Result<(), Error> {
let header = self.send_request(Dma::new(flush)?).await?;
assert_eq!(header.ty.get(), CommandTy::RespOkNodata);
Ok(())
}
async fn map_screen(&mut self, offset: usize) -> Result<usize, Error> {
if let Some(mapped) = self.mapped {
async fn map_screen(&self, offset: usize) -> Result<usize, Error> {
if let Some(mapped) = self.mapped.get() {
return Ok(mapped + offset);
}
@@ -124,74 +140,128 @@ impl<'a> Display<'a> {
self.control_queue.send(command).await;
assert_eq!(header.ty.get(), CommandTy::RespOkNodata);
self.flush().await?;
self.mapped = Some(mapped);
Ok(mapped + offset)
}
async fn flush(&mut self) -> Result<(), Error> {
let rect = GpuRect::new(0, 0, self.width, self.height);
let scanout_request = Dma::new(SetScanout::new(self.id as u32, self.resource_id, rect))?;
let scanout_request = Dma::new(SetScanout::new(
self.id as u32,
self.resource_id,
GpuRect::new(0, 0, self.width, self.height),
))?;
let header = self.send_request(scanout_request).await?;
assert_eq!(header.ty.get(), CommandTy::RespOkNodata);
let rect = GpuRect::new(0, 0, self.width, self.height);
let req = Dma::new(XferToHost2d::new(self.resource_id, rect))?;
self.flush(None).await?;
self.mapped.call_once(|| mapped);
Ok(mapped + offset)
}
/// If `damage` is `None`, the entire screen is flushed.
async fn flush(&self, damage: Option<&Damage>) -> Result<(), Error> {
let damage = if let Some(damage) = damage {
damage.into()
} else {
GpuRect {
x: 0,
y: 0,
width: self.width,
height: self.height,
}
};
let req = Dma::new(XferToHost2d::new(
self.resource_id,
GpuRect {
x: 0,
y: 0,
width: self.width,
height: self.height,
},
))?;
let header = self.send_request(req).await?;
assert_eq!(header.ty.get(), CommandTy::RespOkNodata);
let rect = GpuRect::new(0, 0, self.width, self.height);
self.flush_resource(ResourceFlush::new(self.resource_id, rect))
self.flush_resource(ResourceFlush::new(self.resource_id, damage.clone()))
.await?;
Ok(())
}
/// This detaches any backing pages from the display and unrefs the resource. Also resets the
/// device, which is required to go back to legacy mode.
async fn detach(&self) -> Result<(), Error> {
let request = Dma::new(DetachBacking::new(self.resource_id))?;
let header = self.send_request(request).await?;
assert_eq!(header.ty.get(), CommandTy::RespOkNodata);
let request = Dma::new(ResourceUnref::new(self.resource_id))?;
let header = self.send_request(request).await?;
assert_eq!(header.ty.get(), CommandTy::RespOkNodata);
// Go back to legacy mode.
self.transport.reset();
Ok(())
}
}
pub struct Scheme<'a> {
control_queue: Arc<Queue<'a>>,
cursor_queue: Arc<Queue<'a>>,
config: &'a mut GpuConfig,
/// File descriptor allocator.
next_id: AtomicUsize,
handles: BTreeMap<usize /* file descriptor */, Display<'a>>,
handles: BTreeMap<usize /* file descriptor */, Arc<Display<'a>>>,
inputd_handle: inputd::Handle,
displays: Vec<Arc<Display<'a>>>,
}
impl<'a> Scheme<'a> {
pub fn new(
pub async fn new(
config: &'a mut GpuConfig,
control_queue: Arc<Queue<'a>>,
cursor_queue: Arc<Queue<'a>>,
) -> Result<Self, Error> {
Ok(Self {
control_queue,
cursor_queue,
transport: Arc<StandardTransport<'a>>,
) -> Result<Scheme<'a>, Error> {
let displays = Self::probe(
control_queue.clone(),
cursor_queue.clone(),
transport.clone(),
config,
next_id: AtomicUsize::new(0),
)
.await?;
Ok(Self {
handles: BTreeMap::new(),
inputd_handle: inputd::Handle::new("virtio-gpu").unwrap(),
displays,
})
}
async fn open_display(&self, id: usize) -> Result<Display<'a>, Error> {
let mut display_info = self.get_display_info().await?;
let displays = &mut display_info.display_info[..self.config.num_scanouts() as usize];
async fn probe(
control_queue: Arc<Queue<'a>>,
cursor_queue: Arc<Queue<'a>>,
transport: Arc<StandardTransport<'a>>,
config: &GpuConfig,
) -> Result<Vec<Arc<Display<'a>>>, Error> {
let mut display_info = Self::get_display_info(control_queue.clone()).await?;
let displays = &mut display_info.display_info[..config.num_scanouts() as usize];
let display = displays.get_mut(id).ok_or(SysError::new(syscall::ENOENT))?;
let mut result = vec![];
log::info!(
"virtio-gpu: opening display ({}x{}px)",
display.rect.width(),
display.rect.height()
);
for (id, info) in displays.iter().enumerate() {
log::info!(
"virtio-gpu: opening display ({}x{}px)",
info.rect().width,
info.rect().height
);
Ok(Display::new(
self.control_queue.clone(),
self.cursor_queue.clone(),
display,
id,
))
let display = Display::new(
control_queue.clone(),
cursor_queue.clone(),
transport.clone(),
id,
);
result.push(Arc::new(display));
}
Ok(result)
}
async fn get_display_info(&self) -> Result<Dma<GetDisplayInfo>, Error> {
async fn get_display_info(control_queue: Arc<Queue<'a>>) -> Result<Dma<GetDisplayInfo>, Error> {
let header = Dma::new(ControlHeader {
ty: VolatileCell::new(CommandTy::GetDisplayInfo),
..Default::default()
@@ -203,7 +273,7 @@ impl<'a> Scheme<'a> {
.chain(Buffer::new(&response).flags(DescriptorFlags::WRITE_ONLY))
.build();
self.control_queue.send(command).await;
control_queue.send(command).await;
assert!(response.header.ty.get() == CommandTy::RespOkDisplayInfo);
Ok(response)
@@ -217,16 +287,30 @@ impl<'a> SchemeMut for Scheme<'a> {
let mut parts = path.split('/');
let mut screen = parts.next().unwrap_or("").split('.');
let vt_index = screen.next().unwrap_or("").parse::<usize>().unwrap_or(1);
static YES: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
if YES.load(Ordering::SeqCst) {
YES.store(false, Ordering::SeqCst);
}
let fd = screen.next().unwrap_or("").parse::<usize>().unwrap();
if path.contains("deactivate") {
log::info!("virtio-gpu: deactivating display at file #{}", fd);
let handle = self.handles.get(&fd).unwrap();
futures::executor::block_on(handle.detach()).unwrap();
return Ok(0);
}
let id = screen.next().unwrap_or("").parse::<usize>().unwrap_or(0);
dbg!(&vt_index, &id);
dbg!(&id);
let fd = self.next_id.fetch_add(1, Ordering::SeqCst);
let display = futures::executor::block_on(self.open_display(id))
.map_err(|_| SysError::new(syscall::ENOENT))?;
let display = self.displays.get(id).ok_or(SysError::new(EINVAL))?;
self.handles.insert(fd, display);
let fd = self.inputd_handle.register().unwrap();
self.handles.insert(fd, display.clone());
Ok(fd)
}
@@ -269,7 +353,7 @@ impl<'a> SchemeMut for Scheme<'a> {
fn fsync(&mut self, id: usize) -> syscall::Result<usize> {
let handle = self.handles.get_mut(&id).ok_or(SysError::new(EINVAL))?;
futures::executor::block_on(handle.flush()).unwrap();
futures::executor::block_on(handle.flush(None)).unwrap();
Ok(0)
}
@@ -281,17 +365,16 @@ impl<'a> SchemeMut for Scheme<'a> {
fn write(&mut self, id: usize, buf: &[u8]) -> syscall::Result<usize> {
let handle = self.handles.get_mut(&id).ok_or(SysError::new(EINVAL))?;
let damages = unsafe {
core::slice::from_raw_parts(
buf.as_ptr() as *const Damage,
buf.len() / core::mem::size_of::<Damage>(),
)
};
// SAFETY: lmao
unsafe {
core::ptr::copy_nonoverlapping(
buf.as_ptr(),
handle.mapped.unwrap() as *mut u8,
buf.len(),
);
for damage in damages {
futures::executor::block_on(handle.flush(Some(damage))).unwrap();
}
futures::executor::block_on(handle.flush()).unwrap();
Ok(buf.len())
}