sebastien - in flumotion/branches/transcoder-1: . flumotion/admin flumotion/admin/command flumotion/common flumotion/component flumotion/component/misc/httpfile flumotion/component/producers flumotion/component/producers/playlist flumotion/extern flumotion/manager flumotion/test flumotion/twisted flumotion/wizard flumotion/worker

flumotion-commit at lists.fluendo.com flumotion-commit at lists.fluendo.com
Wed May 9 16:46:03 CEST 2007


Author: sebastien
Date: Wed May  9 16:45:51 2007
New Revision: 4911

Added:
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/Makefile.am
      - copied unchanged from r4904, flumotion/trunk/flumotion/component/producers/playlist/Makefile.am
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/__init__.py
      - copied unchanged from r4904, flumotion/trunk/flumotion/component/producers/playlist/__init__.py
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlist.py
      - copied, changed from r4904, flumotion/trunk/flumotion/component/producers/playlist/playlist.py
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlist.xml
      - copied, changed from r4904, flumotion/trunk/flumotion/component/producers/playlist/playlist.xml
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlistparser.py
      - copied, changed from r4904, flumotion/trunk/flumotion/component/producers/playlist/playlistparser.py
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/singledecodebin.py
      - copied unchanged from r4904, flumotion/trunk/flumotion/component/producers/playlist/singledecodebin.py
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/smartscale.py
      - copied unchanged from r4904, flumotion/trunk/flumotion/component/producers/playlist/smartscale.py
   flumotion/branches/transcoder-1/flumotion/test/test_component_httpserver.py
      - copied unchanged from r4904, flumotion/trunk/flumotion/test/test_component_httpserver.py
Modified:
   flumotion/branches/transcoder-1/ChangeLog
   flumotion/branches/transcoder-1/TODO
   flumotion/branches/transcoder-1/configure.ac
   flumotion/branches/transcoder-1/flumotion/admin/command/commands.py
   flumotion/branches/transcoder-1/flumotion/admin/multi.py
   flumotion/branches/transcoder-1/flumotion/common/common.py
   flumotion/branches/transcoder-1/flumotion/common/config.py
   flumotion/branches/transcoder-1/flumotion/component/feedcomponent.py
   flumotion/branches/transcoder-1/flumotion/component/feedcomponent010.py
   flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/file.py
   flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/httpfile.py
   flumotion/branches/transcoder-1/flumotion/component/producers/Makefile.am
   flumotion/branches/transcoder-1/flumotion/component/producers/playlist/   (props changed)
   flumotion/branches/transcoder-1/flumotion/extern/Makefile.am
   flumotion/branches/transcoder-1/flumotion/manager/manager.py
   flumotion/branches/transcoder-1/flumotion/test/Makefile.am
   flumotion/branches/transcoder-1/flumotion/test/test.xml
   flumotion/branches/transcoder-1/flumotion/test/test_component.py
   flumotion/branches/transcoder-1/flumotion/test/test_component_httpstreamer.py
   flumotion/branches/transcoder-1/flumotion/test/test_config.py
   flumotion/branches/transcoder-1/flumotion/test/test_misc_httpfile.py
   flumotion/branches/transcoder-1/flumotion/twisted/fdserver.py
   flumotion/branches/transcoder-1/flumotion/wizard/save.py
   flumotion/branches/transcoder-1/flumotion/wizard/steps.py
   flumotion/branches/transcoder-1/flumotion/worker/job.py
   flumotion/branches/transcoder-1/flumotion/worker/medium.py
   flumotion/branches/transcoder-1/flumotion/worker/worker.py
Log:
2007-05-09  Sebastien Merle  <sebastien at fluendo.com>

	* Merged flumotion trunk changesets up to [4910].

Modified: flumotion/branches/transcoder-1/ChangeLog
==============================================================================
--- flumotion/branches/transcoder-1/ChangeLog	(original)
+++ flumotion/branches/transcoder-1/ChangeLog	Wed May  9 16:45:51 2007
@@ -1,5 +1,9 @@
 2007-05-09  Sebastien Merle  <sebastien at fluendo.com>
 
+	* Merged flumotion trunk changesets up to [4910].
+
+2007-05-09  Sebastien Merle  <sebastien at fluendo.com>
+
 	* Merged flumotion trunk changesets from [4853] to [4867].
 
 2007-05-09  Sebastien Merle  <sebastien at fluendo.com>
@@ -34,8 +38,225 @@
 	be jellyed and unjellyed. Now an enum value can be directly 
 	used with spread without converting to int.
 
