Allow empty defaults when creating ConfigFileManager
[wifi-radar.git] / wifiradar / __init__.py
blobf4550b0ed8e8e79925731e09344101faad8cf894
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 import logging
36 import logging.handlers
37 from multiprocessing import Pipe, Process
38 from threading import Thread
40 from wifiradar.config import (copy_configuration, make_section_name,
41 ConfigManager)
42 from wifiradar.connections import ConnectionManager, scanner
43 from wifiradar.pubsub import Dispatcher, Message
44 from wifiradar.misc import _, PYVERSION, PipeError, get_new_profile
45 import wifiradar.gui.g2 as ui
46 import wifiradar.gui.g2.transients as transients
48 if PYVERSION < 3:
49 from ConfigParser import NoOptionError, NoSectionError
50 else:
51 from configparser import NoOptionError, NoSectionError
53 # Set up a logging framework.
54 logger = logging.getLogger(__name__)
57 class Main(object):
58 """ The primary component of WiFi Radar.
59 """
60 def __init__(self, config):
61 """ Create WiFi Radar app using :data:`config` for configuration.
62 """
63 self.config = config
65 dispatcher = Dispatcher()
66 scanner_pipe = dispatcher.subscribe(['ALL'])
67 ui_pipe = dispatcher.subscribe(['ALL'])
69 try:
70 fileLogHandler = logging.handlers.RotatingFileHandler(self.config.get_opt('DEFAULT', 'logfile'), maxBytes=64*1024, backupCount=5)
71 except IOError as e:
72 error_pipe = dispatcher.subscribe()
73 error_pipe.send(Message('ERROR',
74 _('Cannot open log file for writing: {ERR}.\n\n'
75 'WiFi Radar will work, but a log file will not '
76 'be recorded.').format(ERR=e.strerror)))
77 dispatcher.unsubscribe(error_pipe)
78 else:
79 fileLogHandler.setFormatter(generic_formatter)
80 logger.addHandler(fileLogHandler)
82 scanner_thread = Thread(name='scanner', target=scanner, args=(config, scanner_pipe))
83 scanner_thread.start()
85 ui_proc = Process(name='ui', target=ui.start, args=(ui_pipe,))
86 ui_proc.start()
88 self.msg_pipe = dispatcher.subscribe(['ALL'])
90 # This is the first run (or, at least, no config file was present),
91 # so pop up the preferences window
92 try:
93 if self.config.get_opt_as_bool('DEFAULT', 'new_file'):
94 self.config.remove_option('DEFAULT', 'new_file')
95 config_copy = copy_configuration(self.config)
96 self.msg_pipe.send(Message('PREFS-EDIT', config_copy))
97 except NoOptionError:
98 pass
99 # Add our known profiles in order.
100 for profile_name in self.config.auto_profile_order:
101 profile = self.config.get_profile(profile_name)
102 self.msg_pipe.send(Message('PROFILE-UPDATE', profile))
103 self.running = True
104 try:
105 self.run()
106 finally:
107 ui_proc.join()
108 scanner_thread.join()
109 dispatcher.close()
111 def run(self):
112 """ Watch for incoming messages and dispatch to subscribers.
114 while self.running:
115 try:
116 msg = self.msg_pipe.recv()
117 except (EOFError, IOError) as e:
118 # This is bad, really bad.
119 logger.critical(_('read on closed Pipe ({PIPE}), '
120 'failing...').format(PIPE=self.msg_pipe))
121 raise PipeError(e)
122 else:
123 self._check_message(msg)
125 def _check_message(self, msg):
128 if msg.topic == 'EXIT':
129 self.msg_pipe.close()
130 self.running = False
131 elif msg.topic == 'PROFILE-ORDER-UPDATE':
132 self._profile_order_update(msg.details)
133 elif msg.topic == 'PROFILE-EDIT-REQUEST':
134 essid, bssid = msg.details
135 self._profile_edit_request(essid, bssid)
136 elif msg.topic == 'PROFILE-EDITED':
137 new_profile, old_profile = msg.details
138 self._profile_replace(new_profile, old_profile)
139 elif msg.topic == 'PROFILE-REMOVE':
140 self._profile_remove(msg.details)
141 elif msg.topic == 'PREFS-EDIT-REQUEST':
142 self._preferences_edit_request()
143 elif msg.topic == 'PREFS-UPDATE':
144 self._preferences_update(msg.details)
145 else:
146 logger.warning(_('unrecognized Message: "{MSG}"').format(MSG=msg))
148 def _profile_order_update(self, profile_order):
149 """ Update the auto profile order in the configuration.
150 :data:`profile_order` is a list of profile names, in order.
152 self.config.auto_profile_order = profile_order
153 try:
154 self.config.write()
155 except IOError as e:
156 self.msg_pipe.send(Message('ERROR',
157 _('Could not save configuration file:\n'
158 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
159 ERR=e.strerror)))
161 def _profile_edit_request(self, essid, bssid):
162 """ Send a message with a profile to be edited. If a profile with
163 :data:`essid` and :data:`bssid` is found in the list of known
164 profiles, that profile is sent for editing. Otherwise, a new
165 profile is sent.
167 apname = make_section_name(essid, bssid)
168 try:
169 profile = self.config.get_profile(apname)
170 except NoSectionError:
171 logger.info(_('The profile "{NAME}" does not exist, '
172 'creating a new profile.').format(NAME=apname))
173 profile = get_new_profile()
174 profile['essid'] = essid
175 profile['bssid'] = bssid
176 self.msg_pipe.send(Message('PROFILE-EDIT', profile))
178 def _profile_replace(self, new_profile, old_profile):
179 """ Update :data:`old_profile` with :data:`new_profile`.
181 new_apname = make_section_name(new_profile['essid'],
182 new_profile['bssid'])
183 old_apname = make_section_name(old_profile['essid'],
184 old_profile['bssid'])
185 if old_apname == new_apname:
186 # Simple update of old_profile with new_profile.
187 self.config.set_section(new_apname, new_profile)
188 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
189 else:
190 # Replace old_profile with new_profile.
191 old_position = self._profile_remove(old_apname)
192 # Add the updated profile like it's new...
193 self.config.set_section(new_apname, new_profile)
194 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
195 if old_position is not None:
196 # ..., but in the old position.
197 self.config.auto_profile_order.insert(old_position, new_apname)
198 self.msg_pipe.send(Message('PROFILE-MOVE', (old_position, new_profile)))
199 if old_profile['known'] is False and new_profile['known'] is True:
200 # The profile has been upgraded from scanned to configured.
201 self.config.auto_profile_order.insert(0, new_apname)
202 self.msg_pipe.send(Message('PROFILE-MOVE', (0, new_profile)))
203 try:
204 self.config.write()
205 except IOError as e:
206 self.msg_pipe.send(Message('ERROR',
207 _('Could not save configuration file:\n'
208 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
209 ERR=e.strerror)))
211 def _profile_remove(self, apname):
212 """ Remove the profile named in :data:`apname`. This method returns
213 the index in the auto-profile order at which the name was found,
214 or None if not matched.
216 try:
217 position = self.config.auto_profile_order.index(apname)
218 except ValueError:
219 return None
220 else:
221 profile = self.config.get_profile(apname)
222 self.config.remove_section(apname)
223 self.config.auto_profile_order.remove(apname)
224 self.msg_pipe.send(Message('PROFILE-UNLIST', profile))
225 try:
226 self.config.write()
227 except IOError as e:
228 self.msg_pipe.send(Message('ERROR',
229 _('Could not save configuration file:\n'
230 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
231 ERR=e.strerror)))
232 return position
234 def _preferences_edit_request(self):
235 """ Pass a :class:`ConfigManager` to the UI for editing.
237 config_copy = copy_configuration(self.config)
238 self.msg_pipe.send(Message('PREFS-EDIT', config_copy))
240 def _preferences_update(self, config):
241 """ Update configuration with :data:`config`.
243 self.config.update(config)
244 try:
245 self.config.write()
246 except IOError as e:
247 self.msg_pipe.send(Message('ERROR',
248 _('Could not save configuration file:\n'
249 '{FILE}\n\n{ERR}').format(FILE=self.config.filename,
250 ERR=e.strerror)))