Updater: Directly wait for parent to exit instead of using shell

Also use NSWorkspace to launch applications.

Should hopefully fix the "updater getting stuck running in the
background" behaviour that started in MacOS 27.
wip3-rebase
Stenzek 1 day ago
parent 108e721f50
commit 4224e9aabf
No known key found for this signature in database

@ -46,8 +46,8 @@ std::optional<std::string> GetBundlePath();
/// Get the bundle path to the actual application without any translocation fun /// Get the bundle path to the actual application without any translocation fun
std::optional<std::string> GetNonTranslocatedBundlePath(); std::optional<std::string> GetNonTranslocatedBundlePath();
/// Launch the given application once this one quits /// Launches an application through LaunchServices, optionally passing command-line arguments.
bool DelayedLaunch(std::string_view file, std::span<const std::string_view> args = {}); bool LaunchApplication(std::string_view path, std::span<const std::string_view> args, Error* error);
/// Returns the size of a NSView in pixels. /// Returns the size of a NSView in pixels.
std::optional<std::pair<int, int>> GetViewSizeInPixels(const void* view); std::optional<std::pair<int, int>> GetViewSizeInPixels(const void* view);

@ -134,30 +134,58 @@ std::optional<std::string> CocoaTools::GetNonTranslocatedBundlePath()
return ret; return ret;
} }
bool CocoaTools::DelayedLaunch(std::string_view file, std::span<const std::string_view> args) bool CocoaTools::LaunchApplication(std::string_view path, std::span<const std::string_view> args, Error* error)
{ {
@autoreleasepool @autoreleasepool
{ {
const int pid = [[NSProcessInfo processInfo] processIdentifier]; if (path.empty())
{
Error::SetString(error, "Cannot launch an application with an empty path.");
return false;
}
// Hopefully we're not too large here... NSMutableArray<NSString*>* const launch_args = [NSMutableArray arrayWithCapacity:args.size()];
std::string task_args = for (const std::string_view& arg : args)
fmt::format("while /bin/ps -p {} > /dev/null; do /bin/sleep 0.1; done; exec /usr/bin/open \"{}\"", pid, file); [launch_args addObject:arg.empty() ? @"" : StringViewToNSString(arg)];
if (!args.empty())
NSWorkspaceOpenConfiguration* const configuration = [NSWorkspaceOpenConfiguration configuration];
[configuration setActivates:YES];
[configuration setAllowsRunningApplicationSubstitution:NO];
[configuration setCreatesNewApplicationInstance:YES];
// Callers present launch failures in their own UI. Asking LaunchServices to present them would result in a
// duplicate system alert followed by the DuckStation/Updater error dialog.
[configuration setPromptsUserIfNeeded:NO];
[configuration setArguments:launch_args];
// NSWorkspace's completion handler runs on a concurrent queue, so it is safe to wait here. Waiting for the
// callback ensures that callers do not exit until LaunchServices has confirmed that the replacement app started.
dispatch_semaphore_t const completion_semaphore = dispatch_semaphore_create(0);
__block bool launch_succeeded = false;
__block NSError* launch_error = nil;
[[NSWorkspace sharedWorkspace]
openApplicationAtURL:[NSURL fileURLWithPath:StringViewToNSString(path) isDirectory:YES]
configuration:configuration
completionHandler:^(NSRunningApplication* app, NSError* nserror) {
launch_succeeded = (app != nil && nserror == nil);
launch_error = [nserror retain];
dispatch_semaphore_signal(completion_semaphore);
}];
dispatch_semaphore_wait(completion_semaphore, DISPATCH_TIME_FOREVER);
if (!launch_succeeded)
{ {
task_args += " --args"; if (launch_error)
for (const std::string_view& arg : args) Error::SetStringFmt(error, "Failed to launch '{}': {}", path, NSErrorToString(launch_error));
{ else
task_args += " \""; Error::SetStringFmt(error, "Failed to launch '{}'.", path);
task_args += arg;
task_args += "\"";
}
} }
[launch_error release];
#if !OS_OBJECT_USE_OBJC
dispatch_release(completion_semaphore);
#endif
NSTask* task = [NSTask new]; return launch_succeeded;
[task setExecutableURL:[NSURL fileURLWithPath:@"/bin/sh"]];
[task setArguments:@[ @"-c", [NSString stringWithUTF8String:task_args.c_str()] ]];
return [task launchAndReturnError:nil];
} }
} }

@ -901,14 +901,21 @@ bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& update_data)
INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}", INFO_LOG("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",
updater_app, zip_path, staging_directory, bundle_path.value()); updater_app, zip_path, staging_directory, bundle_path.value());
const std::string parent_process_id = fmt::format("{}", QCoreApplication::applicationPid());
const std::string_view args[] = { const std::string_view args[] = {
parent_process_id,
zip_path, zip_path,
staging_directory, staging_directory,
bundle_path.value(), bundle_path.value(),
}; };
// Kick off updater! // Kick off updater!
CocoaTools::DelayedLaunch(updater_app, args); if (!CocoaTools::LaunchApplication(updater_app, args, &error))
{
reportError(fmt::format("Failed to start updater: {}", error.GetDescription()));
return false;
}
return true; return true;
} }

