1 | # -*- coding: utf-8 -*-
|
---|
2 | #
|
---|
3 | # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
|
---|
4 | # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
|
---|
5 | #
|
---|
6 | # This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
---|
7 | # the additional special exception to link portions of this program with the OpenSSL library.
|
---|
8 | # See LICENSE for more details.
|
---|
9 | #
|
---|
10 |
|
---|
11 | import logging
|
---|
12 | import subprocess
|
---|
13 | import sys
|
---|
14 |
|
---|
15 | from twisted.internet import defer, reactor, ssl
|
---|
16 | from twisted.internet.protocol import ClientFactory
|
---|
17 |
|
---|
18 | import deluge.common
|
---|
19 | from deluge import error
|
---|
20 | from deluge.transfer import DelugeTransferProtocol
|
---|
21 | from deluge.ui.common import get_localhost_auth
|
---|
22 |
|
---|
23 | RPC_RESPONSE = 1
|
---|
24 | RPC_ERROR = 2
|
---|
25 | RPC_EVENT = 3
|
---|
26 |
|
---|
27 | log = logging.getLogger(__name__)
|
---|
28 |
|
---|
29 |
|
---|
30 | def format_kwargs(kwargs):
|
---|
31 | return ', '.join([key + '=' + str(value) for key, value in kwargs.items()])
|
---|
32 |
|
---|
33 |
|
---|
34 | class DelugeRPCRequest(object):
|
---|
35 | """
|
---|
36 | This object is created whenever there is a RPCRequest to be sent to the
|
---|
37 | daemon. It is generally only used by the DaemonProxy's call method.
|
---|
38 | """
|
---|
39 |
|
---|
40 | request_id = None
|
---|
41 | method = None
|
---|
42 | args = None
|
---|
43 | kwargs = None
|
---|
44 |
|
---|
45 | def __repr__(self):
|
---|
46 | """
|
---|
47 | Returns a string of the RPCRequest in the following form:
|
---|
48 | method(arg, kwarg=foo, ...)
|
---|
49 | """
|
---|
50 | s = self.method + '('
|
---|
51 | if self.args:
|
---|
52 | s += ', '.join([str(x) for x in self.args])
|
---|
53 | if self.kwargs:
|
---|
54 | if self.args:
|
---|
55 | s += ', '
|
---|
56 | s += format_kwargs(self.kwargs)
|
---|
57 | s += ')'
|
---|
58 |
|
---|
59 | return s
|
---|
60 |
|
---|
61 | def format_message(self):
|
---|
62 | """
|
---|
63 | Returns a properly formatted RPCRequest based on the properties. Will
|
---|
64 | raise a TypeError if the properties haven't been set yet.
|
---|
65 |
|
---|
66 | :returns: a properly formated RPCRequest
|
---|
67 | """
|
---|
68 | if self.request_id is None or self.method is None or self.args is None or self.kwargs is None:
|
---|
69 | raise TypeError('You must set the properties of this object before calling format_message!')
|
---|
70 |
|
---|
71 | return (self.request_id, self.method, self.args, self.kwargs)
|
---|
72 |
|
---|
73 |
|
---|
74 | class DelugeRPCProtocol(DelugeTransferProtocol):
|
---|
75 |
|
---|
76 | def connectionMade(self): # NOQA
|
---|
77 | self.__rpc_requests = {}
|
---|
78 | # Set the protocol in the daemon so it can send data
|
---|
79 | self.factory.daemon.protocol = self
|
---|
80 | # Get the address of the daemon that we've connected to
|
---|
81 | peer = self.transport.getPeer()
|
---|
82 | self.factory.daemon.host = peer.host
|
---|
83 | self.factory.daemon.port = peer.port
|
---|
84 | self.factory.daemon.connected = True
|
---|
85 | log.debug('Connected to daemon at %s:%s..', peer.host, peer.port)
|
---|
86 | self.factory.daemon.connect_deferred.callback((peer.host, peer.port))
|
---|
87 |
|
---|
88 | def message_received(self, request):
|
---|
89 | """
|
---|
90 | This method is called whenever we receive a message from the daemon.
|
---|
91 |
|
---|
92 | :param request: a tuple that should be either a RPCResponse, RCPError or RPCSignal
|
---|
93 |
|
---|
94 | """
|
---|
95 | if not isinstance(request, tuple):
|
---|
96 | log.debug('Received invalid message: type is not tuple')
|
---|
97 | return
|
---|
98 | if len(request) < 3:
|
---|
99 | log.debug('Received invalid message: number of items in '
|
---|
100 | 'response is %s', len(request))
|
---|
101 | return
|
---|
102 |
|
---|
103 | message_type = request[0]
|
---|
104 |
|
---|
105 | if message_type == RPC_EVENT:
|
---|
106 | event = request[1]
|
---|
107 | # log.debug("Received RPCEvent: %s", event)
|
---|
108 | # A RPCEvent was received from the daemon so run any handlers
|
---|
109 | # associated with it.
|
---|
110 | if event in self.factory.event_handlers:
|
---|
111 | for handler in self.factory.event_handlers[event]:
|
---|
112 | reactor.callLater(0, handler, *request[2])
|
---|
113 | return
|
---|
114 |
|
---|
115 | request_id = request[1]
|
---|
116 |
|
---|
117 | # We get the Deferred object for this request_id to either run the
|
---|
118 | # callbacks or the errbacks dependent on the response from the daemon.
|
---|
119 | d = self.factory.daemon.pop_deferred(request_id)
|
---|
120 |
|
---|
121 | if message_type == RPC_RESPONSE:
|
---|
122 | # Run the callbacks registered with this Deferred object
|
---|
123 | d.callback(request[2])
|
---|
124 | elif message_type == RPC_ERROR:
|
---|
125 | # Recreate exception and errback'it
|
---|
126 | try:
|
---|
127 | # The exception class is located in deluge.error
|
---|
128 | try:
|
---|
129 | exception_cls = getattr(error, request[2])
|
---|
130 | exception = exception_cls(*request[3], **request[4])
|
---|
131 | except TypeError:
|
---|
132 | log.warn('Received invalid RPC_ERROR (Old daemon?): %s', request[2])
|
---|
133 | return
|
---|
134 |
|
---|
135 | # Ideally we would chain the deferreds instead of instance
|
---|
136 | # checking just to log them. But, that would mean that any
|
---|
137 | # errback on the fist deferred should returns it's failure
|
---|
138 | # so it could pass back to the 2nd deferred on the chain. But,
|
---|
139 | # that does not always happen.
|
---|
140 | # So, just do some instance checking and just log rpc error at
|
---|
141 | # diferent levels.
|
---|
142 | r = self.__rpc_requests[request_id]
|
---|
143 | msg = 'RPCError Message Received!'
|
---|
144 | msg += '\n' + '-' * 80
|
---|
145 | msg += '\n' + 'RPCRequest: ' + r.__repr__()
|
---|
146 | msg += '\n' + '-' * 80
|
---|
147 | if isinstance(exception, error.WrappedException):
|
---|
148 | msg += '\n' + exception.type + '\n' + exception.message + ': '
|
---|
149 | msg += exception.traceback
|
---|
150 | else:
|
---|
151 | msg += '\n' + request[5] + '\n' + request[2] + ': '
|
---|
152 | msg += str(exception)
|
---|
153 | msg += '\n' + '-' * 80
|
---|
154 |
|
---|
155 | if not isinstance(exception, error._ClientSideRecreateError):
|
---|
156 | # Let's log these as errors
|
---|
157 | log.error(msg)
|
---|
158 | else:
|
---|
159 | # The rest just get's logged in debug level, just to log
|
---|
160 | # what's happening
|
---|
161 | log.debug(msg)
|
---|
162 | except Exception:
|
---|
163 | import traceback
|
---|
164 | log.error('Failed to handle RPC_ERROR (Old daemon?): %s\nLocal error: %s',
|
---|
165 | request[2], traceback.format_exc())
|
---|
166 | d.errback(exception)
|
---|
167 | del self.__rpc_requests[request_id]
|
---|
168 |
|
---|
169 | def send_request(self, request):
|
---|
170 | """
|
---|
171 | Sends a RPCRequest to the server.
|
---|
172 |
|
---|
173 | :param request: RPCRequest
|
---|
174 |
|
---|
175 | """
|
---|
176 | try:
|
---|
177 | # Store the DelugeRPCRequest object just in case a RPCError is sent in
|
---|
178 | # response to this request. We use the extra information when printing
|
---|
179 | # out the error for debugging purposes.
|
---|
180 | self.__rpc_requests[request.request_id] = request
|
---|
181 | # log.debug("Sending RPCRequest %s: %s", request.request_id, request)
|
---|
182 | # Send the request in a tuple because multiple requests can be sent at once
|
---|
183 | self.transfer_message((request.format_message(),))
|
---|
184 | except Exception as ex:
|
---|
185 | log.warn('Error occurred when sending message: %s', ex)
|
---|
186 |
|
---|
187 |
|
---|
188 | class DelugeRPCClientFactory(ClientFactory):
|
---|
189 | protocol = DelugeRPCProtocol
|
---|
190 |
|
---|
191 | def __init__(self, daemon, event_handlers):
|
---|
192 | self.daemon = daemon
|
---|
193 | self.event_handlers = event_handlers
|
---|
194 |
|
---|
195 | def startedConnecting(self, connector): # NOQA
|
---|
196 | log.debug('Connecting to daemon at "%s:%s"...',
|
---|
197 | connector.host, connector.port)
|
---|
198 |
|
---|
199 | def clientConnectionFailed(self, connector, reason): # NOQA
|
---|
200 | log.debug('Connection to daemon at "%s:%s" failed: %s',
|
---|
201 | connector.host, connector.port, reason.value)
|
---|
202 | self.daemon.connect_deferred.errback(reason)
|
---|
203 |
|
---|
204 | def clientConnectionLost(self, connector, reason): # NOQA
|
---|
205 | log.debug('Connection lost to daemon at "%s:%s" reason: %s',
|
---|
206 | connector.host, connector.port, reason.value)
|
---|
207 | self.daemon.host = None
|
---|
208 | self.daemon.port = None
|
---|
209 | self.daemon.username = None
|
---|
210 | self.daemon.connected = False
|
---|
211 |
|
---|
212 | if self.daemon.disconnect_deferred and not self.daemon.disconnect_deferred.called:
|
---|
213 | self.daemon.disconnect_deferred.callback(reason.value)
|
---|
214 | self.daemon.disconnect_deferred = None
|
---|
215 |
|
---|
216 | if self.daemon.disconnect_callback:
|
---|
217 | self.daemon.disconnect_callback()
|
---|
218 |
|
---|
219 |
|
---|
220 | class DaemonProxy(object):
|
---|
221 | pass
|
---|
222 |
|
---|
223 |
|
---|
224 | class DaemonSSLProxy(DaemonProxy):
|
---|
225 | def __init__(self, event_handlers=None):
|
---|
226 | if event_handlers is None:
|
---|
227 | event_handlers = {}
|
---|
228 | self.__factory = DelugeRPCClientFactory(self, event_handlers)
|
---|
229 | self.__factory.noisy = False
|
---|
230 | self.__request_counter = 0
|
---|
231 | self.__deferred = {}
|
---|
232 |
|
---|
233 | # This is set when a connection is made to the daemon
|
---|
234 | self.protocol = None
|
---|
235 |
|
---|
236 | # This is set when a connection is made
|
---|
237 | self.host = None
|
---|
238 | self.port = None
|
---|
239 | self.username = None
|
---|
240 | self.authentication_level = 0
|
---|
241 |
|
---|
242 | self.connected = False
|
---|
243 |
|
---|
244 | self.disconnect_deferred = None
|
---|
245 | self.disconnect_callback = None
|
---|
246 |
|
---|
247 | self.auth_levels_mapping = None
|
---|
248 | self.auth_levels_mapping_reverse = None
|
---|
249 |
|
---|
250 | def connect(self, host, port):
|
---|
251 | """
|
---|
252 | Connects to a daemon at host:port
|
---|
253 |
|
---|
254 | :param host: str, the host to connect to
|
---|
255 | :param port: int, the listening port on the daemon
|
---|
256 |
|
---|
257 | :returns: twisted.Deferred
|
---|
258 |
|
---|
259 | """
|
---|
260 | log.debug('sslproxy.connect()')
|
---|
261 | self.host = host
|
---|
262 | self.port = port
|
---|
263 | self.__connector = reactor.connectSSL(self.host, self.port,
|
---|
264 | self.__factory,
|
---|
265 | ssl.ClientContextFactory())
|
---|
266 | self.connect_deferred = defer.Deferred()
|
---|
267 | self.daemon_info_deferred = defer.Deferred()
|
---|
268 |
|
---|
269 | # Upon connect we do a 'daemon.login' RPC
|
---|
270 | self.connect_deferred.addCallback(self.__on_connect)
|
---|
271 | self.connect_deferred.addErrback(self.__on_connect_fail)
|
---|
272 |
|
---|
273 | return self.daemon_info_deferred
|
---|
274 |
|
---|
275 | def disconnect(self):
|
---|
276 | log.debug('sslproxy.disconnect()')
|
---|
277 | self.disconnect_deferred = defer.Deferred()
|
---|
278 | self.__connector.disconnect()
|
---|
279 | return self.disconnect_deferred
|
---|
280 |
|
---|
281 | def call(self, method, *args, **kwargs):
|
---|
282 | """
|
---|
283 | Makes a RPCRequest to the daemon. All methods should be in the form of
|
---|
284 | 'component.method'.
|
---|
285 |
|
---|
286 | :params method: str, the method to call in the form of 'component.method'
|
---|
287 | :params args: the arguments to call the remote method with
|
---|
288 | :params kwargs: the keyword arguments to call the remote method with
|
---|
289 |
|
---|
290 | :return: a twisted.Deferred object that will be activated when a RPCResponse
|
---|
291 | or RPCError is received from the daemon
|
---|
292 |
|
---|
293 | """
|
---|
294 | # Create the DelugeRPCRequest to pass to protocol.send_request()
|
---|
295 | request = DelugeRPCRequest()
|
---|
296 | request.request_id = self.__request_counter
|
---|
297 | request.method = method
|
---|
298 | request.args = args
|
---|
299 | request.kwargs = kwargs
|
---|
300 | # Send the request to the server
|
---|
301 | self.protocol.send_request(request)
|
---|
302 | # Create a Deferred object to return and add a default errback to print
|
---|
303 | # the error.
|
---|
304 | d = defer.Deferred()
|
---|
305 |
|
---|
306 | # Store the Deferred until we receive a response from the daemon
|
---|
307 | self.__deferred[self.__request_counter] = d
|
---|
308 |
|
---|
309 | # Increment the request counter since we don't want to use the same one
|
---|
310 | # before a response is received.
|
---|
311 | self.__request_counter += 1
|
---|
312 |
|
---|
313 | return d
|
---|
314 |
|
---|
315 | def pop_deferred(self, request_id):
|
---|
316 | """
|
---|
317 | Pops a Deferred object. This is generally called once we receive the
|
---|
318 | reply we were waiting on from the server.
|
---|
319 |
|
---|
320 | :param request_id: the request_id of the Deferred to pop
|
---|
321 | :type request_id: int
|
---|
322 |
|
---|
323 | """
|
---|
324 | return self.__deferred.pop(request_id)
|
---|
325 |
|
---|
326 | def register_event_handler(self, event, handler):
|
---|
327 | """
|
---|
328 | Registers a handler function to be called when `:param:event` is received
|
---|
329 | from the daemon.
|
---|
330 |
|
---|
331 | :param event: the name of the event to handle
|
---|
332 | :type event: str
|
---|
333 | :param handler: the function to be called when `:param:event`
|
---|
334 | is emitted from the daemon
|
---|
335 | :type handler: function
|
---|
336 |
|
---|
337 | """
|
---|
338 | if event not in self.__factory.event_handlers:
|
---|
339 | # This is a new event to handle, so we need to tell the daemon
|
---|
340 | # that we're interested in receiving this type of event
|
---|
341 | self.__factory.event_handlers[event] = []
|
---|
342 | if self.connected:
|
---|
343 | self.call('daemon.set_event_interest', [event])
|
---|
344 |
|
---|
345 | # Only add the handler if it's not already registered
|
---|
346 | if handler not in self.__factory.event_handlers[event]:
|
---|
347 | self.__factory.event_handlers[event].append(handler)
|
---|
348 |
|
---|
349 | def deregister_event_handler(self, event, handler):
|
---|
350 | """
|
---|
351 | Deregisters a event handler.
|
---|
352 |
|
---|
353 | :param event: the name of the event
|
---|
354 | :type event: str
|
---|
355 | :param handler: the function registered
|
---|
356 | :type handler: function
|
---|
357 |
|
---|
358 | """
|
---|
359 | if event in self.__factory.event_handlers and handler in self.__factory.event_handlers[event]:
|
---|
360 | self.__factory.event_handlers[event].remove(handler)
|
---|
361 |
|
---|
362 | def __on_connect(self, result):
|
---|
363 | log.debug('__on_connect called')
|
---|
364 |
|
---|
365 | def on_info(daemon_info):
|
---|
366 | self.daemon_info = daemon_info
|
---|
367 | log.debug('Got info from daemon: %s', daemon_info)
|
---|
368 | self.daemon_info_deferred.callback(daemon_info)
|
---|
369 |
|
---|
370 | def on_info_fail(reason):
|
---|
371 | log.debug('Failed to get info from daemon')
|
---|
372 | log.exception(reason)
|
---|
373 | self.daemon_info_deferred.errback(reason)
|
---|
374 |
|
---|
375 | self.call('daemon.info').addCallback(on_info).addErrback(on_info_fail)
|
---|
376 | return self.daemon_info_deferred
|
---|
377 |
|
---|
378 | def __on_connect_fail(self, reason):
|
---|
379 | self.daemon_info_deferred.errback(reason)
|
---|
380 |
|
---|
381 | def authenticate(self, username, password):
|
---|
382 | log.debug('%s.authenticate: %s', self.__class__.__name__, username)
|
---|
383 | login_deferred = defer.Deferred()
|
---|
384 | d = self.call('daemon.login', username, password,
|
---|
385 | client_version=deluge.common.get_version())
|
---|
386 | d.addCallbacks(self.__on_login, self.__on_login_fail, callbackArgs=[username, login_deferred],
|
---|
387 | errbackArgs=[login_deferred])
|
---|
388 | return login_deferred
|
---|
389 |
|
---|
390 | def __on_login(self, result, username, login_deferred):
|
---|
391 | log.debug('__on_login called: %s %s', username, result)
|
---|
392 | self.username = username
|
---|
393 | self.authentication_level = result
|
---|
394 | # We need to tell the daemon what events we're interested in receiving
|
---|
395 | if self.__factory.event_handlers:
|
---|
396 | self.call('daemon.set_event_interest',
|
---|
397 | self.__factory.event_handlers.keys())
|
---|
398 |
|
---|
399 | self.call('core.get_auth_levels_mappings').addCallback(
|
---|
400 | self.__on_auth_levels_mappings
|
---|
401 | )
|
---|
402 |
|
---|
403 | login_deferred.callback(result)
|
---|
404 |
|
---|
405 | def __on_login_fail(self, result, login_deferred):
|
---|
406 | login_deferred.errback(result)
|
---|
407 |
|
---|
408 | def __on_auth_levels_mappings(self, result):
|
---|
409 | auth_levels_mapping, auth_levels_mapping_reverse = result
|
---|
410 | self.auth_levels_mapping = auth_levels_mapping
|
---|
411 | self.auth_levels_mapping_reverse = auth_levels_mapping_reverse
|
---|
412 |
|
---|
413 | def set_disconnect_callback(self, cb):
|
---|
414 | """
|
---|
415 | Set a function to be called when the connection to the daemon is lost
|
---|
416 | for any reason.
|
---|
417 | """
|
---|
418 | self.disconnect_callback = cb
|
---|
419 |
|
---|
420 | def get_bytes_recv(self):
|
---|
421 | return self.protocol.get_bytes_recv()
|
---|
422 |
|
---|
423 | def get_bytes_sent(self):
|
---|
424 | return self.protocol.get_bytes_sent()
|
---|
425 |
|
---|
426 |
|
---|
427 | class DaemonStandaloneProxy(DaemonProxy):
|
---|
428 | def __init__(self, event_handlers=None):
|
---|
429 | if event_handlers is None:
|
---|
430 | event_handlers = {}
|
---|
431 | from deluge.core import daemon
|
---|
432 | self.__daemon = daemon.Daemon(standalone=True)
|
---|
433 | self.__daemon.start()
|
---|
434 | log.debug('daemon created!')
|
---|
435 | self.connected = True
|
---|
436 | self.host = 'localhost'
|
---|
437 | self.port = 58846
|
---|
438 | # Running in standalone mode, it's safe to import auth level
|
---|
439 | from deluge.core.authmanager import (AUTH_LEVEL_ADMIN,
|
---|
440 | AUTH_LEVELS_MAPPING,
|
---|
441 | AUTH_LEVELS_MAPPING_REVERSE)
|
---|
442 | self.username = 'localclient'
|
---|
443 | self.authentication_level = AUTH_LEVEL_ADMIN
|
---|
444 | self.auth_levels_mapping = AUTH_LEVELS_MAPPING
|
---|
445 | self.auth_levels_mapping_reverse = AUTH_LEVELS_MAPPING_REVERSE
|
---|
446 | # Register the event handlers
|
---|
447 | for event in event_handlers:
|
---|
448 | for handler in event_handlers[event]:
|
---|
449 | self.__daemon.core.eventmanager.register_event_handler(event, handler)
|
---|
450 |
|
---|
451 | def disconnect(self):
|
---|
452 | self.connected = False
|
---|
453 | self.__daemon = None
|
---|
454 |
|
---|
455 | def call(self, method, *args, **kwargs):
|
---|
456 | # log.debug("call: %s %s %s", method, args, kwargs)
|
---|
457 |
|
---|
458 | import copy
|
---|
459 |
|
---|
460 | try:
|
---|
461 | m = self.__daemon.rpcserver.get_object_method(method)
|
---|
462 | except Exception as ex:
|
---|
463 | log.exception(ex)
|
---|
464 | return defer.fail(ex)
|
---|
465 | else:
|
---|
466 | return defer.maybeDeferred(
|
---|
467 | m, *copy.deepcopy(args), **copy.deepcopy(kwargs)
|
---|
468 | )
|
---|
469 |
|
---|
470 | def register_event_handler(self, event, handler):
|
---|
471 | """
|
---|
472 | Registers a handler function to be called when `:param:event` is
|
---|
473 | received from the daemon.
|
---|
474 |
|
---|
475 | :param event: the name of the event to handle
|
---|
476 | :type event: str
|
---|
477 | :param handler: the function to be called when `:param:event`
|
---|
478 | is emitted from the daemon
|
---|
479 | :type handler: function
|
---|
480 |
|
---|
481 | """
|
---|
482 | self.__daemon.core.eventmanager.register_event_handler(event, handler)
|
---|
483 |
|
---|
484 | def deregister_event_handler(self, event, handler):
|
---|
485 | """
|
---|
486 | Deregisters a event handler.
|
---|
487 |
|
---|
488 | :param event: the name of the event
|
---|
489 | :type event: str
|
---|
490 | :param handler: the function registered
|
---|
491 | :type handler: function
|
---|
492 |
|
---|
493 | """
|
---|
494 | self.__daemon.core.eventmanager.deregister_event_handler(event, handler)
|
---|
495 |
|
---|
496 |
|
---|
497 | class DottedObject(object):
|
---|
498 | """
|
---|
499 | This is used for dotted name calls to client
|
---|
500 | """
|
---|
501 | def __init__(self, daemon, method):
|
---|
502 | self.daemon = daemon
|
---|
503 | self.base = method
|
---|
504 |
|
---|
505 | def __call__(self, *args, **kwargs):
|
---|
506 | raise Exception("You must make calls in the form of 'component.method'!")
|
---|
507 |
|
---|
508 | def __getattr__(self, name):
|
---|
509 | return RemoteMethod(self.daemon, self.base + '.' + name)
|
---|
510 |
|
---|
511 |
|
---|
512 | class RemoteMethod(DottedObject):
|
---|
513 | """
|
---|
514 | This is used when something like 'client.core.get_something()' is attempted.
|
---|
515 | """
|
---|
516 | def __call__(self, *args, **kwargs):
|
---|
517 | return self.daemon.call(self.base, *args, **kwargs)
|
---|
518 |
|
---|
519 |
|
---|
520 | class Client(object):
|
---|
521 | """
|
---|
522 | This class is used to connect to a daemon process and issue RPC requests.
|
---|
523 | """
|
---|
524 |
|
---|
525 | __event_handlers = {
|
---|
526 | }
|
---|
527 |
|
---|
528 | def __init__(self):
|
---|
529 | self._daemon_proxy = None
|
---|
530 | self.disconnect_callback = None
|
---|
531 | self.__started_standalone = False
|
---|
532 |
|
---|
533 | def connect(self, host='127.0.0.1', port=58846, username='', password='',
|
---|
534 | skip_authentication=False):
|
---|
535 | """
|
---|
536 | Connects to a daemon process.
|
---|
537 |
|
---|
538 | :param host: str, the hostname of the daemon
|
---|
539 | :param port: int, the port of the daemon
|
---|
540 | :param username: str, the username to login with
|
---|
541 | :param password: str, the password to login with
|
---|
542 |
|
---|
543 | :returns: a Deferred object that will be called once the connection
|
---|
544 | has been established or fails
|
---|
545 | """
|
---|
546 |
|
---|
547 | self._daemon_proxy = DaemonSSLProxy(dict(self.__event_handlers))
|
---|
548 | self._daemon_proxy.set_disconnect_callback(self.__on_disconnect)
|
---|
549 |
|
---|
550 | d = self._daemon_proxy.connect(host, port)
|
---|
551 |
|
---|
552 | def on_connected(daemon_version):
|
---|
553 | log.debug('on_connected. Daemon version: %s', daemon_version)
|
---|
554 | return daemon_version
|
---|
555 |
|
---|
556 | def on_connect_fail(reason):
|
---|
557 | log.debug('on_connect_fail: %s', reason)
|
---|
558 | self.disconnect()
|
---|
559 | return reason
|
---|
560 |
|
---|
561 | def on_authenticate(result, daemon_info):
|
---|
562 | log.debug('Authentication successful: %s', result)
|
---|
563 | return result
|
---|
564 |
|
---|
565 | def on_authenticate_fail(reason):
|
---|
566 | log.debug('Failed to authenticate: %s', reason.value)
|
---|
567 | return reason
|
---|
568 |
|
---|
569 | def authenticate(daemon_version, username, password):
|
---|
570 | if not username and host in ('127.0.0.1', 'localhost'):
|
---|
571 | # No username provided and it's localhost, so attempt to get credentials from auth file.
|
---|
572 | username, password = get_localhost_auth()
|
---|
573 |
|
---|
574 | d = self._daemon_proxy.authenticate(username, password)
|
---|
575 | d.addCallback(on_authenticate, daemon_version)
|
---|
576 | d.addErrback(on_authenticate_fail)
|
---|
577 | return d
|
---|
578 |
|
---|
579 | d.addCallback(on_connected)
|
---|
580 | d.addErrback(on_connect_fail)
|
---|
581 | if not skip_authentication:
|
---|
582 | d.addCallback(authenticate, username, password)
|
---|
583 | return d
|
---|
584 |
|
---|
585 | def disconnect(self):
|
---|
586 | """
|
---|
587 | Disconnects from the daemon.
|
---|
588 | """
|
---|
589 | if self.is_standalone():
|
---|
590 | self._daemon_proxy.disconnect()
|
---|
591 | self.stop_standalone()
|
---|
592 | return defer.succeed(True)
|
---|
593 |
|
---|
594 | if self._daemon_proxy:
|
---|
595 | return self._daemon_proxy.disconnect()
|
---|
596 |
|
---|
597 | def start_standalone(self):
|
---|
598 | """
|
---|
599 | Starts a daemon in the same process as the client.
|
---|
600 | """
|
---|
601 | self._daemon_proxy = DaemonStandaloneProxy(self.__event_handlers)
|
---|
602 | self.__started_standalone = True
|
---|
603 |
|
---|
604 | def stop_standalone(self):
|
---|
605 | """
|
---|
606 | Stops the daemon process in the client.
|
---|
607 | """
|
---|
608 | self._daemon_proxy = None
|
---|
609 | self.__started_standalone = False
|
---|
610 |
|
---|
611 | def start_classic_mode(self):
|
---|
612 | """Deprecated"""
|
---|
613 | self.start_standalone()
|
---|
614 |
|
---|
615 | def stop_classic_mode(self):
|
---|
616 | """Deprecated"""
|
---|
617 | self.stop_standalone()
|
---|
618 |
|
---|
619 | def start_daemon(self, port, config):
|
---|
620 | """
|
---|
621 | Starts a daemon process.
|
---|
622 |
|
---|
623 | :param port: the port for the daemon to listen on
|
---|
624 | :type port: int
|
---|
625 | :param config: the path to the current config folder
|
---|
626 | :type config: str
|
---|
627 | :returns: True if started, False if not
|
---|
628 | :rtype: bool
|
---|
629 |
|
---|
630 | :raises OSError: received from subprocess.call()
|
---|
631 |
|
---|
632 | """
|
---|
633 | # subprocess.popen does not work with unicode args (with non-ascii characters) on windows
|
---|
634 | config = config.encode(sys.getfilesystemencoding())
|
---|
635 | try:
|
---|
636 | subprocess.Popen(['deluged', '--port=%s' % port, '--config=%s' % config])
|
---|
637 | except OSError as ex:
|
---|
638 | from errno import ENOENT
|
---|
639 | if ex.errno == ENOENT:
|
---|
640 | log.error(_("Deluge cannot find the 'deluged' executable, it is likely \
|
---|
641 | that you forgot to install the deluged package or it's not in your PATH."))
|
---|
642 | else:
|
---|
643 | log.exception(ex)
|
---|
644 | raise ex
|
---|
645 | except Exception as ex:
|
---|
646 | log.error('Unable to start daemon!')
|
---|
647 | log.exception(ex)
|
---|
648 | return False
|
---|
649 | else:
|
---|
650 | return True
|
---|
651 |
|
---|
652 | def is_localhost(self):
|
---|
653 | """
|
---|
654 | Checks if the current connected host is a localhost or not.
|
---|
655 |
|
---|
656 | :returns: bool, True if connected to a localhost
|
---|
657 |
|
---|
658 | """
|
---|
659 | if (self._daemon_proxy and self._daemon_proxy.host in ('127.0.0.1', 'localhost') or
|
---|
660 | isinstance(self._daemon_proxy, DaemonStandaloneProxy)):
|
---|
661 | return True
|
---|
662 | return False
|
---|
663 |
|
---|
664 | def is_standalone(self):
|
---|
665 | """
|
---|
666 | Checks to see if the client has been started in standalone mode.
|
---|
667 |
|
---|
668 | :returns: bool, True if in standalone mode
|
---|
669 |
|
---|
670 | """
|
---|
671 | return self.__started_standalone
|
---|
672 |
|
---|
673 | def is_classicmode(self):
|
---|
674 | """Deprecated"""
|
---|
675 | self.is_standalone()
|
---|
676 |
|
---|
677 | def connected(self):
|
---|
678 | """
|
---|
679 | Check to see if connected to a daemon.
|
---|
680 |
|
---|
681 | :returns: bool, True if connected
|
---|
682 |
|
---|
683 | """
|
---|
684 | return self._daemon_proxy.connected if self._daemon_proxy else False
|
---|
685 |
|
---|
686 | def connection_info(self):
|
---|
687 | """
|
---|
688 | Get some info about the connection or return None if not connected.
|
---|
689 |
|
---|
690 | :returns: a tuple of (host, port, username) or None if not connected
|
---|
691 | """
|
---|
692 | if self.connected():
|
---|
693 | return (self._daemon_proxy.host, self._daemon_proxy.port, self._daemon_proxy.username)
|
---|
694 |
|
---|
695 | return None
|
---|
696 |
|
---|
697 | def register_event_handler(self, event, handler):
|
---|
698 | """
|
---|
699 | Registers a handler that will be called when an event is received from the daemon.
|
---|
700 |
|
---|
701 | :params event: str, the event to handle
|
---|
702 | :params handler: func, the handler function, f(args)
|
---|
703 | """
|
---|
704 | if event not in self.__event_handlers:
|
---|
705 | self.__event_handlers[event] = []
|
---|
706 | self.__event_handlers[event].append(handler)
|
---|
707 | # We need to replicate this in the daemon proxy
|
---|
708 | if self._daemon_proxy:
|
---|
709 | self._daemon_proxy.register_event_handler(event, handler)
|
---|
710 |
|
---|
711 | def deregister_event_handler(self, event, handler):
|
---|
712 | """
|
---|
713 | Deregisters a event handler.
|
---|
714 |
|
---|
715 | :param event: str, the name of the event
|
---|
716 | :param handler: function, the function registered
|
---|
717 |
|
---|
718 | """
|
---|
719 | if event in self.__event_handlers and handler in self.__event_handlers[event]:
|
---|
720 | self.__event_handlers[event].remove(handler)
|
---|
721 | if self._daemon_proxy:
|
---|
722 | self._daemon_proxy.deregister_event_handler(event, handler)
|
---|
723 |
|
---|
724 | def force_call(self, block=False):
|
---|
725 | # no-op for now.. we'll see if we need this in the future
|
---|
726 | pass
|
---|
727 |
|
---|
728 | def __getattr__(self, method):
|
---|
729 | return DottedObject(self._daemon_proxy, method)
|
---|
730 |
|
---|
731 | def set_disconnect_callback(self, cb):
|
---|
732 | """
|
---|
733 | Set a function to be called whenever the client is disconnected from
|
---|
734 | the daemon for any reason.
|
---|
735 | """
|
---|
736 | self.disconnect_callback = cb
|
---|
737 |
|
---|
738 | def __on_disconnect(self):
|
---|
739 | if self.disconnect_callback:
|
---|
740 | self.disconnect_callback()
|
---|
741 |
|
---|
742 | def get_bytes_recv(self):
|
---|
743 | """
|
---|
744 | Returns the number of bytes received from the daemon.
|
---|
745 |
|
---|
746 | :returns: the number of bytes received
|
---|
747 | :rtype: int
|
---|
748 | """
|
---|
749 | return self._daemon_proxy.get_bytes_recv()
|
---|
750 |
|
---|
751 | def get_bytes_sent(self):
|
---|
752 | """
|
---|
753 | Returns the number of bytes sent to the daemon.
|
---|
754 |
|
---|
755 | :returns: the number of bytes sent
|
---|
756 | :rtype: int
|
---|
757 | """
|
---|
758 | return self._daemon_proxy.get_bytes_sent()
|
---|
759 |
|
---|
760 | def get_auth_user(self):
|
---|
761 | """
|
---|
762 | Returns the current authenticated username.
|
---|
763 |
|
---|
764 | :returns: the authenticated username
|
---|
765 | :rtype: str
|
---|
766 | """
|
---|
767 | return self._daemon_proxy.username
|
---|
768 |
|
---|
769 | def get_auth_level(self):
|
---|
770 | """
|
---|
771 | Returns the authentication level the daemon returned upon authentication.
|
---|
772 |
|
---|
773 | :returns: the authentication level
|
---|
774 | :rtype: int
|
---|
775 | """
|
---|
776 | return self._daemon_proxy.authentication_level
|
---|
777 |
|
---|
778 | @property
|
---|
779 | def auth_levels_mapping(self):
|
---|
780 | return self._daemon_proxy.auth_levels_mapping
|
---|
781 |
|
---|
782 | @property
|
---|
783 | def auth_levels_mapping_reverse(self):
|
---|
784 | return self._daemon_proxy.auth_levels_mapping_reverse
|
---|
785 |
|
---|
786 | # This is the object clients will use
|
---|
787 | client = Client()
|
---|