mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-24 02:04:04 +08:00
331 lines
15 KiB
Python
331 lines
15 KiB
Python
#!/usr/bin/env python
|
|
'''
|
|
Downloads files to temp locations.
|
|
'''
|
|
|
|
import os, sys, optparse, ConfigParser, socket, SocketServer, errno, Queue, threading, subprocess
|
|
|
|
import urllib2, tempfile
|
|
|
|
import time
|
|
|
|
sys.path.insert( 0, os.path.abspath( 'lib' ) )
|
|
from galaxy import eggs
|
|
import galaxy.model.mapping
|
|
from galaxy.util import json, bunch
|
|
|
|
eggs.require( 'python_daemon' )
|
|
from daemon import DaemonContext
|
|
|
|
class ArgHandler( object ):
|
|
"""
|
|
Collect command line flags.
|
|
"""
|
|
def __init__( self ):
|
|
self.parser = optparse.OptionParser()
|
|
self.parser.add_option( '-c', '--config', dest='config', help='Path to Galaxy config file (universe_wsgi.ini)', default='universe_wsgi.ini' )
|
|
self.parser.add_option( '-i', '--transfer-job-id', action='append', dest='transfer_job_ids', help='Initiate management of the specified TransferJob id' )
|
|
self.parser.add_option( '--do', dest='initiate_transfer_job_id', help='Used by this script when it calls itself to actually initiate the download' )
|
|
self.parser.add_option( '-s', '--state-transfer-job-id', action='append', dest='state_transfer_job_ids', help='Report the state of the specified TransferJob id' )
|
|
self.parser.add_option( '-d', '--debug', action='store_true', dest='debug', help="Debug (don't detach)" )
|
|
self.opts = None
|
|
def parse( self ):
|
|
self.opts, args = self.parser.parse_args()
|
|
|
|
class GalaxyApp( object ):
|
|
"""
|
|
A shell Galaxy App to provide access to the Galaxy configuration and
|
|
model/database.
|
|
"""
|
|
def __init__( self, config_file='universe_wsgi.ini' ):
|
|
self.config = ConfigParser.ConfigParser( dict( database_file = 'database/universe.sqlite',
|
|
file_path = 'database/files',
|
|
transfer_manager_port = '8163',
|
|
transfer_manager_log = 'transfer_manager.log' ) )
|
|
self.config.read( config_file )
|
|
self.model = None
|
|
@property
|
|
def sa_session( self ):
|
|
if not self.model:
|
|
default_dburl = 'sqlite:///%s?isolation_level=IMMEDIATE' % self.config.get( 'app:main', 'database_file' )
|
|
try:
|
|
dburl = self.config.get( 'app:main', 'database_connection' )
|
|
except ConfigParser.NoOptionError:
|
|
dburl = default_dburl
|
|
self.model = galaxy.model.mapping.init( self.config.get( 'app:main', 'file_path' ), dburl, create_tables = False )
|
|
return self.model.context.current
|
|
def get_transfer_job( self, id ):
|
|
return self.sa_session.query( self.model.TransferJob ).get( int( id ) )
|
|
|
|
class ListenerServer( SocketServer.ThreadingTCPServer ):
|
|
"""
|
|
The listener will accept state requests and new transfers for as long as
|
|
the manager is running.
|
|
"""
|
|
def __init__( self, server_address, RequestHandlerClass, transfer_manager ):
|
|
SocketServer.ThreadingTCPServer.__init__( self, server_address, RequestHandlerClass )
|
|
self.transfer_manager = transfer_manager
|
|
self.app = transfer_manager.app
|
|
|
|
class ListenerRequestHandler( SocketServer.BaseRequestHandler ):
|
|
"""
|
|
Handle state or transfer requests received on the socket.
|
|
"""
|
|
def handle( self ):
|
|
if not self.server.transfer_manager.accepting:
|
|
# TODO: does the submitter handle this condition? i'm sure it doesn't...
|
|
self.request.send( 'Manager shutting down\n' )
|
|
return
|
|
data = ''
|
|
while len( data ) < 8 * 1024 * 1024:
|
|
# read data up to 8MB (overkill, but be safe)
|
|
chunk = self.request.recv( 1024 )
|
|
if not chunk:
|
|
break
|
|
data += chunk
|
|
if '\n' in data:
|
|
break
|
|
else:
|
|
self.request.send( 'Message too large\n' )
|
|
return
|
|
data = json.from_json_string( data )
|
|
if 'transfer_job_ids' in data:
|
|
# Get all of the TransferJob objects and stick them on the queue.
|
|
print 'Adding transfer job ids to transfer queue: %s' % data['transfer_job_ids']
|
|
[ self.server.transfer_manager.transfer_queue.put( self.server.app.get_transfer_job( transfer_job_id ) ) for transfer_job_id in data['transfer_job_ids'] ]
|
|
self.request.send( "Added jobs to transfer queue: %s\n" % ', '.join( data['transfer_job_ids'] ) )
|
|
elif 'state_transfer_job_ids' in data:
|
|
print 'Servicing state request for transfer job ids: %s' % data['state_transfer_job_ids']
|
|
for state_transfer_job_id in data['state_transfer_job_ids']:
|
|
state = self.server.transfer_manager.get_state( int( state_transfer_job_id ) )
|
|
state['transfer_job_id'] = state_transfer_job_id
|
|
print 'State of transfer job id %s is: %s' % ( state_transfer_job_id, state )
|
|
self.request.send( json.to_json_string( state ) )
|
|
|
|
class Transfer( object ):
|
|
"""
|
|
Instantiated for each transfer to track the progress of the transfer via
|
|
commmunication with a subprocess.
|
|
"""
|
|
states = bunch.Bunch( NEW = 'new',
|
|
UNKNOWN = 'unknown',
|
|
STARTED = 'started',
|
|
PROGRESS = 'progress',
|
|
DONE = 'done',
|
|
ERROR = 'error' )
|
|
def __init__( self, transfer_job ):
|
|
self.transfer_job = transfer_job
|
|
self.state = dict( state = self.states.NEW )
|
|
self.done = False
|
|
def run( self ):
|
|
cmd = '%s -u %s --do %s' % ( sys.executable, os.path.abspath( __file__ ), self.transfer_job.id )
|
|
print cmd
|
|
self.p = subprocess.Popen( cmd, bufsize=0, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT )
|
|
line = self.p.stdout.readline()
|
|
while line:
|
|
try:
|
|
self.state = json.from_json_string( line )
|
|
assert 'state' in self.state
|
|
except:
|
|
print 'Received unknown state from transfer (transfer job id: %s): %s' % ( self.transfer_job.id, line )
|
|
self.state = dict( state = self.states.UNKNOWN, info=line )
|
|
line = self.p.stdout.readline()
|
|
self.p.wait()
|
|
self.done = True
|
|
|
|
class TransferManager( object ):
|
|
"""
|
|
Manage the queue of transfers, handle the setup of new transfers and
|
|
completion of finished transfers.
|
|
"""
|
|
def __init__( self, app, transfer_job_ids, debug=False ):
|
|
self.app = app
|
|
self.daemon_context = None
|
|
self.sa_session = app.sa_session
|
|
self.transfer_job_ids = transfer_job_ids
|
|
self.debug = debug
|
|
self.port = int( app.config.get( 'app:main', 'transfer_manager_port' ) )
|
|
self.log_file_name = app.config.get( 'app:main', 'transfer_manager_log' )
|
|
self.transfer_queue = Queue.Queue()
|
|
self.accepting = True
|
|
self.watchlist = []
|
|
self.listen_or_submit()
|
|
def listen_or_submit( self ):
|
|
"""
|
|
The transfer manager is invoked the same way every time, via the
|
|
command line, but if a transfer manager is already running, the new
|
|
manager will simply submit the request to the old manager and
|
|
terminate.
|
|
"""
|
|
try:
|
|
self.listener_server = ListenerServer( ( 'localhost', self.port ), ListenerRequestHandler, self )
|
|
except socket.error, e:
|
|
if e[0] == errno.EADDRINUSE:
|
|
self.submit()
|
|
sys.exit()
|
|
|
|
# Daemonize
|
|
if not self.debug:
|
|
log_file = open( self.log_file_name, 'a+' )
|
|
self.daemon_context = DaemonContext( files_preserve=[ self.listener_server.fileno() ],
|
|
working_directory=os.getcwd(),
|
|
stdout=log_file,
|
|
stderr=log_file )
|
|
self.daemon_context.open()
|
|
|
|
# Listen for more stuff on the socket
|
|
self.listener = threading.Thread( target=self.listener_server.serve_forever )
|
|
self.listener.start()
|
|
|
|
# Put all the URLs into the queue, additional URLs received before this
|
|
# instance of the manager terminates will also be handled.
|
|
print 'Started up with transfer job ids: %s' % self.transfer_job_ids
|
|
[ self.transfer_queue.put( app.get_transfer_job( tj_id ) ) for tj_id in self.transfer_job_ids ]
|
|
|
|
while True:
|
|
# TODO: max transfer limit
|
|
try:
|
|
transfer_job = self.transfer_queue.get_nowait()
|
|
print 'Fetching transfer job:', transfer_job.id, 'URL is:', transfer_job.params['url']
|
|
t = Transfer( transfer_job )
|
|
transfer_job.state = app.model.TransferJob.states.RUNNING
|
|
self.sa_session.add( transfer_job )
|
|
self.sa_session.flush()
|
|
tt = threading.Thread( target=t.run )
|
|
tt.start()
|
|
self.watchlist.append( t )
|
|
except Queue.Empty:
|
|
# TODO: this will shutdown the queue as soon the queue is
|
|
# empty, which is almost immediately. instead, we need to
|
|
# accept until all transfers are finished, then stop accepting,
|
|
# but finish out the queue in case one was added in the instant
|
|
# between empty and no longer accepting.
|
|
# (check the queue one more time after accepting = False)
|
|
if not self.watchlist:
|
|
self.acecpting = False
|
|
break
|
|
new_watchlist = []
|
|
for transfer in self.watchlist:
|
|
# TODO: handle failure
|
|
if transfer.done:
|
|
if transfer.state['state'] == transfer.states.DONE:
|
|
transfer.transfer_job.state = app.model.TransferJob.states.DONE
|
|
transfer.transfer_job.path = transfer.state['path']
|
|
print 'Transfer of job %s ended successfully, output path is: %s' % ( transfer.transfer_job.id, transfer.transfer_job.path )
|
|
elif transfer.state['state'] == transfer.states.ERROR:
|
|
transfer.transfer_job.info = transfer.state.get( 'info', None )
|
|
transfer.transfer_job.state = app.model.TransferJob.states.ERROR
|
|
print 'Transfer of job %s ended in error: %s' % ( transfer.transfer_job.id, transfer.transfer_job.info )
|
|
else:
|
|
print 'Unknown state received for transfer job %s: %s' % ( transfer.transfer_job.id, transfer.state['state'] )
|
|
transfer.transfer_job.info = 'Unknown error encountered in transfer manager'
|
|
transfer.transfer_job.state = app.model.TransferJob.states.ERROR
|
|
self.sa_session.add( transfer.transfer_job )
|
|
self.sa_session.flush()
|
|
else:
|
|
new_watchlist.append( transfer )
|
|
time.sleep( 1 )
|
|
self.watchlist = new_watchlist
|
|
#except Queue.Empty:
|
|
# self.accepting = False
|
|
self.listener_server.shutdown()
|
|
|
|
def get_state( self, transfer_job_id ):
|
|
rval = {}
|
|
for transfer in self.watchlist:
|
|
if transfer.transfer_job.id == transfer_job_id:
|
|
rval = transfer.state
|
|
break
|
|
else:
|
|
rval['state'] = Transfer.states.UNKNOWN # should be DONE?
|
|
return rval
|
|
|
|
def submit( self ):
|
|
# TODO: can fail if shutdown occurs between failure to bind and submission
|
|
# Needs error handling.
|
|
# This may not work at all right now.
|
|
print "Submitting jobs to running transfer manager: %s" % ', '.join( self.transfer_job_ids )
|
|
sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
|
|
sock.connect( ( 'localhost', self.port ) )
|
|
sock.send( json.to_json_string( dict( transfer_job_ids=self.transfer_job_ids ) ) + '\n' )
|
|
print sock.recv( 8192 ),
|
|
sock.close()
|
|
sys.exit()
|
|
|
|
def do_transfer( transfer_job ):
|
|
"""
|
|
Actually initiate a transfer (used when the transfer manager calls itself
|
|
with the '--do' flag).
|
|
"""
|
|
protocol = transfer_job.params['url'].split( '://' )[0]
|
|
if protocol in ( 'http', 'https' ):
|
|
return do_http_transfer( transfer_job )
|
|
else:
|
|
print >>sys.stderr, 'ERROR: Unsupported protocol: %s' % protocol
|
|
sys.exit( 1 )
|
|
|
|
def do_http_transfer( transfer_job ):
|
|
"""
|
|
"Plugin" for handling http(s) transfers.
|
|
"""
|
|
url = transfer_job.params['url']
|
|
try:
|
|
f = urllib2.urlopen( url )
|
|
except urllib2.URLError, e:
|
|
print json.to_json_string( dict( state=Transfer.states.ERROR,
|
|
info=str( e ) ) )
|
|
return
|
|
size = f.info().getheader( 'Content-Length' )
|
|
if size is not None:
|
|
size = int( size )
|
|
chunksize = 1024 * 1024
|
|
read = 0
|
|
last = 0
|
|
fh, fn = tempfile.mkstemp()
|
|
while True:
|
|
chunk = f.read( chunksize )
|
|
if not chunk:
|
|
break
|
|
os.write( fh, chunk )
|
|
if read == 0 and size is None:
|
|
print json.to_json_string( dict( state=Transfer.states.STARTED,
|
|
size=None ) ) #+ '\n'
|
|
elif read == 0:
|
|
print json.to_json_string( dict( state=Transfer.states.STARTED,
|
|
size=size ) ) #+ '\n'
|
|
read += chunksize
|
|
if size is not None and read < size:
|
|
percent = int( float( read ) / size * 100 )
|
|
if percent != last:
|
|
print json.to_json_string( dict( state=Transfer.states.PROGRESS,
|
|
read=read,
|
|
percent='%s%%' % percent ) ) #+ '\n'
|
|
last = percent
|
|
elif size is None:
|
|
print json.to_json_string( dict( state=Transfer.states.PROGRESS,
|
|
read=read ) )
|
|
os.close( fh )
|
|
print json.to_json_string( dict( state=Transfer.states.DONE, path=fn ) ) #+ '\n'
|
|
|
|
def request_state( state_transfer_job_ids, port ):
|
|
sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
|
|
sock.connect( ( 'localhost', port ) )
|
|
sock.send( json.to_json_string( dict( state_transfer_job_ids=state_transfer_job_ids ) ) + '\n' )
|
|
print sock.recv( 1024 ),
|
|
sock.close()
|
|
sys.exit()
|
|
|
|
if __name__ == '__main__':
|
|
arg_handler = ArgHandler()
|
|
arg_handler.parse()
|
|
app = GalaxyApp( config_file=arg_handler.opts.config )
|
|
if arg_handler.opts.initiate_transfer_job_id is not None:
|
|
do_transfer( app.get_transfer_job( arg_handler.opts.initiate_transfer_job_id ) )
|
|
elif arg_handler.opts.state_transfer_job_ids:
|
|
request_state( arg_handler.opts.state_transfer_job_ids, int( app.config.get( 'app:main', 'transfer_manager_port' ) ) )
|
|
elif arg_handler.opts.transfer_job_ids:
|
|
transfer_manager = TransferManager( app, arg_handler.opts.transfer_job_ids, arg_handler.opts.debug )
|
|
else:
|
|
arg_handler.parser.print_usage( sys.stderr )
|
|
sys.exit( 1 )
|