From 645e1b3eac6d55fdfe48e4442ef88a016cdad192 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 15 Oct 2022 21:31:06 -0700 Subject: [PATCH] v1.7.11 --- .efrocachemap | 90 ++++----- .idea/dictionaries/ericf.xml | 8 + CHANGELOG.md | 8 +- assets/src/ba_data/python/._ba_sources_hash | 2 +- assets/src/ba_data/python/_ba.py | 5 +- assets/src/ba_data/python/ba/__init__.py | 2 + assets/src/ba_data/python/ba/_accountv2.py | 1 + assets/src/ba_data/python/ba/_bootstrap.py | 2 +- assets/src/ba_data/python/ba/_plugin.py | 7 + assets/src/ba_data/python/ba/_workspace.py | 12 +- .../python/bastd/ui/settings/plugins.py | 51 +++++- assets/src/ba_data/python/bastd/ui/url.py | 152 ++++++++------- .../.idea/dictionaries/ericf.xml | 8 + src/ballistica/ballistica.cc | 2 +- .../python/methods/python_methods_ui.cc | 19 +- tests/test_efro/test_rpc.py | 4 +- tools/batools/assetstaging.py | 9 +- tools/batools/pcommand.py | 11 +- tools/batools/project.py | 3 +- tools/batools/pythonenumsmodule.py | 2 +- tools/efro/debug.py | 87 ++++++++- tools/efro/log.py | 4 +- tools/efro/rpc.py | 173 +++++++++++------- 23 files changed, 420 insertions(+), 242 deletions(-) diff --git a/.efrocachemap b/.efrocachemap index f2d000a2..d2780876 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4001,52 +4001,52 @@ "assets/build/workspace/onslaughtplug.py": "https://files.ballistica.net/cache/ba1/08/ed/d671c39a3ece6362a6d985112c8e", "assets/build/workspace/runaroundplug.py": "https://files.ballistica.net/cache/ba1/4d/71/1292911f5369bdb83ef6a34921c0", "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", - "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34", + "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/1c/77/ac670a5118abdf8a7687af0e159b", "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", - "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c0/b2/ff130457d068b900d961e2354adc", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3e/6f/e5eb8e0646c8a56d81a744b83033", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bc/a0/8801adcaae0ebf8e065e50df88e5", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/60/0d/a08147b59d213ef1a61b6a393997", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c0/16/798fbe8f0e563c2a1158f612df18", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b0/f6/6db11e5da130e85b59c1e99f34d2", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c1/95/30cb68910669c012bf5f3e0f5d8d", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9d/53/124cd00020647d0ea73114368d9f", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e4/a0/14debf6938db7d2db3e6a66dd51a", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/af/4d/9d01c0b54f17152439ff229585a8", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d3/ef/35e69f1dc7e0d2ec40814169ee16", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/46/77/675fc277cb7d0b4362b4ad002dbd", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/75/de/899f4b4c7864a775a918735d2050", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ac/31/75bab23c1617aaf9c07c428ee477", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/18/0c/ff933dc5901512c7bcd8fc9d0c2a", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5d/ad/992a5b10cfd2fb13ce402d64e672", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/3f/3e/d7ffee3c48c5e7e61432274c303c", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d2/aa/ac8b35f42058ce4accd33f689366", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/f7/d9/037846d00c1491e4d09609cbbec6", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/ae/47/d79b5b8c9168944641514072c56c", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a0/2a/df32b0c34525af7de212e9a2cf30", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/28/f103463730f785ba4fe6a91f8943", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/29/4a/0df587009671edf52d6939c8f966", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c8/df/12ea6b0a703568e52a84001ca2db", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fb/29/5a74e41f2aa46aaa4ebb62b2612f", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d0/89/1995ef2194458c78c546698de761", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7c/7e/01b2fbad142edaac0c0af7853874", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9c/2f/3d0f4b2439b7da9def68597c14c8", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/26/d8/2a352f23af0375e97752da915d38", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a6/0f/72032ea268956736a6eabce738c3", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/92/5d/a8e2b0137a4e67821fc0c233704f", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/01/7c/c21ceaa90f0a0794979056c0a5dc", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/60/4a/238ad7b1d8b5cac2376e58cdd6de", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/27/b6/676cee2d1996ab592e1006ba4721", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b1/fe/bf79be22fc78040b28b60aa2a8e3", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/37/3e/689be0ffce83ac46775c5dec6446", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/ff/e0/707a7f27737a95cd484a8105cb1e", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/22/40/9c60cea175b1685b6ceb49825753", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/b7/2a/6bf91a3cce672678d4313535e5f8", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/31/97/77ee5fbf99340b8a5368b691fffb", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/de/9c/7232b3307214ae9b18c7c7673afa", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/ec/e8/a98b515b312ab875ee58464050e8", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/21/f0/41a934523b6b34ba5f9e9cee96c6", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/43/97/158153da90bd41bba18cd245a6be", + "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e8/7b/1dbe9dbf1819b08421362d3203cc", + "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8a/16/4ab8922ba3524473860b9e3c3fd0", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ee/55/a23141dd32ae2849664267fc24b3", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/af/c7/5e8d3e6873382d7f9b2e6ecb8273", + "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2e/8e/b4561b47b90255ac660612516890", + "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/05/c0/dd90b84f7fe0a31fdaebd4e5532e", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3d/29/52ca62a01b5df84af8c2578646d2", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/88/17/f7891bf7ba6158598260160df7ba", + "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/13/7c/ec7e0bebd3608bde2dc6434cc3d1", + "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d3/16/ccefa4342be01801c1e152f76aee", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ef/45/e1f2aa107b62245c23ae613022fe", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/aa/d7/65771c73cea67f0549c1a5f69b0b", + "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/56/a2/f0ad7ca2cd6a99c33dab76b6f90a", + "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/48/a6/dbe19639b711ff8c5030eec054b4", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0f/c8/b332d9bc4dd72347613a0a50aacf", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a3/9b/26a032924048ae2a2d94eec53c78", + "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b4/04/7be7602bcb91eee479402875feb7", + "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/52/ba/0aba5f9247a0e7af5738d6487df3", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/42/45/4cc46885f0d29c62b4b08be440c6", + "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/4d/a0/37cfb3c48f7a7026638d68e07855", + "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fc/d7/66e38e026edcd53ab470c74d7224", + "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9a/42/7ddd583f9c874837a4a7f902953f", + "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/22/4a/7771e45d6f8316d4c738d186062e", + "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/44/6b/09b134e2f9b8bd276ff6351b74ee", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4e/d2/43048339ea0b88895e0e6c206f20", + "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/78/cd/021300c2cacc307bdda1763d9845", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f4/b2/9d68ec3b7c0981efed766e0dfdd7", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1a/f2/4bbf3362f5735492e1bb27606013", + "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e6/df/d9e68d3b7b1593a3400b1d2b14c7", + "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e0/e0/58c6709938ed40089213be8c7e37", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/29/d9/2c1234aaccc1c0d50962d7d61f9f", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5d/ae/45084efc0b90744bcfed29112223", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/62/92/0bd650555ce0042a9c56c4a0751f", + "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/be/8b/c9423cd7566404e2e74d0db50506", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cf/15/c8ce7acd9c5a973d6886771c099e", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/fa/59/26dc8860ecd9644b725e9428a312", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/91/39/6ff63efde3ae2c98474712f78b56", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/18/f6/bdf379aadd643ac3ae46094d8545", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/91/c3/9fe6f6916de5e8086f3e1c9c0c1f", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/41/c0/730d363fbccc0639ac24f7318d32", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/18/82/af34d2eececb1b43018d3016f54c", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/86/2f/a7abcfde5205ba2fdbf844decffb", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/a7/ab/854144e721101e2eb404ff0a6d51", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/f4/92/e786733e776c2b63e9f5340f88d8", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/c0/32/b7907e3859a5c5013a3d97b6b523", "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/2d/4f/f4fe67827f36cd59cd5193333a02", "src/ballistica/generated/python_embedded/bootstrap_monolithic.inc": "https://files.ballistica.net/cache/ba1/ef/c1/aa5f1aa10af89f5c0b1e616355fd" diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index e82d8857..f7ed8256 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1063,6 +1063,7 @@ getopt getplayer getpt + getr getrefs getremote getres @@ -1738,6 +1739,7 @@ objid objname objs + objsizes objt objtoyaml objtype @@ -1851,6 +1853,7 @@ pdoc pedit peditui + peerinfo peername pentry perma @@ -1972,6 +1975,7 @@ printobjects printpaths printrefs + printsizes printtypes priv privatetab @@ -2335,6 +2339,8 @@ sitebuiltins skey sline + slist + slists slval smag smallscale @@ -2646,6 +2652,8 @@ toplevel toplevelfiles totaldudes + totalobjmb + totalobjsize totalpts totaltime totalwaves diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d517798..b28a7eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ -### 1.7.11 (build 20899, api 7, 2022-10-10) +### 1.7.11 (build 20909, api 7, 2022-10-15) - Switched our Python autoformatting from yapf to black. The yapf project seems to be mostly dead whereas black seems to be thriving. The final straw was yapf not supporting the `match` statement in Python 3.10. +- Added `has_settings_ui()` and `show_settings_ui()` methods to ba.Plugin. Plugins can use these to enable a 'Settings' button next to them in the plugin manager that brings up a custom UI. +- Fixed workspaces functionality, which I broke rather terribly in 1.7.10 when I forgot to test it against all the internal changes there (sorry). Note that there is a slight downside to having workspace syncing enabled now in that it turns off the fast-v2-relaunch-login optimization from 1.7.10. +- App should now show a message when workspace has been changed and a restart is needed for it to take effect. +- Fixed an issue where `ba.open_url()` would fall back to internal url display window on some newer Android versions instead of opening a browser. It should now correctly open a browser on regular Android. On AndroidTV/iiRcade/VR it will now always display the internal pop-up. It was trying to use fancy logic before to determine if a browser was available but this seemed to be flaky. Holler if this is not working well on your device/situation. +- The internal 'fallback' `ba.open_url()` window which shows a url string when a system browser is not available now has a qrcode and a copy button (where copy/paste is supported). +- Added a 'force_internal' arg to `ba.open_url()` if you would like to always use the internal window instead of attempting to open a browser. Now that we show a copy button and qr code there are some cases where this may be desirable. ### 1.7.10 (build 20895, api 7, 2022-10-09) - Added eval support for cloud-console. This means you can type something like '1+1' in the console and see '2' printed. This is how Python behaves in the stdin console or in-game console or the standard Python interpreter. diff --git a/assets/src/ba_data/python/._ba_sources_hash b/assets/src/ba_data/python/._ba_sources_hash index 8067bbe6..62f1f485 100644 --- a/assets/src/ba_data/python/._ba_sources_hash +++ b/assets/src/ba_data/python/._ba_sources_hash @@ -1 +1 @@ -180157672676216986210895241045962795292 \ No newline at end of file +31242320059036633417109806113241486230 \ No newline at end of file diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index 7b3bdcb5..b1ef6d75 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -2609,14 +2609,15 @@ def open_file_externally(path: str) -> None: return None -def open_url(address: str) -> None: +def open_url(address: str, force_internal: bool = False) -> None: """Open a provided URL. Category: **General Utility Functions** Open the provided url in a web-browser, or display the URL - string in a window if that isn't possible. + string in a window if that isn't possible (or if force_internal + is True). """ return None diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 34e33c98..290d8482 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -64,6 +64,7 @@ from _ba import ( getdata, in_logic_thread, ) +from ba._accountv2 import AccountV2Handle from ba._activity import Activity from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem from ba._actor import Actor @@ -187,6 +188,7 @@ from ba._collision import Collision, getcollision app: App __all__ = [ + 'AccountV2Handle', 'Achievement', 'AchievementSubsystem', 'Activity', diff --git a/assets/src/ba_data/python/ba/_accountv2.py b/assets/src/ba_data/python/ba/_accountv2.py index e05a1e6f..77b4ecdd 100644 --- a/assets/src/ba_data/python/ba/_accountv2.py +++ b/assets/src/ba_data/python/ba/_accountv2.py @@ -77,6 +77,7 @@ class AccountV2Subsystem: ): self._kicked_off_workspace_load = True _ba.app.workspaces.set_active_workspace( + account=account, workspaceid=account.workspaceid, workspacename=account.workspacename, on_completed=self._on_set_active_workspace_completed, diff --git a/assets/src/ba_data/python/ba/_bootstrap.py b/assets/src/ba_data/python/ba/_bootstrap.py index e3238519..a9fc7436 100644 --- a/assets/src/ba_data/python/ba/_bootstrap.py +++ b/assets/src/ba_data/python/ba/_bootstrap.py @@ -47,7 +47,7 @@ def bootstrap() -> None: # Give a soft warning if we're being used with a different binary # version than we expect. - expected_build = 20899 + expected_build = 20909 running_build: int = env['build_number'] if running_build != expected_build: print( diff --git a/assets/src/ba_data/python/ba/_plugin.py b/assets/src/ba_data/python/ba/_plugin.py index efe1ef69..3e896887 100644 --- a/assets/src/ba_data/python/ba/_plugin.py +++ b/assets/src/ba_data/python/ba/_plugin.py @@ -222,3 +222,10 @@ class Plugin: def on_app_shutdown(self) -> None: """Called before closing the application.""" + + def has_settings_ui(self) -> bool: + """Called to ask if we have settings UI we can show.""" + return False + + def show_settings_ui(self, source_widget: ba.Widget | None) -> None: + """Called to show our settings UI.""" diff --git a/assets/src/ba_data/python/ba/_workspace.py b/assets/src/ba_data/python/ba/_workspace.py index 33b3b702..c0997396 100644 --- a/assets/src/ba_data/python/ba/_workspace.py +++ b/assets/src/ba_data/python/ba/_workspace.py @@ -36,6 +36,7 @@ class WorkspaceSubsystem: def set_active_workspace( self, + account: ba.AccountV2Handle, workspaceid: str, workspacename: str, on_completed: Callable[[], None], @@ -46,6 +47,7 @@ class WorkspaceSubsystem: # interactivity. Thread( target=lambda: self._set_active_workspace_bg( + account=account, workspaceid=workspaceid, workspacename=workspacename, on_completed=on_completed, @@ -63,6 +65,7 @@ class WorkspaceSubsystem: def _set_active_workspace_bg( self, + account: ba.AccountV2Handle, workspaceid: str, workspacename: str, on_completed: Callable[[], None], @@ -91,11 +94,12 @@ class WorkspaceSubsystem: state = bacommon.cloud.WorkspaceFetchState(manifest=manifest) while True: - response = _ba.app.cloud.send_message( - bacommon.cloud.WorkspaceFetchMessage( - workspaceid=workspaceid, state=state + with account: + response = _ba.app.cloud.send_message( + bacommon.cloud.WorkspaceFetchMessage( + workspaceid=workspaceid, state=state + ) ) - ) state = response.state self._handle_deletes( workspace_dir=wspath, deletes=response.deletes diff --git a/assets/src/ba_data/python/bastd/ui/settings/plugins.py b/assets/src/ba_data/python/bastd/ui/settings/plugins.py index 8213e42a..a8197551 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/plugins.py +++ b/assets/src/ba_data/python/bastd/ui/settings/plugins.py @@ -21,6 +21,7 @@ class PluginSettingsWindow(ba.Window): origin_widget: ba.Widget | None = None, ): # pylint: disable=too-many-locals + # pylint: disable=too-many-statements app = ba.app # If they provided an origin-widget, scale up from that. @@ -113,11 +114,9 @@ class PluginSettingsWindow(ba.Window): highlight=False, size=(self._scroll_width, self._scroll_height), selection_loops_to_parent=True, + claims_left_right=True, ) ba.widget(edit=self._scrollwidget, right_widget=self._scrollwidget) - self._subcontainer = ba.columnwidget( - parent=self._scrollwidget, selection_loops_to_parent=True - ) if ba.app.meta.scanresults is None: ba.screenmessage( @@ -127,17 +126,32 @@ class PluginSettingsWindow(ba.Window): pluglist = ba.app.plugins.potential_plugins plugstates: dict[str, dict] = ba.app.config.setdefault('Plugins', {}) assert isinstance(plugstates, dict) + + plug_line_height = 50 + sub_width = self._scroll_width + sub_height = len(pluglist) * plug_line_height + self._subcontainer = ba.containerwidget( + parent=self._scrollwidget, + size=(sub_width, sub_height), + background=False, + ) + for i, availplug in enumerate(pluglist): - active = availplug.class_path in ba.app.plugins.active_plugins + plugin = ba.app.plugins.active_plugins.get(availplug.class_path) + active = plugin is not None plugstate = plugstates.setdefault(availplug.class_path, {}) checked = plugstate.get('enabled', False) assert isinstance(checked, bool) + + item_y = sub_height - (i + 1) * plug_line_height check = ba.checkboxwidget( parent=self._subcontainer, text=availplug.display_name, + autoselect=True, value=checked, - maxwidth=self._scroll_width - 100, + maxwidth=self._scroll_width - 200, + position=(10, item_y), size=(self._scroll_width - 40, 50), on_value_change_call=ba.Call( self._check_value_changed, availplug @@ -150,14 +164,35 @@ class PluginSettingsWindow(ba.Window): else (0.6, 0.6, 0.6) ), ) + if plugin is not None and plugin.has_settings_ui(): + button = ba.buttonwidget( + parent=self._subcontainer, + label=ba.Lstr(resource='mainMenu.settingsText'), + autoselect=True, + size=(100, 40), + position=(sub_width - 130, item_y + 6), + ) + ba.buttonwidget( + edit=button, + on_activate_call=ba.Call(plugin.show_settings_ui, button), + ) + else: + button = None + + # Allow getting back to back button. + if i == 0: + ba.widget( + edit=check, + up_widget=self._back_button, + left_widget=self._back_button, + ) + if button is not None: + ba.widget(edit=button, up_widget=self._back_button) # Make sure we scroll all the way to the end when using # keyboard/button nav. ba.widget(edit=check, show_buffer_top=40, show_buffer_bottom=40) - # Keep last from looping to back button when down is pressed. - if i == len(pluglist) - 1: - ba.widget(edit=check, down_widget=check) ba.containerwidget( edit=self._root_widget, selected_child=self._scrollwidget ) diff --git a/assets/src/ba_data/python/bastd/ui/url.py b/assets/src/ba_data/python/bastd/ui/url.py index 7b87b2d7..bcecbb95 100644 --- a/assets/src/ba_data/python/bastd/ui/url.py +++ b/assets/src/ba_data/python/bastd/ui/url.py @@ -17,52 +17,12 @@ class ShowURLWindow(ba.Window): # (for long URLs especially) app = ba.app uiscale = app.ui.uiscale - if app.platform == 'android' and app.subplatform == 'alibaba': - self._width = 500 - self._height = 500 - super().__init__( - root_widget=ba.containerwidget( - size=(self._width, self._height), - transition='in_right', - scale=( - 1.25 - if uiscale is ba.UIScale.SMALL - else 1.25 - if uiscale is ba.UIScale.MEDIUM - else 1.25 - ), - ) - ) - self._cancel_button = ba.buttonwidget( - parent=self._root_widget, - position=(50, self._height - 30), - size=(50, 50), - scale=0.6, - label='', - color=(0.6, 0.5, 0.6), - on_activate_call=self._done, - autoselect=True, - icon=ba.gettexture('crossOut'), - iconscale=1.2, - ) - qr_size = 400 - ba.imagewidget( - parent=self._root_widget, - position=( - self._width * 0.5 - qr_size * 0.5, - self._height * 0.5 - qr_size * 0.5, - ), - size=(qr_size, qr_size), - texture=ba.internal.get_qrcode_texture(address), - ) - ba.containerwidget( - edit=self._root_widget, cancel_button=self._cancel_button - ) - else: - # show it as a simple string... - self._width = 800 - self._height = 200 - self._root_widget = ba.containerwidget( + self._address = address + + self._width = 800 + self._height = 450 + super().__init__( + root_widget=ba.containerwidget( size=(self._width, self._height + 40), transition='in_right', scale=( @@ -73,43 +33,77 @@ class ShowURLWindow(ba.Window): else 1.25 ), ) - ba.textwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._height - 10), - size=(0, 0), - color=ba.app.ui.title_color, - h_align='center', - v_align='center', - text=ba.Lstr(resource='directBrowserToURLText'), - maxwidth=self._width * 0.95, - ) - ba.textwidget( - parent=self._root_widget, - position=(self._width * 0.5, self._height * 0.5 + 29), - size=(0, 0), - scale=1.3, - color=ba.app.ui.infotextcolor, - h_align='center', - v_align='center', - text=address, - maxwidth=self._width * 0.95, - ) - button_width = 200 + ) + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 10), + size=(0, 0), + color=ba.app.ui.title_color, + h_align='center', + v_align='center', + text=ba.Lstr(resource='directBrowserToURLText'), + maxwidth=self._width * 0.95, + ) + ba.textwidget( + parent=self._root_widget, + position=(self._width * 0.5, self._height - 60), + size=(0, 0), + scale=1.3, + color=ba.app.ui.infotextcolor, + h_align='center', + v_align='center', + text=address, + maxwidth=self._width * 0.95, + ) + button_width = 200 + + qr_size = 220 + ba.imagewidget( + parent=self._root_widget, + position=( + self._width * 0.5 - qr_size * 0.5, + self._height * 0.5 - qr_size * 0.5 + 10, + ), + size=(qr_size, qr_size), + texture=ba.internal.get_qrcode_texture(address), + ) + + xoffs = 0 + if ba.clipboard_is_supported(): + xoffs = -150 btn = ba.buttonwidget( parent=self._root_widget, - position=(self._width * 0.5 - button_width * 0.5, 20), + position=( + self._width * 0.5 - button_width * 0.5 + xoffs, + 20, + ), size=(button_width, 65), - label=ba.Lstr(resource='doneText'), - on_activate_call=self._done, - ) - # we have no 'cancel' button but still want to be able to - # hit back/escape/etc to leave.. - ba.containerwidget( - edit=self._root_widget, - selected_child=btn, - start_button=btn, - on_cancel_call=btn.activate, + autoselect=True, + label=ba.Lstr(resource='copyText'), + on_activate_call=self._copy, ) + xoffs = 150 + + btn = ba.buttonwidget( + parent=self._root_widget, + position=(self._width * 0.5 - button_width * 0.5 + xoffs, 20), + size=(button_width, 65), + autoselect=True, + label=ba.Lstr(resource='doneText'), + on_activate_call=self._done, + ) + # we have no 'cancel' button but still want to be able to + # hit back/escape/etc to leave.. + ba.containerwidget( + edit=self._root_widget, + selected_child=btn, + start_button=btn, + on_cancel_call=btn.activate, + ) + + def _copy(self) -> None: + ba.clipboard_set_text(self._address) + ba.screenmessage(ba.Lstr(resource='copyConfirmText'), color=(0, 1, 0)) def _done(self) -> None: ba.containerwidget(edit=self._root_widget, transition='out_left') diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index dd33b099..d2f7fe75 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -567,6 +567,7 @@ getpublicpartyenabled getpublicpartymaxsize getqrcodetexture + getr getrefs getres getsession @@ -918,6 +919,7 @@ objb objexists objid + objsizes objtoyaml objtypes obstack @@ -988,6 +990,7 @@ pdataclass pdoc pdst + peerinfo peername persp pflag @@ -1035,6 +1038,7 @@ printnodes printobjects printrefs + printsizes printtypes priv privatetab @@ -1230,6 +1234,8 @@ simpletype sisssssssss sixteenbits + slist + slists smod smoothering smoothstep @@ -1369,6 +1375,8 @@ tmpmat tomer topos + totalobjmb + totalobjsize touchpad toucs tournamentbutton diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 4838d50d..ab54da9c 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -32,7 +32,7 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20899; +const int kAppBuildNumber = 20909; const char* kAppVersion = "1.7.11"; // Our standalone globals. diff --git a/src/ballistica/python/methods/python_methods_ui.cc b/src/ballistica/python/methods/python_methods_ui.cc index f5a7a6cc..cf1cc08e 100644 --- a/src/ballistica/python/methods/python_methods_ui.cc +++ b/src/ballistica/python/methods/python_methods_ui.cc @@ -2159,13 +2159,19 @@ auto PyBackPress(PyObject* self, PyObject* args, PyObject* keywds) auto PyOpenURL(PyObject* self, PyObject* args, PyObject* keywds) -> PyObject* { BA_PYTHON_TRY; const char* address = nullptr; - static const char* kwlist[] = {"address", nullptr}; - if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", - const_cast(kwlist), &address)) { + int force_internal{0}; + static const char* kwlist[] = {"address", "force_internal", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|p", + const_cast(kwlist), &address, + &force_internal)) { return nullptr; } assert(g_app_flavor); - g_app_flavor->PushOpenURLCall(address); + if (force_internal) { + g_logic->PushShowURLCall(address); + } else { + g_app_flavor->PushOpenURLCall(address); + } Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -2271,14 +2277,15 @@ auto PythonMethodsUI::GetMethods() -> std::vector { "Open the provided file in the default external app."}, {"open_url", (PyCFunction)PyOpenURL, METH_VARARGS | METH_KEYWORDS, - "open_url(address: str) -> None\n" + "open_url(address: str, force_internal: bool = False) -> None\n" "\n" "Open a provided URL.\n" "\n" "Category: **General Utility Functions**\n" "\n" "Open the provided url in a web-browser, or display the URL\n" - "string in a window if that isn't possible.\n"}, + "string in a window if that isn't possible (or if force_internal\n" + "is True).\n"}, {"back_press", (PyCFunction)PyBackPress, METH_VARARGS | METH_KEYWORDS, "back_press() -> None\n" diff --git a/tests/test_efro/test_rpc.py b/tests/test_efro/test_rpc.py index 6f23de65..aa5a795a 100644 --- a/tests/test_efro/test_rpc.py +++ b/tests/test_efro/test_rpc.py @@ -432,7 +432,7 @@ def test_server_interrupt() -> None: await asyncio.sleep(0.2) tester.server.endpoint.close() - asyncio.create_task(_kill_connection()) + _task = asyncio.create_task(_kill_connection()) with pytest.raises(CommunicationError): await tester.server.send_message(_Message(_MessageType.TEST_SLOW)) @@ -448,7 +448,7 @@ def test_client_interrupt() -> None: await asyncio.sleep(0.2) tester.client.endpoint.close() - asyncio.create_task(_kill_connection()) + _task = asyncio.create_task(_kill_connection()) with pytest.raises(CommunicationError): await tester.server.send_message(_Message(_MessageType.TEST_SLOW)) diff --git a/tools/batools/assetstaging.py b/tools/batools/assetstaging.py index 481fb47a..7d6e542c 100755 --- a/tools/batools/assetstaging.py +++ b/tools/batools/assetstaging.py @@ -429,15 +429,12 @@ def _sync_server_files(cfg: Config) -> None: outfilename=os.path.join(cfg.serverdst, 'config_template.yaml'), ) if cfg.win_type is not None: + fname = 'launch_ballisticacore_server.bat' stage_server_file( projroot=cfg.projroot, mode=modeval, - infilename=( - f'{cfg.src}/../src/server/launch_ballisticacore_server.bat' - ), - outfilename=os.path.join( - cfg.serverdst, 'launch_ballisticacore_server.bat' - ), + infilename=f'{cfg.src}/../src/server/{fname}', + outfilename=os.path.join(cfg.serverdst, fname), ) diff --git a/tools/batools/pcommand.py b/tools/batools/pcommand.py index 3552838b..2caa95e7 100644 --- a/tools/batools/pcommand.py +++ b/tools/batools/pcommand.py @@ -183,9 +183,11 @@ def lazy_increment_build() -> None: except FileNotFoundError: lasthash = '' if codehash != lasthash: - print(f'{Clr.SMAG}Source(s) changed; incrementing build...{Clr.RST}') if not update_hash_only: + print( + f'{Clr.SMAG}Source(s) changed; incrementing build...{Clr.RST}' + ) # Just go ahead and bless; this will increment the build as needed. # subprocess.run(['make', 'bless'], check=True) subprocess.run( @@ -1029,11 +1031,10 @@ def win_ci_install_prereqs() -> None: # build to succeed. Normally this would happen through our Makefile # targets but we can't use them under raw window so we need to just # hard-code whatever we need here. + lib_dbg_win32 = 'build/prefab/lib/windows/Debug_Win32' needed_targets: set[str] = { - 'build/prefab/lib/windows/Debug_Win32/' - 'BallisticaCoreGenericInternal.lib', - 'build/prefab/lib/windows/Debug_Win32/' - 'BallisticaCoreGenericInternal.pdb', + f'{lib_dbg_win32}/BallisticaCoreGenericInternal.lib', + f'{lib_dbg_win32}/BallisticaCoreGenericInternal.pdb', 'ballisticacore-windows/Generic/BallisticaCore.ico', } diff --git a/tools/batools/project.py b/tools/batools/project.py index ee54eb59..48c4d561 100755 --- a/tools/batools/project.py +++ b/tools/batools/project.py @@ -703,8 +703,7 @@ class Updater: # CMake android components: fname = ( - 'ballisticacore-android/BallisticaCore' - '/src/main/cpp/CMakeLists.txt' + 'ballisticacore-android/BallisticaCore/src/main/cpp/CMakeLists.txt' ) if not self._public: self._update_cmake_file(fname) diff --git a/tools/batools/pythonenumsmodule.py b/tools/batools/pythonenumsmodule.py index 3ea4de93..bdc7c176 100755 --- a/tools/batools/pythonenumsmodule.py +++ b/tools/batools/pythonenumsmodule.py @@ -44,7 +44,7 @@ def _gen_enums(infilename: str) -> str: out += f'\n\nclass {enum_name}(Enum):\n """' out += '\n '.join(doclines) if len(doclines) > 1: - out += '\n """\n' + out += '\n """\n\n' else: out += '"""\n' diff --git a/tools/efro/debug.py b/tools/efro/debug.py index e5372863..74a72e0e 100644 --- a/tools/efro/debug.py +++ b/tools/efro/debug.py @@ -27,7 +27,9 @@ ABS_MAX_LEVEL = 10 # we're showing some temporary objects that we should be ignoring. -def getobjs(cls: type | str, contains: str | None = None) -> list[Any]: +def getobjs( + cls: type | str, contains: str | None = None, expanded: bool = False +) -> list[Any]: """Return all garbage-collected objects matching criteria. 'type' can be an actual type or a string in which case objects @@ -45,17 +47,51 @@ def getobjs(cls: type | str, contains: str | None = None) -> list[Any]: if not isinstance(contains, str | None): raise TypeError('Expected a string or None for contains') + allobjs = _get_all_objects(expanded=expanded) + if isinstance(cls, str): - objs = [o for o in gc.get_objects() if cls in str(type(o))] + objs = [o for o in allobjs if cls in str(type(o))] else: - objs = [o for o in gc.get_objects() if isinstance(o, cls)] + objs = [o for o in allobjs if isinstance(o, cls)] if contains is not None: objs = [o for o in objs if contains in str(o)] return objs -def getobj(objid: int) -> Any: +# Recursively expand slists objects into olist, using seen to track +# already processed objects. +def _getr(slist: list[Any], olist: list[Any], seen: set[int]) -> None: + for obj in slist: + if id(obj) in seen: + continue + seen.add(id(obj)) + olist.append(obj) + tll = gc.get_referents(obj) + if tll: + _getr(tll, olist, seen) + + +def _get_all_objects(expanded: bool) -> list[Any]: + """Return an expanded list of all objects. + + See https://utcc.utoronto.ca/~cks/space/blog/python/GetAllObjects + """ + gcl = gc.get_objects() + if not expanded: + return gcl + olist: list[Any] = [] + seen: set[int] = set() + # Just in case: + seen.add(id(gcl)) + seen.add(id(olist)) + seen.add(id(seen)) + # _getr does the real work. + _getr(gcl, olist, seen) + return olist + + +def getobj(objid: int, expanded: bool = False) -> Any: """Return a garbage-collected object by its id. Remember that this is VERY inefficient and should only ever be used @@ -65,7 +101,10 @@ def getobj(objid: int) -> Any: raise TypeError(f'Expected an int for objid; got a {type(objid)}.') # Don't wanna return stuff waiting to be garbage-collected. - for obj in gc.get_objects(): + gc.collect() + + allobjs = _get_all_objects(expanded=expanded) + for obj in allobjs: if id(obj) == objid: return obj raise RuntimeError(f'Object with id {objid} not found.') @@ -145,12 +184,14 @@ def printrefs( ) -def printtypes(limit: int = 50, file: TextIO | None = None) -> None: +def printtypes( + limit: int = 50, file: TextIO | None = None, expanded: bool = False +) -> None: """Print a human readable list of which types have the most instances.""" assert limit > 0 objtypes: dict[str, int] = {} gc.collect() # Recommended before get_objects(). - allobjs = gc.get_objects() + allobjs = _get_all_objects(expanded=expanded) allobjc = len(allobjs) for obj in allobjs: modname = type(obj).__module__ @@ -175,6 +216,38 @@ def printtypes(limit: int = 50, file: TextIO | None = None) -> None: print(f'{i+1}: {tpname}: {tpval} ({percent:.2f}%)', file=file) +def printsizes( + limit: int = 50, file: TextIO | None = None, expanded: bool = False +) -> None: + """Print total allocated sizes of different types.""" + assert limit > 0 + objsizes: dict[str, int] = {} + gc.collect() # Recommended before get_objects(). + allobjs = _get_all_objects(expanded=expanded) + totalobjsize = 0 + + for obj in allobjs: + modname = type(obj).__module__ + tpname = type(obj).__qualname__ + if modname != 'builtins': + tpname = f'{modname}.{tpname}' + objsize = sys.getsizeof(obj) + objsizes[tpname] = objsizes.get(tpname, 0) + objsize + totalobjsize += objsize + + totalobjmb = totalobjsize / (1024 * 1024) + print( + f'Types with most allocated bytes ({totalobjmb:.2f} mb total):', + file=file, + ) + for i, tpitem in enumerate( + sorted(objsizes.items(), key=lambda x: x[1], reverse=True)[:limit] + ): + tpname, tpval = tpitem + percent = tpval / totalobjsize * 100.0 + print(f'{i+1}: {tpname}: {tpval} ({percent:.2f}%)', file=file) + + def _desctype(obj: Any) -> str: cls = type(obj) if cls is types.ModuleType: diff --git a/tools/efro/log.py b/tools/efro/log.py index 10f2a5f9..3cc9230f 100644 --- a/tools/efro/log.py +++ b/tools/efro/log.py @@ -292,7 +292,8 @@ class LogHandler(logging.Handler): self._file_chunk_ship_task[ name ] = self._event_loop.create_task( - self._ship_chunks_task(name) + self._ship_chunks_task(name), + name='log ship file chunks', ) except Exception: @@ -321,7 +322,6 @@ class LogHandler(logging.Handler): traceback.print_exc(file=self._echofile) async def _ship_chunks_task(self, name: str) -> None: - await asyncio.sleep(0.1) self._ship_file_chunks(name, cancel_ship_task=False) def _ship_file_chunks(self, name: str, cancel_ship_task: bool) -> None: diff --git a/tools/efro/rpc.py b/tools/efro/rpc.py index 051555b4..ba5d1d22 100644 --- a/tools/efro/rpc.py +++ b/tools/efro/rpc.py @@ -88,6 +88,10 @@ def ssl_stream_writer_force_close_check(writer: asyncio.StreamWriter) -> None: from efro.call import tpartial from threading import Thread + # Disabling for now.. + if bool(True): + return + # Hopefully can remove this in Python 3.11?... # see issue with is_closing() below for more details. transport = getattr(writer, '_transport', None) @@ -128,7 +132,9 @@ class _InFlightMessage: def __init__(self) -> None: self._response: bytes | None = None self._got_response = asyncio.Event() - self.wait_task = asyncio.create_task(self._wait()) + self.wait_task = asyncio.create_task( + self._wait(), name='rpc in flight msg wait' + ) async def _wait(self) -> bytes: await self._got_response.wait() @@ -185,11 +191,11 @@ class RPCEndpoint: self._handle_raw_message_call = handle_raw_message_call self._reader = reader self._writer = writer - self._debug_print = debug_print - self._debug_print_io = debug_print_io + self.debug_print = debug_print + self.debug_print_io = debug_print_io if debug_print_call is None: debug_print_call = print - self._debug_print_call: Callable[[str], None] = debug_print_call + self.debug_print_call: Callable[[str], None] = debug_print_call self._label = label self._thread = current_thread() self._closing = False @@ -207,7 +213,7 @@ class RPCEndpoint: # Need to hold weak-refs to these otherwise it creates dep-loops # which keeps us alive. - self._tasks: list[weakref.ref[asyncio.Task]] = [] + self._tasks: list[asyncio.Task] = [] # When we last got a keepalive or equivalent (time.monotonic value) self._last_keepalive_receive_time: float | None = None @@ -217,9 +223,9 @@ class RPCEndpoint: self._in_flight_messages: dict[int, _InFlightMessage] = {} - if self._debug_print: + if self.debug_print: peername = self._writer.get_extra_info('peername') - self._debug_print_call( + self.debug_print_call( f'{self._label}: connected to {peername} at {self._tm()}.' ) @@ -270,16 +276,19 @@ class RPCEndpoint: core_tasks = [ asyncio.create_task( - self._run_core_task('keepalive', self._run_keepalive_task()) + self._run_core_task('keepalive', self._run_keepalive_task()), + name='rpc keepalive', ), asyncio.create_task( - self._run_core_task('read', self._run_read_task()) + self._run_core_task('read', self._run_read_task()), + name='rpc read', ), asyncio.create_task( - self._run_core_task('write', self._run_write_task()) + self._run_core_task('write', self._run_write_task()), + name='rpc write', ), ] - self._tasks += [weakref.ref(t) for t in core_tasks] + self._tasks += core_tasks # Run our core tasks until they all complete. results = await asyncio.gather(*core_tasks, return_exceptions=True) @@ -309,8 +318,8 @@ class RPCEndpoint: except Exception: logging.exception('Error closing %s.', self._label) - if self._debug_print: - self._debug_print_call(f'{self._label}: finished.') + if self.debug_print: + self.debug_print_call(f'{self._label}: finished.') async def send_message( self, @@ -330,11 +339,23 @@ class RPCEndpoint: override this for a particular message. """ # pylint: disable=too-many-branches + + if self.debug_print_io: + self.debug_print_call( + f'{self._label}: sending message of size {len(message)}' + f' at {self._tm()}.' + ) + self._check_env() if self._closing: raise CommunicationError('Endpoint is closed.') + if self.debug_print_io: + self.debug_print_call( + f'{self._label}: have peerinfo? {self._peer_info is not None}.' + ) + # We need to know their protocol, so if we haven't gotten a handshake # from them yet, just wait. while self._peer_info is None: @@ -349,6 +370,11 @@ class RPCEndpoint: message_id = self._next_message_id self._next_message_id = (self._next_message_id + 1) % 65536 + if self.debug_print_io: + self.debug_print_call( + f'{self._label}: will enqueue at {self._tm()}.' + ) + # FIXME - should handle backpressure (waiting here if there are # enough packets already enqueued). @@ -371,13 +397,19 @@ class RPCEndpoint: + message ) + if self.debug_print_io: + self.debug_print_call( + f'{self._label}: enqueued message of size {len(message)}' + f' at {self._tm()}.' + ) + # Make an entry so we know this message is out there. assert message_id not in self._in_flight_messages msgobj = self._in_flight_messages[message_id] = _InFlightMessage() # Also add its task to our list so we properly cancel it if we die. self._prune_tasks() # Keep our list from filling with dead tasks. - self._tasks.append(weakref.ref(msgobj.wait_task)) + self._tasks.append(msgobj.wait_task) # Note: we always want to incorporate a timeout. Individual # messages may hang or error on the other end and this ensures @@ -392,16 +424,17 @@ class RPCEndpoint: # Question: we assume this means the above wait_for() was # cancelled; what happens if a task running *us* is cancelled # though? - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: message {message_id} was cancelled.' ) if close_on_error: self.close() + raise CommunicationError() from exc except asyncio.TimeoutError as exc: - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: message {message_id} timed out.' ) @@ -424,21 +457,21 @@ class RPCEndpoint: if self._closing: return - if self._debug_print: - self._debug_print_call(f'{self._label}: closing...') + if self.debug_print: + self.debug_print_call(f'{self._label}: closing...') self._closing = True # Kill all of our in-flight tasks. - if self._debug_print: - self._debug_print_call(f'{self._label}: cancelling tasks...') + if self.debug_print: + self.debug_print_call(f'{self._label}: cancelling tasks...') for task in self._get_live_tasks(): task.cancel() # Close our writer. assert not self._did_close_writer - if self._debug_print: - self._debug_print_call(f'{self._label}: closing writer...') + if self.debug_print: + self.debug_print_call(f'{self._label}: closing writer...') self._writer.close() self._did_close_writer = True @@ -474,8 +507,13 @@ class RPCEndpoint: ) live_tasks = self._get_live_tasks() - if self._debug_print: - self._debug_print_call( + + # Don't need our task list anymore; this should + # break any cyclical refs from tasks referring to us. + self._tasks = [] + + if self.debug_print: + self.debug_print_call( f'{self._label}: waiting for tasks to finish: ' f' ({live_tasks=})...' ) @@ -498,8 +536,8 @@ class RPCEndpoint: id(self), ) - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: tasks finished; waiting for writer close...' ) @@ -515,15 +553,19 @@ class RPCEndpoint: # indefinitely. See https://github.com/python/cpython/issues/83939 # It sounds like this should be fixed in 3.11 but for now just # forcing the issue with a timeout here. - await asyncio.wait_for(self._writer.wait_closed(), timeout=30.0) + await asyncio.wait_for( + self._writer.wait_closed(), + # timeout=60.0 * 6.0, + timeout=30.0, + ) except asyncio.TimeoutError: logging.info( 'Timeout on _writer.wait_closed() for %s rpc (transport=%s).', self._label, ssl_stream_writer_underlying_transport_info(self._writer), ) - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: got timeout in _writer.wait_closed();' ' This should be fixed in future Python versions.' ) @@ -531,8 +573,8 @@ class RPCEndpoint: if not self._is_expected_connection_error(exc): logging.exception('Error closing _writer for %s.', self._label) else: - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: silently ignoring error in' f' _writer.wait_closed(): {exc}.' ) @@ -555,14 +597,18 @@ class RPCEndpoint: self._check_env() assert self._peer_info is None + # Bug fix: if we don't have this set we will never time out + # if we never receive any data from the other end. + self._last_keepalive_receive_time = time.monotonic() + # The first thing they should send us is their handshake; then # we'll know if/how we can talk to them. mlen = await self._read_int_32() message = await self._reader.readexactly(mlen) self._peer_info = dataclass_from_json(_PeerInfo, message.decode()) self._last_keepalive_receive_time = time.monotonic() - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: received handshake at {self._tm()}.' ) @@ -576,8 +622,8 @@ class RPCEndpoint: raise RuntimeError('Got multiple handshakes') if mtype is _PacketType.KEEPALIVE: - if self._debug_print_io: - self._debug_print_call( + if self.debug_print_io: + self.debug_print_call( f'{self._label}: received keepalive' f' at {self._tm()}.' ) @@ -606,8 +652,8 @@ class RPCEndpoint: else: msglen = await self._read_int_16() msg = await self._reader.readexactly(msglen) - if self._debug_print_io: - self._debug_print_call( + if self.debug_print_io: + self.debug_print_call( f'{self._label}: received message {msgid}' f' of size {msglen} at {self._tm()}.' ) @@ -617,14 +663,13 @@ class RPCEndpoint: assert not self._closing self._prune_tasks() # Keep from filling with dead tasks. self._tasks.append( - weakref.ref( - asyncio.create_task( - self._handle_raw_message(message_id=msgid, message=msg) - ) + asyncio.create_task( + self._handle_raw_message(message_id=msgid, message=msg), + name='efro rpc message handle', ) ) - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: done handling message at {self._tm()}.' ) @@ -636,8 +681,8 @@ class RPCEndpoint: rsplen = await self._read_int_32() else: rsplen = await self._read_int_16() - if self._debug_print_io: - self._debug_print_call( + if self.debug_print_io: + self.debug_print_call( f'{self._label}: received response {msgid}' f' of size {rsplen} at {self._tm()}.' ) @@ -647,8 +692,8 @@ class RPCEndpoint: # It's possible for us to get a response to a message # that has timed out. In this case we will have no local # record of it. - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: got response for nonexistent' f' message id {msgid}; perhaps it timed out?' ) @@ -730,9 +775,9 @@ class RPCEndpoint: and now - self._last_keepalive_receive_time > self._keepalive_timeout ): - if self._debug_print: + if self.debug_print: since = now - self._last_keepalive_receive_time - self._debug_print_call( + self.debug_print_call( f'{self._label}: reached keepalive time-out' f' ({since:.1f}s).' ) @@ -751,15 +796,15 @@ class RPCEndpoint: tasklabel, ) else: - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: {tasklabel} task will exit cleanly' f' due to {exc!r}.' ) finally: # Any core task exiting triggers shutdown. - if self._debug_print: - self._debug_print_call( + if self.debug_print: + self.debug_print_call( f'{self._label}: {tasklabel} task exiting...' ) self.close() @@ -836,8 +881,8 @@ class RPCEndpoint: """Enqueue a raw packet to be sent. Must be called from our loop.""" self._check_env() - if self._debug_print_io: - self._debug_print_call( + if self.debug_print_io: + self.debug_print_call( f'{self._label}: enqueueing outgoing packet' f' {data[:50]!r} at {self._tm()}.' ) @@ -847,17 +892,7 @@ class RPCEndpoint: self._have_out_packets.set() def _prune_tasks(self) -> None: - out: list[weakref.ref[asyncio.Task]] = [] - for task_weak_ref in self._tasks: - task = task_weak_ref() - if task is not None and not task.done(): - out.append(task_weak_ref) - self._tasks = out + self._tasks = self._get_live_tasks() def _get_live_tasks(self) -> list[asyncio.Task]: - out: list[asyncio.Task] = [] - for task_weak_ref in self._tasks: - task = task_weak_ref() - if task is not None and not task.done(): - out.append(task) - return out + return [t for t in self._tasks if not t.done()]