FreeCAD

Форк
0
/
NetworkManager.py 
700 строк · 30.3 Кб
1
# SPDX-License-Identifier: LGPL-2.1-or-later
2
# ***************************************************************************
3
# *                                                                         *
4
# *   Copyright (c) 2022-2023 FreeCAD Project Association                   *
5
# *                                                                         *
6
# *   This file is part of FreeCAD.                                         *
7
# *                                                                         *
8
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
9
# *   under the terms of the GNU Lesser General Public License as           *
10
# *   published by the Free Software Foundation, either version 2.1 of the  *
11
# *   License, or (at your option) any later version.                       *
12
# *                                                                         *
13
# *   FreeCAD is distributed in the hope that it will be useful, but        *
14
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
15
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
16
# *   Lesser General Public License for more details.                       *
17
# *                                                                         *
18
# *   You should have received a copy of the GNU Lesser General Public      *
19
# *   License along with FreeCAD. If not, see                               *
20
# *   <https://www.gnu.org/licenses/>.                                      *
21
# *                                                                         *
22
# ***************************************************************************
23

24
"""
25
#############################################################################
26
#
27
# ABOUT NETWORK MANAGER
28
#
29
# A wrapper around QNetworkAccessManager providing proxy-handling
30
# capabilities, and simplified access to submitting requests from any
31
# application thread.
32
#
33
#
34
# USAGE
35
#
36
# Once imported, this file provides access to a global object called
37
# AM_NETWORK_MANAGER. This is a QObject running on the main thread, but
38
# designed to be interacted with from any other application thread. It
39
# provides two principal methods: submit_unmonitored_get() and
40
# submit_monitored_get(). Use the unmonitored version for small amounts of
41
# data (suitable for caching in RAM, and without a need to show a progress
42
# bar during download), and the monitored version for larger amounts of data.
43
# Both functions take a URL, and return an integer index. That index allows
44
# tracking of the completed request by attaching to the signals completed(),
45
# progress_made(), and progress_complete(). All three provide, as the first
46
# argument to the signal, the index of the request the signal refers to.
47
# Code attached to those signals should filter them to look for the indices
48
# of the requests they care about. Requests may complete in any order.
49
#
50
# A secondary blocking interface is also provided, for very short network
51
# accesses: the blocking_get() function blocks until the network transmission
52
# is complete, directly returning a QByteArray object with the received data.
53
# Do not run on the main GUI thread!
54
"""
55

56
import threading
57
import os
58
import queue
59
import itertools
60
import tempfile
61
import sys
62
from typing import Dict, List, Optional
63

64
try:
65
    import FreeCAD
66

67
    if FreeCAD.GuiUp:
68
        import FreeCADGui
69

70
    HAVE_FREECAD = True
71
    translate = FreeCAD.Qt.translate
72
except ImportError:
73
    # For standalone testing support working without the FreeCAD import
74
    HAVE_FREECAD = False
75

76
from PySide import QtCore
77

78
if FreeCAD.GuiUp:
79
    from PySide import QtWidgets
80

81

82
# This is the global instance of the NetworkManager that outside code
83
# should access
84
AM_NETWORK_MANAGER = None
85

86
HAVE_QTNETWORK = True
87
try:
88
    from PySide import QtNetwork
89
except ImportError:
90
    if HAVE_FREECAD:
91
        FreeCAD.Console.PrintError(
92
            translate(
93
                "AddonsInstaller",
94
                'Could not import QtNetwork -- it does not appear to be installed on your system. Your provider may have a package for this dependency (often called "python3-pyside2.qtnetwork")',
95
            )
96
            + "\n"
97
        )
98
    else:
99
        print("Could not import QtNetwork, unable to test this file.")
100
        sys.exit(1)
101
    HAVE_QTNETWORK = False
102

103
if HAVE_QTNETWORK:
104

105
    # Added in Qt 5.15
106
    if hasattr(QtNetwork.QNetworkRequest, "DefaultTransferTimeoutConstant"):
107
        timeoutConstant = QtNetwork.QNetworkRequest.DefaultTransferTimeoutConstant
