File indexing completed on 2024-04-21 07:49:02

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2009-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 import socket
0011 import subprocess
0012 import datetime
0013 import os
0014 import sys
0015 from itertools import chain
0016 
0017 from twisted.spread import pb
0018 from twisted.cred import credentials
0019 from twisted.internet.defer import CancelledError
0020 from twisted.internet.task import deferLater
0021 import twisted.internet.error
0022 from twisted.python.failure import Failure
0023 
0024 from qt import QDialog, QDialogButtonBox, QVBoxLayout, \
0025     QLabel, QComboBox, QLineEdit, QFormLayout, \
0026     QSizePolicy, QWidget
0027 
0028 from kde import KUser, KDialog, KDialogButtonBox
0029 from mi18n import i18n, i18nc, english
0030 from dialogs import DeferredDialog, QuestionYesNo
0031 
0032 from log import logWarning, logException, logInfo, logDebug, SERVERMARK
0033 from util import removeIfExists, which
0034 from common import Internal, Options, SingleshotOptions, Debug, isAlive
0035 from common import interpreterName
0036 from common import StrMixin
0037 from common import appdataDir, socketName
0038 from game import Players
0039 from query import Query
0040 from statesaver import StateSaver
0041 
0042 from guiutil import ListComboBox, decorateWindow
0043 from rule import Ruleset
0044 
0045 
0046 class LoginAborted(Exception):
0047 
0048     """the user aborted the login"""
0049 
0050 
0051 class Url(str, StrMixin):
0052 
0053     """holds connection related attributes: host, port, socketname"""
0054     # pylint: disable=too-many-public-methods
0055     def __new__(cls, url):
0056         assert url
0057         host = None
0058         port = None
0059         urlParts = url.split(':')
0060         host = urlParts[0]
0061         if english(host) == Query.localServerName:
0062             host = '127.0.0.1'
0063         if len(urlParts) > 1:
0064             port = int(urlParts[1])
0065         obj = str.__new__(cls, url)
0066         obj.host = host
0067         obj.port = port
0068         if Options.port:
0069             obj.port = int(Options.port)
0070         if obj.port is None and obj.isLocalHost and not obj.useSocket:
0071             obj.port = obj.findFreePort()
0072         if obj.port is None and not obj.isLocalHost:
0073             obj.port = Internal.defaultPort
0074         if Debug.connections:
0075             logDebug(repr(obj))
0076 
0077         return obj
0078 
0079     def __str__(self):
0080         """show all info"""
0081         return socketName() if self.useSocket else '{}:{}'.format(self.host, self.port)
0082 
0083     @property
0084     def useSocket(self):
0085         """do we use socket for current host?"""
0086         return (
0087             self.host == '127.0.0.1'
0088             and os.name != 'nt'
0089             and not Options.port)
0090 
0091     @property
0092     def isLocalGame(self):
0093         """Are we playing a local game not needing the network?"""
0094         return self.host == '127.0.0.1'
0095 
0096     @property
0097     def isLocalHost(self):
0098         """do server and client run on the same host?"""
0099         return self.host in ('127.0.0.1', 'localhost')
0100 
0101     def findFreePort(self):
0102         """find an unused port on the current system.
0103         used when we want to start a local server on windows"""
0104         assert self.isLocalHost
0105         for port in chain([Internal.defaultPort], range(2000, 19000)):
0106             sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
0107             sock.settimeout(1)
0108             try:
0109                 sock.connect((self.host, port))
0110                 sock.close()
0111             except socket.error:
0112                 return port
0113         logException('cannot find a free port')
0114         return None
0115 
0116     def startServer(self, result, waiting=0):
0117         """make sure we have a running local server or network connectivity"""
0118         if self.isLocalHost:
0119             # just wait for that server to appear
0120             if self.__serverListening():
0121                 return result
0122             if waiting == 0:
0123                 self.__startLocalServer()
0124             elif waiting > 30:
0125                 logDebug('Game %s: Server %s not available after 30 seconds, aborting' % (
0126                     SingleshotOptions.game, self))
0127                 raise CancelledError
0128             return deferLater(Internal.reactor, 1, self.startServer, result, waiting + 1)
0129         if which('qdbus'):
0130             try:
0131                 stdoutdata, stderrdata = subprocess.Popen(
0132                     ['qdbus',
0133                      'org.kde.kded',
0134                      '/modules/networkstatus',
0135                      'org.kde.Solid.Networking.status'],
0136                     stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate(timeout=1)
0137             except subprocess.TimeoutExpired as _:
0138                 raise twisted.internet.error.ConnectError() from _
0139             stdoutdata = stdoutdata.strip()
0140             stderrdata = stderrdata.strip()
0141             if stderrdata == '' and stdoutdata != '4':
0142                 raise twisted.internet.error.ConnectError()
0143             # if we have stderrdata, qdbus probably does not provide the
0144             # service we want, so ignore it
0145         return result
0146 
0147     @staticmethod
0148     def __findServerProgram():
0149         """how should we start the server?"""
0150         result = []
0151         if sys.argv[0].endswith('kajongg.py'):
0152             tryServer = sys.argv[0].replace('.py', 'server.py')
0153             if os.path.exists(tryServer):
0154                 result = [interpreterName, tryServer]
0155         elif sys.argv[0].endswith('kajongg.pyw'):
0156             tryServer = sys.argv[0].replace('.pyw', 'server.py')
0157             if os.path.exists(tryServer):
0158                 result = [interpreterName, tryServer]
0159         elif sys.argv[0].endswith('kajongg.exe'):
0160             tryServer = sys.argv[0].replace('.exe', 'server.exe')
0161             if os.path.exists(tryServer):
0162                 result = [tryServer]
0163         else:
0164             result = ['kajonggserver']
0165         if Debug.connections:
0166             logDebug(i18n('trying to start local server %1', result))
0167         return result
0168 
0169     def __startLocalServer(self):
0170         """start a local server"""
0171         try:
0172             args = self.__findServerProgram()
0173             if self.useSocket or os.name == 'nt':  # for nt --socket tells the server to bind to 127.0.0.1
0174                 args.append('--socket=%s' % socketName())
0175                 if removeIfExists(socketName()):
0176                     logInfo(
0177                         i18n('removed stale socket <filename>%1</filename>', socketName()))
0178             if not self.useSocket:
0179                 args.append('--port=%d' % self.port)
0180             if self.isLocalGame:
0181                 args.append(
0182                     '--db={}'.format(
0183                         os.path.normpath(
0184                             os.path.join(appdataDir(), 'local3.db'))))
0185             if Debug.argString:
0186                 args.append('--debug=%s' % Debug.argString)
0187             if os.name == 'nt':
0188                 startupinfo = subprocess.STARTUPINFO()
0189                 startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
0190             else:
0191                 startupinfo = None
0192             process = subprocess.Popen(
0193                 args,
0194                 startupinfo=startupinfo)  # , shell=os.name == 'nt')
0195             if Debug.connections:
0196                 logDebug(
0197                     i18n(
0198                         'started the local kajongg server: pid=<numid>%1</numid> %2',
0199                         process.pid, ' '.join(args)))
0200         except OSError as exc:
0201             exc.filename = ' '.join(args)
0202             logException(exc)
0203 
0204     def __serverListening(self):
0205         """is the expected server listening?"""
0206         if self.useSocket:
0207             proto = socket.AF_UNIX
0208             param = socketName()
0209             if not os.path.exists(param):
0210                 return False
0211         else:
0212             proto = socket.AF_INET
0213             param = (self.host, self.port)
0214         sock = socket.socket(proto, socket.SOCK_STREAM)
0215         sock.settimeout(1)
0216         try:
0217             sock.connect(param)
0218             sock.close()
0219             return True
0220         except socket.error:
0221             return False
0222 
0223     def connect(self, factory):
0224         """return a twisted connector"""
0225         if self.useSocket:
0226             return Internal.reactor.connectUNIX(socketName(), factory, timeout=5)
0227         host = self.host
0228         return Internal.reactor.connectTCP(host, self.port, factory, timeout=5)
0229 
0230 
0231 class LoginDlg(QDialog):
0232 
0233     """login dialog for server"""
0234 
0235     def __init__(self):
0236         """self.servers is a list of tuples containing server and last playername"""
0237         QDialog.__init__(self, None)
0238         decorateWindow(self, i18nc('kajongg', 'Login'))
0239         self.setupUi()
0240 
0241         localName = i18nc('kajongg name for local game server', Query.localServerName)
0242         self.servers = Query(
0243             'select url,lastname from server order by lasttime desc').records
0244         servers = [x[0] for x in self.servers if x[0] != Query.localServerName]
0245         # the first server combobox item should be default: either the last used server
0246         # or localName for autoPlay
0247         if localName not in servers:
0248             servers.append(localName)
0249         if 'kajongg.org' not in servers:
0250             servers.append('kajongg.org')
0251         if Internal.autoPlay:
0252             demoHost = Options.host or localName
0253             if demoHost in servers:
0254                 servers.remove(
0255                     demoHost)  # we want a unique list, it will be re-used for all following games
0256             servers.insert(0, demoHost)
0257                            # in this process but they will not be autoPlay
0258         self.cbServer.addItems(servers)
0259         self.passwords = Query('select url, p.name, passwords.password from passwords, player p '
0260                                'where passwords.player=p.id').records
0261         Players.load()
0262         self.cbServer.editTextChanged.connect(self.serverChanged)
0263         self.cbUser.editTextChanged.connect(self.userChanged)
0264         self.serverChanged()
0265         StateSaver(self)
0266 
0267     def returns(self, unusedButton=None):
0268         """login data returned by this dialog"""
0269         return (Url(self.url), self.username, self.password, self.__defineRuleset())
0270 
0271     def setupUi(self):
0272         """create all Ui elements but do not fill them"""
0273         self.buttonBox = KDialogButtonBox(self)
0274         self.buttonBox.setStandardButtons(
0275             QDialogButtonBox.Cancel | QDialogButtonBox.Ok)
0276         # Ubuntu 11.10 unity is a bit strange - without this, it sets focus on
0277         # the cancel button (which it shows on the left). I found no obvious
0278         # way to use setDefault and setAutoDefault for fixing this.
0279         self.buttonBox.button(QDialogButtonBox.Ok).setFocus()
0280         self.buttonBox.accepted.connect(self.accept)
0281         self.buttonBox.rejected.connect(self.reject)
0282         vbox = QVBoxLayout(self)
0283         self.grid = QFormLayout()
0284         self.cbServer = QComboBox()
0285         self.cbServer.setEditable(True)
0286         self.grid.addRow(i18n('Game server:'), self.cbServer)
0287         self.cbUser = QComboBox()
0288         self.cbUser.setEditable(True)
0289         self.grid.addRow(i18n('Username:'), self.cbUser)
0290         self.edPassword = QLineEdit()
0291         self.edPassword.setEchoMode(QLineEdit.PasswordEchoOnEdit)
0292         self.grid.addRow(i18n('Password:'), self.edPassword)
0293         self.cbRuleset = ListComboBox()
0294         self.grid.addRow(i18nc('kajongg', 'Ruleset:'), self.cbRuleset)
0295         vbox.addLayout(self.grid)
0296         vbox.addWidget(self.buttonBox)
0297         pol = QSizePolicy()
0298         pol.setHorizontalPolicy(QSizePolicy.Expanding)
0299         self.cbUser.setSizePolicy(pol)
0300         self.__port = None
0301 
0302     def serverChanged(self, unusedText=None):
0303         """the user selected a different server"""
0304         records = Query('select player.name from player, passwords '
0305                         'where passwords.url=? and passwords.player = player.id', (self.url,)).records
0306         players = [x[0] for x in records]
0307         preferPlayer = Options.player
0308         if preferPlayer:
0309             if preferPlayer in players:
0310                 players.remove(preferPlayer)
0311             players.insert(0, preferPlayer)
0312         self.cbUser.clear()
0313         self.cbUser.addItems(players)
0314         if not self.cbUser.count():
0315             user = KUser() if os.name == 'nt' else KUser(os.geteuid())
0316             self.cbUser.addItem(user.fullName() or user.loginName())
0317         if not preferPlayer:
0318             userNames = [x[1] for x in self.servers if x[0] == self.url]
0319             if userNames:
0320                 userIdx = self.cbUser.findText(userNames[0])
0321                 if userIdx >= 0:
0322                     self.cbUser.setCurrentIndex(userIdx)
0323         showPW = bool(self.url) and not Url(self.url).isLocalHost
0324         self.grid.labelForField(self.edPassword).setVisible(showPW)
0325         self.edPassword.setVisible(showPW)
0326         self.grid.labelForField(
0327             self.cbRuleset).setVisible(
0328                 not showPW and not Options.ruleset)
0329         self.cbRuleset.setVisible(not showPW and not Options.ruleset)
0330         if not showPW:
0331             self.cbRuleset.clear()
0332             if Options.ruleset:
0333                 self.cbRuleset.items = [Options.ruleset]
0334             else:
0335                 self.cbRuleset.items = Ruleset.selectableRulesets(self.url)
0336         self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(bool(self.url))
0337 
0338     def __defineRuleset(self):
0339         """find out what ruleset to use"""
0340         if Options.ruleset:
0341             return Options.ruleset
0342         if Internal.autoPlay or bool(Options.host):
0343             return Ruleset.selectableRulesets()[0]
0344         return self.cbRuleset.current
0345 
0346     def userChanged(self, text):
0347         """the username has been changed, lookup password"""
0348         if text == '':
0349             self.edPassword.clear()
0350             return
0351         passw = None
0352         for entry in self.passwords:
0353             if entry[0] == self.url and entry[1] == text:
0354                 passw = entry[2]
0355         if passw:
0356             self.edPassword.setText(passw)
0357         else:
0358             self.edPassword.clear()
0359 
0360     @property
0361     def url(self):
0362         """abstracts the url of the dialog"""
0363         return english(self.cbServer.currentText())
0364 
0365     @property
0366     def username(self):
0367         """abstracts the username of the dialog"""
0368         return self.cbUser.currentText()
0369 
0370     @property
0371     def password(self):
0372         """abstracts the password of the dialog"""
0373         return self.edPassword.text()
0374 
0375     @password.setter
0376     def password(self, password):
0377         """abstracts the password of the dialog"""
0378         self.edPassword.setText(password)
0379 
0380 
0381 class AddUserDialog(KDialog):
0382 
0383     """add a user account on a server: This dialog asks for the needed attributes"""
0384     # pylint: disable=too-many-instance-attributes
0385 
0386     def __init__(self, url, username, password):
0387         KDialog.__init__(self)
0388         decorateWindow(self, i18nc("@title:window", "Create User Account"))
0389         self.setButtons(KDialog.ButtonCode(KDialog.Ok | KDialog.Cancel))
0390         vbox = QVBoxLayout()
0391         grid = QFormLayout()
0392         self.lbServer = QLabel()
0393         self.lbServer.setText(url)
0394         grid.addRow(i18n('Game server:'), self.lbServer)
0395         self.lbUser = QLabel()
0396         grid.addRow(i18n('Username:'), self.lbUser)
0397         self.edPassword = QLineEdit()
0398         self.edPassword.setEchoMode(QLineEdit.PasswordEchoOnEdit)
0399         grid.addRow(i18n('Password:'), self.edPassword)
0400         self.edPassword2 = QLineEdit()
0401         self.edPassword2.setEchoMode(QLineEdit.PasswordEchoOnEdit)
0402         grid.addRow(i18n('Repeat password:'), self.edPassword2)
0403         vbox.addLayout(grid)
0404         widget = QWidget(self)
0405         widget.setLayout(vbox)
0406         self.setMainWidget(widget)
0407         pol = QSizePolicy()
0408         pol.setHorizontalPolicy(QSizePolicy.Expanding)
0409         self.lbUser.setSizePolicy(pol)
0410 
0411         self.edPassword.textChanged.connect(self.passwordChanged)
0412         self.edPassword2.textChanged.connect(self.passwordChanged)
0413         StateSaver(self)
0414         self.username = username
0415         self.password = password
0416         self.passwordChanged()
0417         self.edPassword2.setFocus()
0418 
0419     def passwordChanged(self, unusedText=None):
0420         """password changed"""
0421         self.validate()
0422 
0423     def validate(self):
0424         """does the dialog hold valid data?"""
0425         equal = self.edPassword.size(
0426         ) and self.edPassword.text(
0427         ) == self.edPassword2.text(
0428         )
0429         self.button(KDialog.Ok).setEnabled(equal)
0430 
0431     @property
0432     def username(self):
0433         """abstracts the username of the dialog"""
0434         return self.lbUser.text()
0435 
0436     @username.setter
0437     def username(self, username):
0438         """abstracts the username of the dialog"""
0439         self.lbUser.setText(username)
0440 
0441     @property
0442     def password(self):
0443         """abstracts the password of the dialog"""
0444         return self.edPassword.text()
0445 
0446     @password.setter
0447     def password(self, password):
0448         """abstracts the password of the dialog"""
0449         self.edPassword.setText(password)
0450 
0451 
0452 class Connection:
0453 
0454     """creates a connection to server"""
0455 
0456     def __init__(self, client):
0457         self.client = client
0458         self.perspective = None
0459         self.connector = None
0460         self.url = None
0461         self.username = None
0462         self.password = None
0463         self.__ruleset = None
0464         self.dlg = LoginDlg()
0465 
0466     @property
0467     def ruleset(self):
0468         """reader"""
0469         return self.__ruleset
0470 
0471     @ruleset.setter
0472     def ruleset(self, value):
0473         """save changed ruleset as last used ruleset for this server"""
0474         if self.__ruleset != value:
0475             self.__ruleset = value
0476             if value:
0477                 def write():
0478                     """write to database, returns 1 for success"""
0479                     return Query('update server set lastruleset=? where url=?', (value.rulesetId, self.url))
0480                 value.save()
0481                            # make sure we have a valid rulesetId for predefined
0482                            # rulesets
0483                 if not write():
0484                     self.__updateServerInfoInDatabase()
0485                     write()
0486 
0487     def login(self):
0488         """to be called from HumanClient"""
0489         result = DeferredDialog(self.dlg).addCallback(self.__haveLoginData)
0490         result.addCallback(self.__checkExistingConnections)
0491         result.addCallback(self.__startServer)
0492         result.addCallback(self.__loginToServer)
0493         result.addCallback(self.loggedIn)
0494         result.addErrback(self._loginReallyFailed)
0495         if Internal.autoPlay or SingleshotOptions.table or SingleshotOptions.join:
0496             result.clicked()
0497         return result
0498 
0499     def __haveLoginData(self, arguments):
0500         """user entered login data, now try to login to server"""
0501         if not Internal.autoPlay and self.dlg.result() == 0:
0502             self._loginReallyFailed(Failure(CancelledError()))
0503         self.url, self.username, self.password, self.ruleset = arguments
0504         if self.url.isLocalHost:
0505             # we have localhost if we play a Local Game: client and server are identical,
0506             # we have no security concerns about creating a new account
0507             Players.createIfUnknown(self.dlg.cbUser.currentText())
0508 
0509     def __startServer(self, result):
0510         """if needed"""
0511         return self.url.startServer(result)
0512 
0513     def __loginToServer(self, unused=None):
0514         """login to server"""
0515         return self.loginCommand(self.username).addErrback(self._loginFailed)
0516 
0517     def loggedIn(self, perspective):
0518         """successful login on server"""
0519         assert perspective
0520         self.perspective = perspective
0521         self.perspective.notifyOnDisconnect(self.client.serverDisconnected)
0522         self.__updateServerInfoInDatabase()
0523         self.dlg = None
0524         self.pingLater()  # not right now, client.connection is still None
0525         return self
0526 
0527     def __updateServerInfoInDatabase(self):
0528         """we are online. Update table server."""
0529         lasttime = datetime.datetime.now().replace(microsecond=0).isoformat()
0530         with Internal.db:
0531             serverKnown = Query(
0532                 'update server set lastname=?,lasttime=? where url=?',
0533                 (self.username, lasttime, self.url)).rowcount() == 1
0534             if not serverKnown:
0535                 Query(
0536                     'insert into server(url,lastname,lasttime) values(?,?,?)',
0537                     (self.url, self.username, lasttime))
0538         # needed if the server knows our name but our local data base does not:
0539         Players.createIfUnknown(self.username)
0540         playerId = Players.allIds[self.username]
0541         with Internal.db:
0542             if Query('update passwords set password=? where url=? and player=?',
0543                      (self.password, self.url, playerId)).rowcount() == 0:
0544                 Query('insert into passwords(url,player,password) values(?,?,?)',
0545                       (self.url, playerId, self.password))
0546 
0547     def __checkExistingConnections(self, unused=None):
0548         """do we already have a connection to the wanted URL?"""
0549         for client in self.client.humanClients:
0550             if client.connection and client.connection.url == self.url:
0551                 logWarning(
0552                     i18n('You are already connected to server %1', self.url))
0553                 client.tableList.activateWindow()
0554                 raise CancelledError
0555 
0556     def loginCommand(self, username):
0557         """send a login command to server. That might be a normal login
0558         or adduser/deluser/change passwd encoded in the username"""
0559         factory = pb.PBClientFactory(unsafeTracebacks=True)
0560         self.connector = self.url.connect(factory)
0561         utf8Password = self.dlg.password.encode('utf-8')
0562         utf8Username = username.encode('utf-8')
0563         cred = credentials.UsernamePassword(utf8Username, utf8Password)
0564         return factory.login(cred, client=self.client)
0565 
0566     def __adduser(self):
0567         """create a user account"""
0568         assert self.url is not None
0569         if not self.url.isLocalHost:
0570             if not AddUserDialog(self.url,
0571                                  self.dlg.username,
0572                                  self.dlg.password).exec_():
0573                 raise CancelledError
0574             Players.createIfUnknown(self.username)
0575         adduserCmd = SERVERMARK.join(
0576             ['adduser', self.dlg.username, self.dlg.password])
0577         return self.loginCommand(adduserCmd)
0578 
0579     def _loginFailed(self, failure):
0580         """login failed"""
0581         def answered(result):
0582             """user finally answered our question"""
0583             return self.__adduser() if result else Failure(CancelledError())
0584         message = failure.getErrorMessage()
0585         if 'Wrong username' in message:
0586             if self.url.isLocalHost:
0587                 return answered(True)
0588             msg = i18nc('USER is not known on SERVER',
0589                         '%1 is not known on %2, do you want to open an account?', self.dlg.username, self.url.host)
0590             return QuestionYesNo(msg).addCallback(answered)
0591         return self._loginReallyFailed(failure)
0592 
0593     def _loginReallyFailed(self, failure):
0594         """login failed, not fixable by adding missing user"""
0595         msg = None
0596         if not isAlive(Internal.mainWindow):
0597             raise CancelledError
0598         if failure.check(CancelledError):
0599             pass
0600         elif failure.check(twisted.internet.error.TimeoutError):
0601             msg = i18n('Server %1 did not answer', self.url)
0602         elif failure.check(twisted.internet.error.ConnectionRefusedError):
0603             msg = i18n('Server %1 refused connection', self.url)
0604         elif failure.check(twisted.internet.error.ConnectionLost):
0605             msg = i18n('Server %1 does not run a kajongg server', self.url)
0606         elif failure.check(twisted.internet.error.DNSLookupError):
0607             msg = i18n('Address for server %1 cannot be found', self.url)
0608         elif failure.check(twisted.internet.error.ConnectError):
0609             msg = i18n(
0610                 'Login to server %1 failed: You have no network connection',
0611                 self.url)
0612         else:
0613             msg = 'Login to server {} failed: {}/{} Callstack:{}'.format(
0614                 self.url, failure.value.__class__.__name__, failure.getErrorMessage(
0615                 ),
0616                 failure.getTraceback())
0617         # Maybe the server is running but something is wrong with it
0618         if self.url and self.url.useSocket:
0619             if removeIfExists(socketName()):
0620                 logInfo(
0621                     i18n('removed stale socket <filename>%1</filename>', socketName()))
0622             msg += '\n\n\n' + i18n('Please try again')
0623         self.dlg = None
0624         if msg:
0625             logWarning(msg)
0626         raise CancelledError
0627 
0628     def pingLater(self, unusedResult=None):
0629         """ping the server every 5 seconds"""
0630         Internal.reactor.callLater(5, self.ping)
0631 
0632     def ping(self):
0633         """regularly check if server is still there"""
0634         if self.client.connection:
0635             # when pinging starts, we do have a connection and when the
0636             # connection goes away, it does not come back
0637             self.client.callServer(
0638                 'ping').addCallback(
0639                     self.pingLater).addErrback(
0640                         self.client.remote_serverDisconnects)