From ce8c6c29703de74d60350f1ab4fd658e3b993a13 Mon Sep 17 00:00:00 2001
From: Paul Hooijenga <paulhooijenga@gmail.com>
Date: Thu, 20 Oct 2022 13:01:40 +0200
Subject: [PATCH] Include names of processes in PromptQuitDialog

---
 guake/dialogs.py                              | 13 ++++--
 guake/globals.py                              |  1 +
 guake/guake_app.py                            | 10 ++++-
 guake/notebook.py                             | 41 ++++++++++---------
 guake/tests/test_utils.py                     |  6 +++
 guake/utils.py                                | 14 +++++++
 ...t-quit-process-names-32a4e1b37eb36037.yaml |  6 +++
 7 files changed, 66 insertions(+), 25 deletions(-)
 create mode 100644 releasenotes/notes/prompt-quit-process-names-32a4e1b37eb36037.yaml

diff --git a/guake/dialogs.py b/guake/dialogs.py
index 24df429d3..5f4a30941 100644
--- a/guake/dialogs.py
+++ b/guake/dialogs.py
@@ -64,15 +64,20 @@ def __init__(self, parent, procs, tabs, notebooks):
             else:
                 notebooks_str = ""
 
-        if procs == 0:
+        if not procs:
             proc_str = _("There are no processes running")
-        elif procs == 1:
+        elif len(procs) == 1:
             proc_str = _("There is a process still running")
         else:
-            proc_str = _("There are {0} processes still running").format(procs)
+            proc_str = _("There are {0} processes still running").format(len(procs))
+
+        if procs:
+            proc_list = "\n\n" + "\n".join(f"{name} ({pid})" for pid, name in procs)
+        else:
+            proc_list = ""
 
         self.set_markup(primary_msg)
-        self.format_secondary_markup(f"<b>{proc_str}{tab_str}{notebooks_str}.</b>")
+        self.format_secondary_markup(f"<b>{proc_str}{tab_str}{notebooks_str}.</b>{proc_list}")
 
     def quit(self):
         """Run the "are you sure" dialog for quitting Guake"""
diff --git a/guake/globals.py b/guake/globals.py
index 3410a3347..49e0d5d50 100644
--- a/guake/globals.py
+++ b/guake/globals.py
@@ -70,6 +70,7 @@ def is_run_from_git_workdir():
 ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT = range(3)
 ALIGN_TOP, ALIGN_BOTTOM = range(2)
 ALWAYS_ON_PRIMARY = -1
+PROMPT_NEVER, PROMPT_PROCESSES, PROMPT_ALWAYS = range(3)
 
 # TODO this is not as fancy as as it could be
 # pylint: disable=anomalous-backslash-in-string
diff --git a/guake/guake_app.py b/guake/guake_app.py
index 874afb0dc..cb662c372 100644
--- a/guake/guake_app.py
+++ b/guake/guake_app.py
@@ -54,6 +54,8 @@
 from guake.dialogs import PromptQuitDialog
 from guake.globals import MAX_TRANSPARENCY
 from guake.globals import NAME
+from guake.globals import PROMPT_ALWAYS
+from guake.globals import PROMPT_PROCESSES
 from guake.globals import TABS_SESSION_SCHEMA_VERSION
 from guake.gsettings import GSettingHandler
 from guake.keybindings import Keybindings
@@ -906,13 +908,17 @@ def accel_search_terminal(self, *args):
 
     def accel_quit(self, *args):
         """Callback to prompt the user whether to quit Guake or not."""
-        procs = self.notebook_manager.get_running_fg_processes_count()
+        procs = self.notebook_manager.get_running_fg_processes()
         tabs = self.notebook_manager.get_n_pages()
         notebooks = self.notebook_manager.get_n_notebooks()
         prompt_cfg = self.settings.general.get_boolean("prompt-on-quit")
         prompt_tab_cfg = self.settings.general.get_int("prompt-on-close-tab")
         # "Prompt on tab close" config overrides "prompt on quit" config
-        if prompt_cfg or (prompt_tab_cfg == 1 and procs > 0) or (prompt_tab_cfg == 2):
+        if (
+            prompt_cfg
+            or (prompt_tab_cfg == PROMPT_PROCESSES and procs)
+            or (prompt_tab_cfg == PROMPT_ALWAYS)
+        ):
             log.debug("Remaining procs=%r", procs)
             if PromptQuitDialog(self.window, procs, tabs, notebooks).quit():
                 log.info("Quitting Guake")
diff --git a/guake/notebook.py b/guake/notebook.py
index 627d0b808..2bb976f45 100644
--- a/guake/notebook.py
+++ b/guake/notebook.py
@@ -25,9 +25,12 @@
 from guake.callbacks import MenuHideCallback
 from guake.callbacks import NotebookScrollCallback
 from guake.dialogs import PromptQuitDialog
+from guake.globals import PROMPT_ALWAYS
+from guake.globals import PROMPT_PROCESSES
 from guake.menus import mk_notebook_context_menu
 from guake.prefs import PrefsDialog
 from guake.utils import gdk_is_x11_display