@ -4,6 +4,8 @@
#include "cocoa_progress_callback.h" #include "cocoa_progress_callback.h"
#include "updater.h" #include "updater.h"
#include "common/cocoa_tools.h"
#include "common/error.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/log.h" #include "common/log.h"
#include "common/path.h" #include "common/path.h"
@ -11,17 +13,59 @@
#include "common/string_util.h" #include "common/string_util.h"
#include "common/timer.h" #include "common/timer.h"
#include <cerrno>
#include <cstdlib> #include <cstdlib>
#include <sys/event.h>
#include <sys/types.h>
#include <thread> #include <thread>
#include <unistd.h>
static void LaunchApplication(const char* path) static bool WaitForProcessToExit(pid_t process_id, Error* error)
{ {
@autoreleasepool const int queue = kqueue();
if (queue < 0)
{ {
NSTask* task = [[[NSTask alloc] init] autorelease]; Error::SetErrno(error, "kqueue() failed: ", errno);
[task setLaunchPath:[NSString stringWithUTF8String:path]]; return false;
[task launch];
} }
const ScopedGuard queue_closer = [queue]() { close(queue); };
struct kevent change;
EV_SET(&change, static_cast<uintptr_t>(process_id), EVFILT_PROC, EV_ADD | EV_ENABLE | EV_ONESHOT, NOTE_EXIT, 0,
nullptr);
struct kevent event;
int result;
do
{
result = kevent(queue, &change, 1, &event, 1, nullptr);
} while (result < 0 && errno == EINTR);
if (result < 0)
{
Error::SetErrno(error, "kevent() failed while waiting for DuckStation to exit: ", errno);
return false;
}
if (result == 0)
{
Error::SetString(error, "kevent() returned without reporting that DuckStation exited.");
return false;
}
if (event.flags & EV_ERROR)
{
// The process can exit between launching the updater and registering the event. In that case it is safe to
// continue. Any other registration error is fatal, because modifying the bundle while DuckStation is still
// running could leave the installation unusable.
const int event_error = static_cast<int>(event.data);
if (event_error == ESRCH)
return true;
Error::SetErrno(error, "Failed to monitor DuckStation: ", event_error);
return false;
}
return true;
} }
int main(int argc, char* argv[]) int main(int argc, char* argv[])
@ -35,20 +79,22 @@ int main(int argc, char* argv[])
CocoaProgressCallback progress; CocoaProgressCallback progress;
if (argc != 4) if (argc != 5)
{ {
progress.ModalError("Expected 3 arguments: update zip, staging directory, output directory.\n\nThis program is not " progress.ModalError(
"intended to be run manually, please use the Qt frontend and click Help->Check for Updates."); "Expected 4 arguments: parent process id, update zip, staging directory, output directory.\n\nThis program is "
"not intended to be run manually, please use the Qt frontend and click Help->Check for Updates.");
return EXIT_FAILURE; return EXIT_FAILURE;
} }
std::string zip_path = argv[1]; const int parent_process_id = StringUtil::FromChars<int>(argv[1]).value_or(0);
std::string staging_directory = argv[2]; std::string zip_path = argv[2];
std::string destination_directory = argv[3]; std::string staging_directory = argv[3];
std::string destination_directory = argv[4];
if (zip_path.empty() || staging_directory.empty() || destination_directory.empty()) if (parent_process_id <= 0 || zip_path.empty() || staging_directory.empty() || destination_directory.empty())
{ {
progress.ModalError("One or more parameters is empty."); progress.ModalError("One or more parameters is invalid.");
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -59,10 +105,10 @@ int main(int argc, char* argv[])
Log::SetFileOutputParams(true, log_path.c_str()); Log::SetFileOutputParams(true, log_path.c_str());
} }
std::string program_to_launch = Path::Combine(destination_directory, "Contents/MacOS/DuckStation"); std::string application_to_launch = destination_directory;
int result = EXIT_SUCCESS; int result = EXIT_SUCCESS;
std::thread worker([&progress, zip_path = std::move(zip_path), std::thread worker([&progress, parent_process_id, zip_path = std::move(zip_path),
destination_directory = std::move(destination_directory), destination_directory = std::move(destination_directory),
staging_directory = std::move(staging_directory), &result]() { staging_directory = std::move(staging_directory), &result]() {
ScopedGuard app_stopper([]() { ScopedGuard app_stopper([]() {
@ -85,6 +131,15 @@ int main(int argc, char* argv[])
}); });
}); });
progress.FormatStatusText("Waiting for DuckStation process {} to exit...", parent_process_id);
Error wait_error;
if (!WaitForProcessToExit(static_cast<pid_t>(parent_process_id), &wait_error))
{
progress.FormatModalError("Failed to wait for DuckStation to exit: {}", wait_error.GetDescription());
result = EXIT_FAILURE;
return;
}
Updater updater(&progress); Updater updater(&progress);
if (!updater.Initialize(std::move(staging_directory), std::move(destination_directory))) if (!updater.Initialize(std::move(staging_directory), std::move(destination_directory)))
{ {
@ -142,8 +197,15 @@ int main(int argc, char* argv[])
if (result == EXIT_SUCCESS) if (result == EXIT_SUCCESS)
{ {
progress.FormatInformation("Launching '{}'...", program_to_launch); progress.FormatInformation("Launching '{}'...", application_to_launch);
LaunchApplication(program_to_launch.c_str()); Error launch_error;
if (!CocoaTools::LaunchApplication(application_to_launch, {}, &launch_error))
{
progress.FormatModalError("The update was installed successfully, but DuckStation could not be restarted: {}\n\n"
"Please launch DuckStation manually.",
launch_error.GetDescription());
result = EXIT_FAILURE;
}
} }
return result; return result;

Loading…
Cancel
Save