Skip to content
This repository has been archived by the owner on Jul 11, 2023. It is now read-only.

Commit

Permalink
Detect if game process is running and adjust UI accordingly
Browse files Browse the repository at this point in the history
  • Loading branch information
remyroy committed Mar 30, 2016
1 parent 9eae408 commit 8e44bd6
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 22 deletions.
138 changes: 122 additions & 16 deletions cddagl/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
new_build, config_true)
from cddagl.win32 import (
find_process_with_file_handle, get_downloads_directory, get_ui_locale,
activate_window, SimpleNamedPipe, SingleInstance)
activate_window, SimpleNamedPipe, SingleInstance, process_id_from_path,
wait_for_pid)

from .__version__ import version

Expand Down Expand Up @@ -719,6 +720,7 @@ def __init__(self):
self.dir_combo_inserting = False

self.game_process = None
self.game_process_id = None
self.game_started = False

layout = QGridLayout()
Expand Down Expand Up @@ -937,10 +939,15 @@ def restore_previous(self):
self.game_directory_changed()

def focus_game(self):
if self.game_process is None:
if self.game_process is None and self.game_process_id is None:
return

activate_window(self.game_process.pid)
if self.game_process is not None:
pid = self.game_process.pid
elif self.game_process_id is not None:
pid = self.game_process_id

activate_window(pid)

def launch_game(self):
if self.game_started:
Expand Down Expand Up @@ -1176,6 +1183,8 @@ def game_directory_changed(self):
self.launch_game_button.setEnabled(True)
update_group_box.update_button.setText(_('Update game'))

self.check_running_process(self.exe_path)

self.last_game_directory = directory
if not (getattr(sys, 'frozen', False)
and config_true(get_config_value('use_launcher_dir', 'False'))):
Expand Down Expand Up @@ -1242,12 +1251,16 @@ def timeout():
status_bar.removeWidget(self.reading_progress_bar)

status_bar.busy -= 1
if status_bar.busy == 0:
if status_bar.busy == 0 and not self.game_started:
if self.restored_previous:
status_bar.showMessage(_('Previous version restored'))
status_bar.showMessage(
_('Previous version restored'))
else:
status_bar.showMessage(_('Ready'))

if status_bar.busy == 0 and self.game_started:
status_bar.showMessage(_('Game process is running'))

sha256 = self.exe_sha256.hexdigest()

new_version(self.game_version, sha256)
Expand All @@ -1267,7 +1280,8 @@ def timeout():

if (update_group_box.builds is not None
and len(update_group_box.builds) > 0
and status_bar.busy == 0):
and status_bar.busy == 0
and not self.game_started):
last_build = update_group_box.builds[0]

