Skip to content
This repository was archived by the owner on Mar 30, 2022. It is now read-only.

Commit 26b7d2c

Browse files
committed
fix some small but annoying multiprocessing bugs
* reduce (eliminate?) sporadic multiprocessing error messages at exit * Linux no longer requires green to be installed to run unit tests * also fixes gurnec#38
1 parent 14a8b36 commit 26b7d2c

File tree

6 files changed

+63
-40
lines changed

6 files changed

+63
-40
lines changed

btcrecover.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/python
22

33
# btcrecover.py -- Bitcoin wallet password recovery tool
4-
# Copyright (C) 2014-2016 Christopher Gurnee
4+
# Copyright (C) 2014-2017 Christopher Gurnee
55
#
66
# This program is free software: you can redistribute it and/or
77
# modify it under the terms of the GNU General Public License
@@ -28,7 +28,7 @@
2828
from __future__ import print_function
2929

3030
from btcrecover import btcrpass
31-
import sys
31+
import sys, multiprocessing
3232

3333
if __name__ == "__main__":
3434

@@ -39,9 +39,17 @@
3939
btcrpass.safe_print("Password found: '" + password_found + "'")
4040
if any(ord(c) < 32 or ord(c) > 126 for c in password_found):
4141
print("HTML encoded: '" + password_found.encode("ascii", "xmlcharrefreplace") + "'")
42+
retval = 0
4243

4344
elif not_found_msg:
4445
print(not_found_msg, file=sys.stderr if btcrpass.args.listpass else sys.stdout)
46+
retval = 0
4547

4648
else:
47-
sys.exit(1) # An error occurred or Ctrl-C was pressed
49+
retval = 1 # An error occurred or Ctrl-C was pressed
50+
51+
# Wait for any remaining child processes to exit cleanly (to avoid error messages from gc)
52+
for process in multiprocessing.active_children():
53+
process.join(1.0)
54+
55+
sys.exit(retval)

btcrecover/btcrpass.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
# (all optional futures for 2.7)
3030
from __future__ import print_function, absolute_import, division, unicode_literals
3131

32-
__version__ = "0.16.1"
32+
__version__ = "0.16.2"
3333
__ordering_version__ = b"0.6.4" # must be updated whenever password ordering changes
3434

3535
import sys, argparse, itertools, string, re, multiprocessing, signal, os, cPickle, gc, \
@@ -5462,7 +5462,7 @@ def windows_ctrl_handler(signal):
54625462

54635463
# Try to release as much memory as possible (before forking if multiple workers are being used)
54645464
# (the initial counting process can be memory intensive)
5465-
gc.collect(2)
5465+
gc.collect()
54665466

54675467
# Create an iterator which actually checks the (remaining) passwords produced by the password_iterator
54685468
# by executing the return_verified_password_or_false worker function in possibly multiple threads
@@ -5500,6 +5500,7 @@ def windows_ctrl_handler(signal):
55005500
try:
55015501
for password_found, passwords_tried_last in password_found_iterator:
55025502
if password_found:
5503+
if pool: pool.close()
55035504
passwords_tried += passwords_tried_last - 1 # just before the found password
55045505
if progress:
55055506
progress.next_update = 0 # force a screen update
@@ -5511,15 +5512,14 @@ def windows_ctrl_handler(signal):
55115512
if l_savestate and passwords_tried % est_passwords_per_5min == 0:
55125513
do_autosave(args.skip + passwords_tried)
55135514
else: # if the for loop exits normally (without breaking)
5514-
if pool:
5515-
pool.close()
5516-
pool = None
5515+
if pool: pool.close()
55175516
if progress:
55185517
if args.no_eta:
55195518
progress.maxval = passwords_tried
55205519
else:
55215520
progress.widgets.pop() # remove the ETA
55225521
progress.finish()
5522+
if pool: pool.join()
55235523

55245524
# Gracefully handle any exceptions, printing the count completed so far so that it can be
55255525
# skipped if the user restarts the same run. If the exception was expected (Ctrl-C or some
@@ -5528,6 +5528,7 @@ def windows_ctrl_handler(signal):
55285528
except BaseException as e:
55295529
handled = handle_oom() if isinstance(e, MemoryError) and passwords_tried > 0 else False
55305530
if not handled: print() # move to the next line if handle_oom() hasn't already done so
5531+
if pool: pool.close()
55315532

55325533
print("Interrupted after finishing password #", args.skip + passwords_tried, file=sys.stderr)
55335534
if sys.stdout.isatty() ^ sys.stderr.isatty(): # if they're different, print to both to be safe
@@ -5542,5 +5543,4 @@ def windows_ctrl_handler(signal):
55425543
do_autosave(args.skip + passwords_tried)
55435544
autosave_file.close()
55445545

5545-
if pool: pool.terminate()
55465546
return (password_found, "Password search exhausted" if password_found is False else None)

