diff --git a/setup.py b/setup.py index 5eb34d2..d1da022 100755 --- a/setup.py +++ b/setup.py @@ -1,44 +1,45 @@ #!/usr/bin/python3 from distutils.core import setup import os import glob import sipclient setup( name='sipclients3', version=sipclient.__version__, description='SIP SIMPLE client', long_description='Python command line clients using the SIP SIMPLE framework', url='http://sipsimpleclient.org', author='AG Projects', author_email='support@ag-projects.com', platforms=['Platform Independent'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Service Providers', 'License :: GNU Lesser General Public License (LGPL)', 'Operating System :: OS Independent', 'Programming Language :: Python' ], packages=['sipclient', 'sipclient.configuration'], data_files=[('share/sipclients3/sounds', glob.glob(os.path.join('resources', 'sounds', '*.wav'))), ('share/sipclients3/tls', ['resources/tls/ca.crt', 'resources/tls/default.crt'])], scripts=[ 'sip-audio-session3', 'sip-message3', 'sip-publish-presence3', 'sip-register3', 'sip-session3', 'sip-settings3', + 'sip-subscribe-conference3', 'sip-subscribe-mwi3', 'sip-subscribe-presence3', 'sip-subscribe-rls3', 'sip-subscribe-winfo3', 'sip-subscribe-xcap-diff3' ] ) diff --git a/sip-subscribe-conference3 b/sip-subscribe-conference3 new file mode 100755 index 0000000..3111a8d --- /dev/null +++ b/sip-subscribe-conference3 @@ -0,0 +1,633 @@ +#!/usr/bin/env python3 + +import datetime +import os +import random +import select +import sys +import termios +import urllib.request, urllib.parse, urllib.error + +from itertools import chain + +from collections import deque +from optparse import OptionParser +from threading import Thread +from time import time + +from application import log +from application.notification import IObserver, NotificationCenter, NotificationData +from application.python.queue import EventQueue +from eventlib.twistedutil import join_reactor +from twisted.internet import reactor +from twisted.internet.error import ReactorNotRunning +from zope.interface import implementer + +from sipsimple.account import Account, AccountManager, BonjourAccount +from sipsimple.application import SIPApplication +from sipsimple.lookup import DNSLookup, DNSManager +from sipsimple.configuration import ConfigurationError, ConfigurationManager +from sipsimple.configuration.settings import SIPSimpleSettings +from sipsimple.core import ContactHeader, Engine, FromHeader, RouteHeader, SIPCoreError, SIPURI, Subscription, ToHeader, Route, PJSIPError +from sipsimple.payloads import ParserError +from sipsimple.payloads.conference import ConferenceDocument +from sipsimple.storage import FileStorage +from sipsimple.threading import run_in_twisted_thread + +from sipclient.configuration import config_directory +from sipclient.configuration.account import AccountExtension +from sipclient.configuration.settings import SIPSimpleSettingsExtension +from sipclient.log import Logger + + +class InputThread(Thread): + def __init__(self, application): + Thread.__init__(self) + self.application = application + self.daemon = True + self._old_terminal_settings = None + + def run(self): + notification_center = NotificationCenter() + while True: + for char in self._getchars(): + if char == "\x04": + self.application.stop() + return + else: + notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char)) + + def stop(self): + self._termios_restore() + + def _termios_restore(self): + if self._old_terminal_settings is not None: + termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings) + + def _getchars(self): + fd = sys.stdin.fileno() + if os.isatty(fd): + self._old_terminal_settings = termios.tcgetattr(fd) + new = termios.tcgetattr(fd) + new[3] = new[3] & ~termios.ICANON & ~termios.ECHO + new[6][termios.VMIN] = b'\000' + try: + termios.tcsetattr(fd, termios.TCSADRAIN, new) + if select.select([fd], [], [], None)[0]: + return sys.stdin.read(4192) + finally: + self._termios_restore() + else: + return os.read(fd, 4192) + + +@implementer(IObserver) +class SubscriptionApplication(object): + + def __init__(self, account_name, target, trace_sip, trace_pjsip, trace_notifications): + self.account_name = account_name + self.target = target + self.input = InputThread(self) + self.output = EventQueue(self._write) + self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications) + self.success = False + self.account = None + self.subscription = None + self.stopping = False + + self._subscription_routes = None + self._subscription_timeout = 0.0 + self._subscription_wait = 0.5 + + account_manager = AccountManager() + engine = Engine() + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=account_manager) + notification_center.add_observer(self, sender=engine) + notification_center.add_observer(self, sender=self.input) + + log.level.current = log.level.WARNING + + def _write(self, message): + if isinstance(message, str): + message = message.encode(sys.getfilesystemencoding()) + sys.stdout.write(message.decode()+'\n') + + def run(self): + account_manager = AccountManager() + configuration = ConfigurationManager() + self.engine = Engine() + + # start output thread + self.output.start() + + # startup configuration + Account.register_extension(AccountExtension) + BonjourAccount.register_extension(AccountExtension) + SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension) + SIPApplication.storage = FileStorage(config_directory) + try: + configuration.start() + except ConfigurationError as e: + raise RuntimeError("failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e)) + account_manager.load() + if self.account_name is None: + self.account = account_manager.default_account + else: + possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled] + if len(possible_accounts) > 1: + raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts)))) + if len(possible_accounts) == 0: + raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled)))) + self.account = possible_accounts[0] + if self.account is None: + raise RuntimeError("Unknown account %s. Available accounts: %s" % (self.account_name, ', '.join(account.id for account in account_manager.iter_accounts()))) + for account in account_manager.iter_accounts(): + if account == self.account: + account.sip.register = False + else: + account.enabled = False + self.output.put('Using account %s' % self.account.id) + settings = SIPSimpleSettings() + + # start logging + self.logger.start() + + # start the engine + self.engine.start( + auto_sound=False, + events={"conference": ["application/conference-info+xml"],}, + udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None, + tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None, + tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None, + tls_verify_server=self.account.tls.verify_server, + tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None, + tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, + tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None, + user_agent=settings.user_agent, + sample_rate=settings.audio.sample_rate, + rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), + trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout, + log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0 + ) + + if self.target is None: + self.target = ToHeader(SIPURI(user=self.account.id.username, host=self.account.id.domain)) + else: + if '@' not in self.target: + self.target = '%s@%s' % (self.target, self.account.id.domain) + if not self.target.startswith('sip:') and not self.target.startswith('sips:'): + self.target = 'sip:' + self.target + try: + self.target = ToHeader(SIPURI.parse(self.target)) + except SIPCoreError: + self.output.put('Illegal SIP URI: %s' % self.target) + self.engine.stop() + return 1 + self.output.put('Subscribing to %s for the conference event' % self.target.uri) + + # start the input thread + self.input.start() + + reactor.callLater(0, self._subscribe) + + # start twisted + try: + reactor.run() + finally: + self.input.stop() + + # stop the output + self.output.stop() + self.output.join() + + self.logger.stop() + + return 0 if self.success else 1 + + def stop(self): + self.stopping = True + if self.subscription is not None and self.subscription.state.lower() in ('accepted', 'pending', 'active'): + self.subscription.end(timeout=1) + else: + self.engine.stop() + + def print_help(self): + message = 'Available control keys:\n' + message += ' t: toggle SIP trace on the console\n' + message += ' j: toggle PJSIP trace on the console\n' + message += ' n: toggle notifications trace on the console\n' + message += ' Ctrl-d: quit the program\n' + message += ' ?: display this help message\n' + self.output.put('\n'+message) + + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, None) + if handler is not None: + handler(notification) + + def _NH_SIPEngineDidStart(self, notification): + dns_manager = DNSManager() + self.engine.set_nameservers(dns_manager.nameservers) + + def _NH_SIPSubscriptionDidStart(self, notification): + route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) + self._subscription_routes = None + self._subscription_wait = 0.5 + self.output.put('Subscription succeeded at %s:%d;transport=%s' % (route.address, route.port, route.transport)) + self.success = True + + def _NH_SIPSubscriptionChangedState(self, notification): + route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) + if notification.data.state.lower() == "pending": + self.output.put('Subscription pending at %s:%d;transport=%s' % (route.address, route.port, route.transport)) + elif notification.data.state.lower() == "active": + self.output.put('Subscription active at %s:%d;transport=%s' % (route.address, route.port, route.transport)) + + def _NH_SIPSubscriptionDidEnd(self, notification): + notification_center = NotificationCenter() + notification_center.remove_observer(self, sender=notification.sender) + self.subscription = None + route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) + self.output.put('Unsubscribed from %s:%d;transport=%s' % (route.address, route.port, route.transport)) + self.stop() + + def _NH_SIPSubscriptionDidFail(self, notification): + notification_center = NotificationCenter() + notification_center.remove_observer(self, sender=notification.sender) + self.subscription = None + route = Route(notification.sender.route_header.uri.host, notification.sender.route_header.uri.port, notification.sender.route_header.uri.parameters.get('transport', 'udp')) + if notification.data.code: + status = ': %d %s' % (notification.data.code, notification.data.reason) + else: + status = ': %s' % notification.data.reason + self.output.put('Subscription failed at %s:%d;transport=%s%s' % (route.address, route.port, route.transport, status)) + if self.stopping or notification.data.code in (401, 403, 407) or self.success: + self.success = False + self.stop() + else: + if not self._subscription_routes or time() > self._subscription_timeout: + self._subscription_wait = min(self._subscription_wait*2, 30) + timeout = random.uniform(self._subscription_wait, 2*self._subscription_wait) + reactor.callFromThread(reactor.callLater, timeout, self._subscribe) + else: + route = self._subscription_routes.popleft() + route_header = RouteHeader(route.uri) + self.subscription = Subscription(self.target.uri, + FromHeader(self.account.uri, self.account.display_name), + self.target, + ContactHeader(self.account.contact[route]), + b"conference", + route_header, + credentials=self.account.credentials, + refresh=self.account.sip.subscribe_interval) + notification_center.add_observer(self, sender=self.subscription) + self.subscription.subscribe(timeout=5) + + def _NH_SIPSubscriptionGotNotify(self, notification): + self.output.put('Received NOTIFY') + if notification.data.event == 'conference' and notification.data.body: + try: + conference_info = ConferenceDocument.parse(notification.data.body) + except ParserError as e: + self.output.put('Cannot parse body: %s' % str(e)) + else: + conf_desc = conference_info.conference_description + print('Conference description: %s' % str(conf_desc.display_text)) + print('Conference organizer: %s' % str(conference_info.host_info.web_page.value)) + print('Conference participants:') + i = 1 + for user in conference_info.users: + media = list(str(media.media_type) for media in chain(*user)) + print("%d. %s <%s> with %s" % (i, user.display_text.value, user.entity.split(":")[1], ", ".join(media))) + i += 1 + + + + + def _NH_DNSLookupDidSucceed(self, notification): + # create subscription and register to get notifications from it + self._subscription_routes = deque(notification.data.result) + route = self._subscription_routes.popleft() + route_header = RouteHeader(route.uri) + self.subscription = Subscription(self.target.uri, + FromHeader(self.account.uri, self.account.display_name), + self.target, + ContactHeader(self.account.contact[route]), + b"conference", + route_header, + credentials=self.account.credentials, + refresh=self.account.sip.subscribe_interval) + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=self.subscription) + try: + self.subscription.subscribe(timeout=5) + except PJSIPError as e: + self.output.put('Subscription failed: %s' % str(e)) + + def _NH_DNSLookupDidFail(self, notification): + self.output.put('DNS lookup failed: %s' % notification.data.error) + timeout = random.uniform(1.0, 2.0) + reactor.callLater(timeout, self._subscribe) + + def _NH_SAInputWasReceived(self, notification): + settings = SIPSimpleSettings() + key = notification.data.input + if key == 't': + self.logger.sip_to_stdout = not self.logger.sip_to_stdout + self.engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip + self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated')) + elif key == 'j': + self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout + self.engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0 + self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated')) + elif key == 'n': + self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout + self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated')) + elif key == '?': + self.print_help() + + @run_in_twisted_thread + def _NH_SIPEngineDidEnd(self, notification): + self._stop_reactor() + + @run_in_twisted_thread + def _NH_SIPEngineDidFail(self, notification): + self.output.put('Engine failed.') + self._stop_reactor() + + def _NH_SIPEngineGotException(self, notification): + self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback) + + def _stop_reactor(self): + try: + reactor.stop() + except ReactorNotRunning: + pass + + def _subscribe(self): + settings = SIPSimpleSettings() + + self._subscription_timeout = time()+30 + + lookup = DNSLookup() + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=lookup) + if self.account.sip.outbound_proxy is not None: + uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport}) + elif self.account.sip.always_use_my_proxy: + uri = SIPURI(host=self.account.id.domain) + else: + uri = self.target.uri + lookup.lookup_sip_proxy(uri, settings.sip.transport_list, tls_name=self.account.sip.tls_name) + + def _format_note(self, note): + text = "Note" + if hasattr(note, 'lang') and note.lang is not None: + text += "(%s)" % note.lang + else: + text += ": %s" % note + return text + + def _format_person(self, person, pidf): + buf = [] + # display class + if person.rpid_class is not None: + buf.append(" Class: %s" % person.rpid_class) + # display timestamp + if person.timestamp is not None: + buf.append(" Timestamp: %s" % person.timestamp) + # display notes + if person.notes: + for note in person.notes: + buf.append(" %s" % self._format_note(note)) + elif pidf.notes: + for note in pidf.notes: + buf.append(" %s" % self._format_note(note)) + # display activities + if person.activities is not None: + activities = list(person.activities) + if len(activities) > 0: + text = " Activities" + if person.activities.since is not None or person.activities.until is not None: + text += " valid" + if person.activities.since is not None: + text += " from %s" % person.activities.since + if person.activities.until is not None: + text += " until %s" % person.activities.until + text += ": %s" % ', '.join(str(activity) for activity in activities) + buf.append(text) + if len(person.activities.notes) > 0: + for note in person.activities.notes: + buf.append(" %s" % self._format_note(note)) + elif len(person.activities.notes) > 0: + buf.append(" Activities") + for note in person.activities.notes: + buf.append(" %s" % self._format_note(note)) + # display mood + if person.mood is not None: + moods = list(person.mood) + if len(moods) > 0: + text = " Mood" + if person.mood.since is not None or person.mood.until is not None: + text += " valid" + if person.mood.since is not None: + text += " from %s" % person.mood.since + if person.mood.until is not None: + text += " until %s" % person.mood.until + text += ": %s" % ', '.join(str(mood) for mood in moods) + buf.append(text) + if len(person.mood.notes) > 0: + for note in person.mood.notes: + buf.append(" %s" % self._format_note(note)) + # display place is + if person.place_is is not None: + place_info = ', '.join('%s %s' % (key.capitalize(), getattr(person.place_is, key).value) for key in ('audio', 'video', 'text') if getattr(person.place_is, key) and getattr(person.place_is, key).value) + if place_info != '': + buf.append(" Place information: " + place_info) + # display privacy + if person.privacy is not None: + text = " Private conversation possible with: " + private = [] + if person.privacy.audio: + private.append("Audio") + if person.privacy.video: + private.append("Video") + if person.privacy.text: + private.append("Text") + if len(private) > 0: + text += ", ".join(private) + else: + text += "None" + buf.append(text) + # display sphere + if person.sphere is not None: + timeinfo = [] + if person.sphere.since is not None: + timeinfo.append('from %s' % str(person.sphere.since)) + if person.sphere.until is not None: + timeinfo.append('until %s' % str(person.sphere.until)) + if len(timeinfo) != 0: + timeinfo = ' (' + ', '.join(timeinfo) + ')' + else: + timeinfo = '' + buf.append(" Current sphere%s: %s" % (timeinfo, person.sphere.value)) + # display status icon + if person.status_icon is not None: + buf.append(" Status icon: %s" % person.status_icon) + # display time and time offset + if person.time_offset is not None: + ctime = datetime.datetime.utcnow() + datetime.timedelta(minutes=int(person.time_offset)) + time_offset = int(person.time_offset)/60.0 + if time_offset == int(time_offset): + offset_info = '(UTC+%d%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or '')) + else: + offset_info = '(UTC+%.1f%s)' % (time_offset, (person.time_offset.description is not None and (' (%s)' % person.time_offset.description) or '')) + buf.append(" Current user time: %s %s" % (ctime.strftime("%H:%M"), offset_info)) + # display user input + if person.user_input is not None: + buf.append(" User is %s" % person.user_input) + if person.user_input.last_input: + buf.append(" Last input at: %s" % person.user_input.last_input) + if person.user_input.idle_threshold: + buf.append(" Idle threshold: %s seconds" % person.user_input.idle_threshold) + return buf + + def _format_service(self, service, pidf): + buf = [] + # display class + if service.rpid_class is not None: + buf.append(" Class: %s" % service.rpid_class) + # display timestamp + if service.timestamp is not None: + buf.append(" Timestamp: %s" % service.timestamp) + # display notes + for note in service.notes: + buf.append(" %s" % self._format_note(note)) + # display status + if service.status is not None: + if service.status.basic is not None: + buf.append(" Basic status: %s" % service.status.basic) + if service.status.extended is not None: + buf.append(" Extended status: %s" % service.status.extended) + # display contact + if service.contact is not None: + buf.append(" Contact%s: %s" % ((service.contact.priority is not None) and (' priority %s' % service.contact.priority) or '', urllib.parse.unquote(service.contact.value))) + # display device ID + if service.device_info is not None: + description = " (%s)" % urllib.parse.unquote(service.device_info.description.value).decode('utf-8') if service.device_info.description else "" + buf.append(" Service offered by device: %s%s" % (service.device_info.id, description)) + # display relationship + if service.relationship is not None: + buf.append(" Relationship: %s" % service.relationship.value) + # display service-class + if service.service_class is not None: + buf.append(" Service class: %s" % service.service_class.value) + # display status icon + if service.status_icon is not None: + buf.append(" Status icon: %s" % service.status_icon) + # display user input + if service.user_input is not None: + buf.append(" Service is %s" % service.user_input) + if service.user_input.last_input: + buf.append(" Last input at: %s" % service.user_input.last_input) + if service.user_input.idle_threshold: + buf.append(" Idle threshold: %s seconds" % service.user_input.idle_threshold) + return buf + + def _format_device(self, device, pidf): + buf = [] + # display device ID + if device.device_id is not None: + buf.append(" Device id: %s" % device.device_id) + # display class + if device.rpid_class is not None: + buf.append(" Class: %s" % device.rpid_class) + # display timestamp + if device.timestamp is not None: + buf.append(" Timestamp: %s" % device.timestamp) + # display notes + for note in device.notes: + buf.append(" %s" % self._format_note(note)) + # display user input + if device.user_input is not None: + buf.append(" Device is %s" % device.user_input) + if device.user_input.last_input: + buf.append(" Last input at: %s" % device.user_input.last_input) + if device.user_input.idle_threshold: + buf.append(" Idle threshold: %s seconds" % device.user_input.idle_threshold) + return buf + + def _display_pidf(self, pidf): + buf = ["-"*16] + buf.append("Presence for %s:" % urllib.parse.unquote(pidf.entity)) + persons = {} + devices = {} + services = {} + printed_sep = True + for child in pidf: + if isinstance(child, Person): + persons[child.id] = child + elif isinstance(child, Device): + devices[child.id] = child + elif isinstance(child, Service): + services[child.id] = child + + # handle person information + if len(persons) == 0: + if list(pidf.notes): + buf.append(" Person information:") + for note in pidf.notes: + buf.append(" %s" % self._format_note(note)) + printed_sep = False + else: + for person in list(persons.values()): + buf.append(" Person: %s" % person.id) + buf.extend(self._format_person(person, pidf)) + printed_sep = False + + + # handle services informaation + if len(services) > 0: + if not printed_sep: + buf.append(" " + "-"*3) + for service in list(services.values()): + buf.append(" Service: %s" % service.id) + buf.extend(self._format_service(service, pidf)) + + # handle devices informaation + if len(devices) > 0: + if not printed_sep: + buf.append(" " + "-"*3) + for device in list(devices.values()): + buf.append(" Device: %s" % device.id) + buf.extend(self._format_device(device, pidf)) + + buf.append("-"*16) + + # push the data + self.output.put('\n'.join(buf)) + + +if __name__ == "__main__": + description = "This script subscribes to the conference event package published by the specified SIP target. If a SIP target is not specified, it will subscribe to its own address. It will then interprete PIDF bodies contained in NOTIFYs and display their meaning. The program will un-SUBSCRIBE and quit when CTRL+D is pressed." + usage = "%prog [options] [target-user@target-domain.com]" + parser = OptionParser(usage=usage, description=description) + parser.print_usage = parser.print_help + parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.") + parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).") + parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).") + parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).") + options, args = parser.parse_args() + + try: + application = SubscriptionApplication(options.account_name, args[0] if args else None, options.trace_sip, options.trace_pjsip, options.trace_notifications) + return_code = application.run() + except RuntimeError as e: + print("Error: %s" % str(e)) + sys.exit(1) + except SIPCoreError as e: + print("Error: %s" % str(e)) + sys.exit(1) + else: + sys.exit(return_code)