+2007-05-09  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/component/producers/playlist/playlist.py:
+	* flumotion/component/producers/playlist/playlist.xml:
+	* flumotion/component/producers/playlist/playlistparser.py:
+	  Allow audio-only or video-only compositions to be configured.
+
+2007-05-09  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/component/producers/playlist/playlistparser.py:
+	  Remove unused local to satisfy pychecker.
+
+2007-05-09  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/component/producers/playlist/playlistparser.py:
+	  Use the discoverer on files before adding them.
+
+2007-05-09  Zaheer Abbas Merali  <<zaheerabbas at merali dot org>>
+
+	* flumotion/common/config.py (FlumotionConfigXML.parseFeed,
+	  FlumotionConfigXML.addFeed):
+	Used table parser to parse subnodes of eater. This guarantees
+	that only feed nodes are children of the eater node.
+
+2007-05-09  Zaheer Abbas Merali  <<zaheerabbas at merali dot org>>
+
+	* flumotion/wizard/steps.py (Soundcard.update_inputs):
+	Commit forgotten when moving audio checks to audio.py.
+
+2007-05-09  Zaheer Abbas Merali  <<zaheerabbas at merali dot org>>
+
+	* flumotion/component/feedcomponent.py
+	  (MultiInputParseLaunchComponent.get_pipeline_string):
+	* flumotion/component/feedcomponent010.py (FeedComponent.init,
+	  FeedComponent.do_setup, FeedComponent.parseEaterConfig):
+	Use the eater key not the source key for building up the
+	eaters list. Build a mapping for feedId -> name of eater so
+	we can quickly look it up in elements needing to find the eater
+	name that a feedId (hence eater's element name) corresponds to.
+	* flumotion/test/test_component.py (PipelineTest.__init__,
+	  PipelineTest.config):
+	* flumotion/test/test_component_httpstreamer.py
+	  (TestOldProperties.setUp):
+	Fix up tests to use eater key not source key.
+	* flumotion/wizard/save.py (Component.toXML):
+	Output non-deprecated config form for wizard.
+
+2007-05-09  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/component/producers/playlist/Makefile.am:
+	  Add playlistparser.py to Makefile.am
+
+2007-05-09  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/admin/command/commands.py:
+	  Allow flumotion-command invoke arguments to include the contents of
+	  a file as a string argument using the 'F' format character.
+
+2007-05-09  Michael Smith <msmith at fluendo.com>
+
+	* flumotion/component/producers/playlist/playlist.py:
+	  Fix remote method to use proper variable.
+	  
+	* flumotion/component/producers/playlist/playlistparser.py:
+	  When truncating an existing gnlsource duration, also change the
+	  media-duration, to avoid unwanted speedups.
+
+2007-05-09  Thomas Vander Stichele  <thomas at apestaart dot org>
+
+	* flumotion/component/misc/httpfile/file.py (loadMimeTypes):
+	  Add .flv mime type in the code.
+
+2007-05-09  Zaheer Abbas Merali  <<zaheerabbas at merali dot org>>
+
+	* TODO:
+	* flumotion/common/config.py (FlumotionConfigXML._parseEaters):
+	Thanks Andy, using the helper method parseAttributes.
+
+2007-05-08  Zaheer Abbas Merali  <<zaheerabbas at merali dot org>>
+
+	* flumotion/test/test.xml:
+	Update xml flow used in tests to use new way of configuring
+	eaters.
+
+2007-05-08  Zaheer Abbas Merali  <<zaheerabbas at merali dot org>>
+
+	* flumotion/common/config.py (FlumotionConfigXML._parseComponent,
+	  FlumotionConfigXML._parseEaters, FlumotionConfigXML._parseSources):
+	* flumotion/test/test_config.py
+	  (TestConfig.testParseComponentsWithEaters,
+	  TestConfig.testParseComponentsWithEatersNotSpecified,
+	  TestConfig.testParseComponentsWithEatersDeprecatedWay,
+	  TestConfig.testParseComponentsWithTwoEaters,
+	  TestConfig.testParseComponentsWithTwoEatersDeprecatedWay,
+	  TestConfig.testParseComponentsWithMultipleEater,
+	  TestConfig.testParseComponentsWithMultipleEaterDeprecatedWay,
+	  TestConfig.testGetComponentEntriesWrong):
+	Add new way of configuring eaters in the XML along with tests.
+	Fixes #200.
+
+2007-05-08  Michael Smith  <msmith at fluendo.com>
+
+	* configure.ac:
+	* flumotion/component/feedcomponent.py:
+	* flumotion/component/feedcomponent010.py:
+	* flumotion/component/producers/Makefile.am:
+	* flumotion/component/producers/playlist/Makefile.am:
+	* flumotion/component/producers/playlist/__init__.py:
+	* flumotion/component/producers/playlist/playlist.py:
+	* flumotion/component/producers/playlist/playlist.xml:
+	* flumotion/component/producers/playlist/playlistparser.py:
+	* flumotion/component/producers/playlist/singledecodebin.py:
+	* flumotion/component/producers/playlist/smartscale.py:
+	  Merge playlist component back to trunk now that the basics are
+	  functional.
+
+2007-05-08  Thomas Vander Stichele  <thomas at apestaart dot org>
+
+	* flumotion/component/misc/httpfile/file.py (File, File.__init__,
+	  File.getChild, File.renderAuthenticated, MimedFileFactory,
+	  MimedFileFactory.__init__, MimedFileFactory.create, FLVFile,
+	  FLVFile.renderAuthenticated):
+	  Add a subclass for handling FLV files.
+	  Implement handling of the start= GET parameter, just like in
+	  Apache and lighthttpd.  Fixes #618.
+	  Add a MimedFileFactory that allows us to create even the root
+	  resource as a mime-type-dependent subclass.
+	* flumotion/component/misc/httpfile/httpfile.py (HTTPFileStreamer.init,
+	  HTTPFileStreamer.do_start):
+	* flumotion/test/test_misc_httpfile.py (TestDirectory,
+	  TestDirectory.setUp, TestDirectory.tearDown,
+	  TestDirectory.testGetChild, TestDirectory.testFLV,
+	  TestDirectory.finish, TestDirectory.testFLVStart,
+	  TestDirectory.finish, TestDirectory.testFLVStartZero,
+	  TestDirectory.finish):
+	  Add tests for this.
+
+2007-05-07  Thomas Vander Stichele  <thomas at apestaart dot org>
+
+	* flumotion/component/misc/httpfile/file.py (File.getChild):
+	* flumotion/component/misc/httpfile/httpfile.py
+	  (HTTPFileStreamer.do_setup, HTTPFileStreamer.do_start):
+	  We still need to distinguish between the root resource being the
+	  File resource directly, and a tree.  Apparently client.getPage()
+	  in the tests does not mimic correctly what something like wget would
+	  do, making it hard to test for the Root resource tree being set up
+	  correctly.
+
+2007-05-07  Thomas Vander Stichele  <thomas at apestaart dot org>
+
+	* flumotion/component/misc/httpfile/httpfile.py (HTTPFileStreamer.init,
+	  HTTPFileStreamer.getDescription, HTTPFileStreamer.do_setup,
+	  HTTPFileStreamer.do_stop, HTTPFileStreamer.do_start,
+	  HTTPFileStreamer.requestFinished, HTTPFileStreamer.getStreamData):
+	  Rearrange imports.
+	  Privatize some variables.
+	  Store some variables for resources we need to clean up.
+	  Set self.port from what we're actually listening to, so we can
+	  listen to port 0 then figure out which port it chose.
+	  Make sure we can work without any loggers.
+	  Fix handling of empty and / mount points, as well as getting
+	  empty or / resources - fixes #567
+	* flumotion/test/Makefile.am:
+	* flumotion/test/test_component_httpserver.py (MountTest,
+	  MountTest.setUp, MountTest.tearDown, MountTest.start,
+	  MountTest.getURL, MountTest.testDirMountEmpty,
+	  MountTest.testDirMountRoot, MountTest.testDirMountOnDemand,
+	  MountTest.testFileMountEmpty, MountTest.testFileMountOnDemand):
+	  Add tests for http-server component with various mount points
+	  and serving either a directory or a file.
+	* flumotion/component/misc/httpfile/file.py (File.getChild,
+	  File.renderAuthenticated):
+	  Restat the file on every request, to make sure we handle changed
+	  lengths of files.
+	  Handle requests ending with /
+
+2007-05-07  Andy Wingo  <wingo at pobox.com>
+
+	* flumotion/worker/medium.py (WorkerMedium.remote_killJob): New
+	remote method, kills a job by avatarId, defaulting to SIGKILL.
+	Callable via flumotion-command invoke sss workerCallRemote
+	WORKERNAME killJob AVATARID. Fixes #499.
+
+	* flumotion/worker/worker.py (WorkerBrain.killJob): New function,
+	proxies to JobHeaven.killJob.
+
+	* flumotion/worker/job.py (JobHeaven.killJob): New function, kills
+	a job by avatarId.
+	(JobHeaven.kill): Use killJob.
+
+	* flumotion/common/common.py (signalPid): New function.
+	(termPid, killPid, checkPidRunning): Use signalPid.
+
+	* flumotion/manager/manager.py (Vishnu.__init__): Remove a FIXME
+	made irrelevant by [3299].
+
+	* flumotion/twisted/fdserver.py (PassableServerPort.transport):
+	Remove the passable client things, they are in feed.py now.
+
+	* flumotion/extern/Makefile.am (clean-local): rm -rf _trial_temp,
+	not -r.
+
 2007-05-02  Andy Wingo  <wingo at pobox.com>
 
+	* flumotion/worker/worker.py (WorkerBrain.getFeedServerPort):
+	Whoops, fix the case when we don't listen with a feedserver.
+
+	* flumotion/admin/multi.py (MultiAdminModel.__init__): Use a
+	StartSet instead of our own ghetto homebrew.
+	(MultiAdminModel.addManager): API change: take a PBConnectionInfo.
+	Use the StartSet to arbitrate making only one connection per
+	managerId. Returns a deferred that will fire with the admin, or
+	error on error.
+	(MultiAdminModel.removeManager): API change: renamed from
+	close_admin. Uses the start set to cancel any connection, existing
+	or in progress.
+
+	* flumotion/common/errors.py (AlreadyConnectingError): New error.
+
 	* flumotion/admin/gtk/main.py (startAdminFromGreeter.failed): 
 	* flumotion/admin/gtk/dialogs.py (connection_failed_message): 
 	* flumotion/admin/gtk/client.py

Modified: flumotion/branches/transcoder-1/TODO
==============================================================================
--- flumotion/branches/transcoder-1/TODO	(original)
+++ flumotion/branches/transcoder-1/TODO	Wed May  9 16:45:51 2007
@@ -12,7 +12,7 @@
 * MEDIUM	add api for admins to "subscribe" to admin methods in the
 		manager, so only relevant commands get sent
 
-* EASY		deprecate <source> tag and require base= attribute to <comp>
+* EASY		require base= attribute to <comp>
 
 * EASY		remove all old deprecated bundling code everywhere
 

Modified: flumotion/branches/transcoder-1/configure.ac
==============================================================================
--- flumotion/branches/transcoder-1/configure.ac	(original)
+++ flumotion/branches/transcoder-1/configure.ac	Wed May  9 16:45:51 2007
@@ -195,6 +195,7 @@
 flumotion/component/producers/icecast/Makefile
 flumotion/component/producers/ivtv/Makefile
 flumotion/component/producers/pipeline/Makefile
+flumotion/component/producers/playlist/Makefile
 flumotion/component/producers/rtsp/Makefile
 flumotion/component/producers/soundcard/Makefile
 flumotion/component/producers/videotest/Makefile

Modified: flumotion/branches/transcoder-1/flumotion/admin/command/commands.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/admin/command/commands.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/admin/command/commands.py	Wed May  9 16:45:51 2007
@@ -169,11 +169,21 @@
     pass
 
 def _parse_typed_args(spec, args):
+    def _readFile(filename):
+        try:
+            f = open(filename)
+            contents = f.read()
+            f.close()
+            return contents
+        except:
+            raise ParseException("Failed to read file %s" % (filename,))
+            
     def _do_parse_typed_args(spec, args):
         accum = []
         while spec:
             argtype = spec.pop(0)
-            parsers = {'i': int, 's': str, 'b': common.strToBool}
+            parsers = {'i': int, 's': str, 'b': common.strToBool, 
+                'F': _readFile}
             if argtype == ')':
                 return tuple(accum)
             elif argtype == '(':

Modified: flumotion/branches/transcoder-1/flumotion/admin/multi.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/admin/multi.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/admin/multi.py	Wed May  9 16:45:51 2007
@@ -20,8 +20,10 @@
 # Headers in this file shall remain intact.
 
 
+from twisted.internet import defer
+
 from flumotion.twisted import pb as fpb
-from flumotion.common import log, planet, connection, errors
+from flumotion.common import log, planet, connection, errors, startset
 from flumotion.admin import admin
 
 
@@ -75,7 +77,9 @@
         self.admins = WatchedDict() # {managerId: AdminModel}
         # private
         self.listeners = []
-        self._pending = {}
+        self._startSet = startset.StartSet(self.admins.has_key,
+                                           errors.AlreadyConnectingError,
+                                           errors.AlreadyConnectedError)
 
     # Listener implementation
     def emit(self, signal_name, *args, **kwargs):
@@ -94,28 +98,9 @@
         assert not obj in self.listeners
         self.listeners.append(obj)
 
-    def _pushPendingConnection(self, admin):
-        assert admin.managerId not in self._pending
-        self._pending[admin.managerId] = admin
-
-    def _popPendingConnection(self, admin):
-        if admin.managerId in self._pending:
-            self._pending.pop(admin.managerId)
-        else:
-            self.warning('tried to pop nonpending manager %r; please '
-                         'report bug', admin.managerId)
-
-    # FIXME: should take a connectioninfo rather than separate args
-    # Public
-    def addManager(self, host, port, use_insecure, authenticator,
-                   tenacious=False):
+    def addManager(self, connectionInfo, tenacious=False):
         def connected_cb(admin):
-            planet = admin.planet
-            self.info('Connected to manager %s (planet %s)',
-                      admin.managerId, planet.get('name'))
-            self._popPendingConnection(admin)
-            self.admins[admin.managerId] = admin
-            self.emit('addPlanet', admin, planet)
+            self._startSet.avatarStarted(managerId)
 
         def disconnected_cb(admin):
             self.info('Disconnected from manager')
@@ -124,46 +109,90 @@
                 del self.admins[admin.managerId]
             else:
                 self.warning('Could not find admin model %r', admin)
+            if self._startSet.shutdownRegistered(managerId):
+                self._startSet.shutdownSuccess(managerId)
 
         def connection_refused_cb(admin):
+            msg = 'Connection to %s:%d refused.' % (i.host, i.port)
+            self.info('%s', msg)
             if not tenacious:
-                self._popPendingConnection(admin)
-            self.info('Connection to %s:%d refused.', host, port)
+                self._startSet.avatarStopped(managerId,
+                    errors.ConnectionRefusedError(msg))
 
         def connection_failed_cb(admin, string):
+            msg = 'Connection to %s:%d failed: %s' % (i.host, i.port,
+                                                      string)
+            self.info('%s', msg)
             if not tenacious:
-                self._popPendingConnection(admin)
-            self.info('Connection to %s:%d failed: %s', host, port,
-                      string)
+                self._startSet.avatarStopped(managerId,
+                    lambda _: errors.ConnectionFailedError(msg))
 
         def connection_error_cb(admin, obj):
+            msg = 'Error connecting to %s:%d: %r' % (i.host, i.port,
+                                                     obj)
+            self.warning('%s', msg)
             if not tenacious:
-                self._popPendingConnection(admin)
-            self.warning('Error connecting to %s:%d: %r', host, port, obj)
+                self._startSet.avatarStopped(managerId,
+                    lambda _: errors.ConnectionFailedError(msg))
 
-        info = connection.PBConnectionInfo(host, port, not use_insecure,
-                                           authenticator)
+        i = connectionInfo
+        managerId = str(i)
 
-        # a bit of a hack: we know that str(info) is the managerId.
-        if str(info) in self.admins or str(info) in self._pending:
-            raise errors.AlreadyConnectedError('Already connected to %s'
-                                               % info)
+        # can raise errors.AlreadyConnectingError or
+        # errors.AlreadyConnectedError
+        try:
+            d = self._startSet.createStart(managerId)
+        except Exception, e:
+            return defer.fail(e)
 
         a = admin.AdminModel()
-        a.connectToManager(info, tenacious)
-        self._pushPendingConnection(a)
+        a.connectToManager(i, tenacious)
+        assert a.managerId == managerId
+
         a.connect('connected', connected_cb)
         a.connect('disconnected', disconnected_cb)
         a.connect('connection-refused', connection_refused_cb)
         a.connect('connection-failed', connection_failed_cb)
         a.connect('connection-error', connection_error_cb)
-        return a
 
-    def close_admin(self, admin):
-        if admin.managerId not in self.admins:
-            self.debug('removing admin %s from pending', admin.managerId)
-            self._popPendingConnection(admin)
-        admin.shutdown()
+        # the admin should offer a decent deferred-connect interface;
+        # instead here we conflate the startset and the
+        # signal->deferred adaptations in one function
+
+        def emit_add_planet(_):
+            planet = a.planet
+            self.info('Connected to manager %s (planet %s)',
+                      a.managerId, planet.get('name'))
+            self.admins[a.managerId] = a
+            self.emit('addPlanet', a, planet)
+            return a
+
+        def disconnect_on_error(failure):
+            a.shutdown()
+            return failure
+
+        d.addCallbacks(emit_add_planet, disconnect_on_error)
+
+        return d
+
+    def removeManager(self, managerId):
+        self.info('disconnecting from %s', managerId)
+        if managerId in self.admins:
+            self.admins[managerId].shutdown()
+            return self._startSet.shutdownStart(managerId)
+        elif self._startSet.createRegistered(managerId):
+            # this admin has not yet connected; let us assume that in
+            # this window, it will not connect. Firing this makes the
+            # admin shutdown, see disconnect_on_error above.
+            self._startSet.shutdownSuccess(admin.managerId)
+            return defer.succeed(managerId)
+        elif self._startSet.shutdownRegistered(managerId):
+            # some caller is overzealous?
+            return self._startSet.shutdownStart(managerId)
+        else:
+            self.warning('told to remove an unknown manager: %s',
+                         managerId)
+            return defer.succeed(managerId)
 
     def for_each_component(self, object, proc):
         '''Call a procedure on each component that is a child of OBJECT'''

Modified: flumotion/branches/transcoder-1/flumotion/common/common.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/common/common.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/common/common.py	Wed May  9 16:45:51 2007
@@ -393,14 +393,14 @@
  
     return int(pid)
 
-def termPid(pid):
+def signalPid(pid, signum):
     """
-    Send the given process a TERM signal.
+    Send the given process a signal.
 
     @returns: whether or not the process with the given pid was running
     """
     try:
-        os.kill(pid, signal.SIGTERM)
+        os.kill(pid, signum)
         return True
     except OSError, e:
         if not e.errno == errno.ESRCH:
@@ -408,20 +408,21 @@
             raise
         return False
 
+def termPid(pid):
+    """
+    Send the given process a TERM signal.
+
+    @returns: whether or not the process with the given pid was running
+    """
+    return signalPid(pid, signal.SIGTERM)
+
 def killPid(pid):
     """
     Send the given process a KILL signal.
 
     @returns: whether or not the process with the given pid was running
     """
-    try:
-        os.kill(pid, signal.SIGKILL)
-        return True
-    except OSError, e:
-        if not e.errno == errno.ESRCH:
-        # FIXME: unhandled error, maybe give some better info ?
-            raise
-        return False
+    return signalPid(pid, signal.SIGKILL)
 
 def checkPidRunning(pid):
     """
@@ -429,13 +430,7 @@
     
     @returns: whether or not a process with that pid is active.
     """
-    try:
-        os.kill(pid, 0)
-        return True
-    except OSError, e:
-        if e.errno is not errno.ESRCH:
-            raise
-    return False
+    return signalPid(pid, 0)
  
 def waitPidFile(type, name=None):
     """

Modified: flumotion/branches/transcoder-1/flumotion/common/config.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/common/config.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/common/config.py	Wed May  9 16:45:51 2007
@@ -62,7 +62,7 @@
 
 def _buildComponentConfig(parentName, componentType, componentName, 
                           componentProperties, componentPlugs=None,
-                          sources=None, isClockMaster=None, 
+                          eaters=None, isClockMaster=None, 
                           flumotionVersion=None):
     """
     Build a componenet configuration dictionary.
@@ -91,25 +91,29 @@
     # let the component know what its feeds should be called
     config['feed'] = defs.getFeeders()
 
-    eaters = dict([(x.getName(), x) for x in defs.getEaters()])
-    # at this point we don't support assigning certain sources to
-    # certain eaters -- a problem to fix later. for now take the
-    # union of the properties.
-    required = [x for x in eaters.values() if x.getRequired()]
-    multiple = [x for x in eaters.values() if x.getMultiple()]
-
-    if sources == None:
-        sources = list()
-    if len(sources) == 0 and required:
-        raise ConfigError("Component %s wants to eat on %s, but no "
-                          "source specified"
-                          % (componentName, eaters.keys()[0]))
-    elif len(sources) > 1 and not multiple:
-        raise ConfigError("Component %s does not support multiple "
-                          "sources feeding %s (%r)"
-                          % (componentName, eaters.keys()[0], sources))
-    if sources:
-        config['source'] = sources
+    eatersDef = dict([(x.getName(), x) for x in defs.getEaters()])
+    
+    if eaters == None:
+        eaters = dict()
+        
+    for e in eatersDef:
+        if eatersDef[e].getRequired() and not (e in eaters):
+            raise ConfigError("Component %s wants to eat on %s, but no "
+                              "eater with name: %s specified." % (
+                              componentName, e, e))
+        if not eatersDef[e].getMultiple() and (e in eaters):
+            if len(eaters[e]) > 1:
+                raise ConfigError("Component %s does not support multiple "
+                    "sources feeding %s (%r)"
+                    % (componenetName, e, eaters[e]))
+        
+    if eaters:
+        config['eater'] = eaters
+        sources = []
+        for e in eaters:
+            sources.extend(eaters[e])
+        if sources:
+            config['source'] = sources
 
     sockets = defs.getSockets()
     if componentPlugs == None:
@@ -118,11 +122,11 @@
     for socket in sockets:
         plugs[socket] = []
     for plug in componentPlugs:
-        if not plug.socket in sockets:
+        if not plug['socket'] in sockets:
             raise ConfigError("Component %s does not support "
                               "sockets of type %s" 
                               % (componentName, plug['socket']))
-        plugs[plug.socket].append(plug)
+        plugs[plug['socket']].append(plug)
     config['plugs'] = plugs
 
     properties = dict()
@@ -135,8 +139,10 @@
             raise ConfigError("%s: unknown type '%s' for property %s"
                               % (componentName, definition.type, name))
         checker = _property_checkers.get(definition.type)
-        if value and (isinstance(value, tuple) or isinstance(value, list)):
-            if not definition.multiple and len(nodes) > 1:
+        #Don't check for tuple because fraction are tuple but are values
+        #if value and (isinstance(value, tuple) or isinstance(value, list)):
+        if value and isinstance(value, list):
+            if not definition.multiple and len(value) > 1:
                 raise ConfigError("%s: multiple value specified but not "
                                   "allowed for property %s" 
                                   % (componentName, name))
@@ -161,11 +167,11 @@
 
 def buildComponentConfig(parentName, componentType, componentName, 
                          componentProperties, componentPlugs=None,
-                         workerName=None, sources=None, isClockMaster=None,
+                         workerName=None, eaters=None, isClockMaster=None,
                          flumotionVersion=None):
     config = _buildComponentConfig(parentName, componentType, componentName, 
                                    componentProperties, componentPlugs, 
-                                   sources, isClockMaster, flumotionVersion)
+                                   eaters, isClockMaster, flumotionVersion)
     defs = registry.getRegistry().getComponent(componentType)
     return ConfigEntryComponent(componentName, parentName, componentType, 
                                 config, defs, workerName)
