|
3 | 3 | import asyncio |
4 | 4 | import contextlib |
5 | 5 | import logging |
| 6 | +import shutil |
6 | 7 | from functools import wraps |
7 | 8 | from io import BytesIO |
8 | 9 | from pathlib import Path |
@@ -101,6 +102,61 @@ def parse_duration_ns(discover_output: str) -> int: |
101 | 102 | return duration_ns |
102 | 103 |
|
103 | 104 |
|
| 105 | +async def check_and_recover_mcap(mcap_path: Path) -> None: |
| 106 | + """ |
| 107 | + Check if mcap binary is available, run mcap doctor on the file, |
| 108 | + and if it fails, run mcap recover to fix the file. |
| 109 | + """ |
| 110 | + # Check if mcap binary exists |
| 111 | + mcap_binary = shutil.which("mcap") |
| 112 | + if not mcap_binary: |
| 113 | + logger.warning("mcap binary not found, skipping doctor/recover check") |
| 114 | + return |
| 115 | + |
| 116 | + # Ensure path is absolute and exists |
| 117 | + if not mcap_path.exists(): |
| 118 | + logger.warning(f"MCAP file not found: {mcap_path}") |
| 119 | + return |
| 120 | + |
| 121 | + logger.info(f"Running mcap doctor on {mcap_path}") |
| 122 | + # Run mcap doctor |
| 123 | + doctor_cmd = ["mcap", "doctor", str(mcap_path)] |
| 124 | + doctor_proc = await asyncio.create_subprocess_exec( |
| 125 | + *doctor_cmd, |
| 126 | + stdout=asyncio.subprocess.PIPE, |
| 127 | + stderr=asyncio.subprocess.PIPE, |
| 128 | + text=False, |
| 129 | + ) |
| 130 | + stdout_bytes, stderr_bytes = await doctor_proc.communicate() |
| 131 | + stdout = stdout_bytes.decode("utf-8", "ignore") |
| 132 | + stderr = stderr_bytes.decode("utf-8", "ignore") |
| 133 | + |
| 134 | + if doctor_proc.returncode == 0: |
| 135 | + logger.info(f"mcap doctor passed for {mcap_path}: {stdout.strip()}") |
| 136 | + return |
| 137 | + |
| 138 | + logger.error(f"mcap doctor failed for {mcap_path} (code={doctor_proc.returncode}): {stderr}") |
| 139 | + logger.info(f"Running mcap recover to fix {mcap_path}") |
| 140 | + # Run mcap recover to replace the file |
| 141 | + recover_cmd = ["mcap", "recover", str(mcap_path), "-o", str(mcap_path)] |
| 142 | + recover_proc = await asyncio.create_subprocess_exec( |
| 143 | + *recover_cmd, |
| 144 | + stdout=asyncio.subprocess.PIPE, |
| 145 | + stderr=asyncio.subprocess.PIPE, |
| 146 | + text=False, |
| 147 | + ) |
| 148 | + recover_stdout_bytes, recover_stderr_bytes = await recover_proc.communicate() |
| 149 | + recover_stdout = recover_stdout_bytes.decode("utf-8", "ignore") |
| 150 | + recover_stderr = recover_stderr_bytes.decode("utf-8", "ignore") |
| 151 | + |
| 152 | + if recover_proc.returncode != 0: |
| 153 | + logger.error( |
| 154 | + f"mcap recover failed for {mcap_path} (code={recover_proc.returncode}): {recover_stderr}", |
| 155 | + ) |
| 156 | + else: |
| 157 | + logger.info(f"mcap recover completed for {mcap_path}: {recover_stdout.strip()}") |
| 158 | + |
| 159 | + |
104 | 160 | @cached() |
105 | 161 | async def build_thumbnail_bytes(path: Path) -> bytes: |
106 | 162 | """ |
@@ -187,6 +243,9 @@ async def extract_mcap_recordings() -> None: |
187 | 243 | logger.info(f"Skipping MCAP extract, file in use: {mcap_path}") |
188 | 244 | continue |
189 | 245 |
|
| 246 | + # Check and recover MCAP file if mcap binary is available |
| 247 | + await check_and_recover_mcap(mcap_path) |
| 248 | + |
190 | 249 | command = [ |
191 | 250 | "mcap-foxglove-video-extract", |
192 | 251 | str(mcap_path), |
|
0 commit comments