108
        if hasattr(timeoutConstant, "value"):
109
            # Qt 6 changed the timeout constant to have a 'value' attribute.
110
            # The function setTransferTimeout does not accept
111
            # DefaultTransferTimeoutConstant of type
112
            # QtNetwork.QNetworkRequest.TransferTimeoutConstant any
113
            # longer but only an int.
114
            default_timeout = timeoutConstant.value
115
        else:
116
            # In Qt 5.15 we can use the timeoutConstant as is.
117
            default_timeout = timeoutConstant
118
    else:
119
        default_timeout = 30000
120

121
    class QueueItem:
122
        """A container for information about an item in the network queue."""
123

124
        def __init__(self, index: int, request: QtNetwork.QNetworkRequest, track_progress: bool):
125
            self.index = index
126
            self.request = request
127
            self.original_url = request.url()
128
            self.track_progress = track_progress
129

130
    class NetworkManager(QtCore.QObject):
131
        """A single global instance of NetworkManager is instantiated and stored as
132
        AM_NETWORK_MANAGER. Outside threads should send GET requests to this class by
133
        calling the submit_unmonitored_request() or submit_monitored_request() function,
134
        as needed. See the documentation of those functions for details."""
135

136
        # Connect to complete for requests with no progress monitoring (e.g. small amounts of data)
137
        completed = QtCore.Signal(
138
            int, int, QtCore.QByteArray
139
        )  # Index, http response code, received data (if any)
140

141
        # Connect to progress_made and progress_complete for large amounts of data, which get buffered into a temp file
142
        # That temp file should be deleted when your code is done with it
143
        progress_made = QtCore.Signal(int, int, int)  # Index, bytes read, total bytes (may be None)
144

145
        progress_complete = QtCore.Signal(
146
            int, int, os.PathLike
147
        )  # Index, http response code, filename
148

149
        __request_queued = QtCore.Signal()
150

151
        def __init__(self):
152
            super().__init__()
153

154
            self.counting_iterator = itertools.count()
155
            self.queue = queue.Queue()
156
            self.__last_started_index = 0
157
            self.__abort_when_found: List[int] = []
158
            self.replies: Dict[int, QtNetwork.QNetworkReply] = {}
159
            self.file_buffers = {}
160

161
            # We support an arbitrary number of threads using synchronous GET calls:
162
            self.synchronous_lock = threading.Lock()
163
            self.synchronous_complete: Dict[int, bool] = {}
164
            self.synchronous_result_data: Dict[int, QtCore.QByteArray] = {}
165

166
            # Make sure we exit nicely on quit
167
            if QtCore.QCoreApplication.instance() is not None:
168
                QtCore.QCoreApplication.instance().aboutToQuit.connect(self.__aboutToQuit)
169

170
            # Create the QNAM on this thread:
171
            self.QNAM = QtNetwork.QNetworkAccessManager()
172
            self.QNAM.proxyAuthenticationRequired.connect(self.__authenticate_proxy)
173
            self.QNAM.authenticationRequired.connect(self.__authenticate_resource)
174
            self.QNAM.setRedirectPolicy(QtNetwork.QNetworkRequest.ManualRedirectPolicy)
175

176
            qnam_cache = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.CacheLocation)
177
            os.makedirs(qnam_cache, exist_ok=True)
178
            self.diskCache = QtNetwork.QNetworkDiskCache()
179
            self.diskCache.setCacheDirectory(qnam_cache)
180
            self.QNAM.setCache(self.diskCache)
181

182
            self.monitored_connections: List[int] = []
183
            self._setup_proxy()
184

185
            # A helper connection for our blocking interface
186
            self.completed.connect(self.__synchronous_process_completion)
187

188
            # Set up our worker connection
189
            self.__request_queued.connect(self.__setup_network_request)
190

191
        def _setup_proxy(self):
192
            """Set up the proxy based on user preferences or prompts on command line"""
193

194
            # Set up the proxy, if necessary:
195
            if HAVE_FREECAD:
196
                (
197
                    noProxyCheck,
198
                    systemProxyCheck,
199
                    userProxyCheck,
200
                    proxy_string,
201
                ) = self._setup_proxy_freecad()
202
            else:
203
                (
204
                    noProxyCheck,
205
                    systemProxyCheck,
206
                    userProxyCheck,
207
                    proxy_string,
208
                ) = self._setup_proxy_standalone()
209

210
            if noProxyCheck:
211
                pass
212
            elif systemProxyCheck:
213
                query = QtNetwork.QNetworkProxyQuery(
214
                    QtCore.QUrl("https://github.com/FreeCAD/FreeCAD")
215
                )
216
                proxy = QtNetwork.QNetworkProxyFactory.systemProxyForQuery(query)
217
                if proxy and proxy[0]:
218
                    self.QNAM.setProxy(proxy[0])  # This may still be QNetworkProxy.NoProxy
219
            elif userProxyCheck:
220
                host, _, port_string = proxy_string.rpartition(":")
221
                try:
222
                    port = 0 if not port_string else int(port_string)
223
                except ValueError:
224
                    FreeCAD.Console.PrintError(
225
                        translate(
226
                            "AddonsInstaller",
227
                            "Failed to convert the specified proxy port '{}' to a port number",
228
                        ).format(port_string)
229
                        + "\n"
230
                    )
231
                    port = 0
232
                # For now assume an HttpProxy, but eventually this should be a parameter
233
                proxy = QtNetwork.QNetworkProxy(QtNetwork.QNetworkProxy.HttpProxy, host, port)
234
                self.QNAM.setProxy(proxy)
235

236
        def _setup_proxy_freecad(self):
237
            """If we are running within FreeCAD, this uses the config data to set up the proxy"""
238
            noProxyCheck = True
239
            systemProxyCheck = False
240
            userProxyCheck = False
241
            proxy_string = ""
242
            pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
243
            noProxyCheck = pref.GetBool("NoProxyCheck", noProxyCheck)
244
            systemProxyCheck = pref.GetBool("SystemProxyCheck", systemProxyCheck)
245
            userProxyCheck = pref.GetBool("UserProxyCheck", userProxyCheck)
246
            proxy_string = pref.GetString("ProxyUrl", "")
247

248
            # Add some error checking to the proxy setup, since for historical reasons they
249
            # are independent booleans, rather than an enumeration:
250
            option_count = [noProxyCheck, systemProxyCheck, userProxyCheck].count(True)
251
            if option_count != 1:
252
                FreeCAD.Console.PrintWarning(
253
                    translate(
254
                        "AddonsInstaller",
255
                        "Parameter error: mutually exclusive proxy options set. Resetting to default.",
256
                    )
257
                    + "\n"
258
                )
259
                noProxyCheck = True
260
                systemProxyCheck = False
261
                userProxyCheck = False
262
                pref.SetBool("NoProxyCheck", noProxyCheck)
263
                pref.SetBool("SystemProxyCheck", systemProxyCheck)
264
                pref.SetBool("UserProxyCheck", userProxyCheck)
265

266
            if userProxyCheck and not proxy_string:
267
                FreeCAD.Console.PrintWarning(
268
                    translate(
269
                        "AddonsInstaller",
270
                        "Parameter error: user proxy indicated, but no proxy provided. Resetting to default.",
271
                    )
272
                    + "\n"
273
                )
274
                noProxyCheck = True
275
                userProxyCheck = False
276
                pref.SetBool("NoProxyCheck", noProxyCheck)
277
                pref.SetBool("UserProxyCheck", userProxyCheck)
278
            return noProxyCheck, systemProxyCheck, userProxyCheck, proxy_string
279

280
        def _setup_proxy_standalone(self):
281
            """If we are NOT running inside FreeCAD, prompt the user for proxy information"""
282
            noProxyCheck = True
283
            systemProxyCheck = False
284
            userProxyCheck = False
285
            proxy_string = ""
286
            print("Please select a proxy type:")
287
            print("1) No proxy")
288
            print("2) Use system proxy settings")
289
            print("3) Custom proxy settings")
290
            result = input("Choice: ")
291
            if result == "1":