@@ -514,7 +520,7 @@
 
             ret[component.name] = component
         return ret
-
+     
     def _parseComponent(self, node, parent, forManager=False):
         """
         Parse a <component></component> block.
@@ -522,10 +528,13 @@
         @rtype: L{ConfigEntryComponent}
         """
         # <component name="..." type="..." worker="...">
-        #   <source>*
+        #   <source>...</source>* DEPRECATED
+        #   <eater name="...">
+        #     <feed>...</feed>
+        #   </eater>*
         #   <property name="name">value</property>*
         # </component>
-
+        
         if not node.hasAttribute('name'):
             raise ConfigError("<component> must have a name attribute")
         if not node.hasAttribute('type'):
@@ -565,9 +574,9 @@
         except KeyError:
             raise errors.UnknownComponentError(
                 "unknown component type: %s" % type)
-
+        
         possible_node_names = ['source', 'clock-master', 'property',
-                               'plugs']
+                               'plugs', 'eater']
         for subnode in node.childNodes:
             if subnode.nodeType == Node.COMMENT_NODE:
                 continue
@@ -579,10 +588,19 @@
                 raise ConfigError("Invalid subnode of <component>: %s"
                                   % subnode.nodeName)
 
-        sources = self._parseSources(node, defs)
+        eaters = self._parseEaters(node, defs)
+        if not eaters:
+            sources = self._parseSources(node, defs)
+            if sources:
+                # assign sources up to first (and only eater)
+                # if more than one eater, parseSources would have raised
+                # a ConfigError
+                eaters = {defs.getEaters()[0].getName():sources}
+
         isClockMaster = self._parseClockMaster(node)