btcrecover/btcrseed.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,8 +1161,8 @@ def config_mnemonic(self, mnemonic_guess = None, lang = None, passphrase = u"",
11611161
passphrase = unicodedata.normalize("NFKD", passphrase) # problematic w/Python narrow Unicode builds, same as Electrum
11621162
passphrase = passphrase.lower() # (?)
11631163
passphrase = filter(lambda c: not unicodedata.combining(c), passphrase) # remove combining marks
1164-
passphrase = u" ".join(passphrase.split()) # replace whitespace sequences with a single ANSI space
1165-
# remove ANSI whitespace between CJK characters (?)
1164+
passphrase = u" ".join(passphrase.split()) # replace whitespace sequences with a single ASCII space
1165+
# remove ASCII whitespace between CJK characters (?)
11661166
passphrase = u"".join(c for i,c in enumerate(passphrase) if not (
11671167
c in string.whitespace
11681168
and any(intvl[0] <= ord(passphrase[i-1]) <= intvl[1] for intvl in self.CJK_INTERVALS)

btcrecover/test/test_passwords.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,13 @@
4343
warnings.filterwarnings("ignore", r"Not importing directory '.*gen_py': missing __init__.py", ImportWarning)
4444

4545
from btcrecover import btcrpass
46-
import os, unittest, cPickle, tempfile, shutil, multiprocessing, filecmp, sys
46+
import os, unittest, cPickle, tempfile, shutil, multiprocessing, gc, filecmp, sys
4747

4848

4949
class NonClosingBase(object):
5050
pass
5151

52-
# Enables either ANSI or Unicode mode for all tests based on either
52+
# Enables either ASCII or Unicode mode for all tests based on either
5353
# the value of tstr or the value of the BTCR_CHAR_MODE env. variable
5454
tstr = None
5555
def setUpModule():
@@ -72,7 +72,7 @@ def close(self): pass
7272
tstr = str
7373
tchr = chr
7474
utf8_opt = ""
75-
print("** Testing in ANSI character mode **")
75+
print("** Testing in ASCII character mode **")
7676

7777
else:
7878
import io
@@ -918,9 +918,8 @@ def wallet_tester(self, wallet_filename,
918918
force_purepython = False, force_kdf_purepython = False, force_bsddb_purepython = False,
919919
correct_pass = None, blockchain_mainpass = None, android_backuppass = None):
920920
wallet_filename = os.path.join(WALLET_DIR, wallet_filename)
921-
922-
temp_dir = tempfile.mkdtemp("-test-btcr")
923-
pool = None
921+
temp_dir = tempfile.mkdtemp("-test-btcr")
922+
parent_process = True # bug workaround, see finally block below for details
924923
try:
925924
temp_wallet_filename = os.path.join(temp_dir, os.path.basename(wallet_filename))
926925
shutil.copyfile(wallet_filename, temp_wallet_filename)
@@ -951,21 +950,26 @@ def wallet_tester(self, wallet_filename,
951950
(tstr("btcr-wrong-password-3"), correct_pass, tstr("btcr-wrong-password-4"))), (correct_pass, 2))
952951

953952
# Perform the tests in a child process to ensure the wallet can be pickled and all libraries reloaded
953+
parent_process = False
954954
pool = multiprocessing.Pool(1, init_worker, (wallet, tstr, force_purepython, force_kdf_purepython))
955+
parent_process = True
955956
password_found_iterator = pool.imap(btcrpass.return_verified_password_or_false,
956957
( ( tstr("btcr-wrong-password-1"), tstr("btcr-wrong-password-2") ),
957958
( tstr("btcr-wrong-password-3"), correct_pass, tstr("btcr-wrong-password-4") ) ))
958959
self.assertEqual(password_found_iterator.next(), (False, 2))
959960
self.assertEqual(password_found_iterator.next(), (correct_pass, 2))
960961
self.assertRaises(StopIteration, password_found_iterator.next)
961962
pool.close()
962-
pool = None
963+
pool.join()
963964

964965
del wallet
966+
gc.collect()
965967
self.assertTrue(filecmp.cmp(wallet_filename, temp_wallet_filename, False)) # False == always compare file contents
966968
finally:
967-
shutil.rmtree(temp_dir)
968-
if pool: pool.terminate()
969+
# There's a bug which only occurs when combining unittest, multiprocessing, and "real"
970+
# forking (Linux/BSD/WSL); only remove the temp dir if we're sure this is the parent process
971+
if parent_process:
972+
shutil.rmtree(temp_dir)
969973

970974
def test_armory(self):
971975
if not can_load_armory(): self.skipTest("requires Armory and ASCII mode")
@@ -1003,9 +1007,11 @@ def test_electrum27_upgradedfrom_electrum1(self):
10031007

10041008
@unittest.skipUnless(btcrpass.load_aes256_library().__name__ == b"Crypto", "requires PyCrypto")
10051009
def test_electrum28(self):
1010+
if not can_load_armory(permit_unicode=True): self.skipTest("requires Armory")
10061011
self.wallet_tester("electrum28-wallet")
10071012

10081013
def test_electrum28_pp(self):
1014+
if not can_load_armory(permit_unicode=True): self.skipTest("requires Armory")
10091015
self.wallet_tester("electrum28-wallet", force_purepython=True)
10101016

10111017
@unittest.skipUnless(btcrpass.load_aes256_library().__name__ == b"Crypto", "requires PyCrypto")
@@ -1174,19 +1180,15 @@ def bip39_tester(self, force_purepython = False, unicode_pw = False, *args, **kw
11741180
(tstr("btcr-wrong-password-3"), correct_pass, tstr("btcr-wrong-password-4"))), (correct_pass, 2))
11751181

11761182
# Perform the tests in a child process to ensure the wallet can be pickled and all libraries reloaded
1177-
pool = None
1178-
try:
1179-
pool = multiprocessing.Pool(1, init_worker, (wallet, tstr, force_purepython, False))
1180-
password_found_iterator = pool.imap(btcrpass.return_verified_password_or_false,
1181-
( ( tstr("btcr-wrong-password-1"), tstr("btcr-wrong-password-2") ),
1182-
( tstr("btcr-wrong-password-3"), correct_pass, tstr("btcr-wrong-password-4") ) ))
1183-
self.assertEqual(password_found_iterator.next(), (False, 2))
1184-
self.assertEqual(password_found_iterator.next(), (correct_pass, 2))
1185-
self.assertRaises(StopIteration, password_found_iterator.next)
1186-
pool.close()
1187-
pool = None
1188-
finally:
1189-
if pool: pool.terminate()
1183+
pool = multiprocessing.Pool(1, init_worker, (wallet, tstr, force_purepython, False))
1184+
password_found_iterator = pool.imap(btcrpass.return_verified_password_or_false,
1185+
( ( tstr("btcr-wrong-password-1"), tstr("btcr-wrong-password-2") ),
1186+
( tstr("btcr-wrong-password-3"), correct_pass, tstr("btcr-wrong-password-4") ) ))
1187+
self.assertEqual(password_found_iterator.next(), (False, 2))
1188+
self.assertEqual(password_found_iterator.next(), (correct_pass, 2))
1189+
self.assertRaises(StopIteration, password_found_iterator.next)
1190+
pool.close()
1191+
pool.join()
11901192

11911193
@unittest.skipUnless(btcrpass.load_pbkdf2_library().__name__ == b"hashlib",
11921194
"requires Python 2.7.8+")
@@ -1696,6 +1698,8 @@ def test_end_to_end(self):
16961698
data_extract = self.E2E_DATA_EXTRACT,
16971699
autosave = autosave_file)
16981700
self.assertEqual("btcr-test-password", btcrpass.main()[0])
1701+
for process in multiprocessing.active_children():
1702+
process.join() # wait for any remaining child processes to exit cleanly
16991703

