1
"""Preliminary initialisation stuff."""
3
# Copyright (C) 2011 Stephen Fairchild (s-fairchild@users.sourceforge.net)
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation, either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program in the file entitled COPYING.
16
# If not, see <http://www.gnu.org/licenses/>.
19
__all__ = ["ArgumentParserImplementation", "ProfileManager"]
31
from functools import partial
32
from collections import defaultdict
36
from dbus.mainloop.glib import DBusGMainLoop
37
DBusGMainLoop(set_as_default=True)
40
from idjc import FGlobs
41
from idjc import PGlobs
42
from ..utils import Singleton
43
from ..utils import PathStr
47
t = gettext.translation(FGlobs.package_name, FGlobs.localedir, fallback=True)
52
# The name of the default profile.
56
# Yes there are too many of these.
57
config_files = ("config", "controls", "left_session", "main_session",
58
"main_session_files_played", "main_session_tracks", "playerdefaults",
59
"right_session", "s_data", "mic1", "mic2", "mic3", "mic4", "mic5",
60
"mic6", "mic7", "mic8", "mic9", "mic10", "mic11", "mic12")
63
class ArgumentParserError(Exception):
68
class ArgumentParser(argparse.ArgumentParser):
69
def error(self, text):
70
raise ArgumentParserError(text)
73
def exit_with_message(self, text):
74
"""This is just error on the superclass."""
76
super(ArgumentParser, self).error(text)
80
class ArgumentParserImplementation(object):
81
"""To parse the command line arguments, if any."""
83
__metaclass__ = Singleton
86
def __init__(self, args=None, description=None, epilog=None):
90
self._args = list(args)
92
if description is None:
93
description = PGlobs.app_longform
95
ap = self._ap = ArgumentParser(description=description, epilog=epilog)
96
ap.add_argument("-v", "--version", action='version', version=
97
FGlobs.package_name + " " + FGlobs.package_version)
98
sp = self._sp = ap.add_subparsers(
99
# TC: command line switch info from $ idjc --help
100
help=_("sub-option -h for more info"))
101
# TC: a command line option help string.
102
sp_run = sp.add_parser("run", help=_("the default command"),
103
# TC: do not translate run.
104
description=description + " " + _("-- sub-command: run"), epilog=epilog)
105
# TC: a command line option help string.
106
sp_mp = sp.add_parser("generateprofile", help=_("make a new profile"),
107
# TC: do not translate generateprofile.
108
description=description + " " + _("-- sub-command: generateprofile"), epilog=epilog)
110
sp_run.add_argument("-d", "--dialog", dest="dialog", nargs=1,
111
choices=("true", "false"),
112
help=_("""force the appearance or non-appearance of the
113
profile chooser dialog -- when used with the -p option
114
the chosen profile is preselected"""))
115
# TC: command line help placeholder.
116
sp_run.add_argument("-p", "--profile", dest="profile", nargs=1, metavar=_("profile_choice"),
117
help=_("""the profile to use -- overrides the user interface
118
preferences "show profile dialog" option"""))
119
sp_run.add_argument("-j", "--jackserver", dest="jackserver", nargs=1,
120
# TC: command line help placeholder.
121
metavar=_("server_name"), help=_("the named jack sound-server to connect with"))
122
group = sp_run.add_argument_group(_("user interface settings"))
123
group.add_argument("-c", "--channels", dest="channels", nargs="+", metavar="c",
124
help=_("the audio channels to have open at startup"))
125
group.add_argument("-V", "--voip", dest="voip", nargs=1, choices=
126
("off", "private", "public"),
127
help=_("the voip mode at startup"))
128
group.add_argument("-P", "--players", dest="players", nargs="+", metavar="p",
129
help="the players to start among values {1,2}")
130
group.add_argument("-s", "--servers", dest="servers", nargs="+", metavar="s",
131
help=_("attempt connection with the specified servers"))
132
group.add_argument("-x", "--crossfader", dest="crossfader", choices=("1", "2"),
133
help=_("position the crossfader for the specified player"))
134
# TC: command line help placeholder.
135
sp_mp.add_argument("newprofile", metavar=_("profile_name"),
136
help=_("""new profile name -- will form part of the dbus
137
bus/object/interface name and the JACK client ID --
138
restrictions therefore apply"""))
139
# TC: command line help placeholder.
140
sp_mp.add_argument("-t", "--template", dest="template", metavar=_("template_profile"),
141
help=_("an existing profile to use as a template"))
142
# TC: command line help placeholder.
143
sp_mp.add_argument("-i", "--icon", dest="icon", metavar=_("icon_pathname"),
144
help=_("pathname to an icon -- defaults to idjc logo"))
145
# TC: Command line help placeholder for the profile's nickname.
146
# TC: Actual profile names are very restricted in what characters can be used.
147
sp_mp.add_argument("-n", "--nickname", dest="nickname", metavar=_("nickname"),
148
help=_("""the alternate profile name to appear in window title bars"""))
149
sp_mp.add_argument("-d", "--description", dest="description", metavar=_("description_text"),
150
help=_("a description of the profile"))
153
def parse_args(self):
155
return self._ap.parse_args(self._args)
156
except ArgumentParserError as e:
158
for cmd in self._sp.choices.iterkeys():
159
if cmd in self._args:
161
return self._ap.parse_args(self._args + ["run"])
162
except ArgumentParserError:
163
self._ap.exit_with_message(str(e))
166
def error(self, text):
167
self._ap.exit_with_message(text)
170
def exit(self, status=0, message=None):
171
self._ap.exit(status, message)
175
class DBusUptimeReporter(dbus.service.Object):
176
"""Supply uptime to other idjc instances."""
179
interface_name = PGlobs.dbus_bus_basename + ".profile"
180
obj_path = PGlobs.dbus_objects_basename + "/uptime"
184
self._uptime_cache = defaultdict(float)
185
self._interface_cache = {}
186
# Defer base class initialisation.
189
@dbus.service.method(interface_name, out_signature="d")
190
def get_uptime(self):
191
"""Broadcast uptime from the current profile."""
193
return self._get_uptime()
196
def activate_for_profile(self, bus_name, get_uptime):
197
self._get_uptime = get_uptime
198
dbus.service.Object.__init__(self, bus_name, self.obj_path)
201
def get_uptime_for_profile(self, profile):
202
"""Ask and return the uptime of an active profile.
204
Step 1, Issue an async request for new data.
205
Step 2, Return immediately with the cached value.
207
Note: On error the cache is purged.
212
self._uptime_cache[profile] = retval
217
del self._uptime_cache[profile]
221
del self._interface_cache[profile]
227
interface = self._interface_cache[profile]
230
p = dbus.SessionBus().get_object(PGlobs.dbus_bus_basename + \
231
"." + profile, self.obj_path)
232
interface = dbus.Interface(p, self.interface_name)
233
except dbus.exceptions.DBusException as e:
235
return self._uptime_cache.default_factory()
237
self._interface_cache[profile] = interface
239
interface.get_uptime(reply_handler=rh, error_handler=eh)
240
return self._uptime_cache[profile]
244
# Profile length limited for practical reasons. For more descriptive
245
# purposes the nickname parameter was created.
246
MAX_PROFILE_LENGTH = 18
250
def profile_name_valid(p):
252
dbus.validate_bus_name("com." + p)
253
dbus.validate_object_path("/" + p)
254
except (TypeError, ValueError):
256
return len(p) <= MAX_PROFILE_LENGTH
260
class ProfileError(Exception):
261
"""General purpose exception used within the ProfileManager class.
263
Takes two strings so that one can be used for command line messages
264
and the other for displaying in dialog boxes."""
266
def __init__(self, str1, str2=None):
267
Exception.__init__(self, str1)
272
class ProfileManager(object):
273
"""The profile gives each application instance a unique identity.
275
This identity extends to the config file directory if present,
276
to the JACK application ID, to the DBus bus name.
279
__metaclass__ = Singleton
282
_profile = _dbus_bus_name = _profile_dialog = _init_time = None
284
_optionals = ("icon", "nickname", "description")
289
ap = ArgumentParserImplementation()
290
args = ap.parse_args()
292
if PGlobs.profile_dir is not None:
294
if not os.path.isdir(PGlobs.profile_dir / default):
295
self._generate_default_profile()
297
if "newprofile" in args:
298
self._generate_profile(**vars(args))
300
except ProfileError as e:
301
ap.error(_("failed to create profile: %s") % str(e))
303
with open(PGlobs.autoload_profile_pathname, "a"):
306
profile = self.autoloadprofilename
309
dialog_selects = True
311
dialog_selects = False
313
if args.profile is not None:
314
profile = args.profile[0]
315
dialog_selects = False
316
if not profile_name_valid(profile):
317
ap.error(_("the specified profile name is not valid"))
319
if args.dialog is not None:
320
dialog_selects = args.dialog[0] == "true"
322
self._uprep = DBusUptimeReporter()
323
self._profile_dialog = self._get_profile_dialog()
324
self._profile_dialog.connect("delete", self._cb_delete_profile)
325
self._profile_dialog.connect("choose", self._choose_profile)
327
def new_profile(dialog, profile, template, icon, nickname, description):
329
self._generate_profile(profile, template, icon=icon,
330
nickname=nickname, description=description)
331
dialog.destroy_new_profile_dialog()
332
except ProfileError as e:
333
dialog.display_error(_("<span weight='bold' size='12000'>Error while creating new profile.</span>\n\n%s") % e.gui_text,
334
transient_parent=dialog.get_new_profile_dialog(), markup=True)
336
self._profile_dialog.connect("new", new_profile)
337
self._profile_dialog.connect("clone", new_profile)
338
self._profile_dialog.connect("edit", self._cb_edit_profile)
339
self._profile_dialog.connect("auto", self._cb_auto)
340
self._profile_dialog.highlight_profile(profile, scroll=True)
342
self._profile_dialog.run()
343
self._profile_dialog.hide()
345
self._choose_profile(self._profile_dialog, profile, verbose=True)
346
if self._profile is None:
347
ap.error(_("no profile is set"))
356
def iconpathname(self):
357
return self._iconpathname
361
def dbus_bus_name(self):
362
return self._dbus_bus_name
367
"""The base directory of this profile."""
369
return PGlobs.profile_dir / self.profile
373
def jinglesdir(self):
374
"""The directory for jingles storage."""
376
return self.basedir / "jingles"
380
def title_extra(self):
381
"""Window title text indicating which profile is in use."""
385
return " (%s:%s)" % ((self.profile, n))
387
if self.profile == default:
389
return " (%s)" % self.profile
393
def autoloadprofilename(self):
394
"""Which profile would automatically load if given the chance?"""
396
al_profile = self._autoloadprofilename()
397
if al_profile is None:
401
profiledirs = os.walk(PGlobs.profile_dir).next()[1]
402
except (EnvironmentError, StopIteration):
405
return al_profile if al_profile in profiledirs else None
409
def profile_dialog(self):
410
return self._profile_dialog
413
def get_uptime(self):
414
if self._init_time is not None:
415
return time.time() - self._init_time
420
def _autoloadprofilename(self):
421
"""Just the file contents without checking."""
424
with open(PGlobs.autoload_profile_pathname) as f:
425
fcntl.flock(f, fcntl.LOCK_EX)
426
al_profile = f.readline().strip()
432
def _cb_auto(self, dialog, profile):
434
with open(PGlobs.autoload_profile_pathname, "r+") as f:
435
fcntl.flock(f, fcntl.LOCK_EX)
436
al_profile = f.readline().strip()
438
if profile != al_profile:
445
def _cb_edit_profile(self, dialog, newprofile, oldprofile, *opts):
450
busses.append(self._grab_bus_name_for_profile(oldprofile))
451
if newprofile != oldprofile:
452
busses.append(self._grab_bus_name_for_profile(newprofile))
453
except dbus.DBusException:
454
raise ProfileError(None, _("Profile %s is active.") %
455
(oldprofile, newprofile)[len(busses)])
457
if newprofile != oldprofile:
459
shutil.copytree(PGlobs.profile_dir / oldprofile,
460
PGlobs.profile_dir / newprofile)
461
except EnvironmentError as e:
463
raise ProfileError(None,
464
_("Cannot rename profile {0} to {1}, {1} currently exists.").format(
465
oldprofile, newprofile))
467
raise ProfileError(None,
468
_("Error during attempt to rename {0} to {1}.").format(
469
oldprofile, newprofile))
471
shutil.rmtree(PGlobs.profile_dir / oldprofile)
473
for name, data in zip(self._optionals, opts):
474
with open(PGlobs.profile_dir / newprofile / name, "w") as f:
477
except ProfileError, e:
478
text = _("<span weight='bold' size='12000'>Error while editing profile: {0}.</span>\n\n{1}").format(oldprofile, e.gui_text)
479
dialog.display_error(text, markup=True,
480
transient_parent=dialog.get_new_profile_dialog())
482
dialog.destroy_new_profile_dialog()
485
def _cb_delete_profile(self, dialog, profile):
486
if profile is not dialog.profile:
488
busname = self._grab_bus_name_for_profile(profile)
489
shutil.rmtree(PGlobs.profile_dir / profile)
490
except dbus.DBusException:
492
if profile == default:
493
self._generate_default_profile()
496
def _choose_profile(self, dialog, profile, verbose=False):
497
if dialog.profile is None:
499
self._dbus_bus_name = self._grab_bus_name_for_profile(profile)
500
except dbus.DBusException:
502
print _("the profile '%s' is in use") % profile
504
self._init_time = time.time()
505
self._profile = profile
506
self._nickname = self._grab_profile_filetext(
507
profile, "nickname") or ""
508
self._iconpathname = self._grab_profile_filetext(
509
profile, "icon") or PGlobs.default_icon
510
dialog.set_profile(profile, self.title_extra, self._iconpathname)
511
self._uprep.activate_for_profile(self._dbus_bus_name, self.get_uptime)
513
print "%s run -p %s" % (FGlobs.bindir / FGlobs.package_name, profile)
514
subprocess.Popen([FGlobs.bindir / FGlobs.package_name, "run", "-p", profile], close_fds=True)
517
def _generate_profile(self, newprofile, template=None, **kwds):
518
if PGlobs.profile_dir is not None:
519
if len(newprofile) > MAX_PROFILE_LENGTH:
520
raise ProfileError(_("the profile length is too long (max %d characters)") % MAX_PROFILE_LENGTH,
521
_("The profile length is too long (max %d characters).") % MAX_PROFILE_LENGTH)
523
if not profile_name_valid(newprofile):
524
raise ProfileError(_("the new profile name is not valid"),
525
_("The new profile name is not valid."))
528
busname = self._grab_bus_name_for_profile(newprofile)
529
except dbus.DBusException:
530
raise ProfileError(_("the chosen profile is currently running"),
531
_("The chosen profile is currently running."))
534
tmp = PathStr(tempfile.mkdtemp())
535
except EnvironmentError:
536
raise ProfileError(_("temporary directory creation failed"),
537
_("Temporary directory creation failed."))
540
if template is not None:
541
if not profile_name_valid(template):
543
_("the specified template '%s' is not valid") % template,
544
_("The specified template '%s' is not valid.") % template)
546
tdir = PGlobs.profile_dir / template
547
if os.path.isdir(tdir):
548
for x in self._optionals + config_files:
550
shutil.copyfile(tdir / x, tmp / x)
551
except EnvironmentError:
553
shutil.copytree(tdir / "jingles", tmp / "jingles")
556
_("the template profile '%s' does not exist") % template,
557
_("The template profile '%s' does not exist.") % template)
559
for fname in self._optionals:
562
with open(tmp / fname, "w") as f:
564
except EnvironmentError:
565
raise ProfileError(_("could not write file %s") + fname,
566
_("Could not write file %s.") % fname)
569
dest = PGlobs.profile_dir / newprofile
571
shutil.copytree(tmp, dest)
572
except EnvironmentError as e:
573
if e.errno == 17 and os.path.isdir(dest):
574
msg1 = _("the profile directory '%s' already exists") % dest
575
msg2 = _("The profile directory '%s' already exists.") % dest
577
msg1 = _("a non directory path exists at: '%s'") % dest
578
msg2 = _("A Non directory path exists at: '%s'.") % dest
579
raise ProfileError(msg1, msg2)
581
# Failure to clean up is not a critical error.
584
except EnvironmentError:
588
def _generate_default_profile(self):
589
self._generate_profile(default, description=_("The default profile"))
592
def _profile_data(self):
593
a = self._autoloadprofilename()
594
d = PGlobs.profile_dir
596
profdirs = os.walk(d).next()[1]
597
except (EnvironmentError, StopIteration):
599
for profname in profdirs:
600
if profile_name_valid(profname):
601
files = os.walk(d / profname).next()[2]
602
rslt = {"profile": profname}
603
for each in self._optionals:
605
with open(d / profname / each) as f:
606
rslt[each] = f.read()
607
except EnvironmentError:
610
rslt["active"] = self._profile_has_owner(profname)
611
rslt["uptime"] = math.floor(self._uprep.get_uptime_for_profile(profname))
612
rslt["auto"] = (1 if a == profname else 0)
616
def closure(cmd, name):
617
busbase = PGlobs.dbus_bus_basename
619
return cmd(".".join((busbase, profname)))
620
inner.__name__ = name
621
return staticmethod(inner)
623
_profile_has_owner = closure(dbus.SessionBus().name_has_owner,
624
"_profile_has_owner")
626
_grab_bus_name_for_profile = closure(partial(dbus.service.BusName, do_not_queue=True),
627
"_grab_bus_name_for_profile")
633
def _grab_profile_filetext(profile, filename):
635
with open(PGlobs.profile_dir / profile / filename) as f:
636
return f.readline().strip()
637
except EnvironmentError:
641
def _get_profile_dialog(self):
642
from .profiledialog import ProfileDialog
644
return ProfileDialog(default=default, data_function=self._profile_data)