+        
         plugs = []
-        plugParsers = {'plug': (self.parsePlug, plugs.append)}
+        plugParser = {'plug': (self.parsePlug, plugs.append)}
         for subnode in node.childNodes:
             if subnode.nodeName == 'plugs':
                 self.parseFromTable(subnode, plugParser)
@@ -591,7 +609,7 @@
                           lambda msg: ConfigError('%s: %s' % (name, str)))
 
         config = _buildComponentConfig(parent, type, name, properties, plugs, 
-                                       sources, isClockMaster, version)
+                                       eaters, isClockMaster, version)
 
         # fixme: all of the information except the worker is in the
         # config dict: why?
@@ -740,19 +758,65 @@
             raise ConfigError("<%s> value not specified" % name)
         return value
 
+    def _parseEaters(self, node, defs):
+        # <eater name="eater-name">
+        #   <feed>feeding-component:feed-name</feed>*
+        # </eater>
+        eaters = dict([(x.getName(), x) for x in defs.getEaters()])
+
+        nodes = {}
+        hasSourceNodes = False
+        for subnode in node.childNodes:
+            if subnode.nodeName == 'eater':
+                name, = self.parseAttributes(subnode, ('name',))
+                if nodes.has_key(name):
+                    raise ConfigError("Component %s should not have "
+                        "multiple eater nodes configured with same name:"
+                        " %s" % (node.nodeName, name))
+                feedNodes = []
+                def parseFeed(feedNode):
+                    # <feed>feeding-component:feed-name</feed>
+                    return self.get_string_values([feedNode])
+                def addFeed(feedNode):
+                    feedNodes.extend(feedNode)
+                self.parseFromTable(subnode, {'feed': (parseFeed, addFeed)})
+                nodes[name] = feedNodes
+            elif subnode.nodeName == 'source':
+                hasSourceNodes = True
+
+        # for backwards compatibility
+        if len(nodes) == 0 and hasSourceNodes:
+            return nodes
+
+        for e in eaters:
+            if eaters[e].getRequired() and not nodes.has_key(e):
+                raise ConfigError("Component %s wants to eat on %s, but no "
+                                  "eater with name: %s specified." % (
+                                  node.nodeName, e, e))
+            if not eaters[e].getMultiple() and nodes.has_key(e):
+                if len(nodes[e]) > 1:
+                    raise ConfigError("Component %s does not support multiple "
+                        "sources feeding %s (%r)"
+                        % (node.nodeName, e, nodes[e]))
+        return nodes
+
     def _parseSources(self, node, defs):
+        # deprecated in favour of eater tag
         # <source>feeding-component:feed-name</source>
         eaters = dict([(x.getName(), x) for x in defs.getEaters()])
 
+        if len(eaters) > 1:
+            raise ConfigError("Component %s has many eater names specified "
+                "in the registry, the <source> tag cannot be used for this "
+                "and is now deprecated. Use the <eater> tag." % (node.nodeName))
         nodes = []
         for subnode in node.childNodes:
             if subnode.nodeName == 'source':
                 nodes.append(subnode)
         strings = self.get_string_values(nodes)
 
-        # at this point we don't support assigning certain sources to
-        # certain eaters -- a problem to fix later. for now take the
-        # union of the properties.
+        # assigning certain sources to certain eaters can be done
+        # with the <eater> tag, the <source> tag is now deprecated
         required = [x for x in eaters.values() if x.getRequired()]
         multiple = [x for x in eaters.values() if x.getMultiple()]
 
@@ -760,11 +824,13 @@
             raise ConfigError("Component %s wants to eat on %s, but no "
                               "source specified"
                               % (node.nodeName, eaters.keys()[0]))
-        elif len(strings) > 1 and not multiple:
+        if len(strings) > 1 and not multiple:
             raise ConfigError("Component %s does not support multiple "
                               "sources feeding %s (%r)"
                               % (node.nodeName, eaters.keys()[0], strings))
-
+        if len(strings) > 0:
+            self.warning("The <source> tag is deprecated. Please use the "
+                "<eater> tag now")
         return strings
             
     def _parseClockMaster(self, node):

Modified: flumotion/branches/transcoder-1/flumotion/component/feedcomponent.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/component/feedcomponent.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/feedcomponent.py	Wed May  9 16:45:51 2007
@@ -275,17 +275,6 @@
 
         try:
             pipeline = gst.parse_launch(self.pipeline_string)
