Skip to content

Commit 6527f9e

Browse files
committed
Attempt to terminate threads when pyfa is closed
1 parent 9ddfcc8 commit 6527f9e

File tree

7 files changed

+99
-18
lines changed

7 files changed

+99
-18
lines changed

gui/mainFrame.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,14 @@ def OnBkErase(self, event):
9999

100100

101101
class OpenFitsThread(threading.Thread):
102+
102103
def __init__(self, fits, callback):
103104
threading.Thread.__init__(self)
104105
self.name = "LoadingOpenFits"
105106
self.mainFrame = MainFrame.getInstance()
106107
self.callback = callback
107108
self.fits = fits
109+
self.running = True
108110
self.start()
109111

110112
def run(self):
@@ -118,10 +120,15 @@ def run(self):
118120
# We use 1 for all fits except the last one where we use 2 so that we
119121
# have correct calculations displayed at startup
120122
for fitID in self.fits[:-1]:
121-
wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID, startup=1))
123+
if self.running:
124+
wx.PostEvent(self.mainFrame, FitSelected(fitID=fitID, startup=1))
125+
126+
if self.running:
127+
wx.PostEvent(self.mainFrame, FitSelected(fitID=self.fits[-1], startup=2))
128+
wx.CallAfter(self.callback)
122129

123-
wx.PostEvent(self.mainFrame, FitSelected(fitID=self.fits[-1], startup=2))
124-
wx.CallAfter(self.callback)
130+
def stop(self):
131+
self.running = False
125132

126133

127134
# todo: include IPortUser again

pyfa.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,18 @@ def _process_args(self, largs, rargs, values):
151151
else:
152152
pyfa.MainLoop()
153153

154-
# TODO: Add some thread cleanup code here. Right now we bail, and that can lead to orphaned threads or threads not properly exiting.
154+
# When main loop is over, threads have 5 seconds to comply...
155+
import threading
156+
from utils.timer import CountdownTimer
157+
158+
timer = CountdownTimer(5)
159+
stoppableThreads = []
160+
for t in threading.enumerate():
161+
if t is not threading.main_thread() and hasattr(t, 'stop'):
162+
stoppableThreads.append(t)
163+
t.stop()
164+
for t in stoppableThreads:
165+
t.join(timeout=timer.remainder())
166+
167+
# Nah, just kidding, no way to terminate threads - just try to exit
155168
sys.exit()

service/character.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@
4545

4646

4747
class CharacterImportThread(threading.Thread):
48+
4849
def __init__(self, paths, callback):
4950
threading.Thread.__init__(self)
5051
self.name = "CharacterImport"
5152
self.paths = paths
5253
self.callback = callback
54+
self.running = True
5355

5456
def run(self):
5557
paths = self.paths
@@ -61,6 +63,8 @@ def run(self):
6163
all_skill_ids.append(skill.itemID)
6264

6365
for path in paths:
66+
if not self.running:
67+
break
6468
try:
6569
charFile = open(path, mode='r').read()
6670
doc = minidom.parseString(charFile)
@@ -95,6 +99,9 @@ def run(self):
9599

96100
wx.CallAfter(self.callback)
97101

102+
def stop(self):
103+
self.running = False
104+
98105

99106
class SkillBackupThread(threading.Thread):
100107
def __init__(self, path, saveFmt, activeFit, callback):
@@ -104,25 +111,32 @@ def __init__(self, path, saveFmt, activeFit, callback):
104111
self.saveFmt = saveFmt
105112
self.activeFit = activeFit
106113
self.callback = callback
114+
self.running = True
107115

108116
def run(self):
109117
path = self.path
110118
sCharacter = Character.getInstance()
111119

112-
if self.saveFmt == "xml" or self.saveFmt == "emp":
113-
backupData = sCharacter.exportXml()
114-
else:
115-
backupData = sCharacter.exportText()
120+
backupData = None
121+
if self.running:
122+
if self.saveFmt == "xml" or self.saveFmt == "emp":
123+
backupData = sCharacter.exportXml()
124+
else:
125+
backupData = sCharacter.exportText()
116126

117-
if self.saveFmt == "emp":
118-
with gzip.open(path, mode='wb') as backupFile:
119-
backupFile.write(backupData.encode())
120-
else:
121-
with open(path, mode='w', encoding='utf-8') as backupFile:
122-
backupFile.write(backupData)
127+
if self.running and backupData is not None:
128+
if self.saveFmt == "emp":
129+
with gzip.open(path, mode='wb') as backupFile:
130+
backupFile.write(backupData.encode())
131+
else:
132+
with open(path, mode='w', encoding='utf-8') as backupFile:
133+
backupFile.write(backupData)
123134

124135
wx.CallAfter(self.callback)
125136

137+
def stop(self):
138+
self.running = False
139+
126140

127141
class Character:
128142
instance = None
@@ -474,12 +488,14 @@ def _checkRequirements(self, char, subThing, reqs):
474488

475489

476490
class UpdateAPIThread(threading.Thread):
491+
477492
def __init__(self, charID, callback):
478493
threading.Thread.__init__(self)
479494

480495
self.name = "CheckUpdate"
481496
self.callback = callback
482497
self.charID = charID
498+
self.running = True
483499