292
                pass
293
            elif result == "2":
294
                noProxyCheck = False
295
                systemProxyCheck = True
296
            elif result == "3":
297
                noProxyCheck = False
298
                userProxyCheck = True
299
                proxy_string = input("Enter your proxy server (host:port): ")
300
            else:
301
                print(f"Got {result}, expected 1, 2, or 3.")
302
                app.quit()
303
            return noProxyCheck, systemProxyCheck, userProxyCheck, proxy_string
304

305
        def __aboutToQuit(self):
306
            """Called when the application is about to quit. Not currently used."""
307

308
        def __setup_network_request(self):
309
            """Get the next request off the queue and launch it."""
310
            try:
311
                item = self.queue.get_nowait()
312
                if item:
313
                    if item.index in self.__abort_when_found:
314
                        self.__abort_when_found.remove(item.index)
315
                        return  # Do not do anything with this item, it's been aborted...
316
                    if item.track_progress:
317
                        self.monitored_connections.append(item.index)
318
                    self.__launch_request(item.index, item.request)
319
            except queue.Empty:
320
                pass
321

322
        def __launch_request(self, index: int, request: QtNetwork.QNetworkRequest) -> None:
323
            """Given a network request, ask the QNetworkAccessManager to begin processing it."""
324
            reply = self.QNAM.get(request)
325
            self.replies[index] = reply
326

327
            self.__last_started_index = index
328
            reply.finished.connect(self.__reply_finished)
329
            reply.sslErrors.connect(self.__on_ssl_error)
330
            if index in self.monitored_connections:
331
                reply.readyRead.connect(self.__ready_to_read)
332
                reply.downloadProgress.connect(self.__download_progress)
333

334
        def submit_unmonitored_get(
335
            self,
336
            url: str,
337
            timeout_ms: int = default_timeout,
338
        ) -> int:
339
            """Adds this request to the queue, and returns an index that can be used by calling code
340
            in conjunction with the completed() signal to handle the results of the call. All data is
341
            kept in memory, and the completed() call includes a direct handle to the bytes returned. It
342
            is not called until the data transfer has finished and the connection is closed."""
343

344
            current_index = next(self.counting_iterator)  # A thread-safe counter
345
            # Use a queue because we can only put things on the QNAM from the main event loop thread
346
            self.queue.put(
347
                QueueItem(
348
                    current_index, self.__create_get_request(url, timeout_ms), track_progress=False
349
                )
350
            )
351
            self.__request_queued.emit()
352
            return current_index
353

354
        def submit_monitored_get(
355
            self,
356
            url: str,
357
            timeout_ms: int = default_timeout,
358
        ) -> int:
359
            """Adds this request to the queue, and returns an index that can be used by calling code
360
            in conjunction with the progress_made() and progress_completed() signals to handle the
361
            results of the call. All data is cached to disk, and progress is reported periodically
362
            as the underlying QNetworkReply reports its progress. The progress_completed() signal
363
            contains a path to a temporary file with the stored data. Calling code should delete this
364
            file when done with it (or move it into its final place, etc.)."""
365

366
            current_index = next(self.counting_iterator)  # A thread-safe counter
367
            # Use a queue because we can only put things on the QNAM from the main event loop thread
368
            self.queue.put(
369
                QueueItem(
370
                    current_index, self.__create_get_request(url, timeout_ms), track_progress=True
371
                )
372
            )
373
            self.__request_queued.emit()
374
            return current_index
375

376
        def blocking_get(
377
            self,
378
            url: str,
379
            timeout_ms: int = default_timeout,
380
        ) -> Optional[QtCore.QByteArray]:
381
            """Submits a GET request to the QNetworkAccessManager and block until it is complete"""
382

383
            current_index = next(self.counting_iterator)  # A thread-safe counter
384
            with self.synchronous_lock:
385
                self.synchronous_complete[current_index] = False
386

387
            self.queue.put(
388
                QueueItem(
389
                    current_index, self.__create_get_request(url, timeout_ms), track_progress=False
390
                )
391
            )
392
            self.__request_queued.emit()