-
-            # Connect to the client-fd-removed signals on each feeder, so we 
-            # can clean up properly on removal.
-            feeder_element_names = map(lambda n: "feeder:" + n, 
-                self.feeder_names)
-            for feeder in feeder_element_names:
-                element = pipeline.get_by_name(feeder)
-                element.connect('client-fd-removed', self.removeClientCallback)
-                self.debug("Connected %s to removeClientCallback", feeder)
-
-            return pipeline
         except gobject.GError, e:
             self.warning('Could not parse pipeline: %s' % e.message)
             m = messages.Error(T_(N_(
@@ -294,6 +283,10 @@
             self.state.append('messages', m)
             raise errors.PipelineParseError(e.message)
 
+        self.connect_feeders(pipeline)
+
+        return pipeline
+
     def set_pipeline(self, pipeline):
         FeedComponent.set_pipeline(self, pipeline)
         self.configure_pipeline(self.pipeline, self.config['properties'])
@@ -556,12 +549,13 @@
             self.QUEUE_SIZE_BUFFERS)
 
     def get_pipeline_string(self, properties):
-        sources = self.config['source']
+        eaters = self.config['eater']
 
         pipeline = self.get_muxer_string(properties) + ' '
-        for eater in sources:
-            tmpl = '@ eater:%s @ ! muxer. '
-            pipeline += tmpl % eater
+        for e in eaters:
+            for feed in eaters[e]:
+                tmpl = '@ eater:%s @ ! muxer. '
+                pipeline += tmpl % feed
 
         pipeline += 'muxer.'
 

Modified: flumotion/branches/transcoder-1/flumotion/component/feedcomponent010.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/component/feedcomponent010.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/feedcomponent010.py	Wed May  9 16:45:51 2007
@@ -368,7 +368,6 @@
         # add extra keys to state
         self.state.addKey('eaterNames') # feedId of eaters
         self.state.addKey('feederNames') # feedId of feeders
-
         # add keys for eaters and feeders uiState
         self._feeders = {} # feeder feedId -> Feeder
         self._eaters = {} # eater feedId -> Eater
@@ -401,6 +400,8 @@
         self._stateChangeDeferreds = {}
 
         self._gotFirstNewSegment = {}
+        # feedId of eater -> eater name as specified in config
+        self._eaterMapping = {}
 
         # multifdsink's get-stats signal had critical bugs before this version
         tcppluginversion = gstreamer.get_plugin_version('tcp')
@@ -428,7 +429,7 @@
         Invokes the L{create_pipeline} and L{set_pipeline} vmethods,
         which subclasses can provide.
         """
-        eater_config = self.config.get('source', [])
+        eater_config = self.config.get('eater', {})
         feeder_config = self.config.get('feed', [])
 
         self.debug("FeedComponent.do_setup(): eater_config %r" % eater_config)
@@ -558,16 +559,20 @@
 
     def parseEaterConfig(self, eater_config):
         # the source feeder names come from the config
-        # they are specified under <component> as <source> elements in XML
+        # they are specified under <eater> as <feed> elements in XML
         # so if they don't specify a feed name, use "default" as the feed name
-        eater_names = []
-        for block in eater_config:
-            eater_name = block
-            if block.find(':') == -1:
-                eater_name = block + ':default'
-            eater_names.append(eater_name)
-        self.debug('parsed eater config, eater feedIds %r' % eater_names)
-        self.eater_names = eater_names
+        # there is also a deprecated way by specifying them under <component>
+        # as <source> elements in XML
+        feed_ids = []
+        for eater in eater_config:
+            for feed in eater_config[eater]:
+                feed_id = feed
+                if feed.find(':') == -1:
+                    feed_id = feed + ':default'
+                feed_ids.append(feed_id)
+                self._eaterMapping[feed_id] = eater
+        self.debug('parsed eater config, eater feedIds %r' % feed_ids)
+        self.eater_names = feed_ids
         self.state.set('eaterNames', self.eater_names)
             
     def parseFeederConfig(self, feeder_config):
@@ -584,6 +589,16 @@
         self.debug('parsed feeder config, feeders %r' % self.feeder_names)
         self.state.set('feederNames', self.feeder_names)
 
+    def connect_feeders(self, pipeline):
+        # Connect to the client-fd-removed signals on each feeder, so we 
+        # can clean up properly on removal.
+        feeder_element_names = map(lambda n: "feeder:" + n, 
+            self.feeder_names)
+        for feeder in feeder_element_names:
+            element = pipeline.get_by_name(feeder)
+            element.connect('client-fd-removed', self.removeClientCallback)
+            self.debug("Connected %s to removeClientCallback", feeder)
+
     def get_eater_names(self):
         """
         Return the list of feeder names this component eats from.

Modified: flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/file.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/file.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/file.py	Wed May  9 16:45:51 2007
@@ -28,24 +28,27 @@
 from flumotion.component.misc.porter import porterclient
 from flumotion.component.base import http as httpbase
 from twisted.web import resource, server, http
-from twisted.web import error as weberror
+from twisted.web import error as weberror, static
 from twisted.internet import defer, reactor, error, abstract
 from twisted.python import filepath
 from flumotion.twisted import fdserver
 from twisted.cred import credentials
 
-from twisted.web.static import loadMimeTypes, getTypeAndEncoding
+# add our own mime types to the ones parsed from /etc/mime.types
+def loadMimeTypes():
+    d = static.loadMimeTypes()
+    d['.flv'] = 'video/x-flv'
+    return d
 
 # this file is inspired by/adapted from twisted.web.static
 
 class File(resource.Resource, filepath.FilePath, log.Loggable):
     contentTypes = loadMimeTypes()
-
     defaultType = "application/octet-stream"
 
     childNotFound = weberror.NoResource("File not found.")
 
-    def __init__(self, path, component):
+    def __init__(self, path, component, mimeToResource=None):
         """
         @param component: L{flumotion.component.component.BaseComponent}
         """
@@ -53,8 +56,16 @@
         filepath.FilePath.__init__(self, path)
 
         self._component = component
+        # mapping of mime type -> File subclass
+        self._mimeToResource = mimeToResource or {}
+        self._factory = MimedFileFactory(component, self._mimeToResource)
 
     def getChild(self, path, request):
+        self.log('getChild: self %r, path %r', self, path)
+        # we handle a request ending in '/' as well; this is how those come in
+        if path == '':
+            return self
+
         self.restat()
 
         if not self.isdir():
@@ -68,7 +79,7 @@
         if not fpath.exists():
             return self.childNotFound
 
-        return self.createSimilarFile(fpath.path)
+        return self._factory.create(fpath.path)
 
     def openForReading(self):
         """Open a file and return it."""
@@ -92,7 +103,11 @@
 
         return server.NOT_DONE_YET
 
-    def renderAuthenticated(self, _, request):
+    def renderAuthenticated(self, _, request, first=0):
+        """
+        @type  first: int
+        @param first: starting byte to send from
+        """
         # Now that we're authenticated (or authentication wasn't requested), 
         # write the file (or appropriate other response) to the client.
         # We override static.File to implement Range requests, and to get access
@@ -101,6 +116,9 @@
         # self.restat()
         self.debug('renderAuthenticated request %r' % request)
 
+        # make sure we notice changes in the file
+        self.restat()
+
         ext = os.path.splitext(self.basename())[1].lower()
         type = self.contentTypes.get(ext, self.defaultType)
 
@@ -109,6 +127,7 @@
             return self.childNotFound.render(request)
 
         if self.isdir():
+            self.debug("%s is a directory, can't be GET", self.path)
             return self.childNotFound.render(request)
 
         # Different headers not normally set in static.File...        
@@ -137,7 +156,6 @@
 
         fileSize = self.getFileSize()
         # first and last byte offset we will write
-        first = 0
         last = fileSize - 1
 
         range = request.getHeader('range')
@@ -180,15 +198,16 @@
                 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
                 return ''
 
-            # Start sending from the requested position in the file
-            f.seek(first)
-
             # FIXME: is it still partial if the request was for the complete
             # file ? Couldn't find a conclusive answer in the spec.
             request.setResponseCode(http.PARTIAL_CONTENT)
             request.setHeader('Content-Range', "bytes %d-%d/%d" % 
                 (first, last, fileSize))
 
+        # Start sending from the requested position in the file
+        if first:
+            f.seek(first)
+
         request.setHeader("Content-Length", str(last - first + 1))
 
         if request.method == 'HEAD':
@@ -198,10 +217,49 @@
 
         return server.NOT_DONE_YET
 
-    def createSimilarFile(self, path):
-        self.debug("createSimilarFile at %r", path)
-        f = self.__class__(path, self._component)
-        return f
+class MimedFileFactory(log.Loggable):
+    """
+    I create File subclasses based on the mime type of the given path.
+    """
+    contentTypes = loadMimeTypes()
+    defaultType = "application/octet-stream"
+
+    def __init__(self, component, mimeToResource=None):
+        self._component = component
+        self._mimeToResource = mimeToResource or {}
+
+    def create(self, path):
+        """
+        Creates and returns an instance of a File subclass based on the mime
+        type/extension of the given path.
+        """
+
+        self.debug("createMimedFile at %r", path)
+        ext = os.path.splitext(path)[1].lower()
+        mimeType = self.contentTypes.get(ext, self.defaultType)
+        klazz = self._mimeToResource.get(mimeType, File)
+        self.debug("mimetype %s, class %r" % (mimeType, klazz))
+        return klazz(path, self._component, mimeToResource=self._mimeToResource)
+
+class FLVFile(File):
+    """
+    I am a File resource for FLV files.
+    I can handle requests with a 'start' GET parameter.
+    This parameter represents the byte offset from where to start.
+    If it is non-zero, I will output an FLV header so the result is
+    playable.
+    """
+    header = 'FLV\x01\x01\000\000\000\x09\000\000\000\x09'
+    def renderAuthenticated(self, _, request):
+        self.debug('rendering FLV')
+        first = 0
+        # each value is a list
+        start = int(request.args.get('start', ['0'])[0])
+        if start:
+            first = start
+            request.write(self.header)
+
+        return File.renderAuthenticated(self, _, request, first=first)
 
 class FileTransfer:
     """

Modified: flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/httpfile.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/httpfile.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/misc/httpfile/httpfile.py	Wed May  9 16:45:51 2007
@@ -1,4 +1,4 @@
-# -*- Mode: Python -*-
+# -*- Mode: Python; test-case-name: flumotion.test.test_component_httpserver -*-
 # vi:si:et:sw=4:sts=4:ts=4
 #
 # Flumotion - a streaming media server
@@ -22,17 +22,19 @@
 import time
 import string
 
+from twisted.web import resource, static, server, http
+from twisted.web import error as weberror
+from twisted.internet import defer, reactor, error
+from twisted.cred import credentials
+
 from flumotion.component import component
 from flumotion.common import log, messages, errors, netutils, interfaces
 from flumotion.component.component import moods
 from flumotion.component.misc.porter import porterclient
 from flumotion.component.base import http as httpbase
-from twisted.web import resource, static, server, http
-from twisted.web import error as weberror
-from twisted.internet import defer, reactor, error
+
 from flumotion.twisted import fdserver
 from flumotion.twisted.compat import implements
-from twisted.cred import credentials
 
 from flumotion.component.misc.httpfile import file
 
@@ -134,10 +136,10 @@
         self.type = None
         self.port = None
         self.hostname = None
-        self.loggers = []
-        self.logfilter = None
+        self._loggers = []
+        self._logfilter = None
 
-        self.description = 'On-Demand Flumotion Stream',
+        self._description = 'On-Demand Flumotion Stream',
 
         self._singleFile = False
         self._connected_clients = []
@@ -145,17 +147,27 @@
 
         self._pbclient = None
 
+        self._twistedPort = None
+        self._timeoutRequestsCallLater = None
+
+        # FIXME: maybe we want to allow the configuration to specify
+        # additional mime -> File class mapping ?
+        self._mimeToResource = {
+            'video/x-flv': file.FLVFile,
+        }
+
         # store number of connected clients
         self.uiState.addKey("connected-clients", 0)
         self.uiState.addKey("bytes-transferred", 0)
 
     def getDescription(self):
-        return self.description
+        return self._description
 
     def do_setup(self):
         props = self.config['properties']
 
-        mountPoint = props.get('mount-point', '')
+        # always make sure the mount point starts with /
+        mountPoint = props.get('mount-point', '/')
         if not mountPoint.startswith('/'):
             mountPoint = '/' + mountPoint
         self.mountPoint = mountPoint
@@ -171,8 +183,8 @@
             self._porterPath = props['porter-socket-path']
             self._porterUsername = props['porter-username']
             self._porterPassword = props['porter-password']
-        self.loggers = \
-            self.plugs['flumotion.component.plugs.loggers.Logger']
+        self._loggers = \
+            self.plugs.get('flumotion.component.plugs.loggers.Logger', [])
 
         if 'bouncer' in props:
             self.setBouncerName(props['bouncer'])
@@ -182,9 +194,15 @@
             filter = http.LogFilter()
             for f in props['ip-filter']:
                 filter.addIPFilter(f)
-            self.logfilter = filter
-        
+            self._logfilter = filter
+
     def do_stop(self):
+        if self._timeoutRequestsCallLater:
+            self._timeoutRequestsCallLater.cancel()
+            self._timeoutRequestsCallLater = None
+        if self._twistedPort:
+            self._twistedPort.stopListening()
+
         if self.type == 'slave' and self._pbclient:
             return self._pbclient.deregisterPath(self.mountPoint)
 
@@ -220,22 +238,33 @@
                 "Can't specify porter details in master mode")
 
     def do_start(self, *args, **kwargs):
-        #root = HTTPRoot()
-        root = resource.Resource()
-        # TwistedWeb wants the child path to not include the leading /
-        mount = self.mountPoint[1:]
-        # split path on / and add iteratively twisted.web resources
-        children = string.split(mount, '/')
-        current_resource = root
-        for child in children[:-1]:
-            res = resource.Resource()
-            current_resource.putChild(child, res)
-            current_resource = res
-        fileResource = file.File(self.filePath, self)
-        self.debug("Putting File resource at %r", children[-1:][0])
-        current_resource.putChild(children[-1:][0], fileResource)
+        self.debug('Starting with mount point "%s"' % self.mountPoint)
+        factory = file.MimedFileFactory(self,
+            mimeToResource=self._mimeToResource)
+        if self.mountPoint == '/':
+            self.debug('mount point / - create File resource as root')
+            # directly create a File resource for the path
+            root = factory.create(self.filePath)
+        else:
+            # split path on / and add iteratively twisted.web resources
+            # Asking for '' or '/' will retrieve the root Resource's '' child,
+            # so the split on / returning a first list value '' is correct
+            self.debug('mount point %s - creating root Resource and children',
+                self.mountPoint)
+            root = resource.Resource()
+            children = string.split(self.mountPoint[1:], '/')
+            parent = root
+            for child in children[:-1]:
+                res = resource.Resource()
+                self.debug("Putting Resource at %s", child)
+                parent.putChild(child, res)
+                parent = res
+            fileResource = factory.create(self.filePath)
+            self.debug("Putting resource %r at %r", fileResource, children[-1])
+            parent.putChild(children[-1], fileResource)
 
-        reactor.callLater(self.REQUEST_TIMEOUT, self._timeoutRequests)
+        self._timeoutRequestsCallLater = reactor.callLater(
+            self.REQUEST_TIMEOUT, self._timeoutRequests)
 
         d = defer.Deferred()
         if self.type == 'slave':
@@ -257,10 +286,14 @@
         else:
             # File Streamer is standalone.
             try:
-                self.debug('Listening on %s' % self.port)
+                self.debug('Going to listen on port %d' % self.port)
                 iface = ""
-                reactor.listenTCP(self.port, Site(root, self),
-                    interface=iface)
+                # we could be listening on port 0, in which case we need
+                # to figure out the actual port we listen on
+                self._twistedPort = reactor.listenTCP(self.port,
+                    Site(root, self), interface=iface)
+                self.port = self._twistedPort.getHost().port
+                self.debug('Listening on port %d' % self.port)
             except error.CannotListenError:
                 t = 'Port %d is not available.' % self.port
                 self.warning(t)
@@ -294,11 +327,6 @@
                     msg = 'slave mode, missing required property porter-%s' % k
                     return defer.fail(errors.ConfigError(msg))
 
-        if props.get('mount-point', None) is not None: 
-            if props['mount-point'] == '/':
-                return defer.fail(errors.ConfigError(
-                    "A mount-point of / is not supported in this release"))
-
             path = props.get('path', None) 
             if path is None: 
                 msg = "missing required property 'path'"
@@ -334,7 +362,7 @@
         headers = request.getAllHeaders()
 
         ip = request.getClientIP()
-        if not self.logfilter or not self.logfilter.isInRange(ip):
+        if not self._logfilter or not self._logfilter.isInRange(ip):
             args = {'ip': ip,
                     'time': time.gmtime(),
                     'method': request.method,
@@ -348,7 +376,7 @@
                     'user-agent': headers.get('user-agent', None),
                     'time-connected': timeConnected}
 
-            for logger in self.loggers:
+            for logger in self._loggers:
                 logger.event('http_session_completed', args)
 
         self._connected_clients.remove(request)
@@ -369,7 +397,7 @@
         else:
             return {
                 'protocol': 'HTTP',
-                'description': self.description,
+                'description': self._description,
                 'url' : self.getUrl()
                 }
 
@@ -403,7 +431,6 @@
         """
         Close the logfile, then reopen using the previous logfilename
         """
-        for logger in self.loggers:
+        for logger in self._loggers:
             self.debug('rotating logger %r' % logger)
             logger.rotate()
-

Modified: flumotion/branches/transcoder-1/flumotion/component/producers/Makefile.am
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/component/producers/Makefile.am	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/producers/Makefile.am	Wed May  9 16:45:51 2007
@@ -17,6 +17,7 @@
 	icecast \
 	ivtv \
 	pipeline \
+	playlist \
 	rtsp \
 	soundcard \
 	videotest \

Copied: flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlist.py (from r4904, flumotion/trunk/flumotion/component/producers/playlist/playlist.py)
==============================================================================
--- flumotion/trunk/flumotion/component/producers/playlist/playlist.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlist.py	Wed May  9 16:45:51 2007
@@ -91,6 +91,9 @@
         self.basetime = -1
         self.pipeline = None
 
+        self._hasAudio = True
+        self._hasVideo = True
+
         # The gnlcompositions for audio and video
         self.videocomp = None
         self.audiocomp = None
@@ -142,6 +145,10 @@
         pipeline = gst.Pipeline()
 
         for mediatype in ['audio', 'video']:
+            if (mediatype == 'audio' and not self._hasAudio) or (
+                mediatype == 'video' and not self._hasVideo):
+                continue
+
             composition = gst.element_factory_make("gnlcomposition", 
                 mediatype + "-composition")
 
@@ -183,13 +190,15 @@
         return pipeline
 
     def _createDefaultSources(self):
-        vsrc = videotest_gnl_src("videotestdefault", 0, 2**63 - 1, 
-            2**31 - 1)
-        self.videocomp.add(vsrc)
-
-        asrc = audiotest_gnl_src("videotestdefault", 0, 2**63 - 1, 
-            2**31 - 1)
-        self.audiocomp.add(asrc)
+        if self._hasVideo:
+            vsrc = videotest_gnl_src("videotestdefault", 0, 2**63 - 1, 
+                2**31 - 1)
+            self.videocomp.add(vsrc)
+
+        if self._hasAudio:
+            asrc = audiotest_gnl_src("videotestdefault", 0, 2**63 - 1, 
+                2**31 - 1)
+            self.audiocomp.add(asrc)
 
     def _setupClock(self, pipeline):
         # Configure our pipeline to use a known basetime and clock.
@@ -226,20 +235,20 @@
                 item.duration = item.duration + start
                 start = 0
 
-        if item.hasVideo:
+        if self._hasVideo and item.hasVideo:
             item.vsrc = file_gnl_src(None, item.uri, self.videocaps,
                 start, item.duration, item.offset, 0)
             self.videocomp.add(item.vsrc)
-        if item.hasAudio:
+        if self._hasAudio and item.hasAudio:
             item.asrc = file_gnl_src(None, item.uri, self.audiocaps,
                 start, item.duration, item.offset, 0)
             self.audiocomp.add(item.asrc)
 
     def unscheduleItem(self, item):
         self.debug("Unscheduling item at uri %s", item.uri)
-        if item.hasVideo:
+        if self._hasVideo and item.hasVideo:
             self.videocomp.remove(item.vsrc)
-        if item.hasAudio: 
+        if self._hasAudio and item.hasAudio: 
             self.audiocomp.remove(item.asrc)
 
     def addPlaylist(self, data):
@@ -256,6 +265,9 @@
         self._samplerate = props.get('samplerate', 44100)
         self._channels = props.get('channels', 2)
 
+        self._hasAudio = props.get('audio', True)
+        self._hasVideo = props.get('video', True)
+
         pipeline = self._buildPipeline() 
         self._setupClock(pipeline)
 

Copied: flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlist.xml (from r4904, flumotion/trunk/flumotion/component/producers/playlist/playlist.xml)
==============================================================================
--- flumotion/trunk/flumotion/component/producers/playlist/playlist.xml	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlist.xml	Wed May  9 16:45:51 2007
@@ -14,6 +14,11 @@
       <synchronization required="yes" clock-priority="110" />
 
       <properties>
+        <property name="audio" type="boolean"
+                  description="Output audio"/>
+        <property name="video" type="boolean"
+                  description="Output video"/>
+
         <property name="height" type="int"
                   description="Scaled output height of video" />
         <property name="width" type="int"

Copied: flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlistparser.py (from r4904, flumotion/trunk/flumotion/component/producers/playlist/playlistparser.py)
==============================================================================
--- flumotion/trunk/flumotion/component/producers/playlist/playlistparser.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/component/producers/playlist/playlistparser.py	Wed May  9 16:45:51 2007
@@ -20,11 +20,15 @@
 # Headers in this file shall remain intact.
 
 import gst
+from gst.extend import discoverer
+
 import time
 from StringIO import StringIO
 
 from xml.dom import Node
 
+from twisted.internet import reactor
+
 from flumotion.common import log, fxml
 
 import singledecodebin
@@ -68,12 +72,18 @@
 
         self.producer = producer
 
-    def addItem(self, timestamp, uri, offset, duration):
+        self._pending_items = []
+        self._discovering = False
+
+    def addItem(self, timestamp, uri, offset, duration, hasAudio, hasVideo):
         """
         Add an item to the playlist.
         The duration of previous and this entry may be adjusted to make it fit.
         """
         newitem = PlaylistItem(timestamp, uri, offset, duration)
+        newitem.hasAudio = hasAudio
+        newitem.hasVideo = hasVideo
+
         prev = next = None
         item = self.items
         while item:
@@ -108,10 +118,12 @@
         # Duration adjustments -> Reflect into gnonlin timeline
         if prev and prev.timestamp + prev.duration > newitem.timestamp:
             prev.duration = newitem.timestamp - prev.timestamp
-            prev.asrc.props.duration = prev.duration
-            prev.vsrc.props.duration = prev.duration
-            prev.asrc.props.media_duration = prev.duration
-            prev.vsrc.props.media_duration = prev.duration
+            if prev.asrc:
+                prev.asrc.props.duration = prev.duration
+                prev.asrc.props.media_duration = prev.duration
+            if prev.vsrc:
+                prev.vsrc.props.duration = prev.duration
+                prev.vsrc.props.media_duration = prev.duration
         if next and timestamp + newitem.duration > next.timestamp:
             newitem.duration = next.timestamp - newitem.timestamp
 
@@ -153,25 +165,74 @@
                 self.debug("Parsing entry")
                 self._parsePlaylistEntry(parser, child)
 
+        # Now launch the discoverer for any pending items
+        if not self._discovering:
+            self._discoverPending()
+
+    def _discoverPending(self):
+        def _discovered(disc, is_media):
+            self.debug("Discovered!")
+            reactor.callFromThread(_discoverer_done, disc, is_media)
+
+        def _discoverer_done(disc, is_media):
+            if is_media:
+                self.debug("Discovery complete, media found")
+                uri = "file://" + item[0]
+                timestamp = item[1]
+                duration = item[2]
+                offset = item[3]
+
+                hasA = disc.is_audio
+                hasV = disc.is_video
+                durationDiscovered = min(disc.audiolength, 
+                    disc.videolength)
+                if not duration or duration > durationDiscovered:
+                    duration = durationDiscovered
+
+                if duration + offset > durationDiscovered:
+                    offset = 0
+
+                if duration > 0:
+                    self.addItem(timestamp, uri, offset, duration, hasA, hasV)
+                else:
+                    self.warning("Duration of item is zero, not adding")
+            else:
+                self.warning("Discover failed to find media in %s", item[0])
+    
+            self.debug("Continuing on to next file")
+            self._discoverPending()
+
+        if not self._pending_items:
+            self.debug("No more files to discover")
+            self._discovering = False
+            return
+
+        self._discovering = True
+        
+        item = self._pending_items.pop(0)
+
+        self.debug("Discovering file %s", item[0])
+        disc = discoverer.Discoverer(item[0])
+
+        disc.connect('discovered', _discovered)
+        disc.discover()
+
     def _parsePlaylistEntry(self, parser, entry):
-        # TODO: Once we use the discoverer, we should move duration to optional
-        mandatory = ['filename', 'time', 'duration']
-        optional = ['offset']
+        mandatory = ['filename', 'time']
+        optional = ['duration', 'offset']
 
         (filename, timestamp, duration, offset) = parser.parseAttributes(
             entry, mandatory, optional)
 
-        duration = int(float(duration) * gst.SECOND)
+        if duration is not None:
+            duration = int(float(duration) * gst.SECOND)
         if offset is None:
             offset = 0
-        offset = int(offset)
+        offset = int(offset) * gst.SECOND
 
         timestamp = self._parseTimestamp(timestamp)
 
-        uri = 'file://'+filename
-
-        self.debug("Adding item")
-        self.addItem(timestamp, uri, offset, duration)
+        self._pending_items.append((filename, timestamp, duration, offset))
 
     def _parseTimestamp(self, ts):
         # Take TS in YYYY-MM-DDThh:mm:ssZ format, return timestamp in 

Modified: flumotion/branches/transcoder-1/flumotion/extern/Makefile.am
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/extern/Makefile.am	(original)
+++ flumotion/branches/transcoder-1/flumotion/extern/Makefile.am	Wed May  9 16:45:51 2007
@@ -24,7 +24,7 @@
 	PYTHONPATH=$(srcdir):$$PYTHONPATH trial log.test_log
 
 clean-local:
-	rm -r _trial_temp
+	rm -rf _trial_temp
 
 SUBDIRS = fdpass \
 	$(PTI_DIR)

Modified: flumotion/branches/transcoder-1/flumotion/manager/manager.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/manager/manager.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/manager/manager.py	Wed May  9 16:45:51 2007
@@ -259,7 +259,6 @@
         
         # create a portal so that I can be connected to, through our dispatcher
         # implementing the IRealm and a bouncer
-        # FIXME: decide if we allow anonymous login in this small (?) window
         self.portal = fportal.BouncerPortal(self.dispatcher, None)
         #unsafeTracebacks = 1 # for debugging tracebacks to clients
         self.factory = pb.PBServerFactory(self.portal,

Modified: flumotion/branches/transcoder-1/flumotion/test/Makefile.am
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/test/Makefile.am	(original)
+++ flumotion/branches/transcoder-1/flumotion/test/Makefile.am	Wed May  9 16:45:51 2007
@@ -29,6 +29,7 @@
 	test_component.py 		\
 	test_component_init.py 		\
 	test_component_feed.py		\
+	test_component_httpserver.py 	\
 	test_component_httpstreamer.py 	\
 	test_config.py 			\
 	test_credentials.py 		\

Modified: flumotion/branches/transcoder-1/flumotion/test/test.xml
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/test/test.xml	(original)
+++ flumotion/branches/transcoder-1/flumotion/test/test.xml	Wed May  9 16:45:51 2007
@@ -8,14 +8,18 @@
     </component>
   
     <component name="converter-ogg-theora" type="pipeline-converter" worker="worker">
-      <source>producer-video-test</source>
+      <eater name="default">
+        <feed>producer-video-test</feed>
+      </eater>
       <property name="pipeline">
   ffmpegcolorspace ! theoraenc keyframe-force=5 ! oggmux
       </property>
     </component>
   
     <component name="streamer-ogg-theora" type="http-streamer" worker="streamer">
-      <source>converter-ogg-theora</source>
+      <eater name="default">
+        <feed>converter-ogg-theora</feed>
+      </eater>
       <property name="port">8800</property>
       <plugs>
         <plug socket="flumotion.component.plugs.loggers.Logger"

Modified: flumotion/branches/transcoder-1/flumotion/test/test_component.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/test/test_component.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/test/test_component.py	Wed May  9 16:45:51 2007
@@ -37,14 +37,17 @@
 class PipelineTest(ParseLaunchComponent):
     def __init__(self, eaters=None, feeders=None, pipeline='test-pipeline'):
         self.__pipeline = pipeline
-        self._source = eaters or []
+        if eaters:
+            self._eater = {'default':eaters}
+        else:
+            self._eater = {}
         self._feed = feeders or []
 
         ParseLaunchComponent.__init__(self)
 
     def config(self):
         config = {'name': 'fake',
-                  'source': self._source,
+                  'eater': self._eater,
                   'feed': self._feed,
                   'plugs': {},
                   'properties': {}}

Modified: flumotion/branches/transcoder-1/flumotion/test/test_component_httpstreamer.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/test/test_component_httpstreamer.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/test/test_component_httpstreamer.py	Wed May  9 16:45:51 2007
@@ -44,7 +44,7 @@
             'feed': [],
             'name': 'http-video',
             'parent': 'default',
-            'source': ['muxer-video'],
+            'eater': {'default': ['muxer-video']},
             'avatarId': '/default/http-video',
             'clock-master': None,
             'plugs': {

Modified: flumotion/branches/transcoder-1/flumotion/test/test_config.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/test/test_config.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/test/test_config.py	Wed May  9 16:45:51 2007
@@ -51,6 +51,19 @@
     <component type="test-component-sync-provider">
       <synchronization required="true" clock-priority="130"/>
     </component>
+    <component type="test-component-with-feeder">
+      <feeder name="default" />
+    </component>
+    <component type="test-component-with-one-eater">
+      <eater name="default" required="true" />
+    </component>
+    <component type="test-component-with-two-eaters">
+      <eater name="video" required="true" />
+      <eater name="audio" required="true" />
+    </component>
+    <component type="test-component-with-multiple-eater">
+      <eater name="default" multiple="true" />
+    </component>
   </components>
   <plugs>
     <plug socket="foo.bar" type="frobulator">
@@ -493,6 +506,181 @@
         self.failUnless(entries.has_key('/atmosphere/atmocomp'))
         self.failUnless(entries.has_key('/default/flowcomp'))
 
+    def testParseComponentsWithEaters(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-one-eater"
+                           worker="foo">
+                  <eater name="default">
+                    <feed>prod:default</feed>
+                  </eater>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('default'))
+        self.failUnless(cons['eater']['default'] == ["prod:default"])
+        self.failUnless(cons['source'] == ["prod:default"])
+
+    def testParseComponentsWithEatersNotSpecified(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="cons" type="test-component-with-one-eater"
+                           worker="foo">
+                </component>
+              </flow>
+            </planet>
+            """)
+        self.assertRaises(config.ConfigError, conf.parse)
+
+    def testParseComponentsWithEatersDeprecatedWay(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-one-eater"
+                           worker="foo">
+                  <source>prod:default</source>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('source'))
+        self.failUnless(cons['source'] == ["prod:default"])
+        self.failUnless(cons['eater']['default'] == ["prod:default"])
+
+    def testParseComponentsWithTwoEaters(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-two-eaters"
+                           worker="foo">
+                  <eater name="video">
+                    <feed>prod:default</feed>
+                  </eater>
+                  <eater name="audio">
+                    <feed>prod2:default</feed>
+                  </eater>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('video'))
+        self.failUnless(cons['eater']['video'] == ["prod:default"])
+        self.failUnless(cons['eater'].has_key('audio'))
+        self.failUnless(cons['eater']['audio'] == ['prod2:default'])
+
+    def testParseComponentsWithTwoEatersDeprecatedWay(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-two-eaters"
+                           worker="foo">
+                  <source>prod:default</source>
+                  <source>prod2:default</source>
+                </component>
+              </flow>
+            </planet>
+            """)
+        self.assertRaises(config.ConfigError, conf.parse)
+
+    def testParseComponentsWithMultipleEater(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-multiple-eater"
+                           worker="foo">
+                  <eater name="default">
+                    <feed>prod:default</feed>
+                    <feed>prod2:default</feed>
+                  </eater>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('default'))
+        self.failUnless(cons['eater']['default'] == [
+            "prod:default", "prod2:default"])
+        self.failUnless(cons.has_key('source'))
+        self.failUnless(cons['source'] == [
+            "prod:default", "prod2:default"])
+
+    def testParseComponentsWithMultipleEaterDeprecatedWay(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-multiple-eater"
+                           worker="foo">
+                  <source>prod:default</source>
+                  <source>prod2:default</source>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('default'))
+        self.failUnless(cons['eater']['default'] == [
+            "prod:default", "prod2:default"])
+        self.failUnless(cons.has_key('source'))
+        self.failUnless(cons['source'] == [
+            "prod:default", "prod2:default"])
+
     def testGetComponentEntriesWrong(self):
         xml = """
              <planet>

Modified: flumotion/branches/transcoder-1/flumotion/test/test_misc_httpfile.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/test/test_misc_httpfile.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/test/test_misc_httpfile.py	Wed May  9 16:45:51 2007
@@ -168,7 +168,6 @@
             'a text file', 0, 10)
         return fr.finishDeferred
 
-
     def testRangeHead(self):
         fr = FakeRequest(method='HEAD', headers={'range': 'bytes=2-5'})
         self.assertEquals(self.resource.render(fr), server.NOT_DONE_YET)
@@ -176,4 +175,51 @@
             http.PARTIAL_CONTENT, '', 4)
         return fr.finishDeferred
 
