Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support 'shutdown' on Windows #24261

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api/power-monitor.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Emitted when the system changes to AC power.

Emitted when system changes to battery power.

### Event: 'shutdown' _Linux_ _macOS_
### Event: 'shutdown'

Emitted when the system is about to reboot or shut down. If the event handler
invokes `e.preventDefault()`, Electron will attempt to delay system shutdown in
Expand Down
2 changes: 2 additions & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ auto_filenames = {
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"lib/renderer/web-view/web-view-init.ts",
"lib/renderer/win32-queryendsession-setup.ts",
"lib/renderer/window-setup.ts",
"lib/sandboxed_renderer/api/exports/electron.ts",
"lib/sandboxed_renderer/api/module-list.ts",
Expand Down Expand Up @@ -308,6 +309,7 @@ auto_filenames = {
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/web-view/web-view-impl.ts",
"lib/renderer/web-view/web-view-init.ts",
"lib/renderer/win32-queryendsession-setup.ts",
"lib/renderer/window-setup.ts",
"package.json",
"tsconfig.electron.json",
Expand Down
2 changes: 2 additions & 0 deletions filenames.gni
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ filenames = {
"shell/browser/api/electron_api_power_monitor_win.cc",
"shell/browser/api/electron_api_system_preferences_win.cc",
"shell/browser/browser_win.cc",
"shell/browser/lib/shutdown_blocker_win.cc",
"shell/browser/lib/shutdown_blocker_win.h",
"shell/browser/native_window_views_win.cc",
"shell/browser/notifications/win/notification_presenter_win.cc",
"shell/browser/notifications/win/notification_presenter_win.h",
Expand Down
15 changes: 15 additions & 0 deletions lib/browser/api/power-monitor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EventEmitter } from 'events';
import { app } from 'electron/main';
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';

const {
createPowerMonitor,
Expand All @@ -17,6 +18,20 @@ class PowerMonitor extends EventEmitter {
const pm = createPowerMonitor();
pm.emit = this.emit.bind(this);

if (process.platform === 'win32') {
// On Windows we need to handle shutdown event coming from renderers. See
// shutdown_blocker_win.h for more details.
// To prevent any race conditions where multiple renderers receive
// QUERYENDSESSION and forward the message to us, we debounce by using `once`
// to subscribe and reattach the handler 1 second later
ipcMainInternal.once('ELECTRON_BROWSER_QUERYENDSESSION', function remoteQueryEndSessionHandler (event) {
pm.emit('shutdown', event);
setTimeout(() => {
ipcMainInternal.once('ELECTRON_BROWSER_QUERYENDSESSION', remoteQueryEndSessionHandler);
}, 1000);
});
}

if (process.platform === 'linux') {
// On Linux, we inhibit shutdown in order to give the app a chance to
// decide whether or not it wants to prevent the shutdown. We don't
Expand Down
4 changes: 4 additions & 0 deletions lib/renderer/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ const guestInstanceId = parseOption('guestInstanceId', null, value => parseInt(v
const openerId = parseOption('openerId', null, value => parseInt(value));
const appPath = hasSwitch('app-path') ? getSwitchValue('app-path') : null;

if (process.platform === 'win32') {
require('@electron/internal/renderer/win32-queryendsession-setup.ts')(process);
}

// The webContents preload script is loaded after the session preload scripts.
if (preloadScript) {
preloadScripts.push(preloadScript);
Expand Down
8 changes: 8 additions & 0 deletions lib/renderer/win32-queryendsession-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ipcRenderer } from 'electron';

export function setup (binding: typeof process['_linkedBinding']) {
// TODO(codebytere): fix typedef here.
(binding as any).setShutdownHandler(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shutdown handler in renderer returns undefined, which would then always cancel the shutdown.

ipcRenderer.send('ELECTRON_BROWSER_QUERYENDSESSION');
});
}
4 changes: 4 additions & 0 deletions lib/sandboxed_renderer/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ process.on('exit', () => (preloadProcess as events.EventEmitter).emit('exit'));
(process as events.EventEmitter).on('document-start', () => (preloadProcess as events.EventEmitter).emit('document-start'));
(process as events.EventEmitter).on('document-end', () => (preloadProcess as events.EventEmitter).emit('document-end'));

if (process.platform === 'win32') {
require('@electron/internal/renderer/win32-queryendsession-setup.ts').setup(binding);
}

// This is the `require` function that will be visible to the preload script
function preloadRequire (module: string) {
if (loadedModules.has(module)) {
Expand Down
9 changes: 8 additions & 1 deletion shell/browser/api/electron_api_power_monitor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,17 @@ namespace api {

gin::WrapperInfo PowerMonitor::kWrapperInfo = {gin::kEmbedderNativeGin};

PowerMonitor::PowerMonitor(v8::Isolate* isolate) {
PowerMonitor::PowerMonitor(v8::Isolate* isolate)
#ifdef OS_WIN
: shutdown_blocker_(new ShutdownBlockerWin(false))
#endif
{
#if defined(OS_MAC)
Browser::Get()->SetShutdownHandler(base::BindRepeating(
&PowerMonitor::ShouldShutdown, base::Unretained(this)));
#elif defined(OS_WIN)
shutdown_blocker_->SetShutdownHandler(
base::Bind(&PowerMonitor::ShouldShutdown, base::Unretained(this)));
#endif

base::PowerMonitor::AddObserver(this);
Expand Down
9 changes: 9 additions & 0 deletions shell/browser/api/electron_api_power_monitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#ifndef SHELL_BROWSER_API_ELECTRON_API_POWER_MONITOR_H_
#define SHELL_BROWSER_API_ELECTRON_API_POWER_MONITOR_H_

#include <memory>

#include "base/power_monitor/power_observer.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
Expand All @@ -15,6 +17,10 @@
#include "shell/browser/lib/power_observer_linux.h"
#endif

#if defined(OS_WIN)
#include "shell/browser/lib/shutdown_blocker_win.h"
#endif

namespace electron {

namespace api {
Expand Down Expand Up @@ -72,6 +78,9 @@ class PowerMonitor : public gin::Wrappable<PowerMonitor>,

// The window used for processing events.
HWND window_;

// An object that encapsulates logic to handle shutdown/reboot events.
std::unique_ptr<ShutdownBlockerWin> shutdown_blocker_;
#endif

#if defined(OS_LINUX)
Expand Down
130 changes: 130 additions & 0 deletions shell/browser/lib/shutdown_blocker_win.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2020 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include <utility>

#include "base/logging.h"
#include "base/task/post_task.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/win/win_util.h"
#include "base/win/wrapped_window_proc.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "shell/browser/lib/shutdown_blocker_win.h"
#include "ui/gfx/win/hwnd_util.h"

namespace electron {

namespace {
const wchar_t kShutdownBlockerWinWindowClass[] = L"Electron_ShutdownBlockerWin";
}

ShutdownBlockerWin::ShutdownBlockerWin(bool dedicated_message_loop)
: dedicated_message_loop_(dedicated_message_loop),
instance_(NULL),
window_(NULL) {}

ShutdownBlockerWin::~ShutdownBlockerWin() = default;

void ShutdownBlockerWin::SetShutdownHandler(base::Callback<bool()> handler) {
LOG(INFO) << "setting shutdown handler on "
<< (dedicated_message_loop_ ? "renderer" : "browser") << " process";
should_shutdown_ = std::move(handler);
BlockShutdown();
}

void ShutdownBlockerWin::BlockShutdown() {
if (blocking_) {
return;
}
blocking_ = true;
if (dedicated_message_loop_) {
LOG(INFO) << "unblocking shutdown on renderer process";
owner_thread_task_runner_ = base::ThreadTaskRunnerHandle::Get();
owner_thread_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&ShutdownBlockerWin::MessageLoop,
base::Unretained(this)));
} else {
LOG(INFO) << "blocking shutdown on browser process";
CreateHiddenWindow();
}
}

void ShutdownBlockerWin::MessageLoop() {
CreateHiddenWindow();
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DestroyHiddenWindow();
}

void ShutdownBlockerWin::CreateHiddenWindow() {
ATOM atom = RegisterWindowClass();
window_ = CreateWindow(MAKEINTATOM(atom), 0, WS_POPUP, 0, 0, 0, 0, 0, 0,
instance_, 0);
gfx::CheckWindowCreated(window_, ::GetLastError());
gfx::SetWindowUserData(window_, this);
}

void ShutdownBlockerWin::DestroyHiddenWindow() {
DestroyWindow(window_);
UnregisterClass(kShutdownBlockerWinWindowClass, instance_);
}

ATOM ShutdownBlockerWin::RegisterWindowClass() {
// Register our window class
WNDCLASSEX window_class;
base::win::InitializeWindowClass(
kShutdownBlockerWinWindowClass,
&base::win::WrappedWindowProc<ShutdownBlockerWin::WndProc>, 0, 0, 0, NULL,
NULL, NULL, NULL, NULL, &window_class);
ATOM atom = RegisterClassEx(&window_class);
instance_ = window_class.hInstance;
return atom;
}

bool ShutdownBlockerWin::OnQueryEndSession() {
if (dedicated_message_loop_) {
owner_thread_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&ShutdownBlockerWin::OwnerThreadShutdownHandler,
base::Unretained(this)));
return false;
}
return !should_shutdown_ || should_shutdown_.Run();
}

void ShutdownBlockerWin::OwnerThreadShutdownHandler() {
if (should_shutdown_) {
should_shutdown_.Run();
}
}

LRESULT CALLBACK ShutdownBlockerWin::WndProc(HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam) {
switch (message) {
case WM_QUERYENDSESSION: {
ShutdownBlockerWin* blocker = reinterpret_cast<ShutdownBlockerWin*>(
GetWindowLongPtr(hwnd, GWLP_USERDATA));
LOG(INFO) << "Received WM_QUERYENDSESSION"
<< (!blocker ? ""
: (blocker->dedicated_message_loop_
? " on renderer process"
: " on browser process"));
if (blocker && !blocker->OnQueryEndSession()) {
LOG(INFO) << "Shutdown blocked";
ShutdownBlockReasonCreate(hwnd, L"Ensure a clean shutdown");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider making this string configurable - this gets shown on shutdown to users and if it's hard-coded to English, this will result in a Confusing Experience™™™ to people using non-English locales!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh excellent point, will do!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling ShutdownBlockReasonCreate() inside the WM_QUERYENDSESSION handler allows us to notify our code to clean up and shutdown quickly, but we can also use it preemptively when we know that we're running a critical section, like burning a cd, or saving a file to disk.

It would be really nice if an app could call a "block" or "unblock" API when the programmer knows in advance where a shutdown should be blocked, releasing the block afterwards. The "block" API would call ShutdownBlockReasonCreate(hwnd, L"A critical task is running."); The "unblock" API would call ShutdownBlockReasonDestroy(hwnd); The string passed into ShutdownBlockReasonCreate() is shown by windows as the reason for the shutdown being held up.

Application Shutdown Changes in Windows

return FALSE;
}
return TRUE;
}
default:
return ::DefWindowProc(hwnd, message, wparam, lparam);
}
}

} // namespace electron
83 changes: 83 additions & 0 deletions shell/browser/lib/shutdown_blocker_win.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2020 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
//
// The purpose of this class is to block system shutdown on windows so the
// application can exit cleanly. It uses the WM_QUERYENDSESSION message to do
// so, as explained here
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa376890(v=vs.85).aspx
//
// The WM_QUERYENDSESSION message must be handled in every every process managed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with the WM_QUERYENDSESSION message, but if it must be handled in every process then we also need to handle it in GPU/utility/network/... processes then.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo / repeated word:

"handled in every every process" should be "handled in every process"

// by an application, so we encapsulate the logic in a class that is common to
// both browser and renderer processes.
//
// In other words: it is not enough to handle this message in the browser
// process and emit an event to the `powerMonitor` module (like it is done for
// Linux/OSX), because even if the browser process blocks shutdown, windows will
// still kill the renderers leaving the application in an invalid state.
//
// This class has two modes of operation: It can use the main thread message
// loop (browser process) or it can run a dedicated message loop (renderer
// process). When it uses a dedicated message loop, it must run in a separate
// thread to avoid blocking the blink/webkit thread. In this case, when the
// WM_QUERYENDSESSION message is received, it will always return false (blocking
// shutdown) and will also invoke the user callback in the renderer main thread.
//
// This is done to forward the message to the browser process, since when the
// renderer receives the WM_QUERYENDSESSION message first, the browser process
// will not receive it and must be notified that the system is shutting down via
// IPC. See lib/browser/api/power-monitor.js and
// lib/renderer/win32-queryendsession-setup.js for details of how IPC is handled
// on the JS side.
//
// In all my tests the renderer received the WM_QUERYENDSESSION message first,
// blocking the browser process from receiving it, but I've found no evidence
// that child processes will always receive it first, so this class must also be
// used in the browser process in which case it will deal with shutdown
// by directly emitting the powerMonitor "shutdown" event.
#ifndef SHELL_BROWSER_LIB_SHUTDOWN_BLOCKER_WIN_H_
#define SHELL_BROWSER_LIB_SHUTDOWN_BLOCKER_WIN_H_

#include <windows.h>

#include "base/callback.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/single_thread_task_runner.h"

namespace electron {

class ShutdownBlockerWin {
public:
explicit ShutdownBlockerWin(bool dedicated_message_loop);
~ShutdownBlockerWin();

void SetShutdownHandler(base::Callback<bool()> should_shutdown);

private:
void BlockShutdown();
void MessageLoop();
void CreateHiddenWindow();
void DestroyHiddenWindow();
ATOM RegisterWindowClass();
bool OnQueryEndSession();
void OwnerThreadShutdownHandler();

// Static callback invoked when a message comes in to our messaging window.
static LRESULT CALLBACK WndProc(HWND hwnd,
UINT message,
WPARAM wparam,
LPARAM lparam);

bool dedicated_message_loop_;
bool blocking_ = false;
scoped_refptr<base::SingleThreadTaskRunner> owner_thread_task_runner_;
base::Callback<bool()> should_shutdown_;
HINSTANCE instance_;
HWND window_;
DISALLOW_COPY_AND_ASSIGN(ShutdownBlockerWin);
};

} // namespace electron

#endif // SHELL_BROWSER_LIB_SHUTDOWN_BLOCKER_WIN_H_
Loading