17001704
# Verify the exact password number where it was found to ensure password ordering hasn't changed
17011705
autosave_file.seek(SAVESLOT_SIZE)
@@ -1721,6 +1725,8 @@ def test_skip(self):
17211725
data_extract = self.E2E_DATA_EXTRACT,
17221726
autosave = autosave_file)
17231727
self.assertIn("Password search exhausted", btcrpass.main()[1])
1728+
for process in multiprocessing.active_children():
1729+
process.join() # wait for any remaining child processes to exit cleanly
17241730

17251731
# Verify the password number where the search started
17261732
autosave_file.seek(0)

run-all-tests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#!/usr/bin/python
1+
#!/usr/bin/python
22

33
# run-all-tests.py -- runs *all* btcrecover tests
4-
# Copyright (C) 2016 Christopher Gurnee
4+
# Copyright (C) 2016, 2017 Christopher Gurnee
55
#
66
# This file is part of btcrecover.
77
#

seedrecover.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#!/usr/bin/python
1+
#!/usr/bin/python
22

33
# seedrecover.py -- Bitcoin mnemonic sentence recovery tool
4-
# Copyright (C) 2014-2016 Christopher Gurnee
4+
# Copyright (C) 2014-2017 Christopher Gurnee
55
#
66
# This program is free software: you can redistribute it and/or
77
# modify it under the terms of the GNU General Public License
@@ -28,7 +28,7 @@
2828
from __future__ import print_function
2929

3030
from btcrecover import btcrseed
31-
import sys
31+
import sys, multiprocessing
3232

3333
if __name__ == "__main__":
3434

@@ -46,7 +46,16 @@
4646
if btcrseed.tk_root: # if the GUI is being used
4747
btcrseed.show_mnemonic_gui(mnemonic_sentence)
4848

49+
retval = 0
50+
4951
elif mnemonic_sentence is None:
50-
sys.exit(1) # An error occurred or Ctrl-C was pressed inside btcrseed.main()
52+
retval = 1 # An error occurred or Ctrl-C was pressed inside btcrseed.main()
53+
54+
else:
55+
retval = 0 # "Seed not found" has already been printed to the console in btcrseed.main()
56+
57+
# Wait for any remaining child processes to exit cleanly (to avoid error messages from gc)
58+
for process in multiprocessing.active_children():
59+
process.join(1.0)
5160

52-
# else "Seed not found" has already been printed to the console in btcrseed.main()
61+
sys.exit(retval)

0 commit comments

Comments
 (0)