2

I have been trying to determine a method to close Chrome (and other processes) gracefully utilizing Python. I have gone through a few methods but all result in Chrome displaying a message stating "Chrome didn't shut down correctly." with a Restore button. This doesn't happen when you shut down a program from task manager in Windows. I have written a program utilizing Falcon to provide API access to shut down 'managed' programs. The code for that isn't relevant, but here are the methods I have tried that do work to shut down Chrome that work well, but not gracefully.

def stop_app(app_exe):
    for process in psutil.process_iter():
        if process.name() == app_exe:
            process.terminate()

Alternate:

for process in psutil.process_iter():
    if process.name() == app_exe:
        children = process.children(recursive=True)
        children.append(process)
        for p in children:
            p.send_signal(signal.CTRL_BREAK_EVENT)

I have also tried kill() in place of terminate(), various signal types (Windows really only supports three, CTRL_BREAK_EVENT, CNTL_C_EVENT, SIGTERM).

I have also tried using the win32 library and I had the same issue. Win32 also isn't platform agnostic and I had found that win32/wmi libraries utilize a lot of system resources when looking for running windows processes. The psutil library allows for being platform agnostic AND doesn't have the same performance issue when iterating through the Windows processes. Here is the code for that.

c = wmi.WMI()
for process in c.Win32_Process():
    if process.Name == app_exe:
        process.Terminate()

Anyone out there try something similar and come up with a solution?

Quick solution per CristiFati recommendations.:

def enumWindowsProc(hwnd, lParam):
    if (lParam is None) or ((lParam is not None) and 
          win32process.GetWindowThreadProcessId(hwnd)[1] == pid)):
        text = win32gui.GetWindowText(hwnd)
        if text:
            wStyle = win32api.GetWindowLong(hwnd, win32con.GWL_STYLE)
            if wStyle & win32con.WS_VISIBLE:
                win32api.SendMessage(hwnd, win32con.WM_CLOSE)


def stop_app(app_exe):
    for process in psutil.process_iter():
        if process.name() == app_exe:
            win32gui.EnumWindows(enumWindowsProc, process.pid)
Sherd
  • 468
  • 7
  • 17

2 Answers2

2

Apparently, Chrome is not happy when being closed by process termination. It has a mechanism of detecting this exact scenario.
It has a bunch of processes running, and I think it could be a way of gracefully closing it (I might be wrong, but I remember that once I managed to do it), by carefully selecting the order which the processes are killed (crashpad-handler, utility, renderers, before the master one), but now I can't do it, neither do I know how gracefully that actually is.

Note: I personally consider this behavior very useful, because I have many opened Chrome windows each with many tabs, and every time I need to restart my laptop, I close Chrome from Task Manager, so that next time I open it it will restore the current state.

Back, to our problem, one way would be to get its window and sending it the [MS.Docs]: WM_CLOSE message (also check [MS.Docs]: Closing the Window). Note that it's Win specific!

  • You already have the code that gets the processes (pids) with a certain name
  • [SO]: Get the title of a window of another program using the process name (@CristiFati's answer) contains the code that gets windows (titles) from processes (ids). Actually it contains more, but that's what you need from there (enumWindowsProc and enumProcWnds functions)
  • Code that closes the window:

    win32api.SendMessage(0x005C0974, win32con.WM_CLOSE)  # 0x005C0974 was Chrome's (main) window handle in my test
    
  • All you need to do, is combine the above 3 (with slight changes)

Another example that works with windows: [SO]: Keyboard event not sent to window with pywin32 (@CristiFati's answer).

CristiFati
  • 38,250
  • 9
  • 50
  • 87
  • Going to see if I can make this work and will update accordingly. I wonder if FireFox displays similar behavior? – Sherd Feb 27 '19 at 17:23
  • 1
    It doesn't display the error message. If it was abnormally shut down (from *Task Manager*), when started again it will reopen all tabs. If closed normally, when started again, only the homepage (that can be changes from settings) is displayed. – CristiFati Feb 27 '19 at 17:41
  • This worked. I don't like having to rely on the win32 api, though this might be necessary. Will keep looking for alternate using psutil and will update if I do find a working method. Thanks for the help @CristiFati! – Sherd Feb 27 '19 at 18:17
  • you want to be able to run on *Nix* as well? I don't know how the 2 browsers behave there when the process is killed. Also I don't know how to work with their windows system (I suppose it depends on the *X* server *API*). – CristiFati Feb 27 '19 at 18:20
  • Yes, right now the program is platform agnostic. I originally was using win32 per above, but saw those performance issues and one of our goals in the future is to move these terminals away from Windows. For now this solution will fix it, but we might have other options going forward to mitigate this issue. Psutil is platform agnostic, so if I can figure out how to do something similar utilizing that library, it should at least make it easier to get working on Linux. – Sherd Feb 27 '19 at 19:48
  • If the browser behave the same on *Nix* (complain when process is shut down - and personally I don't see why they wouldn't) the I don't think you can use *psutil* (at least not alone). – CristiFati Feb 27 '19 at 19:57
2

A more generic version that sends the close message to apps, including those only visible in the system tray, with all included needed imports:

import win32con
import win32api
import win32gui
import win32process
import psutil

def enumWindowsProc(hwnd, lParam):
    if (lParam is None) or ((lParam is not None) and win32process.GetWindowThreadProcessId(hwnd)[1] == lParam):
        text = win32gui.GetWindowText(hwnd)
        if text:
            win32api.SendMessage(hwnd, win32con.WM_CLOSE)

def stop_app(app_exe):
    for process in psutil.process_iter():
        if process.name() == app_exe:
            win32gui.EnumWindows(enumWindowsProc, process.pid)

stop_app("appname.exe")

(Based off of 'Quick solution per CristiFati recommendation' in the Question, but changes 'pid' to 'lParam', includes needed imports, and also works on apps only visible in the system tray.)

Andrew D. Bond
  • 902
  • 1
  • 11
  • 11