diff --git a/cddagl/ui.py b/cddagl/ui.py index 09d0b62e..c0c25292 100644 --- a/cddagl/ui.py +++ b/cddagl/ui.py @@ -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 @@ -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() @@ -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: @@ -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'))): @@ -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) @@ -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() @@ -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() @@ -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', @@ -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() @@ -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 diff --git a/cddagl/win32.py b/cddagl/win32.py index 6a7d1ef8..fdea0c58 100644 --- a/cddagl/win32.py +++ b/cddagl/win32.py @@ -13,6 +13,8 @@ import win32api import win32event import win32pipe +import win32con +import win32 from pywintypes import error as WinError @@ -24,6 +26,7 @@ ntdll = WinDLL('ntdll') kernel32 = WinDLL('kernel32') +psapi = WinDLL('psapi.dll') PVOID = c_void_p PULONG = POINTER(ULONG) @@ -31,6 +34,7 @@ ACCESS_MASK = DWORD SW_SHOWNORMAL = 1 +MAX_PATH = 260 VISTA_OR_LATER = sys.getwindowsversion()[0] >= 6 @@ -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) @@ -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), @@ -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 @@ -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