Skip to content

Commit 41ddf63

Browse files
committed
Improve handling of exceptions in background threads.
1 parent 3fb50be commit 41ddf63

2 files changed

Lines changed: 55 additions & 15 deletions

File tree

gnupg.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
from io import StringIO
4040
import logging
4141
import os
42+
try:
43+
from queue import Queue, Empty
44+
except ImportError:
45+
from Queue import Queue, Empty
4246
import re
4347
import socket
4448
from subprocess import Popen, PIPE
@@ -137,7 +141,7 @@ def no_quote(s):
137141
return s
138142

139143

140-
def _copy_data(instream, outstream, buffer_size):
144+
def _copy_data(instream, outstream, buffer_size, error_queue):
141145
# Copy one stream to another
142146
assert buffer_size > 0
143147
sent = 0
@@ -150,8 +154,10 @@ def _copy_data(instream, outstream, buffer_size):
150154
# for what is actually a binary file
151155
try:
152156
data = instream.read(buffer_size)
153-
except Exception: # pragma: no cover
157+
except Exception as e: # pragma: no cover
154158
logger.warning('Exception occurred while reading', exc_info=1)
159+
error_queue.put_nowait(e)
160+
logger.debug('queued exception: %s', e)
155161
break
156162
if not data:
157163
break
@@ -161,10 +167,12 @@ def _copy_data(instream, outstream, buffer_size):
161167
outstream.write(data)
162168
except UnicodeError: # pragma: no cover
163169
outstream.write(data.encode(enc))
164-
except Exception: # pragma: no cover
170+
except Exception as e: # pragma: no cover
165171
# Can sometimes get 'broken pipe' errors even when the data has all
166172
# been sent
167173
logger.exception('Error sending data')
174+
error_queue.put_nowait(e)
175+
logger.debug('queued exception: %s', e)
168176
break
169177
try:
170178
outstream.close()
@@ -173,9 +181,9 @@ def _copy_data(instream, outstream, buffer_size):
173181
logger.debug('closed output, %d bytes sent', sent)
174182

175183

176-
def _threaded_copy_data(instream, outstream, buffer_size):
184+
def _threaded_copy_data(instream, outstream, buffer_size, error_queue):
177185
assert buffer_size > 0
178-
wr = threading.Thread(target=_copy_data, args=(instream, outstream, buffer_size))
186+
wr = threading.Thread(target=_copy_data, args=(instream, outstream, buffer_size, error_queue))
179187
wr.daemon = True
180188
logger.debug('data copier: %r, %r, %r', wr, instream, outstream)
181189
wr.start()
@@ -1358,8 +1366,15 @@ def _handle_io(self, args, fileobj_or_path, result, passphrase=None, binary=Fals
13581366
stdin = p.stdin
13591367
if passphrase:
13601368
_write_passphrase(stdin, passphrase, self.encoding)
1361-
writer = _threaded_copy_data(fileobj, stdin, self.buffer_size)
1369+
error_queue = Queue()
1370+
writer = _threaded_copy_data(fileobj, stdin, self.buffer_size, error_queue)
13621371
self._collect_output(p, result, writer, stdin)
1372+
try:
1373+
exc = error_queue.get_nowait()
1374+
# if we get here, that means an error occurred in the copying thread
1375+
raise exc
1376+
except Empty:
1377+
pass
13631378
return result
13641379
finally:
13651380
if writer:
@@ -1481,14 +1496,21 @@ def sign_file(self,
14811496
# passphrase is bad, gpg bails and you can't write the message.
14821497
fileobj = self._get_fileobj(fileobj_or_path)
14831498
p = self._open_subprocess(args, passphrase is not None)
1499+
writer = None
14841500
try:
14851501
stdin = p.stdin
14861502
if passphrase:
14871503
_write_passphrase(stdin, passphrase, self.encoding)
1488-
writer = _threaded_copy_data(fileobj, stdin, self.buffer_size)
1504+
error_queue = Queue()
1505+
writer = _threaded_copy_data(fileobj, stdin, self.buffer_size, error_queue)
1506+
try:
1507+
exc = error_queue.get_nowait()
1508+
# if we get here, that means an error occurred in the copying thread
1509+
raise exc
1510+
except Empty:
1511+
pass
14891512
except IOError: # pragma: no cover
14901513
logging.exception('error writing message')
1491-
writer = None
14921514
finally:
14931515
if writer:
14941516
writer.join(0.01)

test_gnupg.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Copyright (C) 2008-2024 Vinay Sajip. All rights reserved.
66
"""
77
import argparse
8+
import io
89
import json
910
import logging
1011
import os.path
@@ -1167,12 +1168,12 @@ def do_file_encryption_and_decryption(self, encfname, decfname):
11671168
# Try opening the encrypted file in text mode (Issue #39)
11681169
# this doesn't fail in 2.x
11691170
if gnupg._py3k:
1170-
efile = open(encfname, 'r')
1171-
ddata = self.gpg.decrypt_file(efile, passphrase='bbrown', output=decfname)
1172-
self.assertEqual(2, ddata.returncode, 'Unexpected return code')
1173-
self.assertFalse(ddata)
1174-
self.assertEqual(ddata.status, 'no data was provided')
1175-
efile.close()
1171+
logger.debug('about to pass text stream to decrypt_file')
1172+
with open(encfname, 'r') as efile:
1173+
self.assertRaises(UnicodeDecodeError, self.gpg.decrypt_file, efile, passphrase='bbrown', output=decfname)
1174+
# self.assertEqual(2, ddata.returncode, 'Unexpected return code')
1175+
# self.assertFalse(ddata)
1176+
# self.assertEqual(ddata.status, 'no data was provided')
11761177
finally:
11771178
for fn in (encfname, decfname):
11781179
if os.name == 'posix' and mode is not None:
@@ -1587,6 +1588,15 @@ def test_configured_group(self):
15871588
gpg = gnupg.GPG(gnupghome=self.homedir, gpgbinary=GPGBINARY)
15881589
self.assertEqual(gpg.version, self.gpg.version)
15891590

1591+
def test_exception_propagation(self):
1592+
if sys.version_info[0] < 3:
1593+
raise unittest.SkipTest('python 2 is too loose with Unicode')
1594+
key = self.generate_key('Andrew', 'Able', 'alpha.com', passphrase='andy')
1595+
self.assertEqual(0, key.returncode, 'Non-zero return code')
1596+
andrew = key.fingerprint
1597+
stream = io.StringIO(u'Hello, world!') # make the wrong type of stream
1598+
self.assertRaises(TypeError, self.gpg.encrypt_file, stream, [andrew], armor=False)
1599+
15901600

15911601
TEST_GROUPS = {
15921602
'sign':
@@ -1609,7 +1619,7 @@ def test_configured_group(self):
16091619
'basic':
16101620
set(['test_environment', 'test_list_keys_initial', 'test_nogpg', 'test_make_args', 'test_quote_with_shell']),
16111621
'test':
1612-
set(['test_configured_group']),
1622+
set(['test_filenames_with_spaces']),
16131623
}
16141624

16151625

@@ -1630,11 +1640,19 @@ def suite(args=None):
16301640

16311641

16321642
def init_logging():
1643+
class PrimegenFilter(logging.Filter):
1644+
def filter(self, record):
1645+
arg = record.args
1646+
if isinstance(arg, (list, tuple)) and len(arg) > 0:
1647+
arg = arg[0]
1648+
return not arg or not isinstance(arg, unicode) or '[GNUPG:] PROGRESS primegen' not in arg
1649+
16331650
logging.basicConfig(level=logging.DEBUG,
16341651
filename='test_gnupg.log',
16351652
filemode='w',
16361653
format='%(asctime)s %(levelname)-5s %(name)-10s '
16371654
'%(threadName)-10s %(lineno)4d %(message)s')
1655+
logging.root.handlers[0].addFilter(PrimegenFilter())
16381656

16391657

16401658
def main():

0 commit comments

Comments
 (0)