~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to portable/dl_daemon/daemon.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Miro - an RSS based video player application
2
 
# Copyright (C) 2005-2010 Participatory Culture Foundation
3
 
#
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.
8
 
#
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.
13
 
#
14
 
# You should have received a copy of the GNU General Public License
15
 
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
17
 
#
18
 
# In addition, as a special exception, the copyright holders give
19
 
# permission to link the code of portions of this program with the OpenSSL
20
 
# library.
21
 
#
22
 
# You must obey the GNU General Public License in all respects for all of
23
 
# the code used other than OpenSSL. If you modify file(s) with this
24
 
# exception, you may extend this exception to your version of the file(s),
25
 
# but you are not obligated to do so. If you do not wish to do so, delete
26
 
# this exception statement from your version. If you delete this exception
27
 
# statement from all source files in the program, then also delete it here.
28
 
 
29
 
from miro.dl_daemon import command
30
 
import os
31
 
import cPickle
32
 
import socket
33
 
import traceback
34
 
from time import sleep
35
 
from struct import pack, unpack, calcsize
36
 
import tempfile
37
 
from miro import config
38
 
from miro import prefs
39
 
from miro import eventloop
40
 
from miro import util
41
 
import logging
42
 
from miro.plat.utils import launch_download_daemon, kill_process
43
 
from miro import signals
44
 
from miro import trapcall
45
 
from miro.httpclient import ConnectionHandler
46
 
 
47
 
SIZE_OF_INT = calcsize("I")
48
 
 
49
 
class DaemonError(Exception):
50
 
    """Exception while communicating to a daemon (either controller or
51
 
    downloader).
52
 
    """
53
 
    pass
54
 
 
55
 
firstDaemonLaunch = '1'
56
 
def startDownloadDaemon(oldpid, port):
57
 
    global firstDaemonLaunch
58
 
 
59
 
    daemonEnv = {
60
 
        'DEMOCRACY_DOWNLOADER_PORT' : str(port),
61
 
        'DEMOCRACY_DOWNLOADER_FIRST_LAUNCH' : firstDaemonLaunch,
62
 
        'DEMOCRACY_SHORT_APP_NAME' : config.get(prefs.SHORT_APP_NAME),
63
 
    }
64
 
    launch_download_daemon(oldpid, daemonEnv)
65
 
    firstDaemonLaunch = '0'
66
 
 
67
 
def getDataFile(short_app_name):
68
 
    if hasattr(os, "getuid"):
69
 
        uid = os.getuid()
70
 
    elif "USERNAME" in os.environ:
71
 
        # This works for win32, where we don't have getuid()
72
 
        uid = os.environ['USERNAME']
73
 
    elif "USER" in os.environ:
74
 
        uid = os.environ['USER']
75
 
    else:
76
 
        # FIXME - can we do something better here on Windows
77
 
        # platforms?
78
 
        uid = "unknown"
79
 
       
80
 
    return os.path.join(tempfile.gettempdir(),
81
 
            ('%s_Download_Daemon_%s.txt' % (short_app_name, uid)))
82
 
 
83
 
pidfile = None
84
 
def writePid(short_app_name, pid):
85
 
    """Write out our pid.
86
 
 
87
 
    This method locks the pid file until the downloader exits.  On windows
88
 
    this is achieved by keeping the file open.  On Unix/OS X, we use the
89
 
    fcntl.lockf() function.
90
 
    """
91
 
 
92
 
    global pidfile
93
 
    # NOTE: we want to open the file in a mode the standard open() doesn't
94
 
    # support.  We want to create the file if nessecary, but not truncate it
95
 
    # if it's already around.  We can't truncate it because on unix we haven't
96
 
    # locked the file yet.
97
 
    fd = os.open(getDataFile(short_app_name), os.O_WRONLY | os.O_CREAT)
98
 
    pidfile = os.fdopen(fd, 'w')
99
 
    try:
100
 
        import fcntl
101
 
    except (SystemExit, KeyboardInterrupt):
102
 
        raise
103
 
    except:
104
 
        pass
105
 
    else:
106
 
        fcntl.lockf(pidfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
107
 
    pidfile.write("%s\n" % pid)
108
 
    pidfile.flush()
109
 
    # NOTE: There may be extra data after the line we write left around from
110
 
    # previous writes to the pid file.  This is fine since readPid() only reads
111
 
    # the 1st line.
112
 
    #
113
 
    # NOTE 2: we purposely don't close the file, to achieve locking on
114
 
    # windows.
115
 
 
116
 
def readPid(short_app_name):
117
 
    try:
118
 
        f = open(getDataFile(short_app_name), "r")
119
 
    except IOError:
120
 
        return None
121
 
    try:
122
 
        try:
123
 
            return int(f.readline())
124
 
        except ValueError:
125
 
            return None
126
 
    finally:
127
 
        f.close()
128
 
 
129
 
lastDaemon = None
130
 
 
131
 
class Daemon(ConnectionHandler):
132
 
    def __init__(self):
133
 
        ConnectionHandler.__init__(self)
134
 
        global lastDaemon
135
 
        lastDaemon = self
136
 
        self.waitingCommands = {}
137
 
        self.returnValues = {}
138
 
        self.size = 0
139
 
        self.states['ready'] = self.onSize
140
 
        self.states['command'] = self.onCommand
141
 
        self.queuedCommands = []
142
 
        self.shutdown = False
143
 
        self.stream.disableReadTimeout = True
144
 
        # disable read timeouts for the downloader daemon communication.  Our
145
 
        # normal state is to wait for long periods of time for without seeing
146
 
        # any data.
147
 
 
148
 
    def onError(self, error):
149
 
        """Call this when an error occurs.  It forces the
150
 
        daemon to close its connection.
151
 
        """
152
 
        logging.warning ("socket error in daemon, closing my socket")
153
 
        self.closeConnection()
154
 
        raise error
155
 
 
156
 
    def onConnection(self, socket):
157
 
        self.changeState('ready')
158
 
        for (comm, callback) in self.queuedCommands:
159
 
            self.send(comm, callback)
160
 
        self.queuedCommands = []
161
 
 
162
 
    def onSize(self):
163
 
        if self.buffer.length >= SIZE_OF_INT:
164
 
            (self.size,) = unpack("I", self.buffer.read(SIZE_OF_INT))
165
 
            self.changeState('command')
166
 
 
167
 
    def onCommand(self):
168
 
        if self.buffer.length >= self.size:
169
 
            try:
170
 
                comm = cPickle.loads(self.buffer.read(self.size))
171
 
            except (SystemExit, KeyboardInterrupt):
172
 
                raise
173
 
            except:
174
 
                logging.exception ("WARNING: error unpickling command.")
175
 
            else:
176
 
                self.processCommand(comm)
177
 
            self.changeState('ready')
178
 
 
179
 
    def processCommand(self, comm):
180
 
        trapcall.time_trap_call("Running: %s" % (comm,), self.runCommand, comm)
181
 
 
182
 
    def runCommand(self, comm):
183
 
        comm.setDaemon(self)
184
 
        comm.action()
185
 
 
186
 
    def send(self, comm, callback = None):
187
 
        if self.state == 'initializing':
188
 
            self.queuedCommands.append((comm, callback))
189
 
        else:
190
 
            raw = cPickle.dumps(comm, cPickle.HIGHEST_PROTOCOL)
191
 
            self.sendData(pack("I",len(raw)) + raw, callback)
192
 
 
193
 
class DownloaderDaemon(Daemon):
194
 
    def __init__(self, port, short_app_name):
195
 
        # before anything else, write out our PID 
196
 
        writePid(short_app_name, os.getpid())
197
 
        # connect to the controller and start our listen loop
198
 
        Daemon.__init__(self)
199
 
        self.openConnection('127.0.0.1', port, self.onConnection, self.onError)
200
 
        signals.system.connect('error', self.handleError)
201
 
 
202
 
    def handleError(self, obj, report):
203
 
        command.DownloaderErrorCommand(self, report).send()
204
 
 
205
 
    def handleClose(self, type):
206
 
        if self.shutdown:
207
 
            return
208
 
        self.shutdown = True
209
 
        eventloop.quit()
210
 
        logging.warning ("downloader: connection closed -- quitting")
211
 
        from miro.dl_daemon import download
212
 
        download.shutDown()
213
 
        import threading
214
 
        for thread in threading.enumerate():
215
 
            if thread != threading.currentThread() and not thread.isDaemon():
216
 
                thread.join()
217
 
 
218
 
class ControllerDaemon(Daemon):
219
 
    def __init__(self):
220
 
        Daemon.__init__(self)
221
 
        self.stream.acceptConnection('127.0.0.1', 0, self.onConnection, self.onError)
222
 
        self.port = self.stream.port
223
 
        data = {}
224
 
        remoteConfigItems = [prefs.LIMIT_UPSTREAM,
225
 
                   prefs.UPSTREAM_LIMIT_IN_KBS,
226
 
                   prefs.LIMIT_DOWNSTREAM_BT,
227
 
                   prefs.DOWNSTREAM_BT_LIMIT_IN_KBS,
228
 
                   prefs.BT_MIN_PORT,
229
 
                   prefs.BT_MAX_PORT,
230
 
                   prefs.USE_UPNP,
231
 
                   prefs.BT_ENC_REQ,
232
 
                   prefs.MOVIES_DIRECTORY,
233
 
                   prefs.PRESERVE_DISK_SPACE,
234
 
                   prefs.PRESERVE_X_GB_FREE,
235
 
                   prefs.SUPPORT_DIRECTORY,
236
 
                   prefs.SHORT_APP_NAME,
237
 
                   prefs.LONG_APP_NAME,
238
 
                   prefs.APP_PLATFORM,
239
 
                   prefs.APP_VERSION,
240
 
                   prefs.APP_SERIAL,
241
 
                   prefs.APP_REVISION,
242
 
                   prefs.PUBLISHER,
243
 
                   prefs.PROJECT_URL,
244
 
                   prefs.DOWNLOADER_LOG_PATHNAME,
245
 
                   prefs.LOG_PATHNAME,
246
 
                   prefs.GETTEXT_PATHNAME,
247
 
                   prefs.LIMIT_UPLOAD_RATIO,
248
 
                   prefs.UPLOAD_RATIO,
249
 
                   prefs.LIMIT_CONNECTIONS_BT,
250
 
                   prefs.CONNECTION_LIMIT_BT_NUM,
251
 
                ]
252
 
 
253
 
        for desc in remoteConfigItems:
254
 
            data[desc.key] = config.get(desc)
255
 
        c = command.InitialConfigCommand(self, data)
256
 
        c.send()
257
 
        config.add_change_callback(self.updateConfig)
258
 
 
259
 
    def start_downloader_daemon(self):
260
 
        startDownloadDaemon(self.read_pid(), self.port)
261
 
 
262
 
    def updateConfig (self, key, value):
263
 
        if not self.shutdown:
264
 
            c = command.UpdateConfigCommand (self, key, value)
265
 
            c.send()
266
 
 
267
 
    def read_pid(self):
268
 
        short_app_name = config.get(prefs.SHORT_APP_NAME)
269
 
        return readPid(short_app_name)
270
 
            
271
 
    def handleClose(self, type):
272
 
        if not self.shutdown:
273
 
            logging.warning ("Downloader Daemon died")
274
 
            # FIXME: replace with code to recover here, but for now,
275
 
            # stop sending.
276
 
            self.shutdown = True
277
 
            config.remove_change_callback(self.updateConfig)
278
 
 
279
 
    def shutdown_timeout_cb(self):
280
 
        logging.warning ("killing download daemon")
281
 
        kill_process(self.read_pid())
282
 
        self.shutdownResponse()
283
 
 
284
 
    def shutdownResponse(self):
285
 
        if self.shutdown_callback:
286
 
            self.shutdown_callback()
287
 
        self.shutdown_timeout_dc.cancel()
288
 
 
289
 
    def shutdown_downloader_daemon(self, timeout=5, callback = None):
290
 
        """Send the downloader daemon the shutdown command.  If it doesn't
291
 
        reply before timeout expires, kill it.  (The reply is not sent until
292
 
        the downloader daemon has one remaining thread and that thread will
293
 
        immediately exit).
294
 
        """
295
 
        self.shutdown_callback = callback
296
 
        c = command.ShutDownCommand(self)
297
 
        c.send()
298
 
        self.shutdown = True
299
 
        config.remove_change_callback(self.updateConfig)
300
 
        self.shutdown_timeout_dc = eventloop.addTimeout(timeout, self.shutdown_timeout_cb, "Waiting for dl_daemon shutdown")