+from guake.utils import get_process_name
 from guake.utils import save_tabs_when_changed
 
 import gi
@@ -247,15 +250,15 @@ def get_terminals(self):
             terminals += page.get_terminals()
         return terminals
 
-    def get_running_fg_processes_count(self):
-        fg_proc_count = 0
+    def get_running_fg_processes(self):
+        processes = []
         for page in self.iter_pages():
-            fg_proc_count += self.get_running_fg_processes_count_page(self.page_num(page))
-        return fg_proc_count
+            processes += self.get_running_fg_processes_page(page)
+        return processes
 
-    def get_running_fg_processes_count_page(self, index):
-        total_procs = 0
-        for terminal in self.get_terminals_for_page(index):
+    def get_running_fg_processes_page(self, page):
+        processes = []
+        for terminal in page.get_terminals():
             pty = terminal.get_pty()
             if not pty:
                 continue
@@ -265,14 +268,13 @@ def get_running_fg_processes_count_page(self, index):
                 fgpid = posix.tcgetpgrp(fdpty)
                 log.debug("found running pid: %s", fgpid)
                 if fgpid not in (-1, term_pid):
-                    total_procs += 1
+                    processes.append((fgpid, get_process_name(fgpid)))
             except OSError:
                 log.debug(
                     "Cannot retrieve any pid from terminal %s, looks like it is already dead",
-                    index,
+                    terminal,
                 )
-                return 0
-        return total_procs
+        return processes
 
     def has_page(self):
         return self.get_n_pages() > 0
@@ -296,16 +298,17 @@ def delete_page(self, page_num, kill=True, prompt=0):
         if page_num >= self.get_n_pages() or page_num < 0:
             log.error("Can not delete page %s no such index", page_num)
             return
+
+        page = self.get_nth_page(page_num)
         # TODO NOTEBOOK it would be nice if none of the "ui" stuff
         # (PromptQuitDialog) would be in here
-        procs = self.get_running_fg_processes_count_page(page_num)
-        if prompt == 2 or (prompt == 1 and procs > 0):
+        procs = self.get_running_fg_processes_page(page)
+        if prompt == PROMPT_ALWAYS or (prompt == PROMPT_PROCESSES and procs):
             # TODO NOTEBOOK remove call to guake
             if not PromptQuitDialog(self.guake.window, procs, -1, None).close_tab():
                 return
 
-        page = self.get_nth_page(page_num)
-        for terminal in self.get_terminals_for_page(page_num):
+        for terminal in page.get_terminals():
             if kill:
                 terminal.kill()
             terminal.destroy()
@@ -609,8 +612,8 @@ def get_n_pages(self):
     def get_n_notebooks(self):
         return len(self.notebooks.keys())
 
-    def get_running_fg_processes_count(self):
-        r_fg_c = 0
+    def get_running_fg_processes(self):
+        processes = []
         for k in self.notebooks:
-            r_fg_c += self.notebooks[k].get_running_fg_processes_count()
-        return r_fg_c
+            processes += self.notebooks[k].get_running_fg_processes()
+        return processes
diff --git a/guake/tests/test_utils.py b/guake/tests/test_utils.py
index 457d764a1..a0f5d7140 100644
--- a/guake/tests/test_utils.py
+++ b/guake/tests/test_utils.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 # pylint: disable=redefined-outer-name
+import os
 
 from guake.utils import FileManager
+from guake.utils import get_process_name
 
 
 def test_file_manager(fs):
@@ -37,3 +39,7 @@ def test_file_manager_clear(fs):
     assert fm.read("/foo/bar") == "test"
     fm.clear()
     assert fm.read("/foo/bar") == "changed"
+
+
+def test_process_name():
+    assert get_process_name(os.getpid())
diff --git a/guake/utils.py b/guake/utils.py
index b2a645f46..fc2e86003 100644
--- a/guake/utils.py
+++ b/guake/utils.py
@@ -22,6 +22,7 @@
 import enum
 import logging
 import os
+import re
 import subprocess
 import time
 import yaml
@@ -524,3 +525,16 @@ def draw(self, widget, cr):
             cr.paint()
 
         cr.restore()
+
+
+def get_process_name(pid):
+    stat_file = f"/proc/{pid}/stat"
+    try:
+        with open(stat_file) as fp:
+            status = fp.read()
+    except IOError as ex:
+        log.debug("Unable to read %s: %s", stat_file, ex)
+        status = ""
+
+    match = re.match(r"\d+ \(([^)]+)\)", status)
+    return match.group(1) if match else None
diff --git a/releasenotes/notes/prompt-quit-process-names-32a4e1b37eb36037.yaml b/releasenotes/notes/prompt-quit-process-names-32a4e1b37eb36037.yaml
new file mode 100644
index 000000000..50931549a
--- /dev/null
+++ b/releasenotes/notes/prompt-quit-process-names-32a4e1b37eb36037.yaml
@@ -0,0 +1,6 @@
+release_summary: >
+  The "are you sure you want to close" dialog will now include names of any running processes.
+
+features:
+  - |
+    - Include names of any processes in PromptQuitDialog, closes #256