[Fix] Pipes...
Some checks failed
CI / checks (push) Failing after 1m52s

This commit is contained in:
Alex Emmet 2026-07-15 03:41:54 +02:00
commit 6a65e43ca9
10 changed files with 353 additions and 104 deletions

15
Cargo.lock generated
View file

@ -1105,6 +1105,7 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-bindgen-test", "wasm-bindgen-test",
"web-sys",
] ]
[[package]] [[package]]
@ -1837,9 +1838,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.118" version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2185,6 +2186,16 @@ version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920" checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920"
[[package]]
name = "web-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "web-time" name = "web-time"
version = "1.1.0" version = "1.1.0"

View file

@ -8,6 +8,7 @@ use mtp::type_map::TypeMap;
use std::future::Future; use std::future::Future;
use std::path::Path; use std::path::Path;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc;
fn dev_cert_paths() -> (String, String) { fn dev_cert_paths() -> (String, String) {
let cert = std::env::var("MTP_DEV_CERT").unwrap_or_else(|_| { let cert = std::env::var("MTP_DEV_CERT").unwrap_or_else(|_| {
@ -41,13 +42,6 @@ async fn handle_pipe_loopback(
let mut reader = req.accept().await?; let mut reader = req.accept().await?;
println!(" [loopback] Pipe {pipe_id} accepted, reading data ..."); println!(" [loopback] Pipe {pipe_id} accepted, reading data ...");
let mut buf = Vec::new();
tokio::io::AsyncReadExt::read_to_end(&mut reader, &mut buf).await?;
println!(
" [loopback] Pipe {pipe_id} read {} bytes, creating return pipe ...",
buf.len()
);
let handle = conn.create_pipe("loopback").await?; let handle = conn.create_pipe("loopback").await?;
println!( println!(
" [loopback] Return pipe created (id={}), waiting for client ...", " [loopback] Return pipe created (id={}), waiting for client ...",
@ -56,16 +50,19 @@ async fn handle_pipe_loopback(
match handle.wait().await? { match handle.wait().await? {
Some(mut writer) => { Some(mut writer) => {
println!( println!(" [loopback] Client accepted return pipe, echoing incoming bytes ...");
" [loopback] Client accepted return pipe, writing {} bytes ...", let mut total = 0usize;
buf.len() let mut buf = [0u8; 16 * 1024];
); loop {
tokio::io::AsyncWriteExt::write_all(&mut writer, &buf).await?; let n = tokio::io::AsyncReadExt::read(&mut reader, &mut buf).await?;
if n == 0 {
break;
}
total += n;
tokio::io::AsyncWriteExt::write_all(&mut writer, &buf[..n]).await?;
}
writer.finish().await?; writer.finish().await?;
println!( println!(" [loopback] Pipe {pipe_id} loopback complete ({} bytes)", total);
" [loopback] Pipe {pipe_id} loopback complete ({} bytes)",
buf.len()
);
} }
None => { None => {
eprintln!(" [loopback] Return pipe denied by client for pipe {pipe_id}"); eprintln!(" [loopback] Return pipe denied by client for pipe {pipe_id}");
@ -86,8 +83,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (_host_id, host_keyring) = keys::load_or_generate_host_keys("host.mk")?; let (_host_id, host_keyring) = keys::load_or_generate_host_keys("host.mk")?;
keys::export_host_public_keys(&host_keyring)?; keys::export_host_public_keys(&host_keyring)?;
let decrypt_keyring = mtp::crypto::Keyring::from_bytes(&host_keyring.to_bytes()) let decrypt_keyring = Arc::new(
.expect("re-load host keyring for decryption"); mtp::crypto::Keyring::from_bytes(&host_keyring.to_bytes())
.expect("re-load host keyring for decryption"),
);
let (clients, next_id) = clients::load_client_db("clients.json")?; let (clients, next_id) = clients::load_client_db("clients.json")?;
@ -152,61 +151,64 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Server listening on {}", host.local_addr()); println!("Server listening on {}", host.local_addr());
while let Some(conn) = host.accept().await? { while let Some(conn) = host.accept().await? {
let desc = conn.description.as_deref().unwrap_or("(no description)"); let decrypt_keyring = Arc::clone(&decrypt_keyring);
println!( tokio::spawn(async move {
"\n--- New connection (version {}, description: {desc}) ---", let desc = conn.description.as_deref().unwrap_or("(no description)");
conn.version println!(
); "\n--- New connection (version {}, description: {desc}) ---",
println!("Client ID: {}", conn.client_id); conn.version
);
println!("Client ID: {}", conn.client_id);
let tm: &TypeMap = conn.codec.registry().get(&conn.version).unwrap(); let tm: &TypeMap = conn.codec.registry().get(&conn.version).unwrap();
println!("Waiting for messages / pipe requests ..."); println!("Waiting for messages / pipe requests ...");
loop { loop {
tokio::select! { tokio::select! {
biased; biased;
pipe_req = conn.receive_pipe() => { pipe_req = conn.receive_pipe() => {
match pipe_req { match pipe_req {
Ok(req) => { Ok(req) => {
println!(" Pipe request: id={} desc={:?}", req.id(), req.description()); println!(" Pipe request: id={} desc={:?}", req.id(), req.description());
if let Err(e) = handle_pipe_loopback(&conn, req).await { if let Err(e) = handle_pipe_loopback(&conn, req).await {
eprintln!(" Pipe loopback error: {e}"); eprintln!(" Pipe loopback error: {e}");
}
} }
} Err(e) => {
Err(e) => { println!("Pipe channel closed: {e}");
println!("Pipe channel closed: {e}");
break;
}
}
}
msg = conn.receive() => {
match msg {
Ok(msg) => {
println!("Received: {msg}");
let response = handlers::process_and_respond(
&msg,
tm,
conn.client_public_key.as_ref(),
&decrypt_keyring,
);
println!("Sending: {response}");
if let Err(e) = conn.sender.send(&response).await {
eprintln!("Send error: {e}");
break; break;
} }
} }
Err(e) => { }
println!("Connection ended: {e}"); msg = conn.receive() => {
break; match msg {
Ok(msg) => {
println!("Received: {msg}");
let response = handlers::process_and_respond(
&msg,
tm,
conn.client_public_key.as_ref(),
&decrypt_keyring,
);
println!("Sending: {response}");
if let Err(e) = conn.sender.send(&response).await {
eprintln!("Send error: {e}");
break;
}
}
Err(e) => {
println!("Connection ended: {e}");
break;
}
} }
} }
} }
} }
}
conn.sender.close(); conn.sender.close();
println!("Connection closed\n"); println!("Connection closed\n");
});
} }
Ok(()) Ok(())

View file

@ -51,7 +51,7 @@
<body> <body>
<h1>MTP WebTransport Client</h1> <h1>MTP WebTransport Client</h1>
<label for="server-url">Server URL</label> <label for="server-url">Server URL</label>
<input id="server-url" value="https://localhost:8080" /> <input id="server-url" value="https://127.0.0.1:8080" />
<label for="host-public-key">Host public key bundle hex</label> <label for="host-public-key">Host public key bundle hex</label>
<textarea <textarea
@ -82,6 +82,11 @@
</div> </div>
<div id="pipe-status"></div> <div id="pipe-status"></div>
<hr />
<h2>Metrics</h2>
<div id="metrics"></div>
<hr />
<div id="key-status">Initializing...</div> <div id="key-status">Initializing...</div>
<div id="status"></div> <div id="status"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>

View file

@ -3,6 +3,7 @@ import type {
MTPCredentialStorage, MTPCredentialStorage,
MTPLogEvent, MTPLogEvent,
MTPPipeReader, MTPPipeReader,
MTPPipeWriter,
ParsedFrame, ParsedFrame,
} from "mtp"; } from "mtp";
@ -23,6 +24,7 @@ const CLEAR_KEYS = document.getElementById("clear-keys") as HTMLButtonElement;
const STREAM_MIC = document.getElementById("stream-mic") as HTMLButtonElement; const STREAM_MIC = document.getElementById("stream-mic") as HTMLButtonElement;
const STOP_MIC = document.getElementById("stop-mic") as HTMLButtonElement; const STOP_MIC = document.getElementById("stop-mic") as HTMLButtonElement;
const PIPE_STATUS = document.getElementById("pipe-status")!; const PIPE_STATUS = document.getElementById("pipe-status")!;
const METRICS = document.getElementById("metrics")!;
const CREDENTIALS_KEY = "mtp-web-client-credentials"; const CREDENTIALS_KEY = "mtp-web-client-credentials";
const HOST_PUBLIC_KEY_KEY = "mtp-web-client-host-public-key"; const HOST_PUBLIC_KEY_KEY = "mtp-web-client-host-public-key";
@ -41,8 +43,18 @@ let activeClient: ReturnType<typeof createClient> extends Promise<infer T>
: never; : never;
let micStream: MediaStream | null = null; let micStream: MediaStream | null = null;
let mediaRecorder: MediaRecorder | null = null; let mediaRecorder: MediaRecorder | null = null;
let activePipeWriter: MTPPipeWriter | null = null;
let pipeSendCount = 0; let pipeSendCount = 0;
let pendingPipeReaders: MTPPipeReader[] = []; let pendingPipeReaders: MTPPipeReader[] = [];
let audioContext: AudioContext | null = null;
let micMonitorAudio: HTMLAudioElement | null = null;
let currentPipePingMs: number | null = null;
let lastPipeSendStartedAt = 0;
let currentPipeId: number | null = null;
let currentPipeDescription = "";
let currentPipeState = "idle";
let loopbackPlaybackCount = 0;
let hasPipeRequestHandler = false;
const credentialStorage: MTPCredentialStorage = { const credentialStorage: MTPCredentialStorage = {
getItem: (key) => localStorage.getItem(key), getItem: (key) => localStorage.getItem(key),
@ -61,7 +73,126 @@ function pipeLog(msg: string, cls = "pipe") {
const line = document.createElement("div"); const line = document.createElement("div");
line.textContent = msg; line.textContent = msg;
if (cls) line.className = cls; if (cls) line.className = cls;
PIPE_STATUS.appendChild(line); PIPE_STATUS.prepend(line);
}
function setMetric(name: string, value: string) {
const row = document.querySelector(`[data-metric="${name}"]`);
if (row) {
row.querySelector<HTMLElement>(".metric-value")!.textContent = value;
return;
}
const wrapper = document.createElement("div");
wrapper.dataset.metric = name;
wrapper.innerHTML = `<span class="metric-name"></span>: <span class="metric-value"></span>`;
wrapper.querySelector<HTMLElement>(".metric-name")!.textContent = name;
wrapper.querySelector<HTMLElement>(".metric-value")!.textContent = value;
METRICS.appendChild(wrapper);
}
function updateMetrics() {
setMetric(
"Current Pipe",
currentPipeId == null ? "none" : String(currentPipeId),
);
setMetric("Pipe State", currentPipeState);
setMetric("Pipe Description", currentPipeDescription || "n/a");
setMetric(
"Current Pipe Ping",
currentPipePingMs == null ? "n/a" : `${currentPipePingMs.toFixed(1)} ms`,
);
setMetric("Loopback Playback", String(loopbackPlaybackCount));
setMetric("Sent Chunks", String(pipeSendCount));
}
function setPipeState(
state: string,
details: Partial<{
pipeId: number | null;
description: string;
pingMs: number | null;
}>,
) {
if ("pipeId" in details) currentPipeId = details.pipeId ?? null;
if ("description" in details)
currentPipeDescription = details.description ?? "";
if ("pingMs" in details)
currentPipePingMs = details.pingMs ?? currentPipePingMs;
currentPipeState = state;
updateMetrics();
}
function getPipeId(handle: unknown): number | null {
if (handle && typeof handle === "object") {
const candidate = handle as Record<string, unknown>;
const value =
candidate.pipeId ??
candidate.pipe_id ??
candidate["pipe-id"] ??
candidate.id;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
}
return null;
}
function ensureAudioContext() {
if (!audioContext) {
audioContext = new AudioContext();
}
return audioContext;
}
async function playLoopbackAudio(chunks: BlobPart[], mimeType: string) {
if (chunks.length === 0) return;
const blob = new Blob(chunks, { type: mimeType });
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.autoplay = true;
audio.onended = () => URL.revokeObjectURL(url);
audio.onerror = () => URL.revokeObjectURL(url);
try {
await ensureAudioContext().resume();
await audio.play();
loopbackPlaybackCount += 1;
updateMetrics();
pipeLog(`Loopback playback started (${chunks.length} chunks).`);
} catch (e) {
URL.revokeObjectURL(url);
pipeLog(`Loopback playback failed: ${e}`, "error");
}
}
async function startMicPlayback(stream: MediaStream) {
if (micMonitorAudio) {
micMonitorAudio.pause();
micMonitorAudio.srcObject = null;
micMonitorAudio = null;
}
const audio = new Audio();
audio.autoplay = true;
audio.controls = false;
audio.muted = false;
audio.srcObject = stream;
micMonitorAudio = audio;
try {
await ensureAudioContext().resume();
await audio.play();
pipeLog("Microphone monitoring playback started.");
} catch (e) {
pipeLog(`Microphone monitoring playback failed: ${e}`, "error");
}
}
function stopMicPlayback() {
if (!micMonitorAudio) return;
micMonitorAudio.pause();
micMonitorAudio.srcObject = null;
micMonitorAudio = null;
} }
function renderStructured(value: unknown): string { function renderStructured(value: unknown): string {
@ -297,6 +428,7 @@ async function connect() {
log("\nClient running. Waiting for incoming messages..."); log("\nClient running. Waiting for incoming messages...");
STREAM_MIC.disabled = false; STREAM_MIC.disabled = false;
log("\nPipe demo ready. Click 'Stream Microphone' to start.", "pipe"); log("\nPipe demo ready. Click 'Stream Microphone' to start.", "pipe");
updateMetrics();
} catch (error) { } catch (error) {
log(`[error] ${error}`, "error"); log(`[error] ${error}`, "error");
} }
@ -317,35 +449,52 @@ async function startMicStreaming() {
STREAM_MIC.disabled = true; STREAM_MIC.disabled = true;
STOP_MIC.disabled = false; STOP_MIC.disabled = false;
setPipeState("creating", { pipeId: null, description: "mic-audio" });
pipeLog("Microphone acquired. Creating pipe ..."); pipeLog("Microphone acquired. Creating pipe ...");
await startMicPlayback(micStream);
if (!hasPipeRequestHandler) {
activeClient.setOnPipeRequest(async (request) => {
pipeLog(
`Incoming return pipe: id=${request.pipeId} desc=${request.description}`,
);
try {
const reader = await activeClient!.acceptPipe(request.pipeId);
pendingPipeReaders.push(reader);
pipeLog(
`Pipe accepted. Streaming return pipe (pipe id=${getPipeId(reader) ?? "unknown"}) ...`,
);
readLoopbackPipe(reader);
} catch (e) {
pipeLog(`Failed to accept return pipe: ${e}`, "error");
}
});
hasPipeRequestHandler = true;
}
const handle = await activeClient.createPipe("mic-audio"); const handle = await activeClient.createPipe("mic-audio");
const pipeId = getPipeId(handle);
setPipeState("waiting-for-accept", { pipeId, description: "mic-audio" });
pipeLog( pipeLog(
`Pipe created (id=${handle.pipeId}). Waiting for server to accept ...`, `Pipe created (id=${pipeId ?? "unknown"}). Waiting for server to accept ...`,
); );
// Handle incoming pipe requests from the server (loopback return pipes)
activeClient.setOnPipeRequest(async (request) => {
pipeLog(
`Incoming return pipe: id=${request.pipeId} desc=${request.description}`,
);
try {
const reader = await activeClient!.acceptPipe(request.pipeId);
pendingPipeReaders.push(reader);
readLoopbackPipe(reader);
} catch (e) {
pipeLog(`Failed to accept return pipe: ${e}`, "error");
}
});
const writer = await handle.wait(); const writer = await handle.wait();
if (!writer) { if (!writer) {
setPipeState("denied", { pipeId, description: "mic-audio" });
pipeLog("Pipe denied by server.", "error"); pipeLog("Pipe denied by server.", "error");
stopMicStreaming(); stopMicStreaming();
return; return;
} }
activePipeWriter = writer;
pipeLog(`Pipe accepted. Streaming microphone (pipe id=${writer.pipeId}) ...`); setPipeState("streaming", {
pipeId: getPipeId(writer) ?? pipeId,
description: "mic-audio",
});
pipeLog(
`Pipe accepted. Streaming microphone (pipe id=${getPipeId(writer) ?? pipeId ?? "unknown"}) ...`,
);
// Stream microphone audio via MediaRecorder // Stream microphone audio via MediaRecorder
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
@ -361,18 +510,19 @@ async function startMicStreaming() {
const chunkNum = pipeSendCount; const chunkNum = pipeSendCount;
try { try {
lastPipeSendStartedAt = performance.now();
const buffer = await event.data.arrayBuffer(); const buffer = await event.data.arrayBuffer();
const data = new Uint8Array(buffer); const data = new Uint8Array(buffer);
await writer.write(data); await writer.write(data);
pipeLog( currentPipePingMs = performance.now() - lastPipeSendStartedAt;
` [chunk ${chunkNum}] sent ${data.length} bytes (${(performance.now() - startTime).toFixed(1)}ms write)`, updateMetrics();
);
} catch (e) { } catch (e) {
pipeLog(` [chunk ${chunkNum}] send error: ${e}`, "error"); pipeLog(` [chunk ${chunkNum}] send error: ${e}`, "error");
} }
}; };
mediaRecorder.start(200); // emit data every 200ms mediaRecorder.start(200); // emit data every 200ms
updateMetrics();
pipeLog("Streaming started (200ms chunks)."); pipeLog("Streaming started (200ms chunks).");
} }
@ -380,6 +530,10 @@ async function readLoopbackPipe(reader: MTPPipeReader) {
const startTime = performance.now(); const startTime = performance.now();
let totalBytes = 0; let totalBytes = 0;
let chunkCount = 0; let chunkCount = 0;
const chunks: BlobPart[] = [];
const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
? "audio/webm;codecs=opus"
: "audio/webm";
try { try {
while (true) { while (true) {
@ -387,6 +541,7 @@ async function readLoopbackPipe(reader: MTPPipeReader) {
if (data == null) break; // EOF if (data == null) break; // EOF
totalBytes += data.length; totalBytes += data.length;
chunkCount++; chunkCount++;
chunks.push(data.slice().buffer);
} }
} catch (e) { } catch (e) {
pipeLog(` Return pipe read error: ${e}`, "error"); pipeLog(` Return pipe read error: ${e}`, "error");
@ -398,6 +553,8 @@ async function readLoopbackPipe(reader: MTPPipeReader) {
` Return pipe complete: ${chunkCount} chunks, ${totalBytes} bytes, ` + ` Return pipe complete: ${chunkCount} chunks, ${totalBytes} bytes, ` +
`delay=${elapsed.toFixed(1)}ms`, `delay=${elapsed.toFixed(1)}ms`,
); );
setPipeState("loopback-ready", { pingMs: elapsed });
await playLoopbackAudio(chunks, mimeType);
// Clean up the reader from the pending list // Clean up the reader from the pending list
const idx = pendingPipeReaders.indexOf(reader); const idx = pendingPipeReaders.indexOf(reader);
@ -410,13 +567,28 @@ async function stopMicStreaming() {
mediaRecorder = null; mediaRecorder = null;
} }
if (activePipeWriter) {
try {
await activePipeWriter.close();
} catch (e) {
pipeLog(`Pipe close error: ${e}`, "error");
} finally {
activePipeWriter = null;
}
}
if (micStream) { if (micStream) {
micStream.getTracks().forEach((track) => track.stop()); micStream.getTracks().forEach((track) => track.stop());
micStream = null; micStream = null;
} }
stopMicPlayback();
// Close pending pipe readers // Close pending pipe readers
pendingPipeReaders = []; pendingPipeReaders = [];
setPipeState("stopped", {
pipeId: currentPipeId,
description: currentPipeDescription,
});
STREAM_MIC.disabled = false; STREAM_MIC.disabled = false;
STOP_MIC.disabled = true; STOP_MIC.disabled = true;
@ -480,3 +652,5 @@ initWasm()
log(`Fatal error: ${e}`, "error"); log(`Fatal error: ${e}`, "error");
console.error(e); console.error(e);
}); });
updateMetrics();

View file

@ -169,9 +169,18 @@ async fn run_dispatcher(
loop { loop {
match receiver.receive_event().await { match receiver.receive_event().await {
Ok(mtp_transport::TransportEvent::Message(msg)) => { Ok(mtp_transport::TransportEvent::Message(msg)) => {
println!(
"Dispatcher message: type={} id={}",
msg.get_type(),
msg.get_id()
);
if msg.get_type() == pipe_req_type { if msg.get_type() == pipe_req_type {
let pipe_id = msg.get_id(); let pipe_id = msg.get_id();
let description = msg.get_str(DataType::Description).unwrap_or("").to_string(); let description = msg.get_str(DataType::Description).unwrap_or("").to_string();
println!(
"Dispatcher classified PipeRequest: id={} description={:?}",
pipe_id, description
);
let req = PipeRequest { let req = PipeRequest {
pipe_id, pipe_id,
description, description,
@ -185,6 +194,10 @@ async fn run_dispatcher(
if msg.get_type() == pipe_resp_type { if msg.get_type() == pipe_resp_type {
let pipe_id = msg.get_id(); let pipe_id = msg.get_id();
let accepted = msg.get_bool(DataType::Accepted).unwrap_or(false); let accepted = msg.get_bool(DataType::Accepted).unwrap_or(false);
println!(
"Dispatcher classified PipeResponse: id={} accepted={}",
pipe_id, accepted
);
let mut pending = dispatcher.pending_creations.lock().await; let mut pending = dispatcher.pending_creations.lock().await;
if let Some(tx) = pending.remove(&pipe_id) { if let Some(tx) = pending.remove(&pipe_id) {
let _ = tx.send(Ok(accepted)); let _ = tx.send(Ok(accepted));
@ -198,10 +211,28 @@ async fn run_dispatcher(
} }
Ok(mtp_transport::TransportEvent::Pipe(reader)) => { Ok(mtp_transport::TransportEvent::Pipe(reader)) => {
let pipe_id = reader.pipe_id(); let pipe_id = reader.pipe_id();
println!(
"Dispatcher pipe stream: id={} description={:?}",
pipe_id,
reader.description()
);
let mut pending = dispatcher.pending_pipes.lock().await; let mut pending = dispatcher.pending_pipes.lock().await;
if let Some(tx) = pending.remove(&pipe_id) { if let Some(tx) = pending.remove(&pipe_id) {
let _ = tx.send(reader); let _ = tx.send(reader);
continue;
} }
// The WebTransport client opens the pipe request on a fresh stream.
// That stream arrives here as a pipe event, not a message event, so we
// reconstruct the host-side PipeRequest here and hand it to receive_pipe().
println!("Dispatcher treating pipe stream as PipeRequest: id={}", pipe_id);
let req = PipeRequest {
pipe_id,
description: reader.description().to_string(),
sender: sender.clone(),
dispatcher: dispatcher.clone(),
};
let _ = pipe_req_tx.send(req).await;
} }
Err(e) => { Err(e) => {
if app_tx.send(Err(e)).await.is_err() { if app_tx.send(Err(e)).await.is_err() {

View file

@ -101,8 +101,7 @@ pub async fn host(
}; };
let incoming_tx = incoming_tx.clone(); let incoming_tx = incoming_tx.clone();
let policy = policy.clone(); tokio::spawn(handle_connection(connection, incoming_tx, policy.clone()));
tokio::spawn(handle_connection(connection, incoming_tx, policy));
} }
}); });

View file

@ -228,7 +228,7 @@ async fn test_receiver_backpressure_with_small_queue() {
let mut h = start_test_host(cert_pem.clone(), key_pem).await; let mut h = start_test_host(cert_pem.clone(), key_pem).await;
let url = format!("https://127.0.0.1:{}", h.local_addr().port()); let url = format!("https://127.0.0.1:{}", h.local_addr().port());
let policy = Policy::default().with_receiver_queue_capacity(1); let policy = Policy::default().with_receiver_queue_capacity(1);
let (client_tx, client_rx) = connect(&url, Some(cert_pem), policy.clone()).await.unwrap(); let (client_tx, client_rx) = connect(&url, Some(cert_pem), policy).await.unwrap();
let (_host_tx, host_rx) = h.next().await.unwrap(); let (_host_tx, host_rx) = h.next().await.unwrap();
let tm = TypeMap::latest(); let tm = TypeMap::latest();

View file

@ -13,6 +13,7 @@ crate-type = ["cdylib"]
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
js-sys = "0.3" js-sys = "0.3"
web-sys = { version = "0.3", features = ["console"] }
futures-channel = "0.3" futures-channel = "0.3"
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"

View file

@ -797,7 +797,18 @@ impl WasmClient {
let request_bytes = request let request_bytes = request
.to_bytes() .to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?; .map_err(|e| js_error(&format!("encode failed: {}", e)))?;
web_sys::console::log_1(
&format!(
"[mtp wasm] create_pipe sending PipeRequest id={} description={description} bytes={}",
pipe_id,
request_bytes.len()
)
.into(),
);
transport.send_frame(&request_bytes).await?; transport.send_frame(&request_bytes).await?;
web_sys::console::log_1(
&format!("[mtp wasm] create_pipe sent PipeRequest id={pipe_id}").into(),
);
Ok(WasmPipeHandle { Ok(WasmPipeHandle {
pipe_id, pipe_id,
@ -824,7 +835,13 @@ impl WasmClient {
let resp_bytes = resp let resp_bytes = resp
.to_bytes() .to_bytes()
.map_err(|e| js_error(&format!("encode failed: {}", e)))?; .map_err(|e| js_error(&format!("encode failed: {}", e)))?;
web_sys::console::log_1(
&format!("[mtp wasm] accept_pipe sending PipeResponse id={pipe_id} accepted=true bytes={}", resp_bytes.len()).into(),
);
transport.send_frame(&resp_bytes).await?; transport.send_frame(&resp_bytes).await?;
web_sys::console::log_1(
&format!("[mtp wasm] accept_pipe sent PipeResponse id={pipe_id}").into(),
);
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.pending_pipes.borrow_mut().insert(pipe_id, tx); self.pending_pipes.borrow_mut().insert(pipe_id, tx);

View file

@ -23,6 +23,12 @@ pub(crate) fn log_stream_error_code(error: &JsValue, context: &str) {
let stream_error_code = js_sys::Reflect::get(error, &JsValue::from_str("streamErrorCode")) let stream_error_code = js_sys::Reflect::get(error, &JsValue::from_str("streamErrorCode"))
.ok() .ok()
.and_then(|v| v.as_f64()); .and_then(|v| v.as_f64());
if matches!(stream_error_code, Some(0.0)) {
// WebTransport reports peer-driven stream shutdown as code 0 in this
// environment. For one-frame handshake streams, that is expected and
// should not be surfaced as a warning.
return;
}
let message = error let message = error
.as_string() .as_string()
.or_else(|| { .or_else(|| {
@ -138,21 +144,24 @@ impl WasmTransport {
if let Some(hashes) = cert_hashes { if let Some(hashes) = cert_hashes {
let wt_hashes = js_sys::Array::new(); let wt_hashes = js_sys::Array::new();
for h in hashes { for h in hashes {
if let Some((algo, hex_val)) = h.split_once(':') { let (algo, hex_val) = match h.split_once(':') {
if let Ok(bytes) = hex::decode(hex_val) { Some((algo, hex_val)) => (algo, hex_val),
let hash = js_sys::Object::new(); None => ("sha-256", h.as_str()),
js_sys::Reflect::set( };
&hash,
&JsValue::from_str("algorithm"), if let Ok(bytes) = hex::decode(hex_val) {
&JsValue::from_str(algo), let hash = js_sys::Object::new();
)?; js_sys::Reflect::set(
js_sys::Reflect::set( &hash,
&hash, &JsValue::from_str("algorithm"),
&JsValue::from_str("value"), &JsValue::from_str(algo),
&js_sys::Uint8Array::from(&bytes[..]), )?;
)?; js_sys::Reflect::set(
wt_hashes.push(&hash); &hash,
} &JsValue::from_str("value"),
&js_sys::Uint8Array::from(&bytes[..]),
)?;
wt_hashes.push(&hash);
} }
} }
if wt_hashes.length() > 0 { if wt_hashes.length() > 0 {