Attempt to terminate threads when pyfa is closed

This commit is contained in:
DarkPhoenix
2020-02-03 17:12:23 +03:00
parent 9ddfcc894f
commit 6527f9e11e
7 changed files with 99 additions and 18 deletions

View File

@@ -99,12 +99,14 @@ class PFPanel(wx.Panel):
class OpenFitsThread(threading.Thread): class OpenFitsThread(threading.Thread):
def __init__(self, fits, callback): def __init__(self, fits, callback):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "LoadingOpenFits" self.name = "LoadingOpenFits"
self.mainFrame = MainFrame.getInstance() self.mainFrame = MainFrame.getInstance()
self.callback = callback self.callback = callback
self.fits = fits self.fits = fits
self.running = True
self.start() self.start()
def run(self): def run(self):
@@ -118,10 +120,15 @@ class OpenFitsThread(threading.Thread):
# We use 1 for all fits except the last one where we use 2 so that we # We use 1 for all fits except the last one where we use 2 so that we
# have correct calculations displayed at startup # have correct calculations displayed at startup
for fitID in self.fits[:-1]: 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))
wx.PostEvent(self.mainFrame, FitSelected(fitID=self.fits[-1], startup=2)) if self.running:
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 # todo: include IPortUser again

15
pyfa.py
View File

@@ -151,5 +151,18 @@ if __name__ == "__main__":
else: else:
pyfa.MainLoop() 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() sys.exit()

View File

@@ -45,11 +45,13 @@ pyfalog = Logger(__name__)
class CharacterImportThread(threading.Thread): class CharacterImportThread(threading.Thread):
def __init__(self, paths, callback): def __init__(self, paths, callback):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "CharacterImport" self.name = "CharacterImport"
self.paths = paths self.paths = paths
self.callback = callback self.callback = callback
self.running = True
def run(self): def run(self):
paths = self.paths paths = self.paths
@@ -61,6 +63,8 @@ class CharacterImportThread(threading.Thread):
all_skill_ids.append(skill.itemID) all_skill_ids.append(skill.itemID)
for path in paths: for path in paths:
if not self.running:
break
try: try:
charFile = open(path, mode='r').read() charFile = open(path, mode='r').read()
doc = minidom.parseString(charFile) doc = minidom.parseString(charFile)
@@ -95,6 +99,9 @@ class CharacterImportThread(threading.Thread):
wx.CallAfter(self.callback) wx.CallAfter(self.callback)
def stop(self):
self.running = False
class SkillBackupThread(threading.Thread): class SkillBackupThread(threading.Thread):
def __init__(self, path, saveFmt, activeFit, callback): def __init__(self, path, saveFmt, activeFit, callback):
@@ -104,25 +111,32 @@ class SkillBackupThread(threading.Thread):
self.saveFmt = saveFmt self.saveFmt = saveFmt
self.activeFit = activeFit self.activeFit = activeFit
self.callback = callback self.callback = callback
self.running = True
def run(self): def run(self):
path = self.path path = self.path
sCharacter = Character.getInstance() sCharacter = Character.getInstance()
if self.saveFmt == "xml" or self.saveFmt == "emp": backupData = None
backupData = sCharacter.exportXml() if self.running:
else: if self.saveFmt == "xml" or self.saveFmt == "emp":
backupData = sCharacter.exportText() backupData = sCharacter.exportXml()
else:
backupData = sCharacter.exportText()
if self.saveFmt == "emp": if self.running and backupData is not None:
with gzip.open(path, mode='wb') as backupFile: if self.saveFmt == "emp":
backupFile.write(backupData.encode()) with gzip.open(path, mode='wb') as backupFile:
else: backupFile.write(backupData.encode())
with open(path, mode='w', encoding='utf-8') as backupFile: else:
backupFile.write(backupData) with open(path, mode='w', encoding='utf-8') as backupFile:
backupFile.write(backupData)
wx.CallAfter(self.callback) wx.CallAfter(self.callback)
def stop(self):
self.running = False
class Character: class Character:
instance = None instance = None
@@ -474,12 +488,14 @@ class Character:
class UpdateAPIThread(threading.Thread): class UpdateAPIThread(threading.Thread):
def __init__(self, charID, callback): def __init__(self, charID, callback):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.name = "CheckUpdate" self.name = "CheckUpdate"
self.callback = callback self.callback = callback
self.charID = charID self.charID = charID
self.running = True
def run(self): def run(self):
try: try:
@@ -488,20 +504,31 @@ class UpdateAPIThread(threading.Thread):
sEsi = Esi.getInstance() sEsi = Esi.getInstance()
sChar = Character.getInstance() sChar = Character.getInstance()
ssoChar = sChar.getSsoCharacter(char.ID) ssoChar = sChar.getSsoCharacter(char.ID)
if not self.running:
self.callback[0](self.callback[1])
return
resp = sEsi.getSkills(ssoChar.ID) 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? # 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() char.clearSkills()
for skillRow in resp["skills"]: for skillRow in resp["skills"]:
char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"])) 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) resp = sEsi.getSecStatus(ssoChar.ID)
char.secStatus = resp['security_status'] char.secStatus = resp['security_status']
self.callback[0](self.callback[1]) self.callback[0](self.callback[1])
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except Exception as ex: except Exception as ex:
pyfalog.warn(ex) pyfalog.warn(ex)
self.callback[0](self.callback[1], sys.exc_info()) self.callback[0](self.callback[1], sys.exc_info())
def stop(self):
self.running = False

