aboutsummaryrefslogtreecommitdiff
path: root/python/ContextKit/flexiprovider.py
blob: fd9a56ecfde3751022a8bab37785dff05dfaea37 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/python
import os
import sys
import tempfile
import subprocess
import glib, gobject
import dbus, dbus.service, dbus.mainloop.glib

def pkgconfig(*args):
    """Runs `pkg-config $args` and returns the Popen object, augmented
    with an `output' attribute containing stdout."""
    cmd = ['pkg-config'] + list(args)
    process = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    process.output = process.communicate()[0].strip()
    return process

# The following kludge is needed if we want this to be usable without
# installing libcontextprovider.  If pkg-config reports it as
# uninstalled, we extend PYTHONPATH, then we try to import the module.
# If that fails, extend LD_LIBRARY_PATH and re-exec ourselves.

if pkgconfig('--exists', 'contextprovider-1.0').returncode != 0 or \
   pkgconfig('--exists', 'duivaluespace-1.0').returncode != 0:
    raise RuntimeError("You need to make pkg-config find "
                       "contextprovider-1.0 and duivaluespace-1.0 "
                       "somehow.\nTry setting $PKG_CONFIG_PATH.")

if pkgconfig('--uninstalled', 'contextprovider-1.0').returncode == 0:
    sys.path.append(pkgconfig('--variable=pythondir', 'contextprovider-1.0').output)
try:
    import ContextProvider as CP
except ImportError:
    raise
except:
    # Failed, probably because LD_LIBRARY_PATH is not right.  Set it and
    # re-exec ourselves.  To avoid an infinite loop, we try this only
    # when LD_LIBRARY_PATH doesn't yet contain what we want to add.
    libdir = pkgconfig('--variable=libdir', 'contextprovider-1.0').output
    ldpath = [d for d in os.environ.get('LD_LIBRARY_PATH', '').split(':') if d != '']
    if libdir in ldpath:
        raise
    ldpath += [libdir, libdir + '/.libs']
    env = dict(os.environ)
    env.update(LD_LIBRARY_PATH=':'.join(ldpath))
    os.execve(sys.argv[0], sys.argv, env)

class _property(object):
    """Kind-of a template for property types."""
    def __init__(self, datatype, default):
        self.datatype = datatype
        self.default = default
        self.name = None
    def __call__(self, name, initial_value=None):
        """Clones self, setting its name and initial value."""
        if initial_value is None:
            initial_value = self.default
        prop = _property(self.datatype, initial_value)
        prop.name = name
        return prop

# Pre-defined types and their defaults.
INT = _property('INT', 0)
STRING = _property('STRING', '')
DOUBLE = _property('DOUBLE', 0.0)
TRUTH = _property('TRUTH', False)

def xmlfor(busname='ctx.flexiprovider', bus='session', *props):
    """Generates a provider .xml document for the given properties
    (which are instances of _property)."""
    xml = ['<?xml version="1.0"?>\n'
           '<provider bus="%s" service="%s">' % (bus, busname)]
    for p in props:
        node, _, key = p.name.partition('.')
        xml.append('  <node name="%s">\n'
                   '    <key name="%s" type="%s">\n'
                   '       <doc>A phony but very flexible property.</doc>\n'
                   '    </key>\n'
                   '  </node>' % (node, key, p.datatype))
    xml.append('</provider>')
    return '\n'.join(xml)

# Location of the update-context-providers script, initialized lazily
# in the function below.
U_C_P = None
def update_context_providers(xml, dir='.'):
    """Runs the update-context-provider script in $dir after writing
    $xml to a temporary file."""
    global U_C_P
    if U_C_P is None:
        U_C_P = ''
        if pkgconfig('--uninstalled', 'duivaluespace-1.0').returncode == 0:
            U_C_P = '%s/registry/' % pkgconfig('--variable=pcfiledir',
                                              'duivaluespace-1.0').output
        U_C_P += 'update-context-providers'
    tmpf = tempfile.NamedTemporaryFile('w+b', -1, '.xml', 'flexi', dir)
    print >>tmpf, xml
    tmpf.flush()
    subprocess.call([U_C_P, dir])

class Flexiprovider(object):
    def stdin_ready(self, fd, cond):
        if cond & (glib.IO_ERR | glib.IO_HUP):
            self.loop.quit()
            return False
        # We assume that stdin is line-buffered (ie. readline() doesn't
        # block (too much)).  It's true if it's a tty.  If piping from
        # another program, do an entire line.
        # TODO maybe we should rather fix ourselves
        l = sys.stdin.readline().strip()
        if l == '':
            self.loop.quit()
            return False
        try:
            # TODO something more sophisticated?
            code = compile(l, '<input from stdin>', 'single')
            eval(code, {}, dict(INT=INT,
                                STRING=STRING,
                                TRUTH=TRUTH,
                                DOUBLE=DOUBLE,
                                set=self.set,
                                get=self.get,
                                reset=self.reset,
                                quit=self.loop.quit,
                                add=self.add_and_update,
                                info=self.info))
        except:
            # We are ignoring errors.
            import traceback
            traceback.print_exc()
        return True

    def __init__(self, properties, name='ctx.flexiprovider', bus='session'):
        self.busname = name
        self.bus = bus
        self.loop = gobject.MainLoop()
        self.props = {}
        self.values = {}
        # Hook into libcontextprovider.
        self.setters = dict(INT=CP.ContextProvider.set_integer,
                            STRING=CP.ContextProvider.set_string,
                            DOUBLE=CP.ContextProvider.set_double,
                            TRUTH=CP.ContextProvider.set_boolean)
        self.subscribed_cb = CP.ContextProvider.SUBSCRIPTION_CHANGED_CALLBACK(lambda x, y: None)
        CP.ContextProvider.init(dict(session=0, system=1)[self.bus], self.busname)
        # Add properties and set the initial values.
        for p in properties:
            self.addproperty(p)
        self.update_providers()
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    def get(self, prop):
        return self.values.get(prop, None)

    def set(self, prop, val):
        if prop not in self.props:
            return
        self.values[prop] = val
        if val is None:
            CP.ContextProvider.set_null(prop)
        else:
            self.setters[self.props[prop].datatype](prop, val)

    def reset(self, prop):
        if prop not in self.props:
            return
        self.set(prop, self.props[prop].default)

    def addproperty(self, prop):
        self.props[prop.name] = prop
        self.values[prop.name] = prop.default
        CP.ContextProvider.install_key(prop.name, 0, self.subscribed_cb, None)
        self.reset(prop.name)

    def add_and_update(self, prop):
        self.addproperty(prop)
        self.update_providers()

    def update_providers(self):
        update_context_providers(xmlfor(self.busname, self.bus, *self.props.values()))

    def info(self):
        """Dumps information about currently provided properties."""
        for p in self.props.values():
            print p.datatype.ljust(8), p.name.ljust(20), \
                self.values[p.name], '(', p.default, ')'

    def interactive(self):
        """Runs the provider interactively, accepting the following
        commands from the standard input (note that this is Python
        syntax, and it gets eval()ed):

          set(property, value):  sets the given property
          get(property):         prints the value of the property
          reset(property):       resets the property
          add(propertydesc):     adds a new property
          info():                prints properties and their values
          quit():                stops the whole thing

        The provider will stop also if stdin is closed.
        """
        glib.io_add_watch(sys.stdin.fileno(),
                          glib.IO_IN | glib.IO_HUP | glib.IO_ERR,
                          self.stdin_ready)
        self.loop.run()

    def run(self):
        self.loop.run()

__all__ = ('INT', 'STRING', 'DOUBLE', 'TRUTH', 'Flexiprovider')