Merge lp:~diegosarmentero/ubuntuone-control-panel/socket-communication into lp:ubuntuone-control-panel

Proposed by Diego Sarmentero
Status: Merged
Approved by: dobey
Approved revision: 393
Merged at revision: 378
Proposed branch: lp:~diegosarmentero/ubuntuone-control-panel/socket-communication
Merge into: lp:ubuntuone-control-panel
Diff against target: 351 lines (+188/-10)
6 files modified
ubuntuone/controlpanel/gui/qt/gui.py (+16/-0)
ubuntuone/controlpanel/gui/qt/main/tests/test_main.py (+1/-1)
ubuntuone/controlpanel/gui/qt/tests/test_start.py (+11/-0)
ubuntuone/controlpanel/gui/qt/uniqueapp/__init__.py (+56/-0)
ubuntuone/controlpanel/gui/qt/uniqueapp/tests/test_unique_app.py (+99/-4)
ubuntuone/controlpanel/gui/tests/__init__.py (+5/-5)
To merge this branch: bzr merge lp:~diegosarmentero/ubuntuone-control-panel/socket-communication
Reviewer Review Type Date Requested Status
Mike McCracken (community) Approve
dobey (community) Approve
Review via email: mp+132409@code.launchpad.net

Commit message

- Adding socket communication, so the new instances can send messages to the already running instance (LP: #1063927).

Description of the change

Currently this only supports the "switch to tab" option, because it's the only one that is being used, and it didn't make any sense to me to send the other args to the already running application, although it can be easily extended.

You can test this IRL, running an instance of UbuntuOne Client (the one which adds the "Share a File" option will be the best option to test it) adding this branch to the PYTHONPATH of that instance, and choose the "Share a file" option from the menu and see how control panel behaves.

To post a comment you must log in.
378. By Diego Sarmentero

fixing docstring

379. By Diego Sarmentero

executes show normal for not arg cases

Revision history for this message
dobey (dobey) wrote :

This is still not working for me (the tab does switch, but the window doesn't get focus, or raised from minimized state). Also, the latest change seems to result in the unique app support being broken, and I get multiple control panel instances with it.

review: Needs Fixing
380. By Diego Sarmentero

always raise

381. By Diego Sarmentero

improve tests

382. By Diego Sarmentero

fixing activatewindow

383. By Diego Sarmentero

adding showNormal

384. By Diego Sarmentero

removing commented line

385. By Diego Sarmentero

removed unused code

Revision history for this message
dobey (dobey) wrote :

Just updated with the latest revno of this, and it's still opening 2 instances of the control panel for me.

386. By Diego Sarmentero

adding try-except

387. By Diego Sarmentero

force raise

388. By Diego Sarmentero

show on minimized

389. By Diego Sarmentero

improve tests redeability

Revision history for this message
dobey (dobey) wrote :

I still don't like that it doesn't guarantee that focus will be grabbed, or that the window will be raised, but this does seem to be a problem with Qt itself.

review: Approve
390. By Diego Sarmentero

adding comment to explain protocol

Revision history for this message
Mike McCracken (mikemc) wrote :

The code is a little fragile wrt crashes. (This branch hasn't made it any worse, but now's a good time to fix this, since it's short and related.)

If CP crashes and doesn't get to call removeServer, then the socket file is left around and future servers can't be started with the same name, so from then on, every invocation of CP will fail to connect to a server, think it's the unique instance, and start up happily.

I'd like to suggest a couple of improvements:
1. check self.ready, the response from server.listen. If this is false, log server.errorString(). In the current code, we assume server.listen always works and having this log would've made it a lot easier to find the problem I was seeing.

2. call self.cleanup() (or just server.removeServer) before server.listen. In the event that another CP has crashed, leaving around a socket file, this will clean it up so server.listen should always succeed like we want.

This should be ok to call unconditionally, because if we get to this line, we've failed to connect to any server on that socket, and we really should be the unique instance.

NOTE: windows doesn't use socket files, but Qt says that removeServer does nothing on windows, so at least this shouldn't break anything.

With these changes, I am seeing no problems on osx even when I cause a CP crash and verify that there's a zombie socket file before I try re-starting CP.

review: Needs Fixing
391. By Diego Sarmentero

Making sure that the server is cleaned in case of fail

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

> The code is a little fragile wrt crashes. (This branch hasn't made it any
> worse, but now's a good time to fix this, since it's short and related.)
>
> If CP crashes and doesn't get to call removeServer, then the socket file is
> left around and future servers can't be started with the same name, so from
> then on, every invocation of CP will fail to connect to a server, think it's
> the unique instance, and start up happily.
>
> I'd like to suggest a couple of improvements:
> 1. check self.ready, the response from server.listen. If this is false, log
> server.errorString(). In the current code, we assume server.listen always
> works and having this log would've made it a lot easier to find the problem I
> was seeing.
>
> 2. call self.cleanup() (or just server.removeServer) before server.listen. In
> the event that another CP has crashed, leaving around a socket file, this will
> clean it up so server.listen should always succeed like we want.
>
> This should be ok to call unconditionally, because if we get to this line,
> we've failed to connect to any server on that socket, and we really should be
> the unique instance.
>
> NOTE: windows doesn't use socket files, but Qt says that removeServer does
> nothing on windows, so at least this shouldn't break anything.
>
> With these changes, I am seeing no problems on osx even when I cause a CP
> crash and verify that there's a zombie socket file before I try re-starting
> CP.

Done!

392. By Diego Sarmentero

adding more tests

393. By Diego Sarmentero

removing unused import

Revision history for this message
Mike McCracken (mikemc) wrote :

looks good, tests pass on darwin. thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ubuntuone/controlpanel/gui/qt/gui.py'
--- ubuntuone/controlpanel/gui/qt/gui.py 2012-08-20 17:51:54 +0000
+++ ubuntuone/controlpanel/gui/qt/gui.py 2012-11-05 17:30:44 +0000
@@ -61,6 +61,21 @@
61 else:61 else:
62 self.entry = None62 self.entry = None
6363
64 def connect_app_signal(self, app):
65 """Connect App signals."""""
66 app.switch_to.connect(self.switch_to)
67 app.activate_window.connect(self.raise_to_top)
68
69 def raise_to_top(self):
70 """Force the application to raise in top of any other app."""
71 self.activateWindow()
72 self.raise_()
73 if self.isMinimized():
74 self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
75 self.showNormal()
76 self.setWindowFlags(QtCore.Qt.Window)
77 self.showNormal()
78
64 def _setup(self):79 def _setup(self):
65 """Do some extra setupping for the UI."""80 """Do some extra setupping for the UI."""
66 self.ui.control_panel.finished.connect(self.close)81 self.ui.control_panel.finished.connect(self.close)
@@ -128,6 +143,7 @@
128 window.check_updates()143 window.check_updates()
129 window.show()144 window.show()
130 window.raise_()145 window.raise_()
146 window.connect_app_signal(app)
131 else:147 else:
132 window = None148 window = None
133 if with_icon or minimized:149 if with_icon or minimized:
134150
=== modified file 'ubuntuone/controlpanel/gui/qt/main/tests/test_main.py'
--- ubuntuone/controlpanel/gui/qt/main/tests/test_main.py 2012-10-04 20:47:39 +0000
+++ ubuntuone/controlpanel/gui/qt/main/tests/test_main.py 2012-11-05 17:30:44 +0000
@@ -274,7 +274,7 @@
274 """Ensure the new_instance signal is connected."""274 """Ensure the new_instance signal is connected."""
275 main.main([sys.argv[0]])275 main.main([sys.argv[0]])
276 self.assertEqual(self.app.new_instance.target,276 self.assertEqual(self.app.new_instance.target,
277 self.start.window.raise_)277 [self.start.window.raise_])
278278
279 def test_darwin_installs_qt4reactor(self):279 def test_darwin_installs_qt4reactor(self):
280 """Ensure the qt4 reactor is installed when requested."""280 """Ensure the qt4 reactor is installed when requested."""
281281
=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_start.py'
--- ubuntuone/controlpanel/gui/qt/tests/test_start.py 2012-06-28 05:32:24 +0000
+++ ubuntuone/controlpanel/gui/qt/tests/test_start.py 2012-11-05 17:30:44 +0000
@@ -36,6 +36,7 @@
36 def __init__(self):36 def __init__(self):
37 self.args = []37 self.args = []
38 self.updates_checked = defer.Deferred()38 self.updates_checked = defer.Deferred()
39 self.app = None
3940
40 def __call__(self, *args, **kwargs):41 def __call__(self, *args, **kwargs):
41 self.args.append((args, kwargs))42 self.args.append((args, kwargs))
@@ -57,6 +58,10 @@
57 self.updates_checked.callback(None)58 self.updates_checked.callback(None)
58 return self.updates_checked59 return self.updates_checked
5960
61 def connect_app_signal(self, app):
62 """Fake connect_app_signal."""
63 self.app = app
64
6065
61class StartTestCase(TestCase):66class StartTestCase(TestCase):
62 """Test the qt control panel."""67 """Test the qt control panel."""
@@ -124,3 +129,9 @@
124 # a timeout in this test means that the check_updates method129 # a timeout in this test means that the check_updates method
125 # was not called130 # was not called
126 yield self.main_window.updates_checked131 yield self.main_window.updates_checked
132
133 def test_connect_app_signals_is_called(self):
134 """Check that connect_app_signal is properly called on start."""
135 gui.start(close_callback=self.close_cb)
136 app = gui.QtGui.QApplication.instance()
137 self.assertEqual(self.main_window.app, app)
127138
=== modified file 'ubuntuone/controlpanel/gui/qt/uniqueapp/__init__.py'
--- ubuntuone/controlpanel/gui/qt/uniqueapp/__init__.py 2012-04-24 17:59:49 +0000
+++ ubuntuone/controlpanel/gui/qt/uniqueapp/__init__.py 2012-11-05 17:30:44 +0000
@@ -20,15 +20,31 @@
2020
21from PyQt4 import QtNetwork, QtGui, QtCore21from PyQt4 import QtNetwork, QtGui, QtCore
2222
23from ubuntuone.controlpanel.logger import setup_logging
24
25
26logger = setup_logging('uniqueapp')
27
28# Arg with valid value
29SOCKET_MESSAGES = {
30 '--switch-to': ["folders", "share_links", "devices",
31 "settings", "account"],
32}
33
2334
24class UniqueApplication(QtGui.QApplication):35class UniqueApplication(QtGui.QApplication):
2536
26 """A dummy UniqueApplication class."""37 """A dummy UniqueApplication class."""
2738
28 new_instance = QtCore.pyqtSignal()39 new_instance = QtCore.pyqtSignal()
40 switch_to = QtCore.pyqtSignal(unicode)
41 activate_window = QtCore.pyqtSignal()
2942
30 def __init__(self, argv, key):43 def __init__(self, argv, key):
31 super(UniqueApplication, self).__init__(argv)44 super(UniqueApplication, self).__init__(argv)
45 self.mapping_signals = {
46 '--switch-to': self.switch_to,
47 }
32 self.key = key48 self.key = key
33 self.server = QtNetwork.QLocalServer(self)49 self.server = QtNetwork.QLocalServer(self)
34 self.server.newConnection.connect(self.new_instance.emit)50 self.server.newConnection.connect(self.new_instance.emit)
@@ -38,11 +54,51 @@
38 socket.connectToServer(key, QtCore.QIODevice.WriteOnly)54 socket.connectToServer(key, QtCore.QIODevice.WriteOnly)
39 if socket.waitForConnected(500):55 if socket.waitForConnected(500):
40 # Connected, exit56 # Connected, exit
57 self._send_messages(socket, argv)
41 sys.exit()58 sys.exit()
4259
43 # Not connected, start server60 # Not connected, start server
61 self.cleanup()
44 self.ready = self.server.listen(key)62 self.ready = self.server.listen(key)
63 if not self.ready:
64 logger.debug(self.server.errorString())
65 self.server.newConnection.connect(self._process_messages)
4566
46 def cleanup(self):67 def cleanup(self):
47 """Remove the socket when we die."""68 """Remove the socket when we die."""
48 self.server.removeServer(self.key)69 self.server.removeServer(self.key)
70
71 def _send_messages(self, socket, argv):
72 """Send messages to the running application."""
73 # This only take care of those args that are defined in SOCKET_MESSAGES
74 # at this moment just "--switch-to", sending a message to the already
75 # running client with: "arg=arg_value". If we have more than a single
76 # arg, each arg is concatenated with "|".
77 try:
78 data = []
79 size_argv = len(argv)
80 for index in range(2, size_argv):
81 if (argv[index] in SOCKET_MESSAGES and index < size_argv and
82 argv[index + 1] in SOCKET_MESSAGES[argv[index]]):
83 data.append('%s=%s' % (argv[index], argv[index + 1]))
84 message = '|'.join(data)
85 socket.write(message)
86 socket.flush()
87 socket.close()
88 except:
89 # The message couldn't be send through the socket,
90 # but we avoided to open multiple instances of control panel.
91 pass
92
93 def _process_messages(self):
94 """Get the messages from the other instances."""
95 connection = self.server.nextPendingConnection()
96 connection.waitForReadyRead()
97 data = unicode(connection.readAll()).split('|')
98 connection.close()
99 for item in data:
100 item_value = item.split('=')
101 signal = self.mapping_signals.get(item_value[0])
102 if signal:
103 signal.emit(item_value[1])
104 self.activate_window.emit()
49105
=== modified file 'ubuntuone/controlpanel/gui/qt/uniqueapp/tests/test_unique_app.py'
--- ubuntuone/controlpanel/gui/qt/uniqueapp/tests/test_unique_app.py 2012-04-24 17:59:49 +0000
+++ ubuntuone/controlpanel/gui/qt/uniqueapp/tests/test_unique_app.py 2012-11-05 17:30:44 +0000
@@ -19,9 +19,9 @@
19from PyQt4 import QtCore19from PyQt4 import QtCore
20from twisted.internet.defer import inlineCallbacks20from twisted.internet.defer import inlineCallbacks
2121
22from ubuntuone.controlpanel.gui.tests import FakeSignal
22from ubuntuone.controlpanel.gui.qt import uniqueapp23from ubuntuone.controlpanel.gui.qt import uniqueapp
23from ubuntuone.controlpanel.tests import TestCase24from ubuntuone.controlpanel.tests import TestCase
24from ubuntuone.controlpanel.gui.tests import FakeSignal
2525
2626
27#pylint: disable=C010327#pylint: disable=C0103
@@ -32,6 +32,7 @@
32 self.connect_calls = []32 self.connect_calls = []
33 self.connect_timeouts = []33 self.connect_timeouts = []
34 self.connect_succeeds = True34 self.connect_succeeds = True
35 self.message = None
3536
36 def connectToServer(self, *args, **kwargs):37 def connectToServer(self, *args, **kwargs):
37 """Fake connectToServer."""38 """Fake connectToServer."""
@@ -42,18 +43,51 @@
42 self.connect_timeouts.append(timeout)43 self.connect_timeouts.append(timeout)
43 return self.connect_succeeds44 return self.connect_succeeds
4445
46 def write(self, message):
47 """Fake write."""
48 self.message = message
49
50 def flush(self):
51 """Fake flush."""
52
53 def close(self):
54 """Fake close."""
55
56 def waitForReadyRead(self):
57 """Fake waitForReadyRead."""
58
59 def readAll(self):
60 """Fake readAll: return the message."""
61 return self.message
62
4563
46class FakeLocalServer(object):64class FakeLocalServer(object):
4765
48 """A fake QLocalServer."""66 """A fake QLocalServer."""
4967
50 def __init__(self):68 def __init__(self, connected=True):
51 self.newConnection = FakeSignal()69 self.newConnection = FakeSignal()
52 self.listen_args = []70 self.listen_args = []
71 self.socket = None
72 self._removed_key = None
73 self._is_connected = connected
5374
54 def listen(self, *args, **kwargs):75 def listen(self, *args, **kwargs):
55 """Fake listen."""76 """Fake listen."""
56 self.listen_args.append((args, kwargs))77 self.listen_args.append((args, kwargs))
78 return self._is_connected
79
80 def nextPendingConnection(self):
81 """Fake nextPendingConnection."""
82 return self.socket
83
84 def removeServer(self, key):
85 """Fake removeServer."""
86 self._removed_key = key
87
88 def errorString(self):
89 """Fake errorString."""
90 return 'error'
5791
5892
59class FakeApplication(object):93class FakeApplication(object):
@@ -77,6 +111,21 @@
77 self.patch(uniqueapp.UniqueApplication, "aboutToQuit", self.fake_quit)111 self.patch(uniqueapp.UniqueApplication, "aboutToQuit", self.fake_quit)
78 self.patch(uniqueapp.QtGui, "QApplication", FakeApplication)112 self.patch(uniqueapp.QtGui, "QApplication", FakeApplication)
79113
114 def test_cleanup_called_on_init(self):
115 """Check that cleanup is called on initialization."""
116 uniapp = uniqueapp.UniqueApplication([], "key")
117 self.assertEqual("key", uniapp.server._removed_key)
118
119 def test_on_failed_connection(self):
120 """Check the flow of the program on connection fail."""
121 data = []
122 local_server = FakeLocalServer(False)
123 self.patch(uniqueapp.QtNetwork, "QLocalServer",
124 lambda parent: local_server)
125 self.patch(uniqueapp.logger, "debug", data.append)
126 uniqueapp.UniqueApplication([], "key")
127 self.assertEqual(data, ['error'])
128
80 def test_client_socket(self):129 def test_client_socket(self):
81 """Check that the client socket is used correctly."""130 """Check that the client socket is used correctly."""
82 self.local_socket.connect_succeeds = True131 self.local_socket.connect_succeeds = True
@@ -105,10 +154,56 @@
105 app = uniqueapp.UniqueApplication([], "key")154 app = uniqueapp.UniqueApplication([], "key")
106 # Yes, this is ugly. I can't find any other meaningful155 # Yes, this is ugly. I can't find any other meaningful
107 # way to compare them though.156 # way to compare them though.
108 self.assertEqual(str(app.server.newConnection.target.__self__),157 self.assertEqual(str(app.server.newConnection.target[0].__self__),
109 str(app.new_instance))158 str(app.new_instance))
159 self.assertEqual(app.server.newConnection.target[1],
160 app._process_messages)
110161
111 def test_cleanup(self):162 def test_cleanup(self):
112 """Check that cleanup is called with the right key."""163 """Check that cleanup is called with the right key."""
113 app = uniqueapp.UniqueApplication([], "key")164 app = uniqueapp.UniqueApplication([], "key")
114 self.assertEqual(self.fake_quit.target, app.cleanup)165 self.assertEqual(self.fake_quit.target, [app.cleanup])
166
167 def test_send_messages_valid(self):
168 """Check the message is created correctly."""
169 self.local_socket.connect_succeeds = True
170 argv = ['python', 'ubuntuone-control-panel-qt',
171 '--switch-to', 'share_links']
172 uniqueapp.UniqueApplication(argv, "key")
173 expected = "--switch-to=share_links"
174 self.assertEqual(self.local_socket.message, expected)
175
176 def test_send_messages_invalid(self):
177 """Check the message is created correctly."""
178 self.local_socket.connect_succeeds = True
179 argv = ['python', 'ubuntuone-control-panel-qt']
180 uniqueapp.UniqueApplication(argv, "key")
181 expected = ""
182 self.assertEqual(self.local_socket.message, expected)
183
184 def test_process_message_with_message(self):
185 """Check that we are able to parse the message received."""
186 data = []
187 self.local_socket.connect_succeeds = True
188 argv = ['python', 'ubuntuone-control-panel-qt',
189 '--switch-to', 'share_links']
190 app = uniqueapp.UniqueApplication(argv, "key")
191 self.local_server.socket = self.local_socket
192 app.switch_to.connect(data.append)
193 app.activate_window.connect(lambda: data.append(True))
194
195 app.server.newConnection.emit()
196 self.assertEqual(data, ["share_links", True])
197
198 def test_process_message_no_message(self):
199 """Check that we are able to parse the message received."""
200 data = []
201 self.local_socket.connect_succeeds = True
202 argv = ['python', 'ubuntuone-control-panel-qt']
203 app = uniqueapp.UniqueApplication(argv, "key")
204 self.local_server.socket = self.local_socket
205 app.switch_to.connect(data.append)
206 app.activate_window.connect(lambda: data.append(True))
207
208 app.server.newConnection.emit()
209 self.assertEqual(data, [True])
115210
=== modified file 'ubuntuone/controlpanel/gui/tests/__init__.py'
--- ubuntuone/controlpanel/gui/tests/__init__.py 2012-04-24 17:59:49 +0000
+++ ubuntuone/controlpanel/gui/tests/__init__.py 2012-11-05 17:30:44 +0000
@@ -185,17 +185,17 @@
185185
186 def __init__(self, *args, **kwargs):186 def __init__(self, *args, **kwargs):
187 """Initialize."""187 """Initialize."""
188 self.target = None188 self.target = []
189189
190 def connect(self, target):190 def connect(self, target):
191 """Fake connect."""191 """Fake connect."""
192 self.target = target192 self.target.append(target)
193193
194 def disconnect(self, *args):194 def disconnect(self, *args):
195 """Fake disconnect."""195 """Fake disconnect."""
196 self.target = None196 self.target = []
197197
198 def emit(self, *args):198 def emit(self, *args):
199 """Fake emit."""199 """Fake emit."""
200 if self.target:200 for target in self.target:
201 self.target(*args)201 target(*args)

Subscribers

People subscribed via source and target branches