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
37 import logging
.handlers
38 from multiprocessing
import Pipe
, Process
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__
)
56 """ The primary component of WiFi Radar.
58 def __init__(self
, conf_file
):
59 """ Create WiFi Radar app using :data:`config` for configuration.
61 dispatcher
= Dispatcher()
62 scanner_pipe
= dispatcher
.subscribe(['ALL'])
63 ui_pipe
= dispatcher
.subscribe(['ALL'])
64 self
.msg_pipe
= dispatcher
.subscribe(['ALL'])
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
))
72 self
.msg_pipe
.send(Message('ERROR', e
))
74 self
.shutdown([], dispatcher
)
77 logger
.setLevel(self
.config
.get_opt_as_int('GENERAL',
81 fileLogHandler
= logging
.handlers
.RotatingFileHandler(
82 self
.config
.get_opt('GENERAL', 'logfile'),
83 maxBytes
=64*1024, backupCount
=5)
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
)))
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
,))
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
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
:
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
))
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
)
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 '
148 error_message
= error_message
.format(FILE
=conf_file
,
149 VERSION
=WIFI_RADAR_VERSION
)
150 raise ConfigFileError(error_message
)
153 # Missing user configuration file, so read the configuration
154 # defaults file. Then setup the file manager to write to
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.
166 return config
, config_file_manager
169 """ Watch for incoming messages and dispatch to subscribers.
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
))
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
:
190 def _check_message(self
, msg
):
193 if msg
.topic
== 'EXIT':
194 self
.msg_pipe
.close()
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
)
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
219 self
.config_file_man
.write(self
.config
)
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
232 apname
= make_section_name(essid
, bssid
)
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
))
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
)))
269 self
.config_file_man
.write(self
.config
)
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.
282 position
= self
.config
.auto_profile_order
.index(apname
)
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
))
291 self
.config_file_man
.write(self
.config
)
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
)))
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
)
310 self
.config_file_man
.write(self
.config
)
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
)))