484500
def run(self):
485501
try:
@@ -488,20 +504,31 @@ def run(self):
488504
sEsi = Esi.getInstance()
489505
sChar = Character.getInstance()
490506
ssoChar = sChar.getSsoCharacter(char.ID)
507+
508+
if not self.running:
509+
self.callback[0](self.callback[1])
510+
return
491511
resp = sEsi.getSkills(ssoChar.ID)
492512

513+
if not self.running:
514+
self.callback[0](self.callback[1])
515+
return
493516
# 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?
494517
char.clearSkills()
495518
for skillRow in resp["skills"]:
496519
char.addSkill(Skill(char, skillRow["skill_id"], skillRow["trained_skill_level"]))
497520

521+
if not self.running:
522+
self.callback[0](self.callback[1])
523+
return
498524
resp = sEsi.getSecStatus(ssoChar.ID)
499-
500525
char.secStatus = resp['security_status']
501-
502526
self.callback[0](self.callback[1])
503527
except (KeyboardInterrupt, SystemExit):
504528
raise
505529
except Exception as ex:
506530
pyfalog.warn(ex)
507531
self.callback[0](self.callback[1], sys.exc_info())
532+
533+
def stop(self):
534+
self.running = False

service/market.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def __init__(self):
4646
threading.Thread.__init__(self)
4747
pyfalog.debug("Initialize ShipBrowserWorkerThread.")
4848
self.name = "ShipBrowser"
49+
self.running = True
4950

5051
def run(self):
5152
self.queue = queue.Queue()
@@ -60,6 +61,8 @@ def processRequests(self):
6061
cache = self.cache
6162
sMkt = Market.getInstance()
6263
while True:
64+
if not self.running:
65+
break
6366
try:
6467
id_, callback = queue.get()
6568
set_ = cache.get(id_)
@@ -82,6 +85,9 @@ def processRequests(self):
8285
pyfalog.critical("Queue task done failed.")
8386
pyfalog.critical(e)
8487

88+
def stop(self):
89+
self.running = False
90+
8591

8692
class SearchWorkerThread(threading.Thread):
8793
def __init__(self):
@@ -91,6 +97,7 @@ def __init__(self):
9197
# load the jargon while in an out-of-thread context, to spot any problems while in the main thread
9298
self.jargonLoader.get_jargon()
9399
self.jargonLoader.get_jargon().apply('test string')
100+
self.running = True
94101

95102
def run(self):
96103
self.cv = threading.Condition()
@@ -101,6 +108,8 @@ def processSearches(self):
101108
cv = self.cv
102109

103110
while True:
111+
if not self.running:
112+
break
104113
cv.acquire()
105114
while self.searchRequest is None:
106115
cv.wait()
@@ -161,6 +170,8 @@ def scheduleSearch(self, text, callback, filterName=None):
161170
self.cv.notify()
162171
self.cv.release()
163172

173+
def stop(self):
174+
self.running = False
164175

165176
class Market:
166177
instance = None

service/price.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,14 @@ def __init__(self):
235235
self.name = "PriceWorker"
236236
self.queue = queue.Queue()
237237
self.wait = {}
238+
self.running = True
238239
pyfalog.debug("Initialize PriceWorkerThread.")
239240

240241
def run(self):
241242
queue = self.queue
242243
while True:
244+
if not self.running:
245+
break
243246
# Grab our data
244247
callback, requests, fetchTimeout, validityOverride = queue.get()
245248

@@ -265,6 +268,9 @@ def setToWait(self, prices, callback):
265268
callbacks = self.wait.setdefault(price.typeID, [])
266269
callbacks.append(callback)
267270

271+
def stop(self):
272+
self.running = False
273+
268274

269275
# Import market sources only to initialize price source modules, they register on their own
270276
from service.marketSources import evemarketer, evemarketdata, evepraisal # noqa: E402

service/update.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(self, callback):
4141
self.callback = callback
4242
self.settings = UpdateSettings.getInstance()
4343
self.network = Network.getInstance()
44+
self.running = True
4445

4546
def run(self):
4647
network = Network.getInstance()
@@ -49,13 +50,13 @@ def run(self):
4950
try:
5051
response = network.get(
5152
url='https://www.pyfa.io/update_check?pyfa_version={}&client_hash={}'.format(config.version, config.getClientSecret()),
52-
type=network.UPDATE)
53+
type=network.UPDATE, timeout=5)
5354
except (KeyboardInterrupt, SystemExit):
5455
raise
5556
except Exception as e:
5657
response = network.get(
5758
url='https://api.github.com/repos/pyfa-org/Pyfa/releases',
58-
type=network.UPDATE)
59+
type=network.UPDATE, timeout=5)
5960

6061
jsonResponse = response.json()
6162
jsonResponse.sort(
@@ -94,6 +95,9 @@ def run(self):
9495
def versiontuple(v):
9596
return tuple(map(int, (v.split("."))))
9697

98+
def stop(self):
99+
self.running = False
100+
97101

98102
class Update:
99103
instance = None

utils/timer.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,16 @@ def __enter__(self):
3535
def __exit__(self, type, value, traceback):
3636
self.checkpoint('finished')
3737
pass
38+
39+
40+
class CountdownTimer:
41+
42+
def __init__(self, timeout):
43+
self.timeout = timeout
44+
self.start = time.time()
45+
46+
def elapsed(self):
47+
return time.time() - self.start
48+
49+
def remainder(self):
50+
return max(self.timeout - self.elapsed(), 0)

0 commit comments

Comments
 (0)