diff -ru a/config.h.in b/config.h.in --- a/config.h.in 2026-07-31 17:20:11.245975661 +0000 +++ b/config.h.in 2026-07-31 17:20:11.279976543 +0000 @@ -4,6 +4,9 @@ don't. */ #undef HAVE_DECL_STRERROR_R +/* Define to 1 if you have the header file. */ +#undef HAVE_DISPATCH_DISPATCH_H + /* Define to 1 if you have the header file. */ #undef HAVE_DLFCN_H diff -ru a/configure b/configure --- a/configure 2026-07-31 17:20:11.245975661 +0000 +++ b/configure 2026-07-31 17:20:11.283976647 +0000 @@ -3555,6 +3555,12 @@ LDFLAGS="${LDFLAGS} -lobjc -Wl,-framework,IOKit,-framework,Cocoa,-framework,DiskArbitration" SET_FEATURES="${SET_FEATURES} -D_DARWIN_C_SOURCE" + ac_fn_c_check_header_compile "$LINENO" "dispatch/dispatch.h" "ac_cv_header_dispatch_dispatch_h" "$ac_includes_default" +if test "x$ac_cv_header_dispatch_dispatch_h" = xyes +then : + printf "%s\n" "#define HAVE_DISPATCH_DISPATCH_H 1" >>confdefs.h + +fi ;; *) SYS="${host_os}" diff -ru a/configure.ac b/configure.ac --- a/configure.ac 2026-07-31 17:20:11.245975661 +0000 +++ b/configure.ac 2026-07-31 17:20:11.283976647 +0000 @@ -53,6 +53,9 @@ AC_DEFINE([USE_IOKIT], 1, [Use IOKit for MMC access]) LDFLAGS="${LDFLAGS} -lobjc -Wl,-framework,IOKit,-framework,Cocoa,-framework,DiskArbitration" SET_FEATURES="${SET_FEATURES} -D_DARWIN_C_SOURCE" + dnl libdispatch/GCD is available since Mac OS X 10.6 (Snow Leopard). + dnl 10.5 and earlier need a pthread/CFRunLoop based fallback instead. + AC_CHECK_HEADERS([dispatch/dispatch.h]) ;; *) SYS="${host_os}" diff -ru a/src/file/mmc_device_darwin.c b/src/file/mmc_device_darwin.c --- a/src/file/mmc_device_darwin.c 2026-07-31 17:20:11.257975973 +0000 +++ b/src/file/mmc_device_darwin.c 2026-07-31 17:20:11.295976958 +0000 @@ -29,6 +29,8 @@ #include "util/macro.h" #include "util/strutl.h" +#include +#include #include #include @@ -66,6 +68,26 @@ #include #endif +/* + * libdispatch (Grand Central Dispatch) is available since Mac OS X 10.6 + * (Snow Leopard). On 10.5 and earlier there is no libdispatch at all, so + * a pthread + CFRunLoop based fallback is used instead. + * + * Note: Blocks syntax ("^{ ... }") is intentionally never used anywhere + * in this file, even in the GCD path below. Blocks require a compiler + * with Blocks support (Apple GCC or clang); this code must also build + * with a vanilla GCC toolchain (eg. for PowerPC 10.5/10.6 targets), so + * only the plain C function-pointer flavoured dispatch APIs are used. + */ +#ifdef HAVE_DISPATCH_DISPATCH_H +#include +#define MMC_HAVE_DISPATCH 1 +#else +/* No libdispatch (Mac OS X 10.5 and earlier): pthread is always present + * as part of libSystem, so use it directly for the fallback below. */ +#include +#include +#endif /* * @@ -77,6 +99,111 @@ disk_mounting }; +/* + * mmc_sem_t: simple binary/counting semaphore with a timed wait, + * used to synchronize with DiskArbitration completion callbacks. + */ +#ifdef MMC_HAVE_DISPATCH + +typedef dispatch_semaphore_t mmc_sem_t; + +static int mmc_sem_init(mmc_sem_t *sem) +{ + *sem = dispatch_semaphore_create(0); + return *sem ? 0 : -1; +} + +static void mmc_sem_destroy(mmc_sem_t *sem) +{ + if (*sem) { + dispatch_release(*sem); + *sem = NULL; + } +} + +static void mmc_sem_signal(mmc_sem_t *sem) +{ + dispatch_semaphore_signal(*sem); +} + +/* wait_ns == 0 means wait forever */ +static void mmc_sem_wait(mmc_sem_t *sem, uint64_t wait_ns) +{ + dispatch_time_t timeout = wait_ns ? + dispatch_time(DISPATCH_TIME_NOW, (int64_t)wait_ns) : + DISPATCH_TIME_FOREVER; + dispatch_semaphore_wait(*sem, timeout); +} + +#else /* !MMC_HAVE_DISPATCH : pthread mutex/cond fallback (Mac OS X 10.5) */ + +typedef struct { + pthread_mutex_t mutex; + pthread_cond_t cond; + int count; +} mmc_sem_t; + +static int mmc_sem_init(mmc_sem_t *sem) +{ + sem->count = 0; + if (0 != pthread_mutex_init(&sem->mutex, NULL)) { + return -1; + } + if (0 != pthread_cond_init(&sem->cond, NULL)) { + pthread_mutex_destroy(&sem->mutex); + return -1; + } + return 0; +} + +static void mmc_sem_destroy(mmc_sem_t *sem) +{ + pthread_cond_destroy(&sem->cond); + pthread_mutex_destroy(&sem->mutex); +} + +static void mmc_sem_signal(mmc_sem_t *sem) +{ + pthread_mutex_lock(&sem->mutex); + sem->count++; + pthread_cond_signal(&sem->cond); + pthread_mutex_unlock(&sem->mutex); +} + +/* wait_ns == 0 means wait forever */ +static void mmc_sem_wait(mmc_sem_t *sem, uint64_t wait_ns) +{ + struct timespec deadline; + + if (wait_ns) { + struct timeval now; + gettimeofday(&now, NULL); + uint64_t nsec = (uint64_t)now.tv_usec * 1000ULL + wait_ns; + deadline.tv_sec = now.tv_sec + (time_t)(nsec / 1000000000ULL); + deadline.tv_nsec = (long)(nsec % 1000000000ULL); + } + + pthread_mutex_lock(&sem->mutex); + while (sem->count == 0) { + int rc; + if (wait_ns) { + rc = pthread_cond_timedwait(&sem->cond, &sem->mutex, &deadline); + if (rc != 0) { + /* timed out (or error): stop waiting */ + break; + } + } else { + pthread_cond_wait(&sem->cond, &sem->mutex); + } + } + if (sem->count > 0) { + sem->count--; + } + pthread_mutex_unlock(&sem->mutex); +} + +#endif /* MMC_HAVE_DISPATCH */ + struct mmcdev { /* Interfaces required for low-level device communication */ IOCFPlugInInterface **plugInInterface; @@ -90,8 +217,16 @@ DADiskRef disk; DASessionRef session; enum disk_state_e disk_state; - dispatch_semaphore_t sync_sem; + bool session_active; /* true once iokit_da_init() has succeeded */ +#ifdef MMC_HAVE_DISPATCH + mmc_sem_t sync_sem; dispatch_queue_t background_queue; +#else + /* Set by iokit_mount_complete()/iokit_unmount_complete() and polled + * by iokit_wait_op_done() while pumping the run loop on the same + * thread - see comment above iokit_wait_op_done(). */ + volatile bool op_done; +#endif }; int device_send_cmd(MMCDEV *mmc, const uint8_t *cmd, uint8_t *buf, size_t tx, size_t rx) @@ -204,7 +339,11 @@ BD_DEBUG(DBG_MMC, "Disc unmounted\n"); mmc->disk_state = disk_unmounted; } - dispatch_semaphore_signal(mmc->sync_sem); +#ifdef MMC_HAVE_DISPATCH + mmc_sem_signal(&mmc->sync_sem); +#else + mmc->op_done = true; +#endif } static void iokit_mount_complete(DADiskRef disk, DADissenterRef dissenter, @@ -220,12 +359,36 @@ BD_DEBUG(DBG_MMC, "Disc mounted\n"); mmc->disk_state = disk_mounted; } - dispatch_semaphore_signal(mmc->sync_sem); +#ifdef MMC_HAVE_DISPATCH + mmc_sem_signal(&mmc->sync_sem); +#else + mmc->op_done = true; +#endif +} + +#ifndef MMC_HAVE_DISPATCH +/* No libdispatch available (Mac OS X 10.5): the DiskArbitration session + * is scheduled on the calling thread's run loop instead of a background + * queue, so completion callbacks only run while that same thread pumps + * its run loop. Since caller and callback share one thread here, a + * plain flag (mmc->op_done) is enough - there is no concurrent access. + */ +static void iokit_wait_op_done(MMCDEV *mmc, CFTimeInterval timeout_sec) +{ + CFTimeInterval waited = 0.0; + const CFTimeInterval slice = 0.1; + + mmc->op_done = false; + while (!mmc->op_done && waited < timeout_sec) { + CFRunLoopRunInMode(kCFRunLoopDefaultMode, slice, true); + waited += slice; + } } +#endif /* Unmount the disk at mmc->disk - * Note: This MAY NOT be called on the background queue, - * as that would lead to a deadlock. + * Note: With the libdispatch backend, this MAY NOT be called on the + * background queue, as that would lead to a deadlock. */ static int iokit_unmount(MMCDEV *mmc) { if (disk_unmounted == mmc->disk_state) { @@ -235,24 +398,32 @@ BD_DEBUG(DBG_MMC, "Unmounting disk\n"); DADiskUnmount(mmc->disk, kDADiskUnmountOptionForce, iokit_unmount_complete, mmc); - dispatch_semaphore_wait(mmc->sync_sem, DISPATCH_TIME_FOREVER); +#ifdef MMC_HAVE_DISPATCH + mmc_sem_wait(&mmc->sync_sem, 0); +#else + iokit_wait_op_done(mmc, 30.0); +#endif return (mmc->disk_state == disk_unmounted) ? 0 : -1; } /* Mount the disk at mmc->disk - * Note: This MAY NOT be called on the background queue, - * as that would lead to a deadlock. + * Note: With the libdispatch backend, this MAY NOT be called on the + * background queue, as that would lead to a deadlock. */ static int iokit_mount(MMCDEV *mmc) { if (disk_mounted != mmc->disk_state) { if (mmc->disk && mmc->session) { mmc->disk_state = disk_mounting; DADiskMount(mmc->disk, NULL, kDADiskMountOptionDefault, iokit_mount_complete, mmc); - dispatch_semaphore_wait(mmc->sync_sem, DISPATCH_TIME_FOREVER); +#ifdef MMC_HAVE_DISPATCH + mmc_sem_wait(&mmc->sync_sem, 0); +#else + iokit_wait_op_done(mmc, 30.0); +#endif } } - return (mmc->disk_state == disk_unmounted) ? 0 : -1; + return (mmc->disk_state == disk_mounted) ? 0 : -1; } static int iokit_find_service_matching(MMCDEV *mmc, io_service_t *servp) { @@ -363,7 +534,11 @@ } mmc->disk_state = disk_appeared; - dispatch_semaphore_signal(mmc->sync_sem); +#ifdef MMC_HAVE_DISPATCH + mmc_sem_signal(&mmc->sync_sem); +#else + mmc->op_done = true; +#endif CFStringRef reason = CFSTR("Disk is going to be mounted libaacs"); return DADissenterCreate(kCFAllocatorDefault, kDAReturnBusy, reason); @@ -384,13 +559,33 @@ return -1; } +#ifdef MMC_HAVE_DISPATCH mmc->background_queue = dispatch_queue_create("org.videolan.libaacs", DISPATCH_QUEUE_SERIAL); DASessionSetDispatchQueue(mmc->session, mmc->background_queue); +#else + /* No libdispatch: schedule on the calling thread's run loop instead. + * All DA calls and the wait loops below must happen on this same + * thread for callbacks to be delivered - see iokit_wait_op_done(). */ + DASessionScheduleWithRunLoop(mmc->session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); +#endif // Register event callbacks DARegisterDiskMountApprovalCallback(mmc->session, NULL, iokit_mount_approval_cb, mmc); - mmc->sync_sem = dispatch_semaphore_create(0); +#ifdef MMC_HAVE_DISPATCH + if (0 != mmc_sem_init(&mmc->sync_sem)) { + BD_DEBUG(DBG_MMC | DBG_CRIT, "Could not create sync semaphore\n"); + DAUnregisterApprovalCallback(mmc->session, iokit_mount_approval_cb, mmc); + DASessionSetDispatchQueue(mmc->session, NULL); + CFRelease(mmc->disk); + mmc->disk = NULL; + CFRelease(mmc->session); + mmc->session = NULL; + return -1; + } +#endif + + mmc->session_active = true; return 0; } @@ -403,7 +598,11 @@ * can mount it. */ DAUnregisterApprovalCallback(mmc->session, iokit_mount_approval_cb, mmc); +#ifdef MMC_HAVE_DISPATCH DASessionSetDispatchQueue(mmc->session, NULL); +#else + DASessionUnscheduleFromRunLoop(mmc->session, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); +#endif CFRelease(mmc->session); mmc->session = NULL; } @@ -413,7 +612,12 @@ mmc->disk = NULL; } - dispatch_release(mmc->sync_sem); +#ifdef MMC_HAVE_DISPATCH + if (mmc->session_active) { + mmc_sem_destroy(&mmc->sync_sem); + } +#endif + mmc->session_active = false; } static int mmc_open_iokit(const char *path, MMCDEV *mmc) { @@ -426,6 +630,7 @@ mmc->disk = NULL; mmc->session = NULL; mmc->disk_state = disk_mounted; + mmc->session_active = false; /* get the bsd name associated with this mount */ rc = get_mounted_device_from_path(mmc, path); @@ -497,11 +702,35 @@ return mmc; } +#ifdef MMC_HAVE_DISPATCH +struct device_close_ctx { + MMCDEV *mmc; + int rc; +}; + +/* Plain C function-pointer callback for dispatch_sync_f() below. + * Deliberately NOT a Blocks literal ("^{ ... }"): this file must build + * with a vanilla GCC toolchain that has no Blocks support. + */ +static void device_close_check_disk_appeared(void *context) +{ + struct device_close_ctx *ctx = context; + + if (disk_appeared != ctx->mmc->disk_state) { + BD_DEBUG(DBG_MMC | DBG_CRIT, "Timeout waiting for the disc to appear again!\n"); + iokit_da_destroy(ctx->mmc); + ctx->rc = -1; + return; + } + ctx->rc = 0; +} +#endif + void device_close(MMCDEV **pp) { - __block int rc = 0; if (pp && *pp) { MMCDEV *mmc = *pp; + int rc; /* When the exclusive access to the drive is released, * the OS will see the device like a "new" device and @@ -538,7 +767,7 @@ IODestroyPlugInInterface(mmc->plugInInterface); } - if (!mmc->sync_sem) { + if (!mmc->session_active) { /* open failed before iokit_da_init() */ X_FREE(*pp); return; @@ -552,21 +781,30 @@ * so the long timeout shouldn't do much harm for thse * cases. */ - dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 20 * 1E+9); - dispatch_semaphore_wait(mmc->sync_sem, timeout); +#ifdef MMC_HAVE_DISPATCH + mmc_sem_wait(&mmc->sync_sem, 20ULL * 1000000000ULL); /* It is crucial that this is done on the event handling queue * else callbacks could be received while this code runs. */ - dispatch_sync(mmc->background_queue, ^{ - if (disk_appeared != mmc->disk_state) { - BD_DEBUG(DBG_MMC | DBG_CRIT, "Timeout waiting for the disc to appear again!\n"); - iokit_da_destroy(mmc); - rc = -1; - return; - } + struct device_close_ctx ctx = { .mmc = mmc, .rc = 0 }; + dispatch_sync_f(mmc->background_queue, &ctx, device_close_check_disk_appeared); + rc = ctx.rc; +#else + /* No background queue: the wait loop below drives the run loop + * itself, so there is no separate event handling context to + * synchronize with - just check the state directly afterwards. + */ + iokit_wait_op_done(mmc, 20.0); + + if (disk_appeared != mmc->disk_state) { + BD_DEBUG(DBG_MMC | DBG_CRIT, "Timeout waiting for the disc to appear again!\n"); + iokit_da_destroy(mmc); + rc = -1; + } else { rc = 0; - }); + } +#endif if (rc == 0) { /* Disk appeared successfully, mount it.