Browse Source

feat: add query-session-end and improve session-end events on Windows (#44598)

* feat: add query-session-end event for Windows

* fix: remove debug line

* feat: notify with reason on session-end

* docs: add comments and return params

* docs: add same docs to the BrowserWindow

* fix: add shutdown reason if lParam == 0

* docs: remove 'force' word

* docs: revert multithreading.md change

* docs: add reasons documentation, reason variable renamed to reasons

* docs: improve 'shutdown' reason wording

* docs: reword with 'can be'

* fix: pass reasons by reference

* fix: use newer approach which expose reasons value directly on Event object

* docs: add escaping

* style: linter fixes

* fix: project now should compile

* fix: EmitWithoutEvent method added, EmitWithEvent moved to private again

* docs: typo fix

Co-authored-by: Sam Maddock <[email protected]>

* docs: dedicated WindowSessionEndEvent type created

* docs: better wording for session-end event description

Co-authored-by: Will Anderson <[email protected]>

---------

Co-authored-by: Sam Maddock <[email protected]>
Co-authored-by: Will Anderson <[email protected]>
Savely Krasovsky 4 months ago
parent
commit
c5ea177b3d

+ 17 - 2
docs/api/base-window.md

@@ -144,10 +144,24 @@ _**Note**: There is a subtle difference between the behaviors of `window.onbefor
 Emitted when the window is closed. After you have received this event you should
 remove the reference to the window and avoid using it any more.
 
+#### Event: 'query-session-end' _Windows_
+
+Returns:
+
+* `event` [WindowSessionEndEvent][window-session-end-event]
+
+Emitted when a session is about to end due to a shutdown, machine restart, or user log-off.
+Calling `event.preventDefault()` can delay the system shutdown, though it’s generally best
+to respect the user’s choice to end the session. However, you may choose to use it if
+ending the session puts the user at risk of losing data.
+
 #### Event: 'session-end' _Windows_
 
-Emitted when window session is going to end due to force shutdown or machine restart
-or session log off.
+Returns:
+
+* `event` [WindowSessionEndEvent][window-session-end-event]
+
+Emitted when a session is about to end due to a shutdown, machine restart, or user log-off. Once this event fires, there is no way to prevent the session from ending.
 
 #### Event: 'blur'
 
@@ -1429,3 +1443,4 @@ On Linux, the `symbolColor` is automatically calculated to have minimum accessib
 [vibrancy-docs]: https://developer.apple.com/documentation/appkit/nsvisualeffectview?preferredLanguage=objc
 [window-levels]: https://developer.apple.com/documentation/appkit/nswindow/level
 [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
+[window-session-end-event]:../api/structures/window-session-end-event.md

+ 17 - 2
docs/api/browser-window.md

@@ -207,10 +207,24 @@ _**Note**: There is a subtle difference between the behaviors of `window.onbefor
 Emitted when the window is closed. After you have received this event you should
 remove the reference to the window and avoid using it any more.
 
+#### Event: 'query-session-end' _Windows_
+
+Returns:
+
+* `event` [WindowSessionEndEvent][window-session-end-event]
+
+Emitted when a session is about to end due to a shutdown, machine restart, or user log-off.
+Calling `event.preventDefault()` can delay the system shutdown, though it’s generally best
+to respect the user’s choice to end the session. However, you may choose to use it if
+ending the session puts the user at risk of losing data.
+
 #### Event: 'session-end' _Windows_
 
-Emitted when window session is going to end due to force shutdown or machine restart
-or session log off.
+Returns:
+
+* `event` [WindowSessionEndEvent][window-session-end-event]
+
+Emitted when a session is about to end due to a shutdown, machine restart, or user log-off. Once this event fires, there is no way to prevent the session from ending.
 
 #### Event: 'unresponsive'
 
@@ -1672,3 +1686,4 @@ On Linux, the `symbolColor` is automatically calculated to have minimum accessib
 [vibrancy-docs]: https://developer.apple.com/documentation/appkit/nsvisualeffectview?preferredLanguage=objc
 [window-levels]: https://developer.apple.com/documentation/appkit/nswindow/level
 [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
+[window-session-end-event]:../api/structures/window-session-end-event.md

+ 7 - 0
docs/api/structures/window-session-end-event.md

@@ -0,0 +1,7 @@
+# WindowSessionEndEvent Object extends `Event`
+
+* `reasons` string[] - List of reasons for shutdown. Can be 'shutdown', 'close-app', 'critical', or 'logoff'.
+
+Unfortunately, Windows does not offer a way to differentiate between a shutdown and a reboot, meaning the 'shutdown'
+reason is triggered in both scenarios. For more details on the `WM_ENDSESSION` message and its associated reasons,
+refer to the [MSDN documentation](https://learn.microsoft.com/en-us/windows/win32/shutdown/wm-endsession).

+ 1 - 0
filenames.auto.gni

@@ -151,6 +151,7 @@ auto_filenames = {
     "docs/api/structures/web-request-filter.md",
     "docs/api/structures/web-source.md",
     "docs/api/structures/window-open-handler-response.md",
+    "docs/api/structures/window-session-end-event.md",
   ]
 
   sandbox_bundle_deps = [

+ 31 - 2
shell/browser/api/electron_api_base_window.cc

@@ -177,8 +177,37 @@ void BaseWindow::OnWindowClosed() {
       FROM_HERE, GetDestroyClosure());
 }
 
-void BaseWindow::OnWindowEndSession() {
-  Emit("session-end");
+void BaseWindow::OnWindowQueryEndSession(
+    const std::vector<std::string>& reasons,
+    bool* prevent_default) {
+  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+  v8::HandleScope handle_scope(isolate);
+
+  gin::Handle<gin_helper::internal::Event> event =
+      gin_helper::internal::Event::New(isolate);
+  v8::Local<v8::Object> event_object = event.ToV8().As<v8::Object>();
+
+  gin::Dictionary dict(isolate, event_object);
+  dict.Set("reasons", reasons);
+
+  EmitWithoutEvent("query-session-end", event);
+  if (event->GetDefaultPrevented()) {
+    *prevent_default = true;
+  }
+}
+
+void BaseWindow::OnWindowEndSession(const std::vector<std::string>& reasons) {
+  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+  v8::HandleScope handle_scope(isolate);
+
+  gin::Handle<gin_helper::internal::Event> event =
+      gin_helper::internal::Event::New(isolate);
+  v8::Local<v8::Object> event_object = event.ToV8().As<v8::Object>();
+
+  gin::Dictionary dict(isolate, event_object);
+  dict.Set("reasons", reasons);
+
+  EmitWithoutEvent("session-end", event);
 }
 
 void BaseWindow::OnWindowBlur() {

+ 3 - 1
shell/browser/api/electron_api_base_window.h

@@ -57,7 +57,9 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
   // NativeWindowObserver:
   void WillCloseWindow(bool* prevent_default) override;
   void OnWindowClosed() override;
-  void OnWindowEndSession() override;
+  void OnWindowQueryEndSession(const std::vector<std::string>& reasons,
+                               bool* prevent_default) override;
+  void OnWindowEndSession(const std::vector<std::string>& reasons) override;
   void OnWindowBlur() override;
   void OnWindowFocus() override;
   void OnWindowShow() override;

+ 10 - 2
shell/browser/native_window.cc

@@ -532,9 +532,17 @@ void NativeWindow::NotifyWindowClosed() {
   WindowList::RemoveWindow(this);
 }
 
-void NativeWindow::NotifyWindowEndSession() {
+void NativeWindow::NotifyWindowQueryEndSession(
+    const std::vector<std::string>& reasons,
+    bool* prevent_default) {
   for (NativeWindowObserver& observer : observers_)
-    observer.OnWindowEndSession();
+    observer.OnWindowQueryEndSession(reasons, prevent_default);
+}
+
+void NativeWindow::NotifyWindowEndSession(
+    const std::vector<std::string>& reasons) {
+  for (NativeWindowObserver& observer : observers_)
+    observer.OnWindowEndSession(reasons);
 }
 
 void NativeWindow::NotifyWindowBlur() {

+ 3 - 1
shell/browser/native_window.h

@@ -302,7 +302,9 @@ class NativeWindow : public base::SupportsUserData,
   void NotifyWindowRequestPreferredWidth(int* width);
   void NotifyWindowCloseButtonClicked();
   void NotifyWindowClosed();
-  void NotifyWindowEndSession();
+  void NotifyWindowQueryEndSession(const std::vector<std::string>& reasons,
+                                   bool* prevent_default);
+  void NotifyWindowEndSession(const std::vector<std::string>& reasons);
   void NotifyWindowBlur();
   void NotifyWindowFocus();
   void NotifyWindowShow();

+ 5 - 1
shell/browser/native_window_observer.h

@@ -50,8 +50,12 @@ class NativeWindowObserver : public base::CheckedObserver {
   // Called when the window is closed.
   virtual void OnWindowClosed() {}
 
+  // Called when Windows sends WM_QUERYENDSESSION message.
+  virtual void OnWindowQueryEndSession(const std::vector<std::string>& reasons,
+                                       bool* prevent_default) {}
+
   // Called when Windows sends WM_ENDSESSION message
-  virtual void OnWindowEndSession() {}
+  virtual void OnWindowEndSession(const std::vector<std::string>& reasons) {}
 
   // Called when window loses focus.
   virtual void OnWindowBlur() {}

+ 30 - 1
shell/browser/native_window_views_win.cc

@@ -27,6 +27,24 @@ namespace electron {
 
 namespace {
 
+// Convert Win32 WM_QUERYENDSESSIONS to strings.
+const std::vector<std::string> EndSessionToStringVec(LPARAM end_session_id) {
+  std::vector<std::string> params;
+  if (end_session_id == 0) {
+    params.push_back("shutdown");
+    return params;
+  }
+
+  if (end_session_id & ENDSESSION_CLOSEAPP)
+    params.push_back("close-app");
+  if (end_session_id & ENDSESSION_CRITICAL)
+    params.push_back("critical");
+  if (end_session_id & ENDSESSION_LOGOFF)
+    params.push_back("logoff");
+
+  return params;
+}
+
 // Convert Win32 WM_APPCOMMANDS to strings.
 constexpr std::string_view AppCommandToString(int command_id) {
   switch (command_id) {
@@ -389,9 +407,20 @@ bool NativeWindowViews::PreHandleMSG(UINT message,
       }
       return false;
     }
+    case WM_QUERYENDSESSION: {
+      bool prevent_default = false;
+      std::vector<std::string> reasons = EndSessionToStringVec(l_param);
+      NotifyWindowQueryEndSession(reasons, &prevent_default);
+      // Result should be TRUE by default, otherwise WM_ENDSESSION will not be
+      // fired in some cases: More:
+      // https://learn.microsoft.com/en-us/windows/win32/rstmgr/guidelines-for-applications
+      *result = !prevent_default;
+      return prevent_default;
+    }
     case WM_ENDSESSION: {
+      std::vector<std::string> reasons = EndSessionToStringVec(l_param);
       if (w_param) {
-        NotifyWindowEndSession();
+        NotifyWindowEndSession(reasons);
       }
       return false;
     }

+ 11 - 0
shell/common/gin_helper/event_emitter.h

@@ -45,6 +45,17 @@ class EventEmitter : public gin_helper::Wrappable<T> {
     return EmitWithEvent(name, event, std::forward<Args>(args)...);
   }
 
+  // this.emit(name, args...);
+  template <typename... Args>
+  void EmitWithoutEvent(const std::string_view name, Args&&... args) {
+    v8::HandleScope handle_scope(isolate());
+    v8::Local<v8::Object> wrapper = GetWrapper();
+    if (wrapper.IsEmpty())
+      return;
+    gin_helper::EmitEvent(isolate(), GetWrapper(), name,
+                          std::forward<Args>(args)...);
+  }
+
   // disable copy
   EventEmitter(const EventEmitter&) = delete;
   EventEmitter& operator=(const EventEmitter&) = delete;