Recover MTP/USB after Mac sleep without requiring a reboot.
Tear down stale libusb handles on sleep/wake, skip CloseSession on dead pipes, and aggressively rescan until the Switch reconnects.
This commit is contained in:
@@ -90,8 +90,11 @@ MTPSession::MTPSession() {
|
||||
}
|
||||
|
||||
MTPSession::~MTPSession() {
|
||||
if (connected_) disconnect();
|
||||
if (ctx_) libusb_exit(ctx_);
|
||||
try { disconnect(true); } catch (...) {}
|
||||
if (ctx_) {
|
||||
libusb_exit(ctx_);
|
||||
ctx_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Device detection ─────────────────────────────────────────────────────────
|
||||
@@ -293,20 +296,26 @@ DeviceInfo MTPSession::connect() {
|
||||
}
|
||||
|
||||
// ─── Disconnect ───────────────────────────────────────────────────────────────
|
||||
void MTPSession::disconnect() {
|
||||
if (!connected_.load()) return;
|
||||
connected_.store(false);
|
||||
void MTPSession::disconnect(bool force) {
|
||||
const bool was_connected = connected_.exchange(false);
|
||||
if (!was_connected && !handle_) return;
|
||||
|
||||
try {
|
||||
send_command(OpCode::CloseSession);
|
||||
receive_response(); // best-effort; ignore result
|
||||
} catch (...) {}
|
||||
// After host sleep the bulk pipe is often dead — CloseSession would hang
|
||||
// on a 5s timeout and leave Darwin USB in a worse state.
|
||||
if (!force && handle_) {
|
||||
try {
|
||||
send_command(OpCode::CloseSession);
|
||||
receive_response(); // best-effort; ignore result
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
if (handle_) {
|
||||
// Best-effort; after suspend these calls frequently return errors.
|
||||
libusb_release_interface(handle_, ep_.interface_num);
|
||||
libusb_close(handle_);
|
||||
handle_ = nullptr;
|
||||
}
|
||||
transaction_id_ = 0;
|
||||
}
|
||||
|
||||
// ─── Raw USB I/O ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -40,7 +40,8 @@ public:
|
||||
DeviceInfo connect();
|
||||
|
||||
// Close MTP session and release USB resources.
|
||||
void disconnect();
|
||||
// force=true skips CloseSession (use after sleep/wake when the pipe is dead).
|
||||
void disconnect(bool force = false);
|
||||
|
||||
bool is_connected() const { return connected_.load(); }
|
||||
|
||||
|
||||
@@ -61,6 +61,10 @@ public:
|
||||
void do_create_folder(const std::string& name);
|
||||
void reconnect();
|
||||
|
||||
// macOS sleep/wake — tear down stale libusb state and reconnect.
|
||||
void notify_system_sleep();
|
||||
void notify_system_wake();
|
||||
|
||||
bool connected() const { return connected_.load(); }
|
||||
|
||||
// Drop-zone rect (CSS pixels, origin top-left) updated by the web UI.
|
||||
@@ -86,6 +90,8 @@ private:
|
||||
void device_monitor_loop(std::stop_token st);
|
||||
void on_device_connected(mtp::DeviceInfo di);
|
||||
void on_device_disconnected();
|
||||
// Drop session + libusb context without MTP CloseSession (post-sleep recovery).
|
||||
void force_usb_teardown(const std::string& status_key);
|
||||
|
||||
// ── Status bar ────────────────────────────────────────────────────────────
|
||||
// key is an i18n id (e.g. "status.connected"); args fill {name}/{error}/… placeholders.
|
||||
@@ -101,7 +107,11 @@ private:
|
||||
std::unique_ptr<transfer::TransferEngine> engine_;
|
||||
|
||||
std::atomic<bool> connected_{false};
|
||||
std::atomic<bool> usb_recovery_pending_{false};
|
||||
// After wake/sleep, poll USB more often for a while (monitor iterations).
|
||||
std::atomic<int> fast_reconnect_remaining_{0};
|
||||
std::jthread monitor_thread_;
|
||||
void* wake_observer_ = nullptr; // OmniMTPWakeObserver*
|
||||
mutable std::mutex device_mtx_;
|
||||
mtp::DeviceInfo device_info_;
|
||||
|
||||
|
||||
109
src/ui/App.mm
109
src/ui/App.mm
@@ -1,5 +1,6 @@
|
||||
#include "App.hpp"
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#include <stdexcept>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
@@ -51,25 +52,82 @@ static std::string fmt_eta(double s) {
|
||||
char b[16]; snprintf(b,sizeof(b),"%dh%02dm",si/3600,(si%3600)/60); return b;
|
||||
}
|
||||
|
||||
} // namespace omniMTP
|
||||
|
||||
// ─── Sleep/wake observer (must live outside the C++ namespace for ObjC) ────────
|
||||
@interface OmniMTPWakeObserver : NSObject
|
||||
@property (nonatomic, assign) omniMTP::App* app;
|
||||
@end
|
||||
|
||||
@implementation OmniMTPWakeObserver
|
||||
- (void)onWillSleep:(NSNotification*)notification {
|
||||
(void)notification;
|
||||
if (self.app) self.app->notify_system_sleep();
|
||||
}
|
||||
- (void)onDidWake:(NSNotification*)notification {
|
||||
(void)notification;
|
||||
if (self.app) self.app->notify_system_wake();
|
||||
}
|
||||
@end
|
||||
|
||||
namespace omniMTP {
|
||||
|
||||
// ─── App lifecycle ────────────────────────────────────────────────────────────
|
||||
App::App() : download_path_(fs::path(std::getenv("HOME") ? std::getenv("HOME") : "/")) {}
|
||||
App::~App() { shutdown(); }
|
||||
|
||||
void App::init() {
|
||||
download_path_ = fs::path(std::getenv("HOME") ? std::getenv("HOME") : "/");
|
||||
|
||||
OmniMTPWakeObserver* observer = [OmniMTPWakeObserver new];
|
||||
observer.app = this;
|
||||
wake_observer_ = (__bridge_retained void*)observer;
|
||||
NSNotificationCenter* nc = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
[nc addObserver:observer selector:@selector(onWillSleep:)
|
||||
name:NSWorkspaceWillSleepNotification object:nil];
|
||||
[nc addObserver:observer selector:@selector(onDidWake:)
|
||||
name:NSWorkspaceDidWakeNotification object:nil];
|
||||
|
||||
monitor_thread_ = std::jthread([this](std::stop_token st) { device_monitor_loop(st); });
|
||||
}
|
||||
|
||||
void App::shutdown() {
|
||||
if (wake_observer_) {
|
||||
OmniMTPWakeObserver* observer = (__bridge_transfer OmniMTPWakeObserver*)wake_observer_;
|
||||
wake_observer_ = nullptr;
|
||||
observer.app = nullptr;
|
||||
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:observer];
|
||||
}
|
||||
monitor_thread_.request_stop();
|
||||
engine_.reset();
|
||||
if (connected_.load())
|
||||
try { if (session_) session_->disconnect(); } catch (...) {}
|
||||
if (session_)
|
||||
try { session_->disconnect(true); } catch (...) {}
|
||||
}
|
||||
|
||||
void App::notify_system_sleep() {
|
||||
// Drop the session before USB powers down so we don't keep a zombie handle.
|
||||
usb_recovery_pending_.store(true);
|
||||
fast_reconnect_remaining_.store(0);
|
||||
}
|
||||
|
||||
void App::notify_system_wake() {
|
||||
// Fresh libusb context + aggressive rescan until the Switch reappears.
|
||||
usb_recovery_pending_.store(true);
|
||||
fast_reconnect_remaining_.store(120); // ~30–60s of faster retries
|
||||
set_status("status.usb_recovering", {}, 8.f);
|
||||
}
|
||||
|
||||
// ─── Device monitor ───────────────────────────────────────────────────────────
|
||||
void App::device_monitor_loop(std::stop_token st) {
|
||||
while (!st.stop_requested()) {
|
||||
if (usb_recovery_pending_.exchange(false)) {
|
||||
force_usb_teardown("status.usb_recovering");
|
||||
// Let Darwin finish re-enumerating USB after wake.
|
||||
for (int i = 0; i < 6 && !st.stop_requested(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!connected_.load()) {
|
||||
try {
|
||||
auto sess = std::make_unique<mtp::MTPSession>();
|
||||
@@ -86,6 +144,7 @@ void App::device_monitor_loop(std::stop_token st) {
|
||||
});
|
||||
}
|
||||
on_device_connected(std::move(di));
|
||||
fast_reconnect_remaining_.store(0);
|
||||
} catch (...) {}
|
||||
} else {
|
||||
bool transfer_active = false;
|
||||
@@ -105,11 +164,36 @@ void App::device_monitor_loop(std::stop_token st) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < 8 && !st.stop_requested(); ++i)
|
||||
|
||||
int fast = fast_reconnect_remaining_.load();
|
||||
int slices = (fast > 0) ? 2 : 8; // 500ms vs 2s
|
||||
if (fast > 0)
|
||||
fast_reconnect_remaining_.fetch_sub(1);
|
||||
for (int i = 0; i < slices && !st.stop_requested(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
}
|
||||
|
||||
void App::force_usb_teardown(const std::string& status_key) {
|
||||
{
|
||||
std::lock_guard lock(device_mtx_);
|
||||
if (engine_) { engine_->cancel_all(); engine_.reset(); }
|
||||
ops_.reset();
|
||||
if (session_) {
|
||||
// force=true: skip MTP CloseSession; destroy libusb context entirely.
|
||||
try { session_->disconnect(true); } catch (...) {}
|
||||
session_.reset();
|
||||
}
|
||||
storages_.clear();
|
||||
remote_entries_.clear();
|
||||
remote_nav_stack_.clear();
|
||||
}
|
||||
connected_.store(false);
|
||||
remote_needs_refresh_ = false;
|
||||
if (!status_key.empty())
|
||||
set_status(status_key, {}, 8.f);
|
||||
}
|
||||
|
||||
void App::on_device_connected(mtp::DeviceInfo di) {
|
||||
{
|
||||
std::lock_guard lock(device_mtx_);
|
||||
@@ -140,18 +224,7 @@ void App::on_device_connected(mtp::DeviceInfo di) {
|
||||
}
|
||||
|
||||
void App::on_device_disconnected() {
|
||||
{
|
||||
std::lock_guard lock(device_mtx_);
|
||||
if (engine_) { engine_->cancel_all(); engine_.reset(); }
|
||||
ops_.reset();
|
||||
if (session_) { try { session_->disconnect(); } catch (...) {} session_.reset(); }
|
||||
storages_.clear();
|
||||
remote_entries_.clear();
|
||||
remote_nav_stack_.clear();
|
||||
}
|
||||
connected_.store(false);
|
||||
remote_needs_refresh_ = false;
|
||||
set_status("status.disconnected");
|
||||
force_usb_teardown("status.disconnected");
|
||||
}
|
||||
|
||||
// ─── Remote filesystem ────────────────────────────────────────────────────────
|
||||
@@ -402,6 +475,10 @@ void App::do_create_folder(const std::string& name) {
|
||||
catch (...) {}
|
||||
refresh_remote();
|
||||
}
|
||||
void App::reconnect() { on_device_disconnected(); }
|
||||
void App::reconnect() {
|
||||
usb_recovery_pending_.store(true);
|
||||
fast_reconnect_remaining_.store(60);
|
||||
set_status("status.usb_recovering", {}, 8.f);
|
||||
}
|
||||
|
||||
} // namespace omniMTP
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "Über OmniMTP",
|
||||
"about.tagline": "Nintendo Switch MTP-Client für macOS",
|
||||
"about.version": "Version {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "USB wird nach dem Ruhezustand neu verbunden…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "About OmniMTP",
|
||||
"about.tagline": "Nintendo Switch MTP Client for macOS",
|
||||
"about.version": "Version {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "Reconnecting USB after sleep…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "Acerca de OmniMTP",
|
||||
"about.tagline": "Cliente MTP de Nintendo Switch para macOS",
|
||||
"about.version": "Versión {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "Reconectando USB tras la suspensión…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "À propos d’OmniMTP",
|
||||
"about.tagline": "Client MTP Nintendo Switch pour macOS",
|
||||
"about.version": "Version {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "Reconnexion USB après veille…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "Informazioni su OmniMTP",
|
||||
"about.tagline": "Client MTP Nintendo Switch per macOS",
|
||||
"about.version": "Versione {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "Riconnessione USB dopo lo standby…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "OmniMTPについて",
|
||||
"about.tagline": "macOS向け Nintendo Switch MTPクライアント",
|
||||
"about.version": "バージョン {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "スリープ後、USBを再接続しています…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "Over OmniMTP",
|
||||
"about.tagline": "Nintendo Switch MTP-client voor macOS",
|
||||
"about.version": "Versie {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "USB opnieuw verbinden na slaapstand…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "Sobre o OmniMTP",
|
||||
"about.tagline": "Cliente MTP Nintendo Switch para macOS",
|
||||
"about.version": "Versão {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "Reconectando USB após a suspensão…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "О программе OmniMTP",
|
||||
"about.tagline": "MTP-клиент Nintendo Switch для macOS",
|
||||
"about.version": "Версия {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "Повторное подключение USB после сна…"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,6 @@
|
||||
"menu.about": "关于 OmniMTP",
|
||||
"about.tagline": "适用于 macOS 的 Nintendo Switch MTP 客户端",
|
||||
"about.version": "版本 {version}",
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW"
|
||||
"about.copyright": "Copyright © 2026 NiklasCFW",
|
||||
"status.usb_recovering": "休眠后正在重新连接 USB…"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user