Rename PreferencesEditor save to apply
[wifi-radar.git] / wifiradar / __init__.py
blob1f2a60651592aff41599dd26dd640488eae4c1c4
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='ui', target=ui.start, 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):
152 """ Update the auto profile order in the configuration.
153 :data:`profile_order` is a list of profile names, in order.
155 self.config.auto_profile_order = profile_order
156 try:
157 self.config.write()
158 except IOError as e:
159 self.msg_pipe.send(Message('ERROR',
160 'Could not save configuration file:\n' +
161 '{}\n\n{}'.format(self.config.filename, e.strerror)))
163 def _profile_edit_request(self, essid, bssid):
164 """ Send a message with a profile to be edited. If a profile with
165 :data:`essid` and :data:`bssid` is found in the list of known
166 profiles, that profile is sent for editing. Otherwise, a new
167 profile is sent.
169 apname = make_section_name(essid, bssid)
170 try:
171 profile = self.config.get_profile(apname)
172 except NoSectionError:
173 logger.info('The profile "{}" does '.format(apname) +
174 'not exist, creating a new profile.')
175 profile = misc.get_new_profile()
176 profile['essid'] = essid
177 profile['bssid'] = bssid
178 self.msg_pipe.send(Message('PROFILE-EDIT', profile))
180 def _profile_replace(self, new_profile, old_profile):
181 """ Update :data:`old_profile` with :data:`new_profile`.
183 new_apname = make_section_name(new_profile['essid'],
184 new_profile['bssid'])
185 old_apname = make_section_name(old_profile['essid'],
186 old_profile['bssid'])
187 if old_apname == new_apname:
188 # Simple update of old_profile with new_profile.
189 self.config.set_section(new_apname, new_profile)
190 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
191 else:
192 # Replace old_profile with new_profile.
193 old_position = self._profile_remove(old_apname)
194 # Add the updated profile like it's new...
195 self.config.set_section(new_apname, new_profile)
196 self.msg_pipe.send(Message('PROFILE-UPDATE', new_profile))
197 if old_position is not None:
198 # ..., but in the old position.
199 self.config.auto_profile_order.insert(old_position, new_apname)
200 self.msg_pipe.send(Message('PROFILE-MOVE', (old_position, new_profile)))
201 if old_profile['known'] is False and new_profile['known'] is True:
202 # The profile has been upgraded from scanned to configured.
203 self.config.auto_profile_order.insert(0, new_apname)
204 self.msg_pipe.send(Message('PROFILE-MOVE', (0, new_profile)))
205 try:
206 self.config.write()
207 except IOError as e:
208 self.msg_pipe.send(Message('ERROR',
209 'Could not save configuration file:\n' +
210 '{}\n\n{}'.format(self.config.filename, e.strerror)))
212 def _profile_remove(self, apname):
213 """ Remove the profile named in :data:`apname`. This method returns
214 the index in the auto-profile order at which the name was found,
215 or None if not matched.
217 try:
218 position = self.config.auto_profile_order.index(apname)
219 except ValueError:
220 return None
221 else:
222 profile = self.config.get_profile(apname)
223 self.config.remove_section(apname)
224 self.config.auto_profile_order.remove(apname)
225 self.msg_pipe.send(Message('PROFILE-UNLIST', profile))
226 try:
227 self.config.write()
228 except IOError as e:
229 self.msg_pipe.send(Message('ERROR',
230 'Could not save configuration file:\n' +
231 '{}\n\n{}'.format(self.config.filename, 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 '{}\n\n{}'.format(self.config.filename, e.strerror)))