393
            while True:
394
                if QtCore.QThread.currentThread().isInterruptionRequested():
395
                    return None
396
                QtCore.QCoreApplication.processEvents()
397
                with self.synchronous_lock:
398
                    if self.synchronous_complete[current_index]:
399
                        break
400

401
            with self.synchronous_lock:
402
                self.synchronous_complete.pop(current_index)
403
                if current_index in self.synchronous_result_data:
404
                    return self.synchronous_result_data.pop(current_index)
405
                return None
406

407
        def __synchronous_process_completion(
408
            self, index: int, code: int, data: QtCore.QByteArray
409
        ) -> None:
410
            """Check the return status of a completed process, and handle its returned data (if
411
            any)."""
412
            with self.synchronous_lock:
413
                if index in self.synchronous_complete:
414
                    if code == 200:
415
                        self.synchronous_result_data[index] = data
416
                    else:
417
                        FreeCAD.Console.PrintWarning(
418
                            translate(
419
                                "AddonsInstaller",
420
                                "Addon Manager: Unexpected {} response from server",
421
                            ).format(code)
422
                            + "\n"
423
                        )
424
                    self.synchronous_complete[index] = True
425

426
        @staticmethod
427
        def __create_get_request(url: str, timeout_ms: int) -> QtNetwork.QNetworkRequest:
428
            """Construct a network request to a given URL"""
429
            request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
430
            request.setAttribute(
431
                QtNetwork.QNetworkRequest.RedirectPolicyAttribute,
432
                QtNetwork.QNetworkRequest.ManualRedirectPolicy,
433
            )
434
            request.setAttribute(QtNetwork.QNetworkRequest.CacheSaveControlAttribute, True)
435
            request.setAttribute(
436
                QtNetwork.QNetworkRequest.CacheLoadControlAttribute,
437
                QtNetwork.QNetworkRequest.PreferNetwork,
438
            )
439
            if hasattr(request, "setTransferTimeout"):
440
                # Added in Qt 5.15
441
                # In Qt 5, the function setTransferTimeout seems to accept
442
                # DefaultTransferTimeoutConstant of type
443
                # PySide2.QtNetwork.QNetworkRequest.TransferTimeoutConstant,
444
                # whereas in Qt 6, the function seems to only accept an
445
                # integer.
446
                request.setTransferTimeout(timeout_ms)
447
            return request
448

449
        def abort_all(self):
450
            """Abort ALL network calls in progress, including clearing the queue"""
451
            for reply in self.replies.values():
452
                if reply.abort().isRunning():
453
                    reply.abort()
454
            while True:
455
                try:
456
                    self.queue.get()
457
                    self.queue.task_done()
458
                except queue.Empty:
459
                    break
460

461
        def abort(self, index: int):
462
            """Abort a specific request"""
463
            if index in self.replies and self.replies[index].isRunning():
464
                self.replies[index].abort()
465
            elif index < self.__last_started_index:
466
                # It's still in the queue. Mark it for later destruction.
467
                self.__abort_when_found.append(index)
468

469
        def __authenticate_proxy(
470
            self,
471
            reply: QtNetwork.QNetworkProxy,
472
            authenticator: QtNetwork.QAuthenticator,
473
        ):
474
            """If proxy authentication is required, attempt to authenticate. If the GUI is running this displays
475
            a window asking for credentials. If the GUI is not running, it prompts on the command line.
476
            """
477
            if HAVE_FREECAD and FreeCAD.GuiUp:
478
                proxy_authentication = FreeCADGui.PySideUic.loadUi(
479
                    os.path.join(os.path.dirname(__file__), "proxy_authentication.ui")
480
                )
481
                # Show the right labels, etc.
482
                proxy_authentication.labelProxyAddress.setText(f"{reply.hostName()}:{reply.port()}")
483
                if authenticator.realm():
484
                    proxy_authentication.labelProxyRealm.setText(authenticator.realm())
485
                else:
486
                    proxy_authentication.labelProxyRealm.hide()
487
                    proxy_authentication.labelRealmCaption.hide()
488
                result = proxy_authentication.exec()