+class TestDirectory(unittest.TestCase):
+    def setUp(self):
+        self.path = tempfile.mkdtemp()
+        h = open(os.path.join(self.path, 'test.flv'), 'w')
+        h.write('a fake FLV file')
+        h.close()
+        self.component = FakeComponent()
+        # a directory resource
+        self.resource = file.File(self.path, self.component,
+            { 'video/x-flv': file.FLVFile } )
+
+    def tearDown(self):
+        os.system('rm -r %s' % self.path)
+
+    def testGetChild(self):
+        fr = FakeRequest()
+        r = self.resource.getChild('test.flv', fr)
+        self.assertEquals(r.__class__, file.FLVFile)
+
+    def testFLV(self):
+        fr = FakeRequest()
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.data, 'a fake FLV file')
+        fr.finishDeferred.addCallback(finish)
+
+        return fr.finishDeferred
+
+    def testFLVStart(self):
+        fr = FakeRequest(args={'start': [2]})
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.data, file.FLVFile.header + 'fake FLV file')
+        fr.finishDeferred.addCallback(finish)
 
+        return fr.finishDeferred
+        
+    def testFLVStartZero(self):
+        fr = FakeRequest(args={'start': [0]})
+        self.assertEquals(self.resource.getChild('test.flv', fr).render(fr),
+            server.NOT_DONE_YET)
+        def finish(result):
+            self.assertEquals(fr.data, 'a fake FLV file')
+        fr.finishDeferred.addCallback(finish)
+
+        return fr.finishDeferred

