Zackriya-Solutions/meetily · error

Copy task join error: {}

Error message

Copy task join error: {}

What it means

run_import (import.rs:361) spawns the file copy on a blocking thread and the JoinError from tokio::task::spawn_blocking(...).await is wrapped with this message. JoinError means the task never completed normally: it panicked, or it was cancelled at runtime shutdown. The inner copy io::Error has a separate message ('Failed to copy audio file'), so this variant is about the task itself dying, not the filesystem.

Source

Thrown at frontend/src-tauri/src/audio/import.rs:361

    let meeting_folder = create_meeting_folder(&base_folder, &title, false)?;

    // Copy audio file to meeting folder
    emit_progress(&app, "copying", 10, "Copying audio file...");

    let dest_filename = format!(
        "audio.{}",
        source
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("mp4")
    );
    let dest_path = meeting_folder.join(&dest_filename);

    let src = source.clone();
    let dst = dest_path.clone();
    tokio::task::spawn_blocking(move || std::fs::copy(&src, &dst))
        .await
        .map_err(|e| anyhow!("Copy task join error: {}", e))?
        .map_err(|e| anyhow!("Failed to copy audio file: {}", e))?;

    info!("Copied audio to: {}", dest_path.display());

    // Check for cancellation
    if IMPORT_CANCELLED.load(Ordering::SeqCst) {
        // Cleanup: remove the meeting folder
        let _ = std::fs::remove_dir_all(&meeting_folder);
        return Err(anyhow!("Import cancelled"));
    }

    emit_progress(&app, "decoding", 15, "Decoding audio file...");

    // Decode the audio file with progress updates
    let app_for_decode = app.clone();
    let decode_progress = Box::new(move |progress: u32, msg: &str| {
        // Map decode progress: 15% + (progress * 0.05) to go from 15% to 20%
        let overall_progress = 15 + ((progress as f32 * 0.05) as u32);

View on GitHub (pinned to 0281737d87)

Solutions

  1. Check the JoinError details: if it is a panic, the panic payload is in the message string - log it and fix the root cause; if cancelled, the app was shutting down, which is expected during quit.
  2. Harden the path: sanitize/reject incoming path strings containing NUL or control characters before spawning the copy.
  3. Do not quit the app mid-import - or accept that shutdown cancels the copy and clean orphan meeting folders on next launch.
  4. For panic resilience, replace fs::copy with an explicit read/write loop returning io::Result instead of panicking on invalid paths.
  5. If this recurs on shutdown, block window close while IMPORT_IN_PROGRESS is set, or move copies to a resilient queue.

Example fix

// before: fs::copy can panic on a NUL-contaminated path, killing the blocking task
tokio::task::spawn_blocking(move || std::fs::copy(&src, &dst)).await
    .map_err(|e| anyhow!("Copy task join error: {}", e))?;

// after: validate the path, and copy without panic-on-invalid-input
if src.to_str().map_or(true, |s| s.contains('\0')) {
    return Err(anyhow!("Invalid source path"));
}
tokio::task::spawn_blocking(move || std::fs::copy(&src, &dst)).await
    .map_err(|e| anyhow!("Copy task join error: {}", e))??;
Defensive patterns

Strategy: retry

Validate before calling

// Rust: reject paths that can panic fs::copy before spawning
fn copy_is_safe(p: &Path) -> bool {
    p.to_str().map_or(false, |s| !s.contains('\0') && !s.contains('\u{7f}'))
}
if !copy_is_safe(&src) { return Err(anyhow!("Invalid source path")); }

Try / catch

// Retry once on JoinError (transient panic/shutdown race), then surface details
let copied = match tokio::task::spawn_blocking(move || std::fs::copy(&src, &dst)).await {
    Ok(r) => r,
    Err(join) if join.is_panic() => {
        warn!("copy panicked once: {}", join);
        tokio::task::spawn_blocking(move || std::fs::copy(&src2, &dst2)).await
            .map_err(|e| anyhow!("Copy task join error: {}", e))??
    }
    Err(join) => return Err(anyhow!("Copy task join error: {}", join)),
}?;

Prevention

When it happens

Trigger: std::fs::copy panicking inside the blocking task (a path containing a NUL byte from a mangled IPC string is the classic panic source); the tokio runtime being shut down while the import future still awaits (app quit during import); aborting the task handle externally. Note: the cancellation flag is checked only after the copy, so a user Cancel does NOT produce this error - the copy runs to completion first.

Common situations: Closing the Tauri window / quitting mid-copy (runtime drops, spawn_blocking tasks cancelled); a path string crossing IPC with embedded control characters causing fs::copy to panic; very large files over flaky network shares where the app is closed before completion; test harnesses dropping the runtime early.

Related errors


AI-assisted analysis of Zackriya-Solutions/meetily@0281737d87 (2026-08-16). Data as JSON: /api/errors/af778fa0da71503b. Report an issue: GitHub.