View File

@@ -46,6 +46,7 @@ class ShipBrowserWorkerThread(threading.Thread):
threading.Thread.__init__(self) threading.Thread.__init__(self)
pyfalog.debug("Initialize ShipBrowserWorkerThread.") pyfalog.debug("Initialize ShipBrowserWorkerThread.")
self.name = "ShipBrowser" self.name = "ShipBrowser"
self.running = True
def run(self): def run(self):
self.queue = queue.Queue() self.queue = queue.Queue()
@@ -60,6 +61,8 @@ class ShipBrowserWorkerThread(threading.Thread):
cache = self.cache cache = self.cache
sMkt = Market.getInstance() sMkt = Market.getInstance()
while True: while True:
if not self.running:
break
try: try:
id_, callback = queue.get() id_, callback = queue.get()
set_ = cache.get(id_) set_ = cache.get(id_)
@@ -82,6 +85,9 @@ class ShipBrowserWorkerThread(threading.Thread):
pyfalog.critical("Queue task done failed.") pyfalog.critical("Queue task done failed.")
pyfalog.critical(e) pyfalog.critical(e)
def stop(self):
self.running = False
class SearchWorkerThread(threading.Thread): class SearchWorkerThread(threading.Thread):
def __init__(self): def __init__(self):
@@ -91,6 +97,7 @@ class SearchWorkerThread(threading.Thread):
# load the jargon while in an out-of-thread context, to spot any problems while in the main thread # 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()
self.jargonLoader.get_jargon().apply('test string') self.jargonLoader.get_jargon().apply('test string')
self.running = True
def run(self): def run(self):
self.cv = threading.Condition() self.cv = threading.Condition()
@@ -101,6 +108,8 @@ class SearchWorkerThread(threading.Thread):
cv = self.cv cv = self.cv
while True: while True:
if not self.running:
break
cv.acquire() cv.acquire()
while self.searchRequest is None: while self.searchRequest is None:
cv.wait() cv.wait()
@@ -161,6 +170,8 @@ class SearchWorkerThread(threading.Thread):
self.cv.notify() self.cv.notify()
self.cv.release() self.cv.release()
def stop(self):
self.running = False
class Market: class Market:
instance = None instance = None

View File

@@ -235,11 +235,14 @@ class PriceWorkerThread(threading.Thread):
self.name = "PriceWorker" self.name = "PriceWorker"
self.queue = queue.Queue() self.queue = queue.Queue()
self.wait = {} self.wait = {}
self.running = True
pyfalog.debug("Initialize PriceWorkerThread.") pyfalog.debug("Initialize PriceWorkerThread.")
def run(self): def run(self):
queue = self.queue queue = self.queue
while True: while True:
if not self.running:
break
# Grab our data # Grab our data
callback, requests, fetchTimeout, validityOverride = queue.get() callback, requests, fetchTimeout, validityOverride = queue.get()
@@ -265,6 +268,9 @@ class PriceWorkerThread(threading.Thread):
callbacks = self.wait.setdefault(price.typeID, []) callbacks = self.wait.setdefault(price.typeID, [])
callbacks.append(callback) callbacks.append(callback)
def stop(self):
self.running = False
# Import market sources only to initialize price source modules, they register on their own # Import market sources only to initialize price source modules, they register on their own
from service.marketSources import evemarketer, evemarketdata, evepraisal # noqa: E402 from service.marketSources import evemarketer, evemarketdata, evepraisal # noqa: E402

View File

@@ -41,6 +41,7 @@ class CheckUpdateThread(threading.Thread):
self.callback = callback self.callback = callback
self.settings = UpdateSettings.getInstance() self.settings = UpdateSettings.getInstance()
self.network = Network.getInstance() self.network = Network.getInstance()
self.running = True
def run(self): def run(self):
network = Network.getInstance() network = Network.getInstance()
@@ -49,13 +50,13 @@ class CheckUpdateThread(threading.Thread):
try: try:
response = network.get( response = network.get(
url='https://www.pyfa.io/update_check?pyfa_version={}&client_hash={}'.format(config.version, config.getClientSecret()), 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): except (KeyboardInterrupt, SystemExit):
raise raise
except Exception as e: except Exception as e:
response = network.get( response = network.get(
url='https://api.github.com/repos/pyfa-org/Pyfa/releases', url='https://api.github.com/repos/pyfa-org/Pyfa/releases',
type=network.UPDATE) type=network.UPDATE, timeout=5)
jsonResponse = response.json() jsonResponse = response.json()
jsonResponse.sort( jsonResponse.sort(
@@ -94,6 +95,9 @@ class CheckUpdateThread(threading.Thread):
def versiontuple(v): def versiontuple(v):
return tuple(map(int, (v.split(".")))) return tuple(map(int, (v.split("."))))
def stop(self):
self.running = False
class Update: class Update:
instance = None instance = None

View File

@@ -35,3 +35,16 @@ class Timer:
def __exit__(self, type, value, traceback): def __exit__(self, type, value, traceback):
self.checkpoint('finished') self.checkpoint('finished')
pass 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)