diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 5883889343..e6263fccf7 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -99,12 +99,14 @@ def OnBkErase(self, event): class OpenFitsThread(threading.Thread): + def __init__(self, fits, callback): threading.Thread.__init__(self) self.name = "LoadingOpenFits" self.mainFrame = MainFrame.getInstance() self.callback = callback self.fits = fits + self.running = True self.start() def run(self): @@ -118,10 +120,15 @@ def run(self): # We use 1 for all fits except the last one where we use 2 so that we # have correct calculations displayed at startup for fitID in self.fits[:-1]: - wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID, startup=1)) + if self.running: + wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID, startup=1)) + + if self.running: + wx.PostEvent(self.mainFrame, FitSelected(fitID=self.fits[-1], startup=2)) + wx.CallAfter(self.callback) - wx.PostEvent(self.mainFrame, FitSelected(fitID=self.fits[-1], startup=2)) - wx.CallAfter(self.callback) + def stop(self): + self.running = False # todo: include IPortUser again diff --git a/pyfa.py b/pyfa.py index 8917d89de2..fbee4220b9 100755 --- a/pyfa.py +++ b/pyfa.py @@ -151,5 +151,18 @@ def _process_args(self, largs, rargs, values): else: pyfa.MainLoop() - # TODO: Add some thread cleanup code here. Right now we bail, and that can lead to orphaned threads or threads not properly exiting. + # When main loop is over, threads have 5 seconds to comply... + import threading + from utils.timer import CountdownTimer + + timer = CountdownTimer(5) + stoppableThreads = [] + for t in threading.enumerate(): + if t is not threading.main_thread() and hasattr(t, 'stop'): + stoppableThreads.append(t) + t.stop() + for t in stoppableThreads: + t.join(timeout=timer.remainder()) + + # Nah, just kidding, no way to terminate threads - just try to exit sys.exit() diff --git a/service/character.py b/service/character.py index 6e8421468f..93434ae9a7 100644 --- a/service/character.py +++ b/service/character.py @@ -45,11 +45,13 @@ class CharacterImportThread(threading.Thread): + def __init__(self, paths, callback): threading.Thread.__init__(self) self.name = "CharacterImport" self.paths = paths self.callback = callback + self.running = True def run(self): paths = self.paths @@ -61,6 +63,8 @@ def run(self): all_skill_ids.append(skill.itemID) for path in paths: + if not self.running: + break try: charFile = open(path, mode='r').read() doc = minidom.parseString(charFile) @@ -95,6 +99,9 @@ def run(self): wx.CallAfter(self.callback) + def stop(self): + self.running = False + class SkillBackupThread(threading.Thread): def __init__(self, path, saveFmt, activeFit, callback): @@ -104,25 +111,32 @@ def __init__(self, path, saveFmt, activeFit, callback): self.saveFmt = saveFmt self.activeFit = activeFit self.callback = callback + self.running = True def run(self): path = self.path sCharacter = Character.getInstance() - if self.saveFmt == "xml" or self.saveFmt == "emp": - backupData = sCharacter.exportXml() - else: - backupData = sCharacter.exportText() + backupData = None + if self.running: + if self.saveFmt == "xml" or self.saveFmt == "emp": + backupData = sCharacter.exportXml() + else: + backupData = sCharacter.exportText() - if self.saveFmt == "emp": - with gzip.open(path, mode='wb') as backupFile: - backupFile.write(backupData.encode()) - else: - with open(path, mode='w', encoding='utf-8') as backupFile: - backupFile.write(backupData) + if self.running and backupData is not None: + if self.saveFmt == "emp": + with gzip.open(path, mode='wb') as backupFile: + backupFile.write(backupData.encode()) + else: + with open(path, mode='w', encoding='utf-8') as backupFile: + backupFile.write(backupData) wx.CallAfter(self.callback) + def stop(self): + self.running = False + class Character: instance = None @@ -474,12 +488,14 @@ def _checkRequirements(self, char, subThing, reqs): class UpdateAPIThread(threading.Thread): + def __init__(self, charID, callback): threading.Thread.__init__(self) self.name = "CheckUpdate" self.callback = callback self.charID = charID + self.running = True def run(self): try: @@ -488,20 +504,31 @@ def run(self): sEsi = Esi.getInstance() sChar = Character.getInstance() ssoChar = sChar.getSsoCharacter(char.ID) + + if not self.running: + self.callback[0](self.callback[1]) + return resp = sEsi.getSkills(ssoChar.ID) + if not self.running: + self.callback[0](self.callback[1]) + return # todo: check if alpha. if so, pop up a question if they want to apply it as alpha. Use threading events to set the answer? char.clearSkills() for skillRow in resp["skills"]: char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"])) + if not self.running: + self.callback[0](self.callback[1]) + return resp = sEsi.getSecStatus(ssoChar.ID) - char.secStatus = resp['security_status'] - self.callback[0](self.callback[1]) except (KeyboardInterrupt, SystemExit): raise except Exception as ex: pyfalog.warn(ex) self.callback[0](self.callback[1], sys.exc_info()) + + def stop(self): + self.running = False diff --git a/service/market.py b/service/market.py index 4856a1a63c..d3a065d552 100644 --- a/service/market.py +++ b/service/market.py @@ -46,6 +46,7 @@ def __init__(self): threading.Thread.__init__(self) pyfalog.debug("Initialize ShipBrowserWorkerThread.") self.name = "ShipBrowser" + self.running = True def run(self): self.queue = queue.Queue() @@ -60,6 +61,8 @@ def processRequests(self): cache = self.cache sMkt = Market.getInstance() while True: + if not self.running: + break try: id_, callback = queue.get() set_ = cache.get(id_) @@ -82,6 +85,9 @@ def processRequests(self): pyfalog.critical("Queue task done failed.") pyfalog.critical(e) + def stop(self): + self.running = False + class SearchWorkerThread(threading.Thread): def __init__(self): @@ -91,6 +97,7 @@ def __init__(self): # load the jargon while in an out-of-thread context, to spot any problems while in the main thread self.jargonLoader.get_jargon() self.jargonLoader.get_jargon().apply('test string') + self.running = True def run(self): self.cv = threading.Condition() @@ -101,6 +108,8 @@ def processSearches(self): cv = self.cv while True: + if not self.running: + break cv.acquire() while self.searchRequest is None: cv.wait() @@ -161,6 +170,8 @@ def scheduleSearch(self, text, callback, filterName=None): self.cv.notify() self.cv.release() + def stop(self): + self.running = False class Market: instance = None diff --git a/service/price.py b/service/price.py index d444025408..fdc31a3e50 100644 --- a/service/price.py +++ b/service/price.py @@ -235,11 +235,14 @@ def __init__(self): self.name = "PriceWorker" self.queue = queue.Queue() self.wait = {} + self.running = True pyfalog.debug("Initialize PriceWorkerThread.") def run(self): queue = self.queue while True: + if not self.running: + break # Grab our data callback, requests, fetchTimeout, validityOverride = queue.get() @@ -265,6 +268,9 @@ def setToWait(self, prices, callback): callbacks = self.wait.setdefault(price.typeID, []) callbacks.append(callback) + def stop(self): + self.running = False + # Import market sources only to initialize price source modules, they register on their own from service.marketSources import evemarketer, evemarketdata, evepraisal # noqa: E402 diff --git a/service/update.py b/service/update.py index 970ed84840..f1955a766b 100644 --- a/service/update.py +++ b/service/update.py @@ -41,6 +41,7 @@ def __init__(self, callback): self.callback = callback self.settings = UpdateSettings.getInstance() self.network = Network.getInstance() + self.running = True def run(self): network = Network.getInstance() @@ -49,13 +50,13 @@ def run(self): try: response = network.get( url='https://www.pyfa.io/update_check?pyfa_version={}&client_hash={}'.format(config.version, config.getClientSecret()), - type=network.UPDATE) + type=network.UPDATE, timeout=5) except (KeyboardInterrupt, SystemExit): raise except Exception as e: response = network.get( url='https://api.github.com/repos/pyfa-org/Pyfa/releases', - type=network.UPDATE) + type=network.UPDATE, timeout=5) jsonResponse = response.json() jsonResponse.sort( @@ -94,6 +95,9 @@ def run(self): def versiontuple(v): return tuple(map(int, (v.split(".")))) + def stop(self): + self.running = False + class Update: instance = None diff --git a/utils/timer.py b/utils/timer.py index 0058506a22..d56b64730a 100644 --- a/utils/timer.py +++ b/utils/timer.py @@ -35,3 +35,16 @@ def __enter__(self): def __exit__(self, type, value, traceback): self.checkpoint('finished') pass + + +class CountdownTimer: + + def __init__(self, timeout): + self.timeout = timeout + self.start = time.time() + + def elapsed(self): + return time.time() - self.start + + def remainder(self): + return max(self.timeout - self.elapsed(), 0)