Move generic formatter specification to misc module
[wifi-radar.git] / wifiradar / __init__.py
blob5600a7c21815913361d03cc27716acf2d91958a7
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 <robinson@tuxfamily.org>
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:
28 # Free Software Foundation, Inc.
29 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
33 from __future__ import unicode_literals
35 from configparser import NoOptionError, NoSectionError
36 import logging
37 import logging.handlers
38 from multiprocessing import Pipe, Process
39 from threading import Thread
41 from wifiradar.config import (make_section_name, ConfigManager,
42 ConfigFileError, ConfigFileManager)
43 from wifiradar.connections import ConnectionManager, scanner
44 from wifiradar.pubsub import Dispatcher, Message
45 from wifiradar.misc import (_, PYVERSION, WIFI_RADAR_VERSION,
46 PipeError, generic_formatter, get_new_profile)
47 import wifiradar.gui.g2 as ui
48 import wifiradar.gui.g2.transients as transients
50 # Set up a logging framework.
51 logger = logging.getLogger(__name__)
54 class Main(object):
55 """ The primary component of WiFi Radar.
56 """
57 def __init__(self, conf_file):
58 """ Create WiFi Radar app using :data:`config` for configuration.
59 """
60 dispatcher = Dispatcher()
61 scanner_pipe = dispatcher.subscribe(['ALL'])
62 ui_pipe = dispatcher.subscribe(['ALL'])
63 self.msg_pipe = dispatcher.subscribe(['ALL'])
65 try:
66 self.config, self.config_file_man = self.make_config(conf_file)
67 except ConfigFileError as e:
68 self.msg_pipe.send(Message('ERROR', e))
69 logger.critical(e)
70 except IOError as e:
71 self.msg_pipe.send(Message('ERROR', e))
72 logger.critical(e)
73 self.shutdown([], dispatcher)
74 else:
75 if not __debug__:
76 logger.setLevel(self.config.get_opt_as_int('GENERAL',
77 'loglevel'))
79 try:
80 fileLogHandler = logging.handlers.RotatingFileHandler(
81 self.config.get_opt('GENERAL', 'logfile'),
82 maxBytes=64*1024, backupCount=5)
83 except IOError as e:
84 self.msg_pipe.send(Message('ERROR',
85 _('Cannot open log file for writing: {ERR}.\n\n'
86 'WiFi Radar will work, but a log file will not '
87 'be recorded.').format(ERR=e.strerror)))
88 else:
89 fileLogHandler.setFormatter(generic_formatter)
90 logger.addHandler(fileLogHandler)
92 scanner_thread = Thread(name='scanner', target=scanner,
93 args=(self.config.copy(), scanner_pipe))
94 scanner_thread.start()
96 ui_proc = Process(name='ui', target=ui.start, args=(ui_pipe,))
97 ui_proc.start()
99 # This is the first run (or, at least, no config file was present),
100 # so pop up the preferences window
101 try:
102 if self.config.get_opt_as_bool('GENERAL', 'new_file'):
103 self.config.remove_option('GENERAL', 'new_file')
104 config_copy = self.config.copy()
105 self.msg_pipe.send(Message('PREFS-EDIT', config_copy))
106 except NoOptionError:
107 pass
108 # Add our known profiles in order.
109 for profile_name in self.config.auto_profile_order:
110 profile = self.config.get_profile(profile_name)
111 self.msg_pipe.send(Message('PROFILE-UPDATE', profile))
112 self.running = True
113 try:
114 self.run()
115 finally:
116 self.shutdown([ui_proc, scanner_thread], dispatcher)
118 def make_config(self, conf_file):
119 """ Returns a tuple with a :class:`ConfigManager` and a
120 :class:`ConfigFileManager`. These objects are built by reading
121 the configuration data in :data:`conf_file`.
123 # Create a file manager ready to read configuration information.
124 config_file_manager = ConfigFileManager(conf_file)
125 try:
126 config = config_file_manager.read()
127 except (NameError, SyntaxError) as e:
128 error_message = _('A configuration file from a pre-2.0 '
129 'version of WiFi Radar was found at {FILE}.\n\nWiFi '
130 'Radar v2.0.x does not read configuration files from '
131 'previous versions. ')
132 if isinstance(e, NameError):
133 error_message += _('Because {FILE} may contain '
134 'information that you might wish to use when '
135 'configuring WiFi Radar {VERSION}, rename this '
136 'file and run the program again.')
137 elif isinstance(e, SyntaxError):
138 error_message += _('The old configuration file is '
139 'probably empty and can be removed. Rename '
140 '{FILE} if you want to be very careful. After '
141 'removing or renaming {FILE}, run this program '
142 'again.')
143 error_message = error_message.format(FILE=conf_file,
144 VERSION=WIFI_RADAR_VERSION)
145 raise ConfigFileError(error_message)
146 except IOError as e:
147 if e.errno == 2:
148 # Missing user configuration file, so read the configuration
149 # defaults file. Then setup the file manager to write to
150 # the user file.
151 defaults_file = conf_file.replace('.conf', '.defaults')
152 # If conf_file == defaults_file, then this is not the first
153 # time through the recursion and we should fail loudly.
154 if conf_file != defaults_file:
155 config, _cfm = self.make_config(defaults_file)
156 config.set_bool_opt('GENERAL', 'new_file', True)
157 return config, ConfigFileManager(conf_file)
158 # Something went unrecoverably wrong.
159 raise e
160 else:
161 return config, config_file_manager
163 def run(self):
164 """ Watch for incoming messages and dispatch to subscribers.
166 while self.running:
167 try:
168 msg = self.msg_pipe.recv()
169 except (EOFError, IOError) as e:
170 # This is bad, really bad.
171 logger.critical(_('read on closed Pipe ({PIPE}), '
172 'failing...').format(PIPE=self.msg_pipe))
173 raise PipeError(e)
174 else:
175 self._check_message(msg)
177 def shutdown(self, joinables, dispatcher):
178 """ Join processes and threads in the :data:`joinables` list,
179 then close :data:`dispatcher`.
181 for joinable in joinables:
182 joinable.join()
183 dispatcher.close()
185 def _check_message(self, msg):
188 if msg.topic == 'EXIT':
189 self.msg_pipe.close()
190 self.running = False
191 elif msg.topic == 'PROFILE-ORDER-UPDATE':
192 self._profile_order_update(msg.details)
193 elif msg.topic == 'PROFILE-EDIT-REQUEST':
194 essid, bssid = msg.details
195 self._profile_edit_request(essid, bssid)
196 elif msg.topic == 'PROFILE-EDITED':
197 new_profile, old_profile = msg.details
198 self._profile_replace(new_profile, old_profile)
199 elif msg.topic == 'PROFILE-REMOVE':
200 self._profile_remove(msg.details)
201 elif msg.topic == 'PREFS-EDIT-REQUEST':
202 self._preferences_edit_request()
203 elif msg.topic == 'PREFS-UPDATE':
204 self._preferences_update(msg.details)
205 else:
206 logger.warning(_('unrecognized Message: "{MSG}"').format(MSG=msg))
208 def _profile_order_update(self, profile_order):
209 """ Update the auto profile order in the configuration.
210 :data:`profile_order` is a list of profile names, in order.
212 self.config.auto_profile_order = profile_order
213 try:
214 self.config.write()
215 except IOError as e:
216 self.msg_pipe.send(Message('ERROR',
217 _('Could not save configuration file:\n'
218 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
219 ERR=e.strerror)))
221 def _profile_edit_request(self, essid, bssid):
222 """ Send a message with a profile to be edited. If a profile with
223 :data:`essid` and :data:`bssid` is found in the list of known
224 profiles, that profile is sent for editing. Otherwise, a new
225 profile is sent.
227 apname = make_section_name(essid, bssid)
228 try:
229 profile = self.config.get_profile(apname)
230 except NoSectionError:
231 logger.info(_('The profile "{NAME}" does not exist, '
232 'creating a new profile.').format(NAME=apname))
233 profile = get_new_profile()
234 profile['essid'] = essid
235 profile['bssid'] = bssid
236 self.msg_pipe.send(Message('PROFILE-EDIT', profile))
238 def _profile_replace(self, new_profile, old_profile):
239 """ Update :data:`old_profile` with :data:`new_profile`.
241 new_apname = make_section_name(new_profile['essid'],
242 new_profile['bssid'])
243 old_apname = make_section_name(old_profile['essid'],
244 old_profile['bssid'])
245 if old_apname == new_apname:
246 # Simple update of old_profile with new_profile.
247 self.config.set_section(new_apname, new_profile)
248 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
249 else:
250 # Replace old_profile with new_profile.
251 old_position = self._profile_remove(old_apname)
252 # Add the updated profile like it's new...
253 self.config.set_section(new_apname, new_profile)
254 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
255 if old_position is not None:
256 # ..., but in the old position.
257 self.config.auto_profile_order.insert(old_position, new_apname)
258 self.msg_pipe.send(Message('PROFILE-MOVE', (old_position, new_profile)))
259 if old_profile['known'] is False and new_profile['known'] is True:
260 # The profile has been upgraded from scanned to configured.
261 self.config.auto_profile_order.insert(0, new_apname)
262 self.msg_pipe.send(Message('PROFILE-MOVE', (0, new_profile)))
263 try:
264 self.config.write()
265 except IOError as e:
266 self.msg_pipe.send(Message('ERROR',
267 _('Could not save configuration file:\n'
268 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
269 ERR=e.strerror)))
271 def _profile_remove(self, apname):
272 """ Remove the profile named in :data:`apname`. This method returns
273 the index in the auto-profile order at which the name was found,
274 or None if not matched.
276 try:
277 position = self.config.auto_profile_order.index(apname)
278 except ValueError:
279 return None
280 else:
281 profile = self.config.get_profile(apname)
282 self.config.remove_section(apname)
283 self.config.auto_profile_order.remove(apname)
284 self.msg_pipe.send(Message('PROFILE-UNLIST', profile))
285 try:
286 self.config.write()
287 except IOError as e:
288 self.msg_pipe.send(Message('ERROR',
289 _('Could not save configuration file:\n'
290 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
291 ERR=e.strerror)))
292 return position
294 def _preferences_edit_request(self):
295 """ Pass a :class:`ConfigManager` to the UI for editing.
297 config_copy = self.config.copy()
298 self.msg_pipe.send(Message('PREFS-EDIT', config_copy))
300 def _preferences_update(self, config):
301 """ Update configuration with :data:`config`.
303 self.config.update(config)
304 try:
305 self.config.write()
306 except IOError as e:
307 self.msg_pipe.send(Message('ERROR',
308 _('Could not save configuration file:\n'
309 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
310 ERR=e.strerror)))