Phase 13.2.b.3 — client de test visuel pour wl_subsurface
Nouveau crate redox-wl-test-client-subcompositor : client Wayland qui crée un toplevel parent 300×200 bleu nuit avec bordure noire, et lui attache une subsurface 60×60 rouge à l'offset (50, 50). Si le rendering 13.2.b.2 fonctionne, on doit voir visuellement dans la fenêtre QEMU : la fenêtre bleue avec un carré rouge incrusté en haut- gauche, exactement 50px depuis le bord du parent. Cycle complet : 1. Bind 5 globals : wl_compositor, wl_shm, xdg_wm_base, wl_seat, wl_subcompositor (le client échouera si compositor manque l'un d'eux) 2. Parent xdg_toplevel avec ack_configure 3. SHM buffer 300×200 bleu + attach + commit parent 4. wl_compositor.create_surface pour le child 5. wl_subcompositor.get_subsurface(child, parent) + set_position(50,50) + set_desync (pour qu'un commit child suffise à rendre les changements) 6. SHM buffer 60×60 rouge + attach + commit child 7. Re-commit parent (force redraw) 8. Event loop avec ESC → exit propre Reprend le pattern d'event-loop tolérant Interrupted/BrokenPipe de la phase 13.1.b (5e adaptation Redox côté client). run-qemu.sh copie maintenant aussi ce binaire dans /usr/bin de l'image (optionnel, skip silencieux si non compilé). Les 2 tests natifs continuent de PASSer en non-régression. Leyoda 2026 – GPLv3
This commit is contained in:
parent
bba2d7beb6
commit
1dab6ff51b
3 changed files with 406 additions and 0 deletions
10
crates/redox-wl-test-client-subcompositor/Cargo.toml
Normal file
10
crates/redox-wl-test-client-subcompositor/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "redox-wl-test-client-subcompositor"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wayland-client = { path = "../../../wayland-rs/wayland-client", default-features = false }
|
||||||
|
wayland-backend = { path = "../../../wayland-rs/wayland-backend", default-features = false }
|
||||||
|
wayland-protocols = { path = "../../../wayland-rs/wayland-protocols", default-features = false, features = ["client"] }
|
||||||
|
libc = "0.2"
|
||||||
390
crates/redox-wl-test-client-subcompositor/src/main.rs
Normal file
390
crates/redox-wl-test-client-subcompositor/src/main.rs
Normal file
|
|
@ -0,0 +1,390 @@
|
||||||
|
//! Phase 13.2.b.3 — Client de test visuel pour wl_subcompositor.
|
||||||
|
//!
|
||||||
|
//! Crée un toplevel parent 300×200 bleu uni avec bordure noire, et lui
|
||||||
|
//! attache une subsurface 60×60 rouge à l'offset (50, 50). Si le rendering
|
||||||
|
//! côté compositor (phase 13.2.b.2) est correct, on doit voir à l'écran :
|
||||||
|
//!
|
||||||
|
//! - Fenêtre bleue en haut-gauche (selon cascading compositor)
|
||||||
|
//! - Un carré rouge dedans, en haut-gauche de la fenêtre bleue, à 50px
|
||||||
|
//! du coin haut-gauche du parent
|
||||||
|
//!
|
||||||
|
//! Le client tourne ~60s puis exit. ESC ferme proprement avant timeout
|
||||||
|
//! (path keyboard via wl_keyboard.enter envoyé au focus = parent toplevel).
|
||||||
|
//!
|
||||||
|
//! NB : sans cascade sync ni propagation parent-move (limitations 13.2.b.2),
|
||||||
|
//! la subsurface est figée à la position de création. Acceptable pour
|
||||||
|
//! validation visuelle de base.
|
||||||
|
|
||||||
|
use std::ffi::CString;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::fd::{AsFd, FromRawFd, OwnedFd};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::ptr;
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use wayland_client::{
|
||||||
|
backend::Backend,
|
||||||
|
protocol::{
|
||||||
|
wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_keyboard, wl_registry,
|
||||||
|
wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool,
|
||||||
|
wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface,
|
||||||
|
},
|
||||||
|
Connection, Dispatch, EventQueue, Proxy, QueueHandle,
|
||||||
|
};
|
||||||
|
use wayland_protocols::xdg::shell::client::{
|
||||||
|
xdg_surface::{self, XdgSurface},
|
||||||
|
xdg_toplevel::{self, XdgToplevel},
|
||||||
|
xdg_wm_base::{self, XdgWmBase},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOCKET_PATH: &str = "/tmp/redox-wl-comp.sock";
|
||||||
|
|
||||||
|
// Parent : 300×200 bleu nuit
|
||||||
|
const PW: i32 = 300;
|
||||||
|
const PH: i32 = 200;
|
||||||
|
const PSTRIDE: i32 = PW * 4;
|
||||||
|
const PSIZE: i32 = PSTRIDE * PH;
|
||||||
|
const PCOLOR: u32 = 0xFF_22_44_AA;
|
||||||
|
|
||||||
|
// Subsurface : 60×60 rouge, offset (50, 50)
|
||||||
|
const SW: i32 = 60;
|
||||||
|
const SH: i32 = 60;
|
||||||
|
const SSTRIDE: i32 = SW * 4;
|
||||||
|
const SSIZE: i32 = SSTRIDE * SH;
|
||||||
|
const SCOLOR: u32 = 0xFF_CC_30_30;
|
||||||
|
const SOFF_X: i32 = 50;
|
||||||
|
const SOFF_Y: i32 = 50;
|
||||||
|
|
||||||
|
struct DebugSink(Mutex<Option<std::fs::File>>);
|
||||||
|
impl DebugSink {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(Mutex::new(
|
||||||
|
OpenOptions::new().write(true).open("/scheme/debug").ok(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn writeln(&self, s: &str) {
|
||||||
|
println!("{s}");
|
||||||
|
if let Ok(mut g) = self.0.lock() {
|
||||||
|
if let Some(f) = g.as_mut() {
|
||||||
|
let _ = writeln!(f, "{s}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn dlog(s: &str) {
|
||||||
|
static SINK: OnceLock<DebugSink> = OnceLock::new();
|
||||||
|
SINK.get_or_init(DebugSink::new).writeln(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
compositor: Option<WlCompositor>,
|
||||||
|
shm: Option<WlShm>,
|
||||||
|
wm_base: Option<XdgWmBase>,
|
||||||
|
seat: Option<WlSeat>,
|
||||||
|
subcompositor: Option<WlSubcompositor>,
|
||||||
|
pending_serial: Option<u32>,
|
||||||
|
configured: bool,
|
||||||
|
running: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
registry: &wl_registry::WlRegistry,
|
||||||
|
event: wl_registry::Event,
|
||||||
|
_: &(),
|
||||||
|
_conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let wl_registry::Event::Global {
|
||||||
|
name,
|
||||||
|
interface,
|
||||||
|
version,
|
||||||
|
} = event
|
||||||
|
{
|
||||||
|
match interface.as_str() {
|
||||||
|
"wl_compositor" => {
|
||||||
|
state.compositor = Some(registry.bind(name, version.min(5), qh, ()));
|
||||||
|
}
|
||||||
|
"wl_shm" => {
|
||||||
|
state.shm = Some(registry.bind(name, 1, qh, ()));
|
||||||
|
}
|
||||||
|
"xdg_wm_base" => {
|
||||||
|
state.wm_base = Some(registry.bind(name, version.min(5), qh, ()));
|
||||||
|
}
|
||||||
|
"wl_seat" => {
|
||||||
|
state.seat = Some(registry.bind(name, version.min(7), qh, ()));
|
||||||
|
}
|
||||||
|
"wl_subcompositor" => {
|
||||||
|
state.subcompositor = Some(registry.bind(name, 1, qh, ()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! noop {
|
||||||
|
($ty:ty) => {
|
||||||
|
impl Dispatch<$ty, ()> for State {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &$ty,
|
||||||
|
_: <$ty as Proxy>::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
noop!(WlCompositor);
|
||||||
|
noop!(WlShm);
|
||||||
|
noop!(WlShmPool);
|
||||||
|
noop!(WlBuffer);
|
||||||
|
noop!(WlSurface);
|
||||||
|
noop!(WlSeat);
|
||||||
|
noop!(WlSubcompositor);
|
||||||
|
noop!(WlSubsurface);
|
||||||
|
|
||||||
|
impl Dispatch<XdgWmBase, ()> for State {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
wm_base: &XdgWmBase,
|
||||||
|
event: xdg_wm_base::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let xdg_wm_base::Event::Ping { serial } = event {
|
||||||
|
wm_base.pong(serial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<XdgSurface, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &XdgSurface,
|
||||||
|
event: xdg_surface::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let xdg_surface::Event::Configure { serial } = event {
|
||||||
|
state.pending_serial = Some(serial);
|
||||||
|
state.configured = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dispatch<XdgToplevel, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &XdgToplevel,
|
||||||
|
event: xdg_toplevel::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if matches!(event, xdg_toplevel::Event::Close) {
|
||||||
|
state.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC → exit. Même logique qu'en 13.1.b sur le client simple_window.
|
||||||
|
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
_: &wl_keyboard::WlKeyboard,
|
||||||
|
event: wl_keyboard::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
if let wl_keyboard::Event::Key { key, .. } = event {
|
||||||
|
if key == 1 {
|
||||||
|
dlog("[sub-client] ESC → exit");
|
||||||
|
state.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn create_shm(name: &str, size: i32, w: i32, h: i32, color: u32) -> Result<OwnedFd, String> {
|
||||||
|
let cname = CString::new(name).unwrap();
|
||||||
|
let _ = libc::shm_unlink(cname.as_ptr());
|
||||||
|
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR | libc::O_CREAT, 0o600);
|
||||||
|
if fd < 0 {
|
||||||
|
return Err("shm_open failed".into());
|
||||||
|
}
|
||||||
|
if libc::ftruncate(fd, size as _) != 0 {
|
||||||
|
libc::close(fd);
|
||||||
|
return Err("ftruncate failed".into());
|
||||||
|
}
|
||||||
|
let p = libc::mmap(
|
||||||
|
ptr::null_mut(),
|
||||||
|
size as usize,
|
||||||
|
libc::PROT_READ | libc::PROT_WRITE,
|
||||||
|
libc::MAP_SHARED,
|
||||||
|
fd,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
if p == libc::MAP_FAILED {
|
||||||
|
libc::close(fd);
|
||||||
|
return Err("mmap failed".into());
|
||||||
|
}
|
||||||
|
let pixels = std::slice::from_raw_parts_mut(p as *mut u32, (w * h) as usize);
|
||||||
|
// Remplissage uni avec bordure noire 2px
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let on_border = x < 2 || x >= w - 2 || y < 2 || y >= h - 2;
|
||||||
|
pixels[(y * w + x) as usize] = if on_border { 0xFF_10_10_10 } else { color };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
libc::munmap(p, size as usize);
|
||||||
|
Ok(OwnedFd::from_raw_fd(fd))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
dlog("[sub-client] start (Phase 13.2.b.3 — visual subsurface)");
|
||||||
|
|
||||||
|
for i in 0..50 {
|
||||||
|
if std::path::Path::new(SOCKET_PATH).exists() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if i == 49 {
|
||||||
|
return Err("compositor socket missing after 5s".into());
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = UnixStream::connect(SOCKET_PATH)?;
|
||||||
|
let backend = Backend::connect(stream)?;
|
||||||
|
let conn = Connection::from_backend(backend);
|
||||||
|
let mut event_queue: EventQueue<State> = conn.new_event_queue();
|
||||||
|
let qh = event_queue.handle();
|
||||||
|
let _registry = conn.display().get_registry(&qh, ());
|
||||||
|
|
||||||
|
let mut state = State {
|
||||||
|
running: true,
|
||||||
|
..State::default()
|
||||||
|
};
|
||||||
|
event_queue.roundtrip(&mut state)?;
|
||||||
|
let compositor = state.compositor.clone().ok_or("no wl_compositor")?;
|
||||||
|
let shm = state.shm.clone().ok_or("no wl_shm")?;
|
||||||
|
let wm_base = state.wm_base.clone().ok_or("no xdg_wm_base")?;
|
||||||
|
let subcomp = state
|
||||||
|
.subcompositor
|
||||||
|
.clone()
|
||||||
|
.ok_or("no wl_subcompositor (compositor patché 13.2.b.1 ?)")?;
|
||||||
|
let seat = state.seat.clone();
|
||||||
|
|
||||||
|
// Bind keyboard pour gérer ESC
|
||||||
|
if let Some(s) = &seat {
|
||||||
|
let _kb = s.get_keyboard(&qh, ());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Parent toplevel ---
|
||||||
|
let parent_surface = compositor.create_surface(&qh, ());
|
||||||
|
let xdg_surface = wm_base.get_xdg_surface(&parent_surface, &qh, ());
|
||||||
|
let toplevel = xdg_surface.get_toplevel(&qh, ());
|
||||||
|
toplevel.set_title("Phase 13.2.b.3 — subcompositor visual".into());
|
||||||
|
toplevel.set_app_id("redox.wl.test.client.subcompositor".into());
|
||||||
|
parent_surface.commit();
|
||||||
|
dlog("[sub-client] parent xdg_toplevel créé");
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
while !state.configured && start.elapsed() < Duration::from_secs(5) {
|
||||||
|
event_queue.roundtrip(&mut state)?;
|
||||||
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
let serial = state.pending_serial.ok_or("no initial configure")?;
|
||||||
|
xdg_surface.ack_configure(serial);
|
||||||
|
state.pending_serial = None;
|
||||||
|
dlog(&format!("[sub-client] ack_configure({serial})"));
|
||||||
|
|
||||||
|
// Parent buffer + attach + commit
|
||||||
|
let parent_fd = unsafe { create_shm("redox-sub-client-parent", PSIZE, PW, PH, PCOLOR) }?;
|
||||||
|
let parent_pool = shm.create_pool(parent_fd.as_fd(), PSIZE, &qh, ());
|
||||||
|
let parent_buffer = parent_pool.create_buffer(
|
||||||
|
0,
|
||||||
|
PW,
|
||||||
|
PH,
|
||||||
|
PSTRIDE,
|
||||||
|
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||||
|
&qh,
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
parent_surface.attach(Some(&parent_buffer), 0, 0);
|
||||||
|
parent_surface.commit();
|
||||||
|
dlog("[sub-client] parent buffer attached + commit");
|
||||||
|
|
||||||
|
// --- Subsurface ---
|
||||||
|
let child_surface = compositor.create_surface(&qh, ());
|
||||||
|
let subsurface: WlSubsurface =
|
||||||
|
subcomp.get_subsurface(&child_surface, &parent_surface, &qh, ());
|
||||||
|
subsurface.set_position(SOFF_X, SOFF_Y);
|
||||||
|
subsurface.set_desync(); // mode desync → commit du child suffit pour appliquer
|
||||||
|
dlog(&format!(
|
||||||
|
"[sub-client] subsurface créée + set_position({SOFF_X}, {SOFF_Y}) + set_desync"
|
||||||
|
));
|
||||||
|
|
||||||
|
let child_fd = unsafe { create_shm("redox-sub-client-child", SSIZE, SW, SH, SCOLOR) }?;
|
||||||
|
let child_pool = shm.create_pool(child_fd.as_fd(), SSIZE, &qh, ());
|
||||||
|
let child_buffer = child_pool.create_buffer(
|
||||||
|
0,
|
||||||
|
SW,
|
||||||
|
SH,
|
||||||
|
SSTRIDE,
|
||||||
|
wayland_client::protocol::wl_shm::Format::Argb8888,
|
||||||
|
&qh,
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
child_surface.attach(Some(&child_buffer), 0, 0);
|
||||||
|
child_surface.commit();
|
||||||
|
dlog("[sub-client] child buffer attached + commit");
|
||||||
|
|
||||||
|
// Re-commit du parent pour rendre les changements visibles en sync.
|
||||||
|
// (En desync ce n'est pas strictement nécessaire mais ça force un redraw.)
|
||||||
|
parent_surface.commit();
|
||||||
|
|
||||||
|
dlog("[sub-client] entering event loop");
|
||||||
|
let deadline = std::time::Instant::now() + Duration::from_secs(60);
|
||||||
|
while state.running && std::time::Instant::now() < deadline {
|
||||||
|
match event_queue.blocking_dispatch(&mut state) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
let s = format!("{e}");
|
||||||
|
if s.contains("Broken pipe") || s.contains("Connection reset") {
|
||||||
|
dlog("[sub-client] compositor disconnected → exit cleanly");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dlog("[sub-client] loop exited cleanly");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match run() {
|
||||||
|
Ok(()) => {
|
||||||
|
dlog("[sub-client] PASS");
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
dlog(&format!("[sub-client] FAIL: {e}"));
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,8 @@ CLIENT_BIN="$ROOT/crates/redox-wl-real-client-simple-window/target/x86_64-unknow
|
||||||
# Phase 13.2.a : client de test version-gating wl_output (optionnel, skip
|
# Phase 13.2.a : client de test version-gating wl_output (optionnel, skip
|
||||||
# silencieusement si pas compilé).
|
# silencieusement si pas compilé).
|
||||||
TEST_OUTPUT_BIN="$ROOT/crates/redox-wl-test-wl-output/target/x86_64-unknown-redox/release/redox-wl-test-wl-output"
|
TEST_OUTPUT_BIN="$ROOT/crates/redox-wl-test-wl-output/target/x86_64-unknown-redox/release/redox-wl-test-wl-output"
|
||||||
|
# Phase 13.2.b.3 : client de test visuel subcompositor.
|
||||||
|
TEST_SUBCOMP_BIN="$ROOT/crates/redox-wl-test-client-subcompositor/target/x86_64-unknown-redox/release/redox-wl-test-client-subcompositor"
|
||||||
|
|
||||||
NO_QEMU=0
|
NO_QEMU=0
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
|
|
@ -133,6 +135,10 @@ cp -v "$CLIENT_BIN" "$MOUNT/usr/bin/"
|
||||||
if [[ -e "$TEST_OUTPUT_BIN" ]]; then
|
if [[ -e "$TEST_OUTPUT_BIN" ]]; then
|
||||||
cp -v "$TEST_OUTPUT_BIN" "$MOUNT/usr/bin/"
|
cp -v "$TEST_OUTPUT_BIN" "$MOUNT/usr/bin/"
|
||||||
fi
|
fi
|
||||||
|
# Phase 13.2.b.3 : test client subcompositor (optionnel)
|
||||||
|
if [[ -e "$TEST_SUBCOMP_BIN" ]]; then
|
||||||
|
cp -v "$TEST_SUBCOMP_BIN" "$MOUNT/usr/bin/"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 4. umount avant make qemu (sinon QEMU et FUSE se battent sur le même fichier) ---
|
# --- 4. umount avant make qemu (sinon QEMU et FUSE se battent sur le même fichier) ---
|
||||||
echo "==> démonter $MOUNT"
|
echo "==> démonter $MOUNT"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue