1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
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; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
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
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.
29
from miro.dl_daemon import command
34
from time import sleep
35
from struct import pack, unpack, calcsize
37
from miro import config
38
from miro import prefs
39
from miro import eventloop
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
47
SIZE_OF_INT = calcsize("I")
49
class DaemonError(Exception):
50
"""Exception while communicating to a daemon (either controller or
55
firstDaemonLaunch = '1'
56
def startDownloadDaemon(oldpid, port):
57
global firstDaemonLaunch
60
'DEMOCRACY_DOWNLOADER_PORT' : str(port),
61
'DEMOCRACY_DOWNLOADER_FIRST_LAUNCH' : firstDaemonLaunch,
62
'DEMOCRACY_SHORT_APP_NAME' : config.get(prefs.SHORT_APP_NAME),
64
launch_download_daemon(oldpid, daemonEnv)
65
firstDaemonLaunch = '0'
67
def getDataFile(short_app_name):
68
if hasattr(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']
76
# FIXME - can we do something better here on Windows
80
return os.path.join(tempfile.gettempdir(),
81
('%s_Download_Daemon_%s.txt' % (short_app_name, uid)))
84
def writePid(short_app_name, pid):
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.
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')
101
except (SystemExit, KeyboardInterrupt):
106
fcntl.lockf(pidfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
107
pidfile.write("%s\n" % pid)
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
113
# NOTE 2: we purposely don't close the file, to achieve locking on
116
def readPid(short_app_name):
118
f = open(getDataFile(short_app_name), "r")
123
return int(f.readline())
131
class Daemon(ConnectionHandler):
133
ConnectionHandler.__init__(self)
136
self.waitingCommands = {}
137
self.returnValues = {}
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
148
def onError(self, error):
149
"""Call this when an error occurs. It forces the
150
daemon to close its connection.
152
logging.warning ("socket error in daemon, closing my socket")
153
self.closeConnection()
156
def onConnection(self, socket):
157
self.changeState('ready')
158
for (comm, callback) in self.queuedCommands:
159
self.send(comm, callback)
160
self.queuedCommands = []
163
if self.buffer.length >= SIZE_OF_INT:
164
(self.size,) = unpack("I", self.buffer.read(SIZE_OF_INT))
165
self.changeState('command')
168
if self.buffer.length >= self.size:
170
comm = cPickle.loads(self.buffer.read(self.size))
171
except (SystemExit, KeyboardInterrupt):
174
logging.exception ("WARNING: error unpickling command.")
176
self.processCommand(comm)
177
self.changeState('ready')
179
def processCommand(self, comm):
180
trapcall.time_trap_call("Running: %s" % (comm,), self.runCommand, comm)
182
def runCommand(self, comm):
186
def send(self, comm, callback = None):
187
if self.state == 'initializing':
188
self.queuedCommands.append((comm, callback))
190
raw = cPickle.dumps(comm, cPickle.HIGHEST_PROTOCOL)
191
self.sendData(pack("I",len(raw)) + raw, callback)
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)
202
def handleError(self, obj, report):
203
command.DownloaderErrorCommand(self, report).send()
205
def handleClose(self, type):
210
logging.warning ("downloader: connection closed -- quitting")
211
from miro.dl_daemon import download
214
for thread in threading.enumerate():
215
if thread != threading.currentThread() and not thread.isDaemon():
218
class ControllerDaemon(Daemon):
220
Daemon.__init__(self)
221
self.stream.acceptConnection('127.0.0.1', 0, self.onConnection, self.onError)
222
self.port = self.stream.port
224
remoteConfigItems = [prefs.LIMIT_UPSTREAM,
225
prefs.UPSTREAM_LIMIT_IN_KBS,
226
prefs.LIMIT_DOWNSTREAM_BT,
227
prefs.DOWNSTREAM_BT_LIMIT_IN_KBS,
232
prefs.MOVIES_DIRECTORY,
233
prefs.PRESERVE_DISK_SPACE,
234
prefs.PRESERVE_X_GB_FREE,
235
prefs.SUPPORT_DIRECTORY,
236
prefs.SHORT_APP_NAME,
244
prefs.DOWNLOADER_LOG_PATHNAME,
246
prefs.GETTEXT_PATHNAME,
247
prefs.LIMIT_UPLOAD_RATIO,
249
prefs.LIMIT_CONNECTIONS_BT,
250
prefs.CONNECTION_LIMIT_BT_NUM,
253
for desc in remoteConfigItems:
254
data[desc.key] = config.get(desc)
255
c = command.InitialConfigCommand(self, data)
257
config.add_change_callback(self.updateConfig)
259
def start_downloader_daemon(self):
260
startDownloadDaemon(self.read_pid(), self.port)
262
def updateConfig (self, key, value):
263
if not self.shutdown:
264
c = command.UpdateConfigCommand (self, key, value)
268
short_app_name = config.get(prefs.SHORT_APP_NAME)
269
return readPid(short_app_name)
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,
277
config.remove_change_callback(self.updateConfig)
279
def shutdown_timeout_cb(self):
280
logging.warning ("killing download daemon")
281
kill_process(self.read_pid())
282
self.shutdownResponse()
284
def shutdownResponse(self):
285
if self.shutdown_callback:
286
self.shutdown_callback()
287
self.shutdown_timeout_dc.cancel()
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
295
self.shutdown_callback = callback
296
c = command.ShutDownCommand(self)
299
config.remove_change_callback(self.updateConfig)
300
self.shutdown_timeout_dc = eventloop.addTimeout(timeout, self.shutdown_timeout_cb, "Waiting for dl_daemon shutdown")