source: deluge/ui/client.py@ af6b27

2.0.x develop
Last change on this file since af6b27 was af6b27, checked in by Calum Lind <calumlind+deluge@gmail.com>, 9 years ago

[Lint] Add flake8-quotes to tox and fix bad quotes

  • Property mode set to 100644
File size: 26.6 KB
Line 
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
11import logging
12import subprocess
13import sys
14
15from twisted.internet import defer, reactor, ssl
16from twisted.internet.protocol import ClientFactory
17
18import deluge.common
19from deluge import error
20from deluge.transfer import DelugeTransferProtocol
21from deluge.ui.common import get_localhost_auth
22
23RPC_RESPONSE = 1
24RPC_ERROR = 2
25RPC_EVENT = 3
26
27log = logging.getLogger(__name__)
28
29
30def format_kwargs(kwargs):
31 return ', '.join([key + '=' + str(value) for key, value in kwargs.items()])
32
33
34class 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
74class 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
188class 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
220class DaemonProxy(object):
221 pass
222
223
224class 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
427class 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
497class 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
512class 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
520class 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 \
641that 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
787client = Client()
Note: See TracBrowser for help on using the repository browser.