Unify profile update and profile replace concepts
[wifi-radar.git] / wifiradar / __init__.py
blob7b315decc4909823b1b3ab4e27855e077bdf3d46
1 # -*- coding: utf-8 -*-
3 # __init__.py - main logic for operating WiFi Radar
5 # Part of WiFi Radar: A utility for managing WiFi profiles on GNU/Linux.
7 # Copyright (C) 2004-2005 Ahmad Baitalmal <ahmad@baitalmal.com>
8 # Copyright (C) 2005 Nicolas Brouard <nicolas.brouard@mandrake.org>
9 # Copyright (C) 2005-2009 Brian Elliott Finley <brian@thefinleys.com>
10 # Copyright (C) 2006 David Decotigny <com.d2@free.fr>
11 # Copyright (C) 2006 Simon Gerber <gesimu@gmail.com>
12 # Copyright (C) 2006-2007 Joey Hurst <jhurst@lucubrate.org>
13 # Copyright (C) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
14 # Copyright (C) 2009-2010,2014 Sean Robinson <seankrobinson@gmail.com>
15 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
17 # This program is free software; you can redistribute it and/or modify
18 # it under the terms of the GNU General Public License as published by
19 # the Free Software Foundation; version 2 of the License.
21 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License in LICENSE.GPL for more details.
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
32 try:
33 # Py2
34 from ConfigParser import NoOptionError, NoSectionError
35 except ImportError:
36 # Py3
37 from configparser import NoOptionError, NoSectionError
39 from itertools import chain
40 import logging
41 from multiprocessing import Pipe, Process
42 from threading import Thread
44 from wifiradar.config import (copy_configuration, make_section_name,
45 ConfigManager)
46 from wifiradar.connections import ConnectionManager, scanner
47 from wifiradar.pubsub import Dispatcher, Message
48 import wifiradar.misc as misc
49 import wifiradar.gui.g2 as ui
50 import wifiradar.gui.g2.transients as transients
52 # Set up a logging framework.
53 logger = logging.getLogger("wifiradar")
56 class Main(object):
57 """ The primary component of WiFi Radar.
58 """
59 def __init__(self, config):
60 """ Create WiFi Radar app using :data:`config` for configuration.
61 """
62 self.config = config
64 if __debug__:
65 logger.setLevel(logging.DEBUG)
66 else:
67 logger.setLevel(self.config.get_opt_as_int('DEFAULT', 'loglevel'))
69 dispatcher = Dispatcher()
70 scanner_pipe = dispatcher.subscribe(['ALL'])
71 ui_pipe = dispatcher.subscribe(['ALL'])
73 try:
74 fileLogHandler = logging.handlers.RotatingFileHandler(self.config.get_opt('DEFAULT', 'logfile'), maxBytes=64*1024, backupCount=5)
75 except IOError as e:
76 error_pipe = dispatcher.subscribe()
77 error_pipe.send(Message('ERROR',
78 'Cannot open log file for writing: %s.\n\n'.format(e.strerror) +
79 'WiFi Radar will work, but a log file will not be recorded.'))
80 dispatcher.unsubscribe(error_pipe)
81 else:
82 fileLogHandler.setFormatter(generic_formatter)
83 logger.addHandler(fileLogHandler)
85 scanner_thread = Thread(name='scanner', target=scanner, args=(config, scanner_pipe))
86 scanner_thread.start()
88 ui_proc = Process(name='iu', target=ui.RadarWindow, args=(ui_pipe,))
89 ui_proc.start()
91 self.msg_pipe = dispatcher.subscribe(['ALL'])
93 # This is the first run (or, at least, no config file was present),
94 # so pop up the preferences window
95 try:
96 if self.config.get_opt_as_bool('DEFAULT', 'new_file'):
97 self.config.remove_option('DEFAULT', 'new_file')
98 config_copy = ConfigManager({})
99 self.msg_pipe.send(Message('PREFS-EDIT', config_copy))
100 except NoOptionError:
101 pass
102 # Add our known profiles in order.
103 for profile_name in self.config.auto_profile_order:
104 profile = self.config.get_profile(profile_name)
105 self.msg_pipe.send(Message('PROFILE-UPDATE', profile))
106 self.running = True
107 try:
108 self.run()
109 finally:
110 ui_proc.join()
111 scanner_thread.join()
112 dispatcher.close()
114 def run(self):
115 """ Watch for incoming messages and dispatch to subscribers.
117 while self.running:
118 try:
119 msg = self.msg_pipe.recv()
120 except (EOFError, IOError) as e:
121 # This is bad, really bad.
122 logger.critical('read on closed ' +
123 'Pipe ({}), failing...'.format(self.msg_pipe))
124 raise misc.PipeError(e)
125 else:
126 self._check_message(msg)
128 def _check_message(self, msg):
131 if msg.topic == 'EXIT':
132 self.msg_pipe.close()
133 self.running = False
134 elif msg.topic == 'PROFILE-ORDER-UPDATE':
135 self._profile_order_update(msg.details)
136 elif msg.topic == 'PROFILE-EDIT-REQUEST':
137 essid, bssid = msg.details
138 self._profile_edit_request(essid, bssid)
139 elif msg.topic == 'PROFILE-EDITED':
140 new_profile, old_profile = msg.details
141 self._profile_replace(new_profile, old_profile)
142 elif msg.topic == 'PROFILE-REMOVE':
143 self._profile_remove(msg.details)
144 elif msg.topic == 'PREFS-EDIT-REQUEST':
145 self._preferences_edit_request()
146 elif msg.topic == 'PREFS-UPDATE':
147 self._preferences_update(msg.details)
148 else:
149 logger.warning('unrecognized Message: "{}"'.format(msg))
151 def _profile_order_update(self, profile_order):
154 self.config.auto_profile_order = profile_order
156 def _profile_edit_request(self, essid, bssid):
157 """ Send a message with a profile to be edited. If a profile with
158 :data:`essid` and :data:`bssid` is found in the list of known
159 profiles, that profile is sent for editing. Otherwise, a new
160 profile is sent.
162 apname = make_section_name(essid, bssid)
163 try:
164 profile = self.config.get_profile(apname)
165 except NoSectionError:
166 logger.info('The profile "{}" does '.format(apname) +
167 'not exist, creating a new profile.')
168 profile = misc.get_new_profile()
169 profile['essid'] = essid
170 profile['bssid'] = bssid
171 self.msg_pipe.send(Message('PROFILE-EDIT', profile))
173 def _profile_replace(self, new_profile, old_profile):
174 """ Update :data:`old_profile` with :data:`new_profile`.
176 new_apname = make_section_name(new_profile['essid'],
177 new_profile['bssid'])
178 old_apname = make_section_name(old_profile['essid'],
179 old_profile['bssid'])
180 if old_apname == new_apname:
181 # Simple update of old_profile with new_profile.
182 self.config.set_section(new_apname, new_profile)
183 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
184 else:
185 # Replace old_profile with new_profile.
186 old_position = self._profile_remove(old_apname)
187 # Add the updated profile like it's new, but in the old position.
188 self.config.auto_profile_order.insert(old_position, new_apname)
189 self.config.set_section(new_apname, new_profile)
190 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
191 self.msg_pipe.send(Message('PROFILE-MOVE', (old_position, new_profile)))
192 if old_profile['known'] is False and new_profile['known'] is True:
193 # The profile has been upgraded from scanned to configured.
194 self.config.auto_profile_order.insert(0, new_apname)
195 self.msg_pipe.send(Message('PROFILE-MOVE', (0, new_profile)))
196 try:
197 self.config.write()
198 except IOError as e:
199 self.msg_pipe.send(Message('ERROR',
200 'Could not save configuration file:\n' +
201 '{}\n\n{}'.format(self.config.filename, e.strerror)))
203 def _profile_remove(self, apname):
204 """ Remove the profile named in :data:`apname`. This method returns
205 the index in the auto-profile order at which the name was found,
206 or None if not matched.
208 try:
209 position = self.config.auto_profile_order.index(apname)
210 except ValueError:
211 return None
212 else:
213 profile = self.config.get_profile(apname)
214 self.config.remove_section(apname)
215 self.config.auto_profile_order.remove(apname)
216 self.msg_pipe.send(Message('PROFILE-UNLIST', profile))
217 try:
218 self.config.write()
219 except IOError as e:
220 self.msg_pipe.send(Message('ERROR',
221 'Could not save configuration file:\n' +
222 '{}\n\n{}'.format(self.config.filename, e.strerror)))
223 return position
225 def _preferences_edit_request(self):
226 """ Pass a :class:`ConfigManager` to the UI for editing.
228 config_copy = copy_configuration(self.config)
229 self.msg_pipe.send(Message('PREFS-EDIT', config_copy))
231 def _preferences_update(self, config):
232 """ Update configuration with :data:`config`.
234 self.config.update(config)
235 try:
236 self.config.write()
237 except IOError as e:
238 self.msg_pipe.send(Message('ERROR',
239 'Could not save configuration file:\n' +
240 '{}\n\n{}'.format(self.config.filename, e.strerror)))