Modified: flumotion/branches/transcoder-1/flumotion/twisted/fdserver.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/twisted/fdserver.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/twisted/fdserver.py	Wed May  9 16:45:51 2007
@@ -176,14 +176,3 @@
 class PassableServerPort(tcp.Port):
     transport = PassableServerConnection
 
-class PassableClientConnection(_SocketMaybeCloser, tcp.Client):
-    pass
-
-class PassableClientConnector(tcp.Connector):
-    # It is unfortunate, but it seems that either we override this
-    # private-ish method or reimplement BaseConnector.connect(). This is
-    # the path that tcp.py takes, so we take it too.
-    def _makeTransport(self):
-        return PassableClientConnection(self.host, self.port,
-                                        self.bindAddress, self,
-                                        self.reactor)

Modified: flumotion/branches/transcoder-1/flumotion/wizard/save.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/wizard/save.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/wizard/save.py	Wed May  9 16:45:51 2007
@@ -78,9 +78,10 @@
         s = '    <component name="%s" type="%s" ' \
             'project="flumotion" version="%s"%s>\n' % (
             self.name, self.type, configure.version, extra)
-
+        s += '     <eater name="default">\n'
         for sourceName in self.getFeeders():
-            s += "      <source>%s</source>\n" % sourceName
+            s += "      <feed>%s</feed>\n" % sourceName
+        s+= '      </eater>\n'
                     
         if self.props:
             s += "\n"

