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