diff --git a/docs/References/App.md b/docs/References/App.md index 8f8a3b91d5..5b0c0a34a1 100644 --- a/docs/References/App.md +++ b/docs/References/App.md @@ -55,6 +55,12 @@ Mark the Application cache group specified by manifest_url obsolete. This method Send the `close` event to all windows of current app, if no window is blocking the `close` event, then the app will quit after all windows have done shutdown. Use this method to quit an app will give windows a chance to save data. +## App.restart() + +Restart current app. The app is relaunched during shutdown and keeps the current NW.js app path, command line switches, and arguments. + +This method will **not** send `close` event to windows. Use `App.closeAllWindows()` first if windows need a chance to save data. + ## App.crashBrowser() ## App.crashRenderer() diff --git a/src/api/app/app.cc b/src/api/app/app.cc index 30f4b6d084..36d15affef 100644 --- a/src/api/app/app.cc +++ b/src/api/app/app.cc @@ -135,10 +135,10 @@ void App::Call(Shell* shell, CommandLine::StringVector args = command_line->GetArgs(); CommandLine::StringVector argv = command_line->original_argv(); - // Ignore first non-switch arg if it's not a standalone package. - bool ignore_arg = !package->self_extract(); + // Ignore the package path only when it was passed as a positional argument. + bool ignore_arg = !package->self_extract() && !command_line->HasSwitch("nwapp"); for (unsigned i = 1; i < argv.size(); ++i) { - if (ignore_arg && argv[i] == args[0]) { + if (ignore_arg && args.size() && argv[i] == args[0]) { ignore_arg = false; continue; } diff --git a/src/api/app/app.js b/src/api/app/app.js index 5573d6d4f2..664ffa23ed 100644 --- a/src/api/app/app.js +++ b/src/api/app/app.js @@ -36,6 +36,10 @@ App.prototype.quit = function() { nw.callStaticMethod('App', 'Quit', [ ]); } +App.prototype.restart = function() { + nw.App.restart(); +} + App.prototype.closeAllWindows = function() { nw.callStaticMethod('App', 'CloseAllWindows', [ ]); } diff --git a/src/api/nw_app.idl b/src/api/nw_app.idl index 120a1aa37b..f5c460eaa6 100644 --- a/src/api/nw_app.idl +++ b/src/api/nw_app.idl @@ -23,6 +23,7 @@ namespace nw.App { static void enableComponent(ComponentExtensions extension_id, ComponentCallback callback); static void updateComponent(ComponentExtensions extension_id, ErrorCallback callback); static void quit(); + static void restart(); static void closeAllWindows(); static void clearCache(); static void clearAppCache(DOMString manifest_url); diff --git a/src/api/nw_app_api.cc b/src/api/nw_app_api.cc index 66ebf0ed83..07a3b3955e 100644 --- a/src/api/nw_app_api.cc +++ b/src/api/nw_app_api.cc @@ -8,7 +8,13 @@ #include "content/public/common/content_features.h" #include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/functional/bind.h" +#include "base/logging.h" #include "base/memory/ptr_util.h" +#include "base/path_service.h" +#include "base/process/launch.h" #include "base/strings/utf_string_conversions.h" #include "base/task/current_thread.h" #include "content/public/browser/browser_task_traits.h" @@ -26,6 +32,7 @@ #include "components/keep_alive_registry/keep_alive_registry.h" #include "components/keep_alive_registry/keep_alive_types.h" #include "content/nw/src/api/nw_app.h" +#include "content/nw/src/browser/nw_content_browser_hooks.h" #include "content/nw/src/nw_base.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" @@ -44,18 +51,133 @@ #include "net/url_request/url_request_context_getter.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" +#if defined(OS_MAC) +#include "chrome/browser/mac/relauncher.h" +#endif + using namespace extensions::nwapi::nw__app; namespace extensions { -NwAppQuitFunction::NwAppQuitFunction() { +namespace { + +base::FilePath GetAbsolutePackagePath() { + nw::Package* package = nw::package(); + if (!package) + return base::FilePath(); + base::FilePath path = package->path().NormalizePathSeparators(); + if (path.empty() || path.IsAbsolute()) + return path; + + base::FilePath current_directory; + if (!base::GetCurrentDirectory(¤t_directory)) + return path; + + return current_directory.Append(path).NormalizePathSeparators(); } -NwAppQuitFunction::~NwAppQuitFunction() { +bool ReplaceNwappSwitch(base::CommandLine::StringVector* argv, + const base::FilePath& package_path) { +#if defined(OS_WIN) + const base::CommandLine::StringType nwapp_switch = L"--nwapp="; + const base::CommandLine::StringType alt_nwapp_switch = L"-nwapp="; + const base::CommandLine::StringType win_nwapp_switch = L"/nwapp="; +#else + const base::CommandLine::StringType nwapp_switch = "--nwapp="; + const base::CommandLine::StringType alt_nwapp_switch = "-nwapp="; +#endif + + for (auto& arg : *argv) { + if (arg.compare(0, nwapp_switch.length(), nwapp_switch) == 0) { + arg = nwapp_switch + package_path.value(); + return true; + } + if (arg.compare(0, alt_nwapp_switch.length(), alt_nwapp_switch) == 0) { + arg = alt_nwapp_switch + package_path.value(); + return true; + } +#if defined(OS_WIN) + if (arg.compare(0, win_nwapp_switch.length(), win_nwapp_switch) == 0) { + arg = win_nwapp_switch + package_path.value(); + return true; + } +#endif + } + + return false; } -void NwAppQuitFunction::DoJob(extensions::ExtensionRegistrar* registrar, - std::string extension_id) { +bool PathReferencesPackage(const base::CommandLine::StringType& arg, + const base::FilePath& package_path) { + base::FilePath arg_path(arg); + if (!arg_path.IsAbsolute()) { + base::FilePath current_directory; + if (!base::GetCurrentDirectory(¤t_directory)) + return false; + arg_path = current_directory.Append(arg_path); + } + + return arg_path.NormalizePathSeparators() == package_path; +} + +void ReplacePackagePathArg(base::CommandLine::StringVector* argv, + const base::FilePath& package_path) { + for (auto it = argv->begin() + 1; it != argv->end(); ++it) { + if (PathReferencesPackage(*it, package_path)) { + *it = package_path.value(); + return; + } + } +} + +base::CommandLine BuildRelaunchCommandLine() { + const base::CommandLine& current_command_line = + *base::CommandLine::ForCurrentProcess(); + base::CommandLine::StringVector argv = current_command_line.original_argv(); + + base::FilePath exe; + if (base::PathService::Get(base::FILE_EXE, &exe) && !argv.empty()) + argv[0] = exe.value(); + + base::FilePath package_path = GetAbsolutePackagePath(); + if (!package_path.empty()) { + bool has_nwapp_switch = ReplaceNwappSwitch(&argv, package_path); + if (!has_nwapp_switch && argv.size() > 1) + ReplacePackagePathArg(&argv, package_path); + } + + return base::CommandLine(argv); +} + +void RelaunchCurrentApp() { + base::CommandLine command_line = BuildRelaunchCommandLine(); + base::FilePath exe; + if (base::PathService::Get(base::FILE_EXE, &exe)) + command_line.SetProgram(exe); + +#if defined(OS_MAC) + if (!mac_relauncher::RelaunchApp(command_line.argv())) + LOG(ERROR) << "Failed to relaunch NW app"; +#else + base::LaunchOptions options; +#if defined(OS_WIN) + if (!exe.empty()) { + options.current_directory = exe.DirName(); + options.grant_foreground_privilege = true; + } +#endif + +#if defined(OS_LINUX) + options.allow_new_privs = true; +#endif + + if (!base::LaunchProcess(command_line, options).IsValid()) + LOG(ERROR) << "Failed to relaunch NW app"; +#endif +} + +void QuitApp(extensions::ExtensionRegistrar* registrar, + const std::string& extension_id) { if (base::FeatureList::IsEnabled(::features::kNWNewWin)) { chrome::CloseAllBrowsersAndQuit(true); // trigger BrowserProcessImpl::Unpin() @@ -70,6 +192,20 @@ void NwAppQuitFunction::DoJob(extensions::ExtensionRegistrar* registrar, registrar->GetWeakPtr(), extension_id)); } +} // namespace + +NwAppQuitFunction::NwAppQuitFunction() { + +} + +NwAppQuitFunction::~NwAppQuitFunction() { +} + +void NwAppQuitFunction::DoJob(extensions::ExtensionRegistrar* registrar, + std::string extension_id) { + QuitApp(registrar, extension_id); +} + ExtensionFunction::ResponseAction NwAppQuitFunction::Run() { extensions::ExtensionRegistrar* registrar = @@ -81,6 +217,24 @@ NwAppQuitFunction::Run() { return RespondNow(NoArguments()); } +void NwAppRestartFunction::DoJob(extensions::ExtensionRegistrar* registrar, + std::string extension_id) { + nw::ScheduleRelaunchOnShutdown(&RelaunchCurrentApp); + QuitApp(registrar, extension_id); +} + +ExtensionFunction::ResponseAction +NwAppRestartFunction::Run() { + extensions::ExtensionRegistrar* registrar = + extensions::ExtensionRegistrar::Get(browser_context()); + base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, + base::BindOnce(&NwAppRestartFunction::DoJob, + registrar, extension_id())); + + return RespondNow(NoArguments()); +} + void NwAppCloseAllWindowsFunction::DoJob(AppWindowRegistry* registry, std::string id) { if (base::FeatureList::IsEnabled(::features::kNWNewWin)) { chrome::CloseAllBrowsers(); @@ -165,8 +319,8 @@ bool NwAppGetArgvSyncFunction::RunNWSync(base::ListValue* response, std::string* base::CommandLine::StringVector args = command_line->GetArgs(); base::CommandLine::StringVector argv = command_line->original_argv(); - // Ignore first non-switch arg if it's not a standalone package. - bool ignore_arg = !package->self_extract(); + // Ignore the package path only when it was passed as a positional argument. + bool ignore_arg = !package->self_extract() && !command_line->HasSwitch("nwapp"); for (unsigned i = 1; i < argv.size(); ++i) { if (ignore_arg && args.size() && argv[i] == args[0]) { ignore_arg = false; diff --git a/src/api/nw_app_api.h b/src/api/nw_app_api.h index 7a07ee32a3..50404cc054 100644 --- a/src/api/nw_app_api.h +++ b/src/api/nw_app_api.h @@ -29,6 +29,20 @@ class NwAppQuitFunction : public ExtensionFunction { void Callback(); }; +class NwAppRestartFunction : public ExtensionFunction { + public: + NwAppRestartFunction() {} + + static void DoJob(extensions::ExtensionRegistrar* registrar, + std::string extension_id); + protected: + ~NwAppRestartFunction() override {} + + // ExtensionFunction: + ResponseAction Run() override; + DECLARE_EXTENSION_FUNCTION("nw.App.restart", UNKNOWN) +}; + class NwAppCloseAllWindowsFunction : public ExtensionFunction { public: NwAppCloseAllWindowsFunction() {} diff --git a/src/browser/nw_content_browser_hooks.cc b/src/browser/nw_content_browser_hooks.cc index 46a49d555b..a826d49113 100644 --- a/src/browser/nw_content_browser_hooks.cc +++ b/src/browser/nw_content_browser_hooks.cc @@ -58,6 +58,8 @@ bool g_pinning_renderer = true; bool g_mixed_context = false; bool g_in_webview_apply_attr = false; bool g_in_webview_apply_attr_allow_nw = false; +RelaunchCallback g_relaunch_on_shutdown = nullptr; + } //namespace #if defined(OS_MAC) @@ -98,9 +100,15 @@ bool GetPackageImage(nw::Package* package, } void MainPartsPostDestroyThreadsHook() { + if (g_relaunch_on_shutdown) + g_relaunch_on_shutdown(); ReleaseNWPackage(); } +void ScheduleRelaunchOnShutdown(RelaunchCallback relaunch) { + g_relaunch_on_shutdown = relaunch; +} + void RendererProcessTerminatedHook(content::RenderProcessHost* process, const content::ChildProcessTerminationInfo& info) { int exit_code = info.exit_code; diff --git a/src/browser/nw_content_browser_hooks.h b/src/browser/nw_content_browser_hooks.h index 5fcbb5182c..0b6bd2a996 100644 --- a/src/browser/nw_content_browser_hooks.h +++ b/src/browser/nw_content_browser_hooks.h @@ -28,6 +28,7 @@ namespace content { namespace nw { class Package; +using RelaunchCallback = void (*)(); // // implemented in nw_content_browser_hooks.cc @@ -38,6 +39,7 @@ int MainPartsPreCreateThreadsHook(); void MainPartsPreMainMessageLoopRunHook(); // ref in chrome/browser/chrome_browser_main.cc CONTENT_EXPORT void MainPartsPostDestroyThreadsHook(); +CONTENT_EXPORT void ScheduleRelaunchOnShutdown(RelaunchCallback relaunch); // ref in chrome/browser/extensions/extension_service.cc CONTENT_EXPORT void RendererProcessTerminatedHook(content::RenderProcessHost* process, const content::ChildProcessTerminationInfo& info); diff --git a/test/sanity/app-restart/index.html b/test/sanity/app-restart/index.html new file mode 100644 index 0000000000..7ad30557d9 --- /dev/null +++ b/test/sanity/app-restart/index.html @@ -0,0 +1,128 @@ + + + + + + app-restart + + + + + diff --git a/test/sanity/app-restart/package.json b/test/sanity/app-restart/package.json new file mode 100644 index 0000000000..389dce665b --- /dev/null +++ b/test/sanity/app-restart/package.json @@ -0,0 +1,4 @@ +{ + "name": "app-restart", + "main": "index.html" +} diff --git a/test/sanity/app-restart/test.py b/test/sanity/app-restart/test.py new file mode 100644 index 0000000000..e5f5b6fe95 --- /dev/null +++ b/test/sanity/app-restart/test.py @@ -0,0 +1,99 @@ +import json +import os +import platform +import shutil +import subprocess +import time + +testdir = os.path.dirname(os.path.abspath(__file__)) +os.chdir(testdir) + +pkg = os.path.dirname(os.environ['CHROMEDRIVER']) + +if platform.system() == 'Darwin': + exe = os.path.join(pkg, 'nwjs.app', 'Contents', 'MacOS', 'nwjs') +elif platform.system() == 'Linux': + exe = os.path.join(pkg, 'nw') +else: + exe = os.path.join(pkg, 'nw.exe') + +workdir = os.path.join(testdir, 'tmp') + +try: + shutil.rmtree(workdir) +except OSError: + pass + +os.mkdir(workdir) + +def copy_app(target): + os.mkdir(target) + for filename in ['index.html', 'package.json']: + shutil.copy2(os.path.join(testdir, filename), + os.path.join(target, filename)) + +def run_restart_scenario(name, method, launch_mode, app_path, cwd=None): + scenario_dir = os.path.join(workdir, name) + datadir = os.path.join(scenario_dir, 'userdata') + result = os.path.join(scenario_dir, name + '-result.json') + switch_arg = '--restart-test-flag=' + name + plain_arg = 'plain-restart-arg-' + name + + os.mkdir(scenario_dir) + + command = [ + exe, + '--nwjs-test-mode2', + '--user-data-dir=' + datadir, + switch_arg, + ] + if launch_mode == 'nwapp-switch': + command.append('--nwapp=' + app_path) + else: + command.append(app_path) + command.append(plain_arg) + + env = dict(os.environ, + APP_RESTART_TEST_DIR=scenario_dir, + APP_RESTART_SCENARIO=name, + APP_RESTART_METHOD=method, + APP_RESTART_EXPECT_APP_PATH=os.path.abspath( + os.path.join(cwd, app_path) if cwd else app_path), + APP_RESTART_EXPECT_APP_PATH_IN_ARGV=( + '1' if launch_mode == 'nwapp-switch' else '0'), + APP_RESTART_EXPECT_SWITCH=switch_arg, + APP_RESTART_EXPECT_ARG=plain_arg) + + print('running %s: %s' % (name, command)) + ret = subprocess.call(command, cwd=cwd, env=env) + assert ret == 0 + + deadline = time.time() + 30 + while not os.path.exists(result) and time.time() < deadline: + time.sleep(1) + + assert os.path.exists(result), '%s did not write a result' % name + + with open(result, 'r') as result_file: + data = json.load(result_file) + + print(data) + assert data['success'], '%s failed: %s' % (name, data['message']) + +relative_root = os.path.join(workdir, 'relative root') +os.mkdir(relative_root) +relative_app = 'relative app' +copy_app(os.path.join(relative_root, relative_app)) +run_restart_scenario('relative-positional', 'nw', 'positional', + relative_app, cwd=relative_root) + +switch_app = os.path.join(workdir, 'switch app with spaces') +copy_app(switch_app) +run_restart_scenario('nwapp-switch', 'gui', 'nwapp-switch', switch_app) + +absolute_app = os.path.join(workdir, 'absolute-app') +copy_app(absolute_app) +run_restart_scenario('absolute-positional', 'nw', 'positional', + absolute_app) + +shutil.rmtree(workdir)