Fix compositor output routing and pointer hints; format imports

This commit is contained in:
Samuel Onoja committed 2026-09-30 06:39:58 +01:00
1 parent 88e4f3c135
commit 46509f880a
108 files changed
+3682 -8371

No files matched your search

+137 -251
View File
@@ -1,32 +1,26 @@
use std::env;
use std::fs;
use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{
Arc,
mpsc::{SyncSender, sync_channel},
};
use std::thread;
use std::sync::mpsc::{SyncSender, sync_channel};
use std::time::Duration;
use std::{env, fs, io, thread};
use ferese_core::LayoutMode;
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
use ferese_layout::Direction;
use serde_json::{Value, json};
use smithay::output::Output;
use smithay::reexports::calloop::{EventLoop, LoopSignal, channel, timer};
use smithay::utils::Transform;
use crate::handlers::screencopy;
use crate::handlers::screenshot::{
Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan,
};
use crate::handlers::screenshot_worker::{self, Encoded, Job, Worker};
use crate::{Ferese, config::OutputTransform};
use smithay::output::Output;
use crate::Ferese;
use crate::config::OutputTransform;
use crate::handlers::screenshot::{Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan};
use crate::handlers::screenshot_worker::{Encoded, Job, Worker};
use crate::handlers::{screencopy, screenshot_worker};
const REQUEST_QUEUE_CAPACITY: usize = 128;
const MAX_CONNECTIONS: usize = 64;
@@ -90,18 +84,14 @@ pub(crate) struct IpcSocketGuard {
impl Drop for IpcSocketGuard {
fn drop(&mut self) {
if self.path.symlink_metadata().is_ok_and(|metadata| {
metadata.file_type().is_socket()
&& metadata.dev() == self.device
&& metadata.ino() == self.inode
metadata.file_type().is_socket() && metadata.dev() == self.device && metadata.ino() == self.inode
}) {
let _ = fs::remove_file(&self.path);
}
}
}
pub(crate) fn init(
event_loop: &mut EventLoop<'static, Ferese>,
) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
let path = socket_path()?;
prepare_parent(&path)?;
let listener = bind_listener(&path)?;
@@ -115,101 +105,88 @@ pub(crate) fn init(
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
channel::sync_channel(REQUEST_QUEUE_CAPACITY);
event_loop
.handle()
.insert_source(receiver, |event, _, state| {
if let channel::Event::Msg(event) = event {
let call = match event {
IpcEvent::Call(call) => call,
IpcEvent::Closed(owner) => {
let captured = state.input_capture.active();
state.input_capture.remove_owner(owner);
if captured && !state.input_capture.active() {
state.restore_input_capture_focus();
}
state.portal_shortcuts.remove(owner);
state.portal_session.remove(owner);
state.refresh_idle_inhibition();
return;
event_loop.handle().insert_source(receiver, |event, _, state| {
if let channel::Event::Msg(event) = event {
let call = match event {
IpcEvent::Call(call) => call,
IpcEvent::Closed(owner) => {
let captured = state.input_capture.active();
state.input_capture.remove_owner(owner);
if captured && !state.input_capture.active() {
state.restore_input_capture_focus();
}
};
if call.request.command.starts_with("input-capture-") && !call.native_portal {
state.portal_shortcuts.remove(owner);
state.portal_session.remove(owner);
state.refresh_idle_inhibition();
return;
}
};
if call.request.command.starts_with("input-capture-") && !call.native_portal {
let _ = call.response.try_send(Response::error(
call.request.id,
"access_denied",
"Input capture is restricted to the native portal",
));
} else if call.request.command == "input-capture-watch" {
if let Err(error) = validate_request(&call.request) {
let _ = call
.response
.try_send(Response::error(call.request.id, error.code, error.message));
} else if let Some(id) = call.request.args["session"].as_u64() {
state
.input_capture
.watch(call.owner, call.request.id, id, call.response);
} else {
let _ = call.response.try_send(Response::error(
call.request.id,
"access_denied",
"Input capture is restricted to the native portal",
"invalid_argument",
"Missing capture session",
));
} else if call.request.command == "input-capture-watch" {
if let Err(error) = validate_request(&call.request) {
let _ = call.response.try_send(Response::error(
call.request.id,
error.code,
error.message,
));
} else if let Some(id) = call.request.args["session"].as_u64() {
state
.input_capture
.watch(call.owner, call.request.id, id, call.response);
} else {
let _ = call.response.try_send(Response::error(
call.request.id,
"invalid_argument",
"Missing capture session",
));
}
} else if call.request.command == "session-watch" {
if let Err(error) = validate_request(&call.request) {
let _ = call.response.send(Response::error(
call.request.id,
error.code,
error.message,
));
} else if let Some(since) = call.request.args["since"].as_u64() {
state.portal_session.watch(
call.owner,
call.request.id,
since,
call.response,
);
} else {
let _ = call.response.send(Response::error(
call.request.id,
"invalid_argument",
"Missing session revision",
));
}
} else if call.request.command == "screenshot-window" {
state.start_window_screenshot(call.request, call.response);
} else if call.request.command == "screenshot" {
// Deferred: this path answers the caller itself, exactly
// once, whenever the request finishes or is terminated.
state.start_screenshot(call.request, call.response);
} else {
let response = state.handle_ipc_request(call.owner, call.request);
let _ = call.response.send(response);
}
} else if call.request.command == "session-watch" {
if let Err(error) = validate_request(&call.request) {
let _ = call
.response
.send(Response::error(call.request.id, error.code, error.message));
} else if let Some(since) = call.request.args["since"].as_u64() {
state
.portal_session
.watch(call.owner, call.request.id, since, call.response);
} else {
let _ = call.response.send(Response::error(
call.request.id,
"invalid_argument",
"Missing session revision",
));
}
} else if call.request.command == "screenshot-window" {
state.start_window_screenshot(call.request, call.response);
} else if call.request.command == "screenshot" {
// Deferred: this path answers the caller itself, exactly
// once, whenever the request finishes or is terminated.
state.start_screenshot(call.request, call.response);
} else {
let response = state.handle_ipc_request(call.owner, call.request);
let _ = call.response.send(response);
}
})?;
}
})?;
// Screenshot readbacks publish parts here, so the loop wakes as soon as a
// readback lands, whichever backend produced it.
let (parts, part_events) = channel::channel::<PartOutcome>();
event_loop
.handle()
.insert_source(part_events, |event, _, state| {
if let channel::Event::Msg(outcome) = event {
state.on_screenshot_part(outcome);
}
})?;
event_loop.handle().insert_source(part_events, |event, _, state| {
if let channel::Event::Msg(outcome) = event {
state.on_screenshot_part(outcome);
}
})?;
let (encoded, encoded_events) = channel::sync_channel::<Encoded>(RESULT_QUEUE_CAPACITY);
event_loop
.handle()
.insert_source(encoded_events, |event, _, state| {
if let channel::Event::Msg(result) = event {
state.on_screenshot_encoded(result);
}
})?;
event_loop.handle().insert_source(encoded_events, |event, _, state| {
if let channel::Event::Msg(result) = event {
state.on_screenshot_encoded(result);
}
})?;
let worker = Worker::spawn(encoded);
let signal = event_loop.get_signal();
@@ -231,9 +208,9 @@ pub(crate) fn init(
// Readbacks are published by the render path, so a request whose outputs
// are not redrawn would otherwise wait forever. This re-arms to the exact
// remaining time whenever a request is outstanding, and idles otherwise.
event_loop.handle().insert_source(
timer::Timer::from_duration(DEADLINE_IDLE),
|_, _, state| {
event_loop
.handle()
.insert_source(timer::Timer::from_duration(DEADLINE_IDLE), |_, _, state| {
// Abandoned requests have already been answered, so their
// readbacks are simply dropped: they are not failed, because
// that would publish into a request that no longer exists.
@@ -244,8 +221,7 @@ pub(crate) fn init(
}
let next = state.screenshot.next_deadline().unwrap_or(DEADLINE_IDLE);
timer::TimeoutAction::ToDuration(next)
},
)?;
})?;
tracing::info!(path = %path.display(), "Ferese IPC is accepting connections");
@@ -265,11 +241,7 @@ pub(crate) struct ScreenshotInit {
pub(crate) _guard: IpcSocketGuard,
}
fn accept_connections(
listener: UnixListener,
sender: channel::SyncSender<IpcEvent>,
signal: LoopSignal,
) {
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal) {
let active_connections = Arc::new(AtomicUsize::new(0));
let connection_ids = AtomicU64::new(1);
@@ -296,10 +268,7 @@ fn accept_connections(
}
let Some(permit) = try_acquire_connection(&active_connections) else {
tracing::warn!(
limit = MAX_CONNECTIONS,
"rejected IPC connection at worker limit"
);
tracing::warn!(limit = MAX_CONNECTIONS, "rejected IPC connection at worker limit");
continue;
};
let sender = sender.clone();
@@ -329,17 +298,10 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
}
}
Some(ConnectionPermit {
active: active.clone(),
})
Some(ConnectionPermit { active: active.clone() })
}
fn serve_connection(
mut stream: UnixStream,
sender: channel::SyncSender<IpcEvent>,
signal: LoopSignal,
owner: u64,
) {
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal, owner: u64) {
let native_portal = crate::handlers::window_capture::is_portal(&stream);
let _connection = IpcConnection {
owner,
@@ -351,9 +313,7 @@ fn serve_connection(
Err(ferese_ipc::FrameError::Io(error))
if matches!(
error.kind(),
io::ErrorKind::UnexpectedEof
| io::ErrorKind::ConnectionReset
| io::ErrorKind::BrokenPipe
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
) =>
{
return;
@@ -365,8 +325,7 @@ fn serve_connection(
};
let (response, receiver) = sync_channel(1);
let exit_requested = request.command == "exit";
let screenshot_requested =
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
let screenshot_requested = matches!(request.command.as_str(), "screenshot" | "screenshot-window");
let call = IpcEvent::Call(IpcCall {
native_portal,
owner,
@@ -432,10 +391,7 @@ fn u32_arg(args: &Value, key: &str) -> Result<u32, CommandError> {
fn discard_undelivered_screenshot(response: &Response) {
if response.error.is_none()
&& let Some(path) = response
.result
.as_ref()
.and_then(|result| result["path"].as_str())
&& let Some(path) = response.result.as_ref().and_then(|result| result["path"].as_str())
&& let Err(error) = fs::remove_file(path)
&& error.kind() != io::ErrorKind::NotFound
{
@@ -459,11 +415,7 @@ impl Ferese {
| "cancel-session-end"
)
{
return Response::error(
request.id,
"session_locked",
"IPC unavailable while session is locked",
);
return Response::error(request.id, "session_locked", "IPC unavailable while session is locked");
}
if let Err(error) = validate_request(&request) {
return Response::error(request.id, error.code, error.message);
@@ -475,12 +427,7 @@ impl Ferese {
}
}
fn dispatch_ipc_command(
&mut self,
owner: u64,
command: &str,
args: &Value,
) -> Result<Value, CommandError> {
fn dispatch_ipc_command(&mut self, owner: u64, command: &str, args: &Value) -> Result<Value, CommandError> {
match command {
"portal-inhibit" => {
let inhibition = serde_json::from_value(args.clone())
@@ -522,9 +469,9 @@ impl Ferese {
.validate_end(
u32_arg(args, "token")?,
u32_arg(args, "inhibitor-revision")?,
args["force"].as_bool().ok_or_else(|| {
CommandError::new("invalid_argument", "Missing force confirmation")
})?,
args["force"]
.as_bool()
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
)
.map_err(|error| CommandError::new("inhibited", error))?;
return Ok(json!({}));
@@ -534,17 +481,17 @@ impl Ferese {
.commit_end(
u32_arg(args, "token")?,
u32_arg(args, "inhibitor-revision")?,
args["force"].as_bool().ok_or_else(|| {
CommandError::new("invalid_argument", "Missing force confirmation")
})?,
args["force"]
.as_bool()
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
)
.map_err(|error| CommandError::new("inhibited", error))?;
return Ok(json!({}));
}
"logind-session-ending" => {
let ending = args["ending"].as_bool().ok_or_else(|| {
CommandError::new("invalid_argument", "Missing shutdown state")
})?;
let ending = args["ending"]
.as_bool()
.ok_or_else(|| CommandError::new("invalid_argument", "Missing shutdown state"))?;
self.portal_session.set_phase(if ending { 3 } else { 1 });
return Ok(json!({}));
}
@@ -573,8 +520,7 @@ impl Ferese {
let xkb = state.xkb().lock().unwrap();
// SAFETY: the ref-counted keymap does not escape the locked Xkb;
// only its owned string is retained for the EIS keyboard.
unsafe { xkb.keymap() }
.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
unsafe { xkb.keymap() }.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
});
return Ok(json!({"session":id, "keymap":keymap}));
}
@@ -583,9 +529,9 @@ impl Ferese {
return self
.input_capture
.zones(
args["session"].as_u64().ok_or_else(|| {
CommandError::new("invalid_capture", "Missing session")
})?,
args["session"]
.as_u64()
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?,
)
.map_err(|e| CommandError::new("invalid_capture", e));
}
@@ -642,12 +588,8 @@ impl Ferese {
let result = if command == "input-capture-disable" {
self.input_capture.disable(id)
} else {
self.input_capture.release(
id,
args["activation_id"]
.as_u64()
.and_then(|id| u32::try_from(id).ok()),
)
self.input_capture
.release(id, args["activation_id"].as_u64().and_then(|id| u32::try_from(id).ok()))
};
result.map_err(|e| CommandError::new("invalid_capture", e))?;
if active && !self.input_capture.active() {
@@ -696,9 +638,7 @@ impl Ferese {
.get("pid")
.and_then(Value::as_u64)
.and_then(|pid| u32::try_from(pid).ok())
.ok_or_else(|| {
CommandError::new("invalid_argument", "pid must be an unsigned process ID")
})?;
.ok_or_else(|| CommandError::new("invalid_argument", "pid must be an unsigned process ID"))?;
return Ok(json!(self.has_client_surfaces(pid)));
}
"get-workspaces" => return Ok(self.workspaces_json()),
@@ -783,10 +723,7 @@ impl Ferese {
return;
}
};
let specs = planned
.iter()
.map(|part| part.spec(&layouts[part.index]))
.collect();
let specs = planned.iter().map(|part| part.spec(&layouts[part.index])).collect();
// admit stores the reply only on success, so a clone survives the
// rejection path and every caller is answered exactly once.
@@ -794,8 +731,7 @@ impl Ferese {
let id = match self.screenshot.admit(request.id, response, specs) {
Ok(id) => id,
Err(message) => {
let _ =
fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
return;
}
};
@@ -819,11 +755,7 @@ impl Ferese {
fn start_window_screenshot(&mut self, request: Request, response: SyncSender<Response>) {
let reject = |message: String| {
let _ = response.try_send(Response::error(
request.id,
"window_capture_failed",
message,
));
let _ = response.try_send(Response::error(request.id, "window_capture_failed", message));
};
if self.session_lock.active || !screencopy::capture_allowed() {
reject("Screen capture is unavailable".into());
@@ -877,19 +809,13 @@ impl Ferese {
let capture = match self.screenshot.admit(request.id, response, vec![spec]) {
Ok(capture) => capture,
Err(error) => {
let _ =
fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
return;
}
};
let result = if let Some(backend) = &self.nested_backend {
match backend.try_borrow_mut() {
Ok(mut backend) => crate::winit::capture_window_buffer(
backend.renderer(),
&window,
geometry,
scale,
),
Ok(mut backend) => crate::winit::capture_window_buffer(backend.renderer(), &window, geometry, scale),
Err(_) => Err("Window renderer is busy".into()),
}
} else if let Some(backend) = &mut self.direct_backend {
@@ -905,9 +831,7 @@ impl Ferese {
}
pub(crate) fn on_screenshot_part(&mut self, outcome: PartOutcome) {
let action = self
.screenshot
.on_part(outcome.request, outcome.part, outcome.result);
let action = self.screenshot.on_part(outcome.request, outcome.part, outcome.result);
self.apply_screenshot_action(action);
}
@@ -925,8 +849,7 @@ impl Ferese {
.as_ref()
.is_some_and(|worker| worker.submit(Job { request, frames }).is_ok());
if !submitted {
self.screenshot
.reject(request, "Screenshot encoding queue is full");
self.screenshot.reject(request, "Screenshot encoding queue is full");
}
}
Action::Delivered(path) => {
@@ -955,13 +878,12 @@ impl Ferese {
fn has_client_surfaces(&self, pid: u32) -> bool {
use smithay::reexports::wayland_server::Resource;
let belongs_to =
|surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
surface
.client()
.and_then(|client| client.get_credentials(&self.display_handle).ok())
.is_some_and(|credentials| credentials.pid as u32 == pid)
};
let belongs_to = |surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
surface
.client()
.and_then(|client| client.get_credentials(&self.display_handle).ok())
.is_some_and(|credentials| credentials.pid as u32 == pid)
};
self.window_ids
.keys()
.filter_map(|window| window.toplevel())
@@ -974,19 +896,15 @@ impl Ferese {
}
fn windows_json(&self) -> Value {
use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData};
use smithay::wayland::compositor::with_states;
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
let mut windows = self
.window_ids
.iter()
.filter_map(|(window, id)| {
let toplevel = window.toplevel()?;
let (app_id, title) = with_states(toplevel.wl_surface(), |states| {
let attributes = states
.data_map
.get::<XdgToplevelSurfaceData>()
.unwrap()
.lock()
.unwrap();
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
(
attributes.app_id.clone().unwrap_or_default(),
attributes.title.clone().unwrap_or_default(),
@@ -1044,10 +962,7 @@ impl Ferese {
.connected_outputs
.iter()
.map(|info| {
let mapped = self
.space
.outputs()
.find(|output| output.name() == info.connector);
let mapped = self.space.outputs().find(|output| output.name() == info.connector);
let id = mapped.and_then(|output| self.output_id(output));
let geometry = mapped.and_then(|output| self.space.output_geometry(output));
let workspace = id.and_then(|id| self.output_workspaces.active_workspace(id));
@@ -1105,9 +1020,7 @@ impl Ferese {
})
})
.collect::<Vec<_>>();
outputs.sort_by(|left, right| {
left["connector"].as_str().cmp(&right["connector"].as_str())
});
outputs.sort_by(|left, right| left["connector"].as_str().cmp(&right["connector"].as_str()));
return Value::Array(outputs);
}
@@ -1163,9 +1076,7 @@ fn geometry_arg(args: &Value) -> Result<Geometry, String> {
match args.get("geometry") {
None | Some(Value::Null) => Ok(Geometry::All),
Some(value) => {
let text = value
.as_str()
.ok_or("geometry must be a string like \"x,y WxH\"")?;
let text = value.as_str().ok_or("geometry must be a string like \"x,y WxH\"")?;
parse_geometry(text)
}
}
@@ -1179,10 +1090,7 @@ fn validate_request(request: &Request) -> Result<(), CommandError> {
));
}
if request.kind != "command" {
return Err(CommandError::new(
"invalid_type",
"expected type \"command\"",
));
return Err(CommandError::new("invalid_type", "expected type \"command\""));
}
Ok(())
@@ -1247,12 +1155,7 @@ fn workspace_arg(args: &Value) -> Result<u32, CommandError> {
.and_then(Value::as_u64)
.and_then(|index| u32::try_from(index).ok())
.filter(|index| *index > 0)
.ok_or_else(|| {
CommandError::new(
"invalid_argument",
"index must be a positive 32-bit integer",
)
})
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
}
fn socket_path() -> Result<PathBuf, io::Error> {
@@ -1264,9 +1167,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
}
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
let parent = socket
.parent()
.expect("the Ferese control socket always has a parent");
let parent = socket.parent().expect("the Ferese control socket always has a parent");
match fs::symlink_metadata(parent) {
Ok(metadata) => {
if !metadata.is_dir() || metadata.file_type().is_symlink() {
@@ -1311,11 +1212,7 @@ fn bind_listener(path: &Path) -> Result<UnixListener, io::Error> {
}
fn peer_uid(stream: &UnixStream) -> Result<u32, io::Error> {
let mut credentials = libc::ucred {
pid: 0,
uid: 0,
gid: 0,
};
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
// SAFETY: `credentials` and `length` point to initialized, correctly sized
@@ -1361,10 +1258,7 @@ mod tests {
#[test]
fn parses_direction_arguments() {
assert_eq!(
direction_arg(&json!({ "direction": "left" })).unwrap(),
Direction::Left
);
assert_eq!(direction_arg(&json!({ "direction": "left" })).unwrap(), Direction::Left);
assert!(direction_arg(&json!({ "direction": "diagonal" })).is_err());
}
@@ -1402,9 +1296,7 @@ mod tests {
let worker = thread::spawn(move || serve_connection(server, sender, signal, 1));
let invalid = b"not-json";
client
.write_all(&(invalid.len() as u32).to_be_bytes())
.unwrap();
client.write_all(&(invalid.len() as u32).to_be_bytes()).unwrap();
client.write_all(invalid).unwrap();
client.shutdown(Shutdown::Write).unwrap();
client
@@ -1451,10 +1343,7 @@ mod tests {
let socket = root.join("control.sock");
let first = bind_listener(&socket).unwrap();
assert_eq!(
bind_listener(&socket).unwrap_err().kind(),
io::ErrorKind::AddrInUse
);
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
drop(first);
let replacement = bind_listener(&socket).unwrap();
@@ -1470,10 +1359,7 @@ mod tests {
let socket = root.join("control.sock");
fs::write(&socket, b"keep").unwrap();
assert_eq!(
bind_listener(&socket).unwrap_err().kind(),
io::ErrorKind::AddrInUse
);
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
assert_eq!(fs::read(&socket).unwrap(), b"keep");
fs::remove_dir_all(root).unwrap();
}