489
                if result == QtWidgets.QDialogButtonBox.Ok:
490
                    authenticator.setUser(proxy_authentication.lineEditUsername.text())
491
                    authenticator.setPassword(proxy_authentication.lineEditPassword.text())
492
            else:
493
                username = input("Proxy username: ")
494
                import getpass
495

496
                password = getpass.getpass()
497
                authenticator.setUser(username)
498
                authenticator.setPassword(password)
499

500
        def __authenticate_resource(
501
            self,
502
            _reply: QtNetwork.QNetworkReply,
503
            _authenticator: QtNetwork.QAuthenticator,
504
        ):
505
            """Unused."""
506

507
        def __on_ssl_error(self, reply: str, errors: List[str] = None):
508
            """Called when an SSL error occurs: prints the error information."""
509
            if HAVE_FREECAD:
510
                FreeCAD.Console.PrintWarning(
511
                    translate("AddonsInstaller", "Error with encrypted connection") + "\n:"
512
                )
513
                FreeCAD.Console.PrintWarning(reply)
514
                if errors is not None:
515
                    for error in errors:
516
                        FreeCAD.Console.PrintWarning(error)
517
            else:
518
                print("Error with encrypted connection")
519
                if errors is not None:
520
                    for error in errors:
521
                        print(error)
522

523
        def __download_progress(self, bytesReceived: int, bytesTotal: int) -> None:
524
            """Monitors download progress and emits a progress_made signal"""
525
            sender = self.sender()
526
            if not sender:
527
                return
528
            for index, reply in self.replies.items():
529
                if reply == sender:
530
                    self.progress_made.emit(index, bytesReceived, bytesTotal)
531
                    return
532

533
        def __ready_to_read(self) -> None:
534
            """Called when data is available, this reads that data."""
535
            sender = self.sender()
536
            if not sender:
537
                return
538

539
            for index, reply in self.replies.items():
540
                if reply == sender:
541
                    self.__data_incoming(index, reply)
542
                    return
543

544
        def __data_incoming(self, index: int, reply: QtNetwork.QNetworkReply) -> None:
545
            """Read incoming data and attach it to a data object"""
546
            if not index in self.replies:
547
                # We already finished this reply, this is a vestigial signal
548
                return
549
            buffer = reply.readAll()
550
            if not index in self.file_buffers:
551
                f = tempfile.NamedTemporaryFile("wb", delete=False)
552
                self.file_buffers[index] = f
553
            else:
554
                f = self.file_buffers[index]
555
            try:
556
                f.write(buffer.data())
557
            except OSError as e:
558
                if HAVE_FREECAD:
559
                    FreeCAD.Console.PrintError(f"Network Manager internal error: {str(e)}")
560
                else:
561
                    print(f"Network Manager internal error: {str(e)}")
562

563
        def __reply_finished(self) -> None:
564
            """Called when a reply has been completed: this makes sure the data has been read and
565
            any notifications have been called."""
566
            reply = self.sender()
567
            if not reply:
568
                # This can happen during a cancellation operation: silently do nothing
569
                return
570

571
            index = None
572
            for key, value in self.replies.items():
573
                if reply == value:
574
                    index = key
575
                    break
576
            if index is None:
577
                return
578

579
            response_code = reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute)
580
            redirect_codes = [301, 302, 303, 305, 307, 308]
581
            if response_code in redirect_codes:  # This is a redirect
582
                timeout_ms = default_timeout
583
                if hasattr(reply, "request"):
584
                    request = reply.request()
585
                    if hasattr(request, "transferTimeout"):
586
                        timeout_ms = request.transferTimeout()
587
                new_url = reply.attribute(QtNetwork.QNetworkRequest.RedirectionTargetAttribute)
588
                self.__launch_request(index, self.__create_get_request(new_url, timeout_ms))
589
                return  # The task is not done, so get out of this method now
590
            if reply.error() != QtNetwork.QNetworkReply.NetworkError.OperationCanceledError:
591
                # It this was not a timeout, make sure we mark the queue task done
592
                self.queue.task_done()
