From 636ffe74d7eb84b100068b39dc0100915af1fc08 Mon Sep 17 00:00:00 2001 From: Rob Loach Date: Sat, 5 Sep 2026 16:06:28 -0400 Subject: [PATCH] cli: Add a --cloudsync command to sync the Cloud from the CLI --- CHANGES.md | 1 + docs/retroarch.6 | 4 +++ retroarch.c | 73 +++++++++++++++++++++++++++++++++++--- tasks/task_cloudsync.c | 58 +++++++++++++++++++++++------- tasks/tasks_internal.h | 5 ++- ui/drivers/ui_cocoatouch.m | 2 +- 6 files changed, 124 insertions(+), 19 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4b55601348a4..1af9246dc143 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,7 @@ - CONFIG: Fix saving main configuration after load configuration - DOS: RetroArch for DOS can now start up and handle keyboard correctly - CLOUDSYNC: Conflict resolution options +- CLOUDSYNC: Add --cloudsync command line option to run a sync and exit - EMSCRIPTEN: Added dice to core selection dropdown - EMSCRIPTEN: Numerous bugfixes - FFMPEG: new features for save state, variable strength seek, audio/subtitle stream controls diff --git a/docs/retroarch.6 b/docs/retroarch.6 index d38a8ee565f0..15641ae3717a 100644 --- a/docs/retroarch.6 +++ b/docs/retroarch.6 @@ -61,6 +61,10 @@ If no arguments are passed to retroarch, it is equivalent to calling retroarch w \fB--features\fR Prints available features compiled into RetroArch, then exits. +.TP +\fB--cloudsync\fR +Runs a cloud sync with the configured cloud sync driver, then exits. + .TP \fB-L PATH, --libretro PATH\fR Path to a libretro implementation which is to be used. diff --git a/retroarch.c b/retroarch.c index 8d555a98cc76..a911798459ff 100644 --- a/retroarch.c +++ b/retroarch.c @@ -334,7 +334,8 @@ enum RA_OPT_SET_SHADER, RA_OPT_DATABASE_SCAN, RA_OPT_ACCESSIBILITY, - RA_OPT_LOAD_MENU_ON_ERROR + RA_OPT_LOAD_MENU_ON_ERROR, + RA_OPT_CLOUDSYNC }; /* DRIVERS */ @@ -4141,7 +4142,7 @@ bool command_event(enum event_command cmd, void *data) #ifdef HAVE_CLOUDSYNC /* Sync on core unload if in automatic mode */ if (settings->uints.cloud_sync_sync_mode == CLOUD_SYNC_MODE_AUTOMATIC) - task_push_cloud_sync(); + task_push_cloud_sync(NULL, NULL); #endif } @@ -5162,7 +5163,7 @@ bool command_event(enum event_command cmd, void *data) break; #ifdef HAVE_CLOUDSYNC case CMD_EVENT_CLOUD_SYNC: - task_push_cloud_sync(); + task_push_cloud_sync(NULL, NULL); break; case CMD_EVENT_CLOUD_SYNC_RESOLVE_KEEP_LOCAL: task_push_cloud_sync_resolve_keep_local(); @@ -6721,7 +6722,7 @@ int rarch_main(int argc, char *argv[], void *data) ); #ifdef HAVE_CLOUDSYNC if (settings->uints.cloud_sync_sync_mode == CLOUD_SYNC_MODE_AUTOMATIC) - task_push_cloud_sync(); + task_push_cloud_sync(NULL, NULL); #endif #ifdef HAVE_LAKKA sd_notify(0, "READY=1"); @@ -7306,6 +7307,11 @@ static void retroarch_print_help(const char *arg0) " --scan=PATH|FILE " "Import content from path.\n"); #endif +#ifdef HAVE_CLOUDSYNC + strlcpy_append(buf, sizeof(buf), &_len, + " --cloudsync " + "Run a cloud sync, then exits.\n"); +#endif strlcpy_append(buf, sizeof(buf), &_len, " -f, --fullscreen " @@ -7625,6 +7631,15 @@ void handle_dbscan_finished(retro_task_t *task, void *task_data, void *user_data, const char *err); #endif +#ifdef HAVE_CLOUDSYNC +/* --cloudsync: records the verdict where the blocked caller can read it. */ +static void handle_cloud_sync_cli_finished(retro_task_t *task, + void *task_data, void *user_data, const char *err) +{ + *(int*)user_data = err ? EXIT_FAILURE : EXIT_SUCCESS; +} +#endif + /** * retroarch_parse_input_and_config: * @argc : Count of (commandline) arguments. @@ -7718,6 +7733,9 @@ static bool retroarch_parse_input_and_config( { "entryslot", 1, NULL, 'e' }, #ifdef HAVE_LIBRETRODB { "scan", 1, NULL, RA_OPT_DATABASE_SCAN }, +#endif +#ifdef HAVE_CLOUDSYNC + { "cloudsync", 0, NULL, RA_OPT_CLOUDSYNC }, #endif { NULL, 0, NULL, 0 } }; @@ -7901,6 +7919,13 @@ static bool retroarch_parse_input_and_config( #ifdef HAVE_LIBRETRODB verbosity_enable(); retroarch_override_setting_set(RARCH_OVERRIDE_SETTING_DATABASE_SCAN, NULL); +#endif + break; + case RA_OPT_CLOUDSYNC: +#ifdef HAVE_CLOUDSYNC + /* Enable verbosity so that the sync's summary is printed. */ + verbosity_enable(); + retroarch_override_setting_set(RARCH_OVERRIDE_SETTING_VERBOSITY, NULL); #endif break; @@ -8357,6 +8382,46 @@ static bool retroarch_parse_input_and_config( exit(0); } } +#endif + break; + case RA_OPT_CLOUDSYNC: +#ifdef HAVE_CLOUDSYNC + { + int result = -1; + settings_t *settings = config_get_ptr(); + int reinit_flags = DRIVERS_CMD_ALL & + ~(DRIVER_VIDEO_MASK | DRIVER_AUDIO_MASK | DRIVER_MICROPHONE_MASK | DRIVER_INPUT_MASK | DRIVER_MIDI_MASK); + + if (!settings->bools.cloud_sync_enable) + { + RARCH_WARN("[CloudSync] Cloud Sync is disabled, nothing to sync.\n"); + exit(0); + } + + /* This is normally done in retroarch_main_init(), which isn't run here. */ + drivers_init(settings, reinit_flags, (enum driver_lifetime_flags)0, false); + retroarch_init_task_queue(); + cloud_sync_find_driver(settings->arrays.cloud_sync_driver, + "cloud sync driver", verbosity_is_enabled()); + + /* Create the task queue and repeatedly check it until it's complete. */ + if (task_push_cloud_sync(handle_cloud_sync_cli_finished, &result)) + { + while (result < 0) + { + task_queue_check(); + retro_sleep(10); + } + } + else + result = EXIT_FAILURE; + driver_uninit(DRIVERS_CMD_ALL, (enum driver_lifetime_flags)0); + task_queue_deinit(); +#ifdef HAVE_NETWORKING + net_http_deinit(); +#endif + exit(result); + } #endif break; default: diff --git a/tasks/task_cloudsync.c b/tasks/task_cloudsync.c index 42b8178db823..62f696fc91b2 100644 --- a/tasks/task_cloudsync.c +++ b/tasks/task_cloudsync.c @@ -99,6 +99,9 @@ typedef struct * than a process-wide cache: a fresh list per sync is a handful of * strdups, and it cannot go stale against the sync toggles. */ struct string_list *dirlist; + /* Optional completion callback supplied by whoever pushed the sync. */ + retro_task_callback_t cb; + void *user_data; } task_cloud_sync_state_t; /* Serialises appends to updated_server_manifest / @@ -188,6 +191,7 @@ static void task_cloud_sync_begin_handler(void *user_data, const char *path, boo else { RARCH_WARN(CSPFX "Begin failed.\n"); + sync_state->failures = true; task_free_title(task); task_set_title(task, strdup("Cloud Sync failed")); task_set_flags(task, RETRO_TASK_FLG_FINISHED, true); @@ -618,7 +622,9 @@ static void task_cloud_sync_add_to_updated_manifest(task_cloud_sync_state_t *syn idx = list->size; file_list_append(list, NULL, NULL, 0, 0, 0); file_list_set_alt_at_offset(list, idx, key); - list->list[idx].userdata = hash; + /* This is copied with strdup() so that the manifest owns its hash + * and can be freed without worrying about other callers. */ + list->list[idx].userdata = hash ? strdup(hash) : NULL; #ifdef HAVE_THREADS slock_unlock(tcs_manifest_lock); #endif @@ -847,6 +853,7 @@ static void task_cloud_sync_fetch_cb(void *user_data, const char *path, bool suc if (!string_is_equal(hash, CS_FILE_HASH(server_file))) sync_state->need_manifest_uploaded = true; sync_state->downloads++; + free(hash); } else { @@ -1048,7 +1055,10 @@ static void task_cloud_sync_upload_current_file(task_cloud_sync_state_t *sync_st RARCH_LOG(CSPFX "Uploading \"%s\".\n", path); - item->userdata = task_cloud_sync_md5_rfile(file); + /* The three-way compare may already have hashed this item; + * reassigning here leaked that hash. */ + if (!item->userdata) + item->userdata = task_cloud_sync_md5_rfile(file); filestream_seek(file, 0, SEEK_SET); task_cloud_sync_waiting_inc(sync_state); @@ -1637,6 +1647,7 @@ static void task_cloud_sync_task_handler(retro_task_t *task) if (!cloud_sync_begin(task_cloud_sync_begin_handler, task)) { RARCH_WARN(CSPFX "Could not begin.\n"); + sync_state->failures = true; task_free_title(task); task_set_title(task, strdup("Cloud Sync failed")); goto task_finished; @@ -1678,7 +1689,22 @@ static void task_cloud_sync_task_handler(retro_task_t *task) static void task_cloud_sync_cb(retro_task_t *task, void *task_data, void *user_data, const char *error) { - task_cloud_sync_state_t *sync_state = (task_cloud_sync_state_t *)task_data; + task_cloud_sync_state_t *sync_state = (task_cloud_sync_state_t *)task->state; + + /* The title is the sync's own summary ("Cloud Sync failed", + * "Cloud Sync finished with failures and conflicts"), so it + * doubles as the error string. */ + if (sync_state && sync_state->cb) + sync_state->cb(task, NULL, sync_state->user_data, + (sync_state->failures || sync_state->conflicts) + ? task->title : NULL); +} + +/* The state used to be released from the callback above, but as + * task_data - which this task never sets - so it leaked every sync. */ +static void task_cloud_sync_cleanup(retro_task_t *task) +{ + task_cloud_sync_state_t *sync_state = (task_cloud_sync_state_t *)task->state; if (!sync_state) return; @@ -1699,6 +1725,7 @@ static void task_cloud_sync_cb(retro_task_t *task, void *task_data, string_list_free(sync_state->dirlist); free(sync_state); + task->state = NULL; } static bool task_cloud_sync_task_finder(retro_task_t *task, void *user_data) @@ -1710,7 +1737,8 @@ static bool task_cloud_sync_task_finder(retro_task_t *task, void *user_data) return task->handler == task_cloud_sync_task_handler; } -static void task_push_cloud_sync_with_mode(int conflict_resolution) +static bool task_push_cloud_sync_with_mode(int conflict_resolution, + retro_task_callback_t cb, void *user_data) { char task_title[128]; task_finder_data_t find_data; @@ -1719,7 +1747,7 @@ static void task_push_cloud_sync_with_mode(int conflict_resolution) bool cloud_sync_enable = config_get_ptr()->bools.cloud_sync_enable; if (!cloud_sync_enable) - return; + return false; #ifdef HAVE_THREADS if (!tcs_manifest_lock) @@ -1730,12 +1758,12 @@ static void task_push_cloud_sync_with_mode(int conflict_resolution) if (task_queue_find(&find_data)) { RARCH_LOG(CSPFX "Already in progress.\n"); - return; + return false; } sync_state = (task_cloud_sync_state_t *)calloc(1, sizeof(task_cloud_sync_state_t)); if (!sync_state) - return; + return false; /* Captured here, on the main thread, for the worker the threaded * task queue runs the handler on. */ @@ -1750,14 +1778,14 @@ static void task_push_cloud_sync_with_mode(int conflict_resolution) if (!sync_state->dirlist) { free(sync_state); - return; + return false; } if (!(task = task_init())) { string_list_free(sync_state->dirlist); free(sync_state); - return; + return false; } /* calloc zero-fill is not a portable initializer for an atomic @@ -1769,6 +1797,8 @@ static void task_push_cloud_sync_with_mode(int conflict_resolution) task_cloud_sync_phase_set(sync_state, CLOUD_SYNC_PHASE_BEGIN); sync_state->start_time = cpu_features_get_time_usec(); sync_state->conflict_resolution = conflict_resolution; + sync_state->cb = cb; + sync_state->user_data = user_data; strlcpy_lit(task_title, "Cloud Sync in progress", sizeof(task_title)); @@ -1776,14 +1806,16 @@ static void task_push_cloud_sync_with_mode(int conflict_resolution) task->title = strdup(task_title); task->handler = task_cloud_sync_task_handler; task->callback = task_cloud_sync_cb; + task->cleanup = task_cloud_sync_cleanup; task->progress_cb = task_window_progress_cb; task_queue_push(task); + return true; } -void task_push_cloud_sync(void) +bool task_push_cloud_sync(retro_task_callback_t cb, void *user_data) { - task_push_cloud_sync_with_mode(0); + return task_push_cloud_sync_with_mode(0, cb, user_data); } void task_push_cloud_sync_update_driver(void) @@ -1806,11 +1838,11 @@ void task_push_cloud_sync_update_driver(void) void task_push_cloud_sync_resolve_keep_local(void) { RARCH_LOG(CSPFX "Starting sync with conflict resolution: keep local.\n"); - task_push_cloud_sync_with_mode(1); + task_push_cloud_sync_with_mode(1, NULL, NULL); } void task_push_cloud_sync_resolve_keep_server(void) { RARCH_LOG(CSPFX "Starting sync with conflict resolution: keep server.\n"); - task_push_cloud_sync_with_mode(2); + task_push_cloud_sync_with_mode(2, NULL, NULL); } diff --git a/tasks/tasks_internal.h b/tasks/tasks_internal.h index 8c9861cc60cc..41338b9fce03 100644 --- a/tasks/tasks_internal.h +++ b/tasks/tasks_internal.h @@ -419,7 +419,10 @@ extern const char* const input_builtin_autoconfs[]; /* cloud sync tasks */ void task_push_cloud_sync_update_driver(void); -void task_push_cloud_sync(void); +/* The cb parameter is the callback that's called when the sync is done. + * Its error argument is non-NULL when the sync failed or finished with + * failures or conflicts. Returns false if no sync was queued. */ +bool task_push_cloud_sync(retro_task_callback_t cb, void *user_data); void task_push_cloud_sync_resolve_keep_local(void); void task_push_cloud_sync_resolve_keep_server(void); diff --git a/ui/drivers/ui_cocoatouch.m b/ui/drivers/ui_cocoatouch.m index 886c76df042a..575ab01efdda 100644 --- a/ui/drivers/ui_cocoatouch.m +++ b/ui/drivers/ui_cocoatouch.m @@ -1313,7 +1313,7 @@ - (void)applicationDidBecomeActive:(UIApplication *)application if ( [[NSDate date] timeIntervalSinceDate:self.bgDate] > 60.0f && ( !(runloop_get_flags() & RUNLOOP_FLAG_CORE_RUNNING) || retroarch_ctl(RARCH_CTL_IS_DUMMY_CORE, NULL))) - task_push_cloud_sync(); + task_push_cloud_sync(NULL, NULL); self.bgDate = nil; } #endif