message = status_bar.currentMessage()
Expand Down Expand Up @@ -1311,6 +1325,89 @@ def timeout():
import pdb; pdb.set_trace()
pyqtRestoreInputHook()'''

def check_running_process(self, exe_path):
pid = process_id_from_path(exe_path)

if pid is not None:
self.game_started = True
self.game_process_id = pid

main_window = self.get_main_window()
status_bar = main_window.statusBar()

if status_bar.busy == 0:
status_bar.showMessage(_('Game process is running'))

main_tab = self.get_main_tab()
update_group_box = main_tab.update_group_box

self.disable_controls()
update_group_box.disable_controls(True)

soundpacks_tab = main_tab.get_soundpacks_tab()
mods_tab = main_tab.get_mods_tab()
settings_tab = main_tab.get_settings_tab()
backups_tab = main_tab.get_backups_tab()

soundpacks_tab.disable_tab()
mods_tab.disable_tab()
settings_tab.disable_tab()
backups_tab.disable_tab()

self.launch_game_button.setText(_('Show current game'))
self.launch_game_button.setEnabled(True)

class ProcessWaitThread(QThread):
ended = pyqtSignal()

def __init__(self, pid):
super(ProcessWaitThread, self).__init__()

self.pid = pid

def __del__(self):
self.wait()

def run(self):
wait_for_pid(self.pid)
self.ended.emit()

def process_ended():
self.process_wait_thread = None

self.game_process_id = None
self.game_started = False

status_bar.showMessage(_('Game process has ended'))

self.enable_controls()
update_group_box.enable_controls()

soundpacks_tab.enable_tab()
mods_tab.enable_tab()
settings_tab.enable_tab()
backups_tab.enable_tab()

self.launch_game_button.setText(_('Launch game'))

self.get_main_window().setWindowState(Qt.WindowActive)

self.update_saves()

if config_true(get_config_value('backup_on_end', 'False')):
backups_tab.prune_auto_backups()

name = '{auto}_{name}'.format(auto=_('auto'),
name=_('after_end'))

backups_tab.backup_saves(name)

process_wait_thread = ProcessWaitThread(self.game_process_id)
process_wait_thread.ended.connect(process_ended)
process_wait_thread.start()

self.process_wait_thread = process_wait_thread

def add_game_dir(self):
new_game_dir = self.dir_combo.currentText()

Expand Down Expand Up @@ -2705,11 +2802,19 @@ def lb_http_finished(self):
status_bar.removeWidget(self.fetching_label)
status_bar.removeWidget(self.fetching_progress_bar)

main_tab = self.get_main_tab()
game_dir_group_box = main_tab.game_dir_group_box

status_bar.busy -= 1
if status_bar.busy == 0:
status_bar.showMessage(_('Ready'))

self.enable_controls()
if not game_dir_group_box.game_started:
if status_bar.busy == 0:
status_bar.showMessage(_('Ready'))

self.enable_controls()
else:
if status_bar.busy == 0:
status_bar.showMessage(_('Game process is running'))

self.lb_html.seek(0)
document = html5lib.parse(self.lb_html, treebuilder='lxml',
Expand Down Expand Up @@ -2764,16 +2869,19 @@ def lb_http_finished(self):
self.builds_combo.addItem(_('{number} ({delta})').format(
number=build['number'], delta=human_delta))

self.builds_combo.setEnabled(True)

main_tab = self.get_main_tab()
game_dir_group_box = main_tab.game_dir_group_box
if not game_dir_group_box.game_started:
self.builds_combo.setEnabled(True)
self.update_button.setEnabled(True)
else:
self.previous_bc_enabled = True
self.previous_ub_enabled = True

if game_dir_group_box.exe_path is not None:
self.update_button.setText(_('Update game'))

if (game_dir_group_box.current_build is not None
and status_bar.busy == 0):
and status_bar.busy == 0
and not game_dir_group_box.game_started):
last_build = self.builds[0]

message = status_bar.currentMessage()
Expand All @@ -2788,8 +2896,6 @@ def lb_http_finished(self):
else:
self.update_button.setText(_('Install game'))

self.update_button.setEnabled(True)

else:
self.builds = None

Expand Down
105 changes: 99 additions & 6 deletions cddagl/win32.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import win32api
import win32event
import win32pipe
import win32con
import win32

from pywintypes import error as WinError

Expand All @@ -24,13 +26,15 @@

ntdll = WinDLL('ntdll')
kernel32 = WinDLL('kernel32')
psapi = WinDLL('psapi.dll')

PVOID = c_void_p
PULONG = POINTER(ULONG)
ULONG_PTR = WPARAM
ACCESS_MASK = DWORD

SW_SHOWNORMAL = 1
MAX_PATH = 260

VISTA_OR_LATER = sys.getwindowsversion()[0] >= 6

Expand Down Expand Up @@ -107,9 +111,13 @@ def __repr__(self):
STATUS_INFO_LENGTH_MISMATCH = NTSTATUS(0xC0000004)
STATUS_ACCESS_DENIED = NTSTATUS(0xC0000022)

SYNCHRONIZE = DWORD(0x00100000)
PROCESS_DUP_HANDLE = DWORD(0x0040)
PROCESS_QUERY_INFORMATION = DWORD(0x0400)
PROCESS_QUERY_LIMITED_INFORMATION = DWORD(0x1000)
PROCESS_VM_READ = DWORD(0x0010)

INFINITE = DWORD(0xFFFFFFFF)

def WinErrorFromNtStatus(status):
last_error = ntdll.RtlNtStatusToDosError(status)
Expand Down Expand Up @@ -260,13 +268,19 @@ class PROCESS_INFO_CLASS(Enumeration):
kernel32.OpenProcess.argtypes = (
DWORD, # DesiredAccess
BOOL, # InheritHandle
DWORD # ProcessId
)
DWORD) # ProcessId


kernel32.WaitForSingleObject.restype = DWORD
kernel32.WaitForSingleObject.argtypes = (
HANDLE, # hHandle
DWORD) # dwMilliseconds

kernel32.GetLastError.restype = DWORD
kernel32.GetCurrentProcess.restype = HANDLE
kernel32.CloseHandle.restype = BOOL


class GUID(Structure): # [1]
_fields_ = [
("Data1", DWORD),
Expand Down Expand Up @@ -377,19 +391,36 @@ class FOLDERID: # [2]
VideosLibrary = UUID('{491E922F-5643-4AF4-A7EB-4E7A138D8174}')
Windows = UUID('{F38BF404-1D43-42F2-9305-67DE0B28FC23}')


class UserHandle:
current = HANDLE(0)
common = HANDLE(-1)

_CoTaskMemFree = windll.ole32.CoTaskMemFree
_CoTaskMemFree.restype= None
_CoTaskMemFree.argtypes = [c_void_p]
_CoTaskMemFree.restype = None
_CoTaskMemFree.argtypes = (c_void_p, )

if VISTA_OR_LATER:
_SHGetKnownFolderPath = windll.shell32.SHGetKnownFolderPath
_SHGetKnownFolderPath.argtypes = [
_SHGetKnownFolderPath.argtypes = (
POINTER(GUID), DWORD, HANDLE, POINTER(c_wchar_p)
]
)

QueryFullProcessImageName = kernel32.QueryFullProcessImageNameW
QueryFullProcessImageName.restype = BOOL
QueryFullProcessImageName.argtypes = (
HANDLE, # hProcess
DWORD, # dwFlags
LPWSTR, # lpExeName
PDWORD) # lpdwSize

GetModuleFileNameEx = psapi.GetModuleFileNameExW
GetModuleFileNameEx.restype = DWORD
GetModuleFileNameEx.argtypes = (
HANDLE, # hProcess
HMODULE, # hModule
LPWSTR, # lpFilename
DWORD) # nSize


class PathNotFoundException(Exception): pass
Expand Down Expand Up @@ -428,6 +459,68 @@ def list_handles():
raise WinErrorFromNtStatus(status)
return info.Handles

def process_id_from_path(path):
lower_path = path.lower()

pids = win32process.EnumProcesses()

for pid in pids:
if VISTA_OR_LATER:
desired_access = PROCESS_QUERY_LIMITED_INFORMATION
phandle = kernel32.OpenProcess(desired_access, BOOL(False), pid)

if phandle is None:
continue

path = ctypes.create_unicode_buffer(MAX_PATH)
length = DWORD(MAX_PATH)
flags = DWORD(0)
ret = QueryFullProcessImageName(phandle, flags, path, byref(length))

if ret != 0:
found_path = path.value.lower()

if found_path == lower_path:
kernel32.CloseHandle(phandle)

return pid
else:
desired_access = DWORD(PROCESS_VM_READ.value |
PROCESS_QUERY_INFORMATION.value)
phandle = kernel32.OpenProcess(desired_access, BOOL(False), pid)

if phandle is None:
continue

path = ctypes.create_unicode_buffer(MAX_PATH)
length = DWORD(MAX_PATH)
ret = GetModuleFileNameEx(phandle, None, path, length)

if ret > 0:
found_path = path.value.lower()

if found_path == lower_path:
kernel32.CloseHandle(phandle)

return pid

kernel32.CloseHandle(phandle)

return None

def wait_for_pid(pid):
desired_access = SYNCHRONIZE
phandle = kernel32.OpenProcess(desired_access, BOOL(False), pid)

if phandle is None:
return False

kernel32.WaitForSingleObject(phandle, INFINITE)

kernel32.CloseHandle(phandle)

return True

# Find the process which is using the file handle
def find_process_with_file_handle(path):
# Check for drive absolute path
Expand Down

0 comments on commit 8e44bd6

Please sign in to comment.