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