/bin/wifi: Lock /bin/wifi to prevent simultaneous calls.
This will help avoid cfg80211 race conditions where one process tries
to start wpa_supplicant while another process is in the middle of
scanning.
Change-Id: I0a9021ba4ff0b8219ba6c1082a055602bfffe6ed
diff --git a/wifi/utils.py b/wifi/utils.py
index e665654..3d53428 100644
--- a/wifi/utils.py
+++ b/wifi/utils.py
@@ -5,6 +5,7 @@
from __future__ import print_function
import collections
+import fcntl
import os
import re
import subprocess
@@ -300,3 +301,20 @@
raise BinWifiException('PSK is not of a valid length: %d', len(psk))
return psk
+
+
+def lock(lockfile, timeout_sec):
+ for i in range(timeout_sec):
+ try:
+ fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ return
+ except IOError:
+ time.sleep(1)
+ log('Trying to obtain lock %s for %s seconds', lockfile.name, i + 1)
+
+ raise BinWifiException('Failed to obtain lock %s after %d seconds',
+ lockfile.name, timeout_sec)
+
+
+def unlock(lockfile):
+ fcntl.lockf(lockfile, fcntl.LOCK_UN)
diff --git a/wifi/utils_test.py b/wifi/utils_test.py
index be91859..deb737b 100755
--- a/wifi/utils_test.py
+++ b/wifi/utils_test.py
@@ -3,7 +3,11 @@
"""Tests for utils.py."""
import collections
+import multiprocessing
import os
+import shutil
+import tempfile
+import time
import utils
from wvtest import wvtest
@@ -123,5 +127,67 @@
wvtest.WVPASSEQ('g' * 63, utils.validate_and_sanitize_psk('g' * 63))
+@wvtest.wvtest
+def lock_and_unlock_test():
+ """Test utils.lock and utils.unlock."""
+ try:
+ temp_dir = tempfile.mkdtemp()
+ lockfile = open(os.path.join(temp_dir, 'test.lock'), 'w')
+
+ def lock_until_queue_nonempty(q, timeout_sec):
+ try:
+ utils.lock(lockfile, timeout_sec)
+ except utils.BinWifiException:
+ q.put('timed out')
+ return
+ q.get()
+ q.put('releasing')
+ utils.unlock(lockfile)
+
+ q1 = multiprocessing.Queue()
+ p1 = multiprocessing.Process(target=lock_until_queue_nonempty, args=(q1, 1))
+
+ q2 = multiprocessing.Queue()
+ p2 = multiprocessing.Process(target=lock_until_queue_nonempty, args=(q2, 3))
+
+ p1.start()
+ p2.start()
+
+ wvtest.WVPASS(q1.empty())
+ wvtest.WVPASS(q2.empty())
+
+ q1.put('release')
+ # Wait for p1 to take this off the queue and relase the lock.
+ time.sleep(0.5)
+ wvtest.WVFAIL(q1.empty())
+ wvtest.WVPASSEQ(q1.get(), 'releasing')
+ wvtest.WVPASS(q2.empty())
+
+ q2.put('release')
+ # Wait for p2 to take this off the queue and relase the lock.
+ time.sleep(0.5)
+ wvtest.WVPASS(q1.empty())
+ wvtest.WVFAIL(q2.empty())
+ wvtest.WVPASSEQ(q2.get(), 'releasing')
+
+ # Now test that the timeout works.
+ q3 = multiprocessing.Queue()
+ p3 = multiprocessing.Process(target=lock_until_queue_nonempty, args=(q3, 1))
+
+ q4 = multiprocessing.Queue()
+ p4 = multiprocessing.Process(target=lock_until_queue_nonempty, args=(q4, 1))
+
+ p3.start()
+ p4.start()
+ wvtest.WVPASSEQ(q4.get(), 'timed out')
+
+ q3.put('release')
+
+ finally:
+ for p in [p1, p2, p3, p4]:
+ p.join()
+ shutil.rmtree(temp_dir)
+
+
if __name__ == '__main__':
wvtest.wvtest_main()
diff --git a/wifi/wifi.py b/wifi/wifi.py
index c4fde30..1071031 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -49,9 +49,11 @@
Y,yottasecond-timeouts Don't rotate any keys: PTK, GTK, or GMK
P,persist For set commands, persist options so we can restore them with 'wifi restore'. For stop commands, remove persisted options.
S,interface-suffix= Interface suffix []
+lock-timeout= How long, in seconds, to wait for another /bin/wifi process to finish before giving up. [60]
"""
_FINGERPRINTS_DIRECTORY = '/tmp/wifi/fingerprints'
+_LOCKFILE = '/tmp/wifi/wifi.lock'
# pylint: disable=protected-access
@@ -990,31 +992,37 @@
opt.band = '2.4'
try:
- function = {
- 'set': set_wifi,
- 'stop': stop_wifi,
- 'off': stop_wifi,
- 'restore': restore_wifi,
- 'show': show_wifi,
- 'setclient': set_client_wifi,
- 'stopclient': stop_client_wifi,
- 'stopap': stop_ap_wifi,
- 'scan': scan_wifi,
- }[extra[0]]
- except KeyError:
- parser.fatal('Unrecognized command %s' % extra[0])
- success = function(opt)
+ lockfile = open(_LOCKFILE, 'w')
+ try:
+ utils.lock(lockfile, int(opt.lock_timeout))
+ function = {
+ 'set': set_wifi,
+ 'stop': stop_wifi,
+ 'off': stop_wifi,
+ 'restore': restore_wifi,
+ 'show': show_wifi,
+ 'setclient': set_client_wifi,
+ 'stopclient': stop_client_wifi,
+ 'stopap': stop_ap_wifi,
+ 'scan': scan_wifi,
+ }[extra[0]]
+ except KeyError:
+ parser.fatal('Unrecognized command %s' % extra[0])
- if success:
- if extra[0] in ('set', 'setclient'):
- program = 'hostapd' if extra[0] == 'set' else 'wpa_supplicant'
- if opt.persist:
- phy = iw.find_phy(opt.band, opt.channel)
- for band in iw.phy_bands(phy):
- if band != opt.band:
- persist.delete_options(program, band)
- persist.save_options(program, opt.band, argv)
- persist.save_options(program, opt.band, argv, tmp=True)
+ success = function(opt)
+
+ if success:
+ if extra[0] in ('set', 'setclient'):
+ program = 'hostapd' if extra[0] == 'set' else 'wpa_supplicant'
+ if opt.persist:
+ phy = iw.find_phy(opt.band, opt.channel)
+ for band in iw.phy_bands(phy):
+ if band != opt.band:
+ persist.delete_options(program, band)
+ persist.save_options(program, opt.band, argv)
+ persist.save_options(program, opt.band, argv, tmp=True)
+ finally:
+ utils.unlock(lockfile)
return success