-
Notifications
You must be signed in to change notification settings - Fork 15.8k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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(() => { | ||
ipcRenderer.send('ELECTRON_BROWSER_QUERYENDSESSION'); | ||
}); | ||
} |
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(); | ||
jkleinsc marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh excellent point, will do! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling 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 |
||
return FALSE; | ||
} | ||
return TRUE; | ||
} | ||
default: | ||
return ::DefWindowProc(hwnd, message, wparam, lparam); | ||
} | ||
} | ||
|
||
} // namespace electron |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_ |
There was a problem hiding this comment.
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.