Modified: flumotion/branches/transcoder-1/flumotion/wizard/steps.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/wizard/steps.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/wizard/steps.py	Wed May  9 16:45:51 2007
@@ -769,7 +769,7 @@
         e = self.combobox_channels.get_enum()
         channels = 2
         if e: channels = e.intvalue
-        d = self.workerRun('flumotion.worker.checks.video', 'checkMixerTracks',
+        d = self.workerRun('flumotion.worker.checks.audio', 'checkMixerTracks',
                            enum.element, device, channels, id='soundcard-check')
         def soundcardCheckComplete((deviceName, tracks)):
             self.clear_msg('soundcard-check')

Modified: flumotion/branches/transcoder-1/flumotion/worker/job.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/worker/job.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/worker/job.py	Wed May  9 16:45:51 2007
@@ -25,6 +25,7 @@
 
 import os
 import sys
+import signal
 
 from twisted.cred import portal
 from twisted.internet import defer, reactor
@@ -456,11 +457,18 @@
         ret.addCallback(stopListening)
         return ret
 
-    def kill(self):
+    def kill(self, signum=signal.SIGKILL):
         self.warning("Killing all children immediately")
-        for jobInfo in self.getJobInfos():
-            self.debug("Sending SIGKILL to pid %d", jobInfo.pid)
-            common.killPid(jobInfo.pid)
+        for avatarId in self.getJobAvatarIds():
+            self.killJob(avatarId, signum)
+
+    def killJob(self, avatarId, signum):
+        if avatarId not in self._jobInfos:
+            raise errors.UnknownComponentError(avatarId)
+        jobInfo = self._jobInfos[avatarId]
+        self.debug("Sending signal %d to job %s at pid %d", signum,
+                   avatarId, jobInfo.pid)
+        common.signalPid(jobInfo.pid, signum)
 
 class JobAvatar(fpb.Avatar, log.Loggable):
     """

Modified: flumotion/branches/transcoder-1/flumotion/worker/medium.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/worker/medium.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/worker/medium.py	Wed May  9 16:45:51 2007
@@ -23,6 +23,8 @@
 worker-side objects to handle worker clients
 """
 
+import signal
+
 from twisted.internet import reactor
 import twisted.cred.error
 from twisted.internet import error
@@ -242,3 +244,21 @@
         @returns: a list of componentAvatarIds
         """
         return self.brain.getComponents()
+
+    def remote_killJob(self, avatarId, signum=signal.SIGKILL):
+        """Kill one of the worker's jobs.
+
+        This method is intended for exceptional purposes only; a normal
+        component shutdown is performed by the manager via calling
+        remote_stop() on the component avatar.
+
+        Raises L{flumotion.common.errors.UnknownComponentError} if the
+        job is unknown.
+
+        @param avatarId: the avatar Id of the component, e.g.
+        '/default/audio-encoder'
+        @type avatarId: string
+        @param signum: Signal to send, optional. Defaults to SIGKILL.
+        @type signum: int
+        """
+        self.brain.killJob(avatarId, signum)

Modified: flumotion/branches/transcoder-1/flumotion/worker/worker.py
==============================================================================
--- flumotion/branches/transcoder-1/flumotion/worker/worker.py	(original)
+++ flumotion/branches/transcoder-1/flumotion/worker/worker.py	Wed May  9 16:45:51 2007
@@ -223,7 +223,10 @@
         return self.ports
 
     def getFeedServerPort(self):
-        return self.feedServer.getPortNum()
+        if self.feedServer:
+            return self.feedServer.getPortNum()
+        else:
+            return None
 
     def create(self, avatarId, type, moduleName, methodName, nice=0):
         def getBundles():
@@ -278,3 +281,6 @@
 
     def getComponents(self):
         return self.jobHeaven.getJobAvatarIds()
+
+    def killJob(self, avatarId, signum):
+        self.jobHeaven.killJob(avatarId, signum)


More information about the flumotion-commit mailing list