593
            if reply.error() == QtNetwork.QNetworkReply.NetworkError.NoError:
594
                if index in self.monitored_connections:
595
                    # Make sure to read any remaining data
596
                    self.__data_incoming(index, reply)
597
                    self.monitored_connections.remove(index)
598
                    f = self.file_buffers[index]
599
                    f.close()
600
                    self.progress_complete.emit(index, response_code, f.name)
601
                else:
602
                    data = reply.readAll()
603
                    self.completed.emit(index, response_code, data)
604
            else:
605
                if index in self.monitored_connections:
606
                    self.progress_complete.emit(index, response_code, "")
607
                else:
608
                    self.completed.emit(index, response_code, None)
609
            self.replies.pop(index)
610

611
else:  # HAVE_QTNETWORK is false:
612

613
    class NetworkManager(QtCore.QObject):
614
        """A dummy class to enable an offline mode when the QtNetwork package is not yet installed"""
615

616
        completed = QtCore.Signal(
617
            int, int, bytes
618
        )  # Emitted as soon as the request is made, with a connection failed error
619
        progress_made = QtCore.Signal(int, int, int)  # Never emitted, no progress is made here
620
        progress_complete = QtCore.Signal(
621
            int, int, os.PathLike
622
        )  # Emitted as soon as the request is made, with a connection failed error
623

624
        def __init__(self):
625
            super().__init__()
626
            self.monitored_queue = queue.Queue()
627
            self.unmonitored_queue = queue.Queue()
628

629
        def submit_unmonitored_request(self, _) -> int:
630
            """Returns a fake index that can be used for testing -- nothing is actually queued"""
631
            current_index = next(itertools.count())
632
            self.unmonitored_queue.put(current_index)
633
            return current_index
634

635
        def submit_monitored_request(self, _) -> int:
636
            """Returns a fake index that can be used for testing -- nothing is actually queued"""
637
            current_index = next(itertools.count())
638
            self.monitored_queue.put(current_index)
639
            return current_index
640

641
        def blocking_get(self, _: str) -> QtCore.QByteArray:
642
            """No operation - returns None immediately"""
643
            return None
644

645
        def abort_all(
646
            self,
647
        ):
648
            """There is nothing to abort in this case"""
649

650
        def abort(self, _):
651
            """There is nothing to abort in this case"""
652

653

654
def InitializeNetworkManager():
655
    """Called once at the beginning of program execution to create the appropriate manager object"""
656
    global AM_NETWORK_MANAGER
657
    if AM_NETWORK_MANAGER is None:
658
        AM_NETWORK_MANAGER = NetworkManager()
659

660

661
if __name__ == "__main__":
662
    app = QtCore.QCoreApplication()
663

664
    InitializeNetworkManager()
665

666
    count = 0
667

668
    # For testing, create several network requests and send them off in quick succession:
669
    # (Choose small downloads, no need for significant data)
670
    urls = [
671
        "https://api.github.com/zen",
672
        "http://climate.ok.gov/index.php/climate/rainfall_table/local_data",
673
        "https://tigerweb.geo.census.gov/arcgis/rest/services/TIGERweb/AIANNHA/MapServer",
674
    ]
675

676
    def handle_completion(index: int, code: int, data):
677
        """Attached to the completion signal, prints diagnostic information about the network access"""
678
        global count
679
        if code == 200:
680
            print(f"For request {index+1}, response was {data.size()} bytes.", flush=True)
681
        else:
682
            print(
683
                f"For request {index+1}, request failed with HTTP result code {code}",
684
                flush=True,
685
            )
686

687
        count += 1
688
        if count >= len(urls):
689
            print("Shutting down...", flush=True)
690
            AM_NETWORK_MANAGER.requestInterruption()
691
            AM_NETWORK_MANAGER.wait(5000)
692
            app.quit()
693

694
    AM_NETWORK_MANAGER.completed.connect(handle_completion)
695
    for test_url in urls:
696
        AM_NETWORK_MANAGER.submit_unmonitored_get(test_url)
697

698
    app.exec_()
699

700
    print("Done with all requests.")
701

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.