portacode 2.0.0.dev0__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {portacode-2.0.0.dev0 → portacode-2.0.2}/PKG-INFO +1 -1
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/_version.py +2 -2
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/guest_host_protocol.py +13 -3
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +20 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/__init__.py +2 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/proxmox_infra.py +107 -37
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/registry.py +31 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/system_handlers.py +1 -1
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_proxmox_infra.py +71 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/terminal.py +11 -6
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/test_guest_host_terminal.py +43 -3
- portacode-2.0.2/portacode/tests/test_terminal_periodic_system_info.py +59 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/SOURCES.txt +1 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/.gitignore +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/AGENTS.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/LICENSE +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/MANIFEST.in +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/Makefile +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/build_android.sh +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/connect.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/connect.sh +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/creative-team-brief-portacode.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/device-health-alerting-plan.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/devops-messaging-ab-tests.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/guest-host-requests-plan.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/homepage-dashboard-positioning-fixes.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/docs/template-guide.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/Dockerfile +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/__main__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/cli.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/codex_loopback_proxy.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/codex_prepare.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/codex_usage_limit.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/client.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/codex_app_server.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/base.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/browser_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/codex_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/file_transfer_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/test_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/resumable_transfer_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/runtime_user.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/session.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_browser_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_cloudflare_forwarding.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_file_move_copy.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_runtime_user.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/multiplex.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/test_guest_host_protocol.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/test_webmin_proxy_config.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/webmin_proxy_config.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/data.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/exit_codes.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/github_credential.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/keypair.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/__pycache__/__init__.cpython-38.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-38.pyc +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/elinks +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/links +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/links2 +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/lynx +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/w3m +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/logging_categories.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/pairing.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/provisioning_cache.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/restart.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/service.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/singleton.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_runtime_paths.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_service.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_singleton.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/test_terminal_exposed_services.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_app_server.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_loopback_proxy.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_codex_prepare.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_github_credential.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_image_read_handler.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_resumable_transfer_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tests/test_system_handlers.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/cloudflared_login.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/ensure_cloudflared.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/ensure_pyyaml.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/forwarding_state.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/get_domain.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/privileged.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/service_install.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/tunneling/state.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/updater.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/diff_apply.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/diff_renderer.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/ntp_clock.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/utils/runtime_paths.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/requires.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/portacode.egg-info/top_level.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/pyproject.toml +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/run_tests.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/scripts/diagnose_codex_gateway.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/setup.cfg +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/setup.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test.sh +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_device_online.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_file_operations.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_git_status_ui.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_login_flow.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_modules/test_terminal_start.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_request_id.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/test_session_terminal_shutdown.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/.env.example +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/README.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/cli.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/__init__.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/base_test.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/cli_manager.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/runner.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/core/test_discovery.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/testing_framework/requirements.txt +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/agent_context_management.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/django_server_time_sync.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/websocket_client_silently_dead.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/generate_play_store_assets.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/pairing_tester.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/run_screenshot_suite.sh +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/tools/test_python_ntp_clock.py +0 -0
- {portacode-2.0.0.dev0 → portacode-2.0.2}/validate.sh +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (2, 0,
|
|
21
|
+
__version__ = version = '2.0.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 2)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -24,6 +24,7 @@ GUEST_HOST_REQUEST_COMMANDS = frozenset(
|
|
|
24
24
|
"configure_proxmox_container_expose_ports",
|
|
25
25
|
"start_proxmox_container",
|
|
26
26
|
"stop_proxmox_container",
|
|
27
|
+
"restart_proxmox_container",
|
|
27
28
|
"remove_proxmox_container",
|
|
28
29
|
"create_proxmox_container",
|
|
29
30
|
}
|
|
@@ -32,14 +33,23 @@ GUEST_HOST_COMMAND_OPERATIONS = {
|
|
|
32
33
|
"configure_proxmox_container_expose_ports": "expose_ports",
|
|
33
34
|
"start_proxmox_container": "power",
|
|
34
35
|
"stop_proxmox_container": "power",
|
|
36
|
+
"restart_proxmox_container": "power",
|
|
35
37
|
"remove_proxmox_container": "delete",
|
|
36
38
|
"create_proxmox_container": "provision",
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
+
# Every advertised operation is mapped to its existing, independently
|
|
42
|
+
# registered legacy handler. Server-side capability and runtime gates still
|
|
43
|
+
# control which operations are actually sent through this protocol.
|
|
41
44
|
GUEST_HOST_EXECUTABLE_COMMANDS = frozenset(
|
|
42
|
-
{
|
|
45
|
+
{
|
|
46
|
+
"configure_proxmox_container_expose_ports",
|
|
47
|
+
"start_proxmox_container",
|
|
48
|
+
"stop_proxmox_container",
|
|
49
|
+
"restart_proxmox_container",
|
|
50
|
+
"remove_proxmox_container",
|
|
51
|
+
"create_proxmox_container",
|
|
52
|
+
}
|
|
43
53
|
)
|
|
44
54
|
|
|
45
55
|
|
{portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -88,6 +88,18 @@ versions continue using existing commands while flags are disabled. Unknown
|
|
|
88
88
|
message types or commands must fail closed; they must never fall through to an
|
|
89
89
|
ordinary client-session handler.
|
|
90
90
|
|
|
91
|
+
Protocol version 1 defines these request types and command mappings:
|
|
92
|
+
|
|
93
|
+
- `expose_ports` → `configure_proxmox_container_expose_ports`;
|
|
94
|
+
- `power` → `start_proxmox_container`, `stop_proxmox_container`, or `restart_proxmox_container`;
|
|
95
|
+
- `delete` → `remove_proxmox_container`;
|
|
96
|
+
- `provision` → `create_proxmox_container`.
|
|
97
|
+
|
|
98
|
+
Supporting CLI versions advertise the enabled set in
|
|
99
|
+
`system_info.protocol_capabilities.guest_host_requests.request_types`. The
|
|
100
|
+
server must check that per-host list as well as its per-operation runtime flag
|
|
101
|
+
before selecting this transport. Otherwise it uses the unchanged legacy path.
|
|
102
|
+
|
|
91
103
|
This document describes the complete protocol for communicating with devices through the server, guiding app developers on how to get their client sessions to communicate with devices.
|
|
92
104
|
|
|
93
105
|
## Table of Contents
|
|
@@ -534,6 +546,14 @@ Stops a running Portacode-managed container. Handled by [`StopProxmoxContainerHa
|
|
|
534
546
|
* Emits a [`proxmox_container_action`](#proxmox_container_action-event) event with `action="stop"` and the refreshed infra snapshot.
|
|
535
547
|
* Emits an [`error`](#error) event on failure.
|
|
536
548
|
|
|
549
|
+
### `restart_proxmox_container`
|
|
550
|
+
|
|
551
|
+
Stops and starts a Portacode-managed container as one validated host-side
|
|
552
|
+
operation. It accepts the same required `child_device_id` and optional `ctid`
|
|
553
|
+
fields as the start/stop actions. The handler verifies the child-to-container
|
|
554
|
+
mapping before changing state, then emits `proxmox_container_action` with
|
|
555
|
+
`action="restart"`.
|
|
556
|
+
|
|
537
557
|
### `remove_proxmox_container`
|
|
538
558
|
|
|
539
559
|
Deletes a managed container from Proxmox (stopping it first if necessary) and removes the stored metadata file. Handled by [`RemoveProxmoxContainerHandler`](./proxmox_infra.py).
|
|
@@ -60,6 +60,7 @@ from .proxmox_infra import (
|
|
|
60
60
|
StartPortacodeServiceHandler,
|
|
61
61
|
StartProxmoxContainerHandler,
|
|
62
62
|
StopProxmoxContainerHandler,
|
|
63
|
+
RestartProxmoxContainerHandler,
|
|
63
64
|
RemoveProxmoxContainerHandler,
|
|
64
65
|
)
|
|
65
66
|
from .cloudflare_tunnel import CloudflareTunnelSetupHandler
|
|
@@ -132,6 +133,7 @@ __all__ = [
|
|
|
132
133
|
"StartPortacodeServiceHandler",
|
|
133
134
|
"StartProxmoxContainerHandler",
|
|
134
135
|
"StopProxmoxContainerHandler",
|
|
136
|
+
"RestartProxmoxContainerHandler",
|
|
135
137
|
"RemoveProxmoxContainerHandler",
|
|
136
138
|
"UpdatePortacodeHandler",
|
|
137
139
|
"RevertProxmoxInfraHandler",
|
|
@@ -1327,7 +1327,7 @@ def _refresh_container_statuses(records: List[Dict[str, Any]], config: Optional[
|
|
|
1327
1327
|
|
|
1328
1328
|
|
|
1329
1329
|
def _initialize_managed_containers_state(force: bool = False) -> Dict[str, Any]:
|
|
1330
|
-
with
|
|
1330
|
+
with _acquire_state_lock("managed state initialization check"):
|
|
1331
1331
|
if _MANAGED_CONTAINERS_STATE["initialized"] and not force:
|
|
1332
1332
|
base = _MANAGED_CONTAINERS_STATE["base_summary"]
|
|
1333
1333
|
return _copy_summary(base) if base else {}
|
|
@@ -1352,7 +1352,7 @@ def _initialize_managed_containers_state(force: bool = False) -> Dict[str, Any]:
|
|
|
1352
1352
|
"cpu_share": float(base_summary.get("total_cpu_share") or 0.0),
|
|
1353
1353
|
}
|
|
1354
1354
|
|
|
1355
|
-
with
|
|
1355
|
+
with _acquire_state_lock("managed state initialization commit"):
|
|
1356
1356
|
_MANAGED_CONTAINERS_STATE["initialized"] = True
|
|
1357
1357
|
_MANAGED_CONTAINERS_STATE["base_summary"] = base_summary
|
|
1358
1358
|
_MANAGED_CONTAINERS_STATE["initial_totals"] = initial_totals
|
|
@@ -1374,7 +1374,7 @@ def reconcile_managed_containers_inventory() -> Dict[str, Any]:
|
|
|
1374
1374
|
never replaced by reconciliation.
|
|
1375
1375
|
"""
|
|
1376
1376
|
_initialize_managed_containers_state()
|
|
1377
|
-
with
|
|
1377
|
+
with _acquire_state_lock("inventory reconciliation snapshot"):
|
|
1378
1378
|
revision = int(_MANAGED_CONTAINERS_STATE.get("revision") or 0)
|
|
1379
1379
|
records = [record.copy() for record in _MANAGED_CONTAINERS_STATE.get("records", {}).values()]
|
|
1380
1380
|
|
|
@@ -1401,38 +1401,46 @@ def reconcile_managed_containers_inventory() -> Dict[str, Any]:
|
|
|
1401
1401
|
|
|
1402
1402
|
cleaned_vmids: List[int] = []
|
|
1403
1403
|
cleanup_errors: List[Dict[str, str]] = []
|
|
1404
|
-
with
|
|
1404
|
+
with _acquire_state_lock("inventory reconciliation pre-cleanup check"):
|
|
1405
1405
|
if int(_MANAGED_CONTAINERS_STATE.get("revision") or 0) != revision:
|
|
1406
1406
|
return {"refreshed": False, "reason": "state_changed"}
|
|
1407
|
-
# Serialize cleanup with create/delete state mutations. This prevents a
|
|
1408
|
-
# stale inventory scan from clearing metadata after a CTID is reused.
|
|
1409
|
-
for record in missing_records:
|
|
1410
|
-
try:
|
|
1411
|
-
vmid = int(record["vmid"])
|
|
1412
|
-
except (KeyError, TypeError, ValueError):
|
|
1413
|
-
continue
|
|
1414
|
-
device_id = str(record.get("device_id") or "").strip()
|
|
1415
|
-
try:
|
|
1416
|
-
if device_id:
|
|
1417
|
-
from .cloudflare_forwarding import set_container_forwarding_rules
|
|
1418
1407
|
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1408
|
+
# Forwarding cleanup may call cloudflared and touch its configuration. It
|
|
1409
|
+
# must never run while holding the managed-state lock: reservations,
|
|
1410
|
+
# deletion, and system snapshots all need that lock to remain responsive.
|
|
1411
|
+
for record in missing_records:
|
|
1412
|
+
try:
|
|
1413
|
+
vmid = int(record["vmid"])
|
|
1414
|
+
except (KeyError, TypeError, ValueError):
|
|
1415
|
+
continue
|
|
1416
|
+
device_id = str(record.get("device_id") or "").strip()
|
|
1417
|
+
try:
|
|
1418
|
+
if device_id:
|
|
1419
|
+
from .cloudflare_forwarding import set_container_forwarding_rules
|
|
1420
|
+
|
|
1421
|
+
try:
|
|
1422
|
+
set_container_forwarding_rules(device_id, [])
|
|
1423
|
+
except RuntimeError as exc:
|
|
1424
|
+
message = str(exc).lower()
|
|
1425
|
+
if (
|
|
1426
|
+
"cloudflare tunnel is not configured yet" not in message
|
|
1427
|
+
and "domain or tunnel name missing" not in message
|
|
1428
|
+
):
|
|
1429
|
+
raise
|
|
1430
|
+
cleaned_vmids.append(vmid)
|
|
1431
|
+
except Exception as exc: # retry on the next reconciliation pass
|
|
1432
|
+
logger.warning(
|
|
1433
|
+
"Unable to clean Proxmox-side metadata for missing container %s: %s",
|
|
1434
|
+
vmid,
|
|
1435
|
+
exc,
|
|
1436
|
+
)
|
|
1437
|
+
cleanup_errors.append({"vmid": str(vmid), "error": str(exc)})
|
|
1438
|
+
|
|
1439
|
+
with _acquire_state_lock("inventory reconciliation commit"):
|
|
1440
|
+
if int(_MANAGED_CONTAINERS_STATE.get("revision") or 0) != revision:
|
|
1441
|
+
return {"refreshed": False, "reason": "state_changed"}
|
|
1442
|
+
# The revision check prevents stale scan results from removing metadata
|
|
1443
|
+
# after provisioning or deletion changed the managed state.
|
|
1436
1444
|
for vmid in cleaned_vmids:
|
|
1437
1445
|
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
1438
1446
|
path.unlink(missing_ok=True)
|
|
@@ -1856,7 +1864,7 @@ def _compose_managed_containers_summary(
|
|
|
1856
1864
|
|
|
1857
1865
|
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
1858
1866
|
_initialize_managed_containers_state(force=force)
|
|
1859
|
-
with
|
|
1867
|
+
with _acquire_state_lock("managed container summary snapshot"):
|
|
1860
1868
|
base_summary = _MANAGED_CONTAINERS_STATE.get("base_summary") or {}
|
|
1861
1869
|
records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
|
|
1862
1870
|
pending = list(_MANAGED_CONTAINERS_STATE.get("pending", {}).values())
|
|
@@ -1872,7 +1880,7 @@ def _reserve_container_resources(payload: Dict[str, Any], *, device_id: str, req
|
|
|
1872
1880
|
disk_gib = int(payload.get("disk_gib") or 0)
|
|
1873
1881
|
cpu_share = float(payload.get("cpus") or 0.0)
|
|
1874
1882
|
reservation_id = secrets.token_hex(8)
|
|
1875
|
-
with
|
|
1883
|
+
with _acquire_state_lock("container resource reservation"):
|
|
1876
1884
|
base_summary = _MANAGED_CONTAINERS_STATE.get("base_summary") or {}
|
|
1877
1885
|
records = list(_MANAGED_CONTAINERS_STATE.get("records", {}).values())
|
|
1878
1886
|
pending = list(_MANAGED_CONTAINERS_STATE.get("pending", {}).values())
|
|
@@ -1904,7 +1912,7 @@ def _reserve_container_resources(payload: Dict[str, Any], *, device_id: str, req
|
|
|
1904
1912
|
def _release_container_reservation(reservation_id: Optional[str]) -> None:
|
|
1905
1913
|
if not reservation_id:
|
|
1906
1914
|
return
|
|
1907
|
-
with
|
|
1915
|
+
with _acquire_state_lock("container reservation release"):
|
|
1908
1916
|
_MANAGED_CONTAINERS_STATE.get("pending", {}).pop(reservation_id, None)
|
|
1909
1917
|
|
|
1910
1918
|
|
|
@@ -2990,7 +2998,7 @@ def _require_successful_proxmox_task(
|
|
|
2990
2998
|
|
|
2991
2999
|
def _register_container_record(vmid: int, payload: Dict[str, Any], reservation_id: Optional[str] = None) -> None:
|
|
2992
3000
|
_initialize_managed_containers_state()
|
|
2993
|
-
with
|
|
3001
|
+
with _acquire_state_lock("container record update"):
|
|
2994
3002
|
if reservation_id:
|
|
2995
3003
|
_MANAGED_CONTAINERS_STATE["pending"].pop(reservation_id, None)
|
|
2996
3004
|
_MANAGED_CONTAINERS_STATE["records"][str(vmid)] = payload.copy()
|
|
@@ -3001,7 +3009,7 @@ def _register_container_record(vmid: int, payload: Dict[str, Any], reservation_i
|
|
|
3001
3009
|
|
|
3002
3010
|
def _unregister_container_record(vmid: int) -> None:
|
|
3003
3011
|
_initialize_managed_containers_state()
|
|
3004
|
-
with
|
|
3012
|
+
with _acquire_state_lock("container record removal"):
|
|
3005
3013
|
_MANAGED_CONTAINERS_STATE["records"].pop(str(vmid), None)
|
|
3006
3014
|
_MANAGED_CONTAINERS_STATE["revision"] = int(
|
|
3007
3015
|
_MANAGED_CONTAINERS_STATE.get("revision") or 0
|
|
@@ -5165,6 +5173,68 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
5165
5173
|
}
|
|
5166
5174
|
|
|
5167
5175
|
|
|
5176
|
+
class RestartProxmoxContainerHandler(SyncHandler):
|
|
5177
|
+
"""Restart a managed container as one validated host-side operation."""
|
|
5178
|
+
|
|
5179
|
+
@property
|
|
5180
|
+
def command_name(self) -> str:
|
|
5181
|
+
return "restart_proxmox_container"
|
|
5182
|
+
|
|
5183
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
5184
|
+
child_device_id = (message.get("child_device_id") or "").strip()
|
|
5185
|
+
if not child_device_id:
|
|
5186
|
+
raise ValueError("child_device_id is required for restart_proxmox_container")
|
|
5187
|
+
config = _ensure_infra_configured()
|
|
5188
|
+
proxmox = _connect_proxmox(config)
|
|
5189
|
+
node = _get_node_from_config(config)
|
|
5190
|
+
try:
|
|
5191
|
+
vmid = _parse_ctid(message)
|
|
5192
|
+
except ValueError:
|
|
5193
|
+
try:
|
|
5194
|
+
vmid = _resolve_vmid_for_device(child_device_id)
|
|
5195
|
+
except _DeviceLookupError:
|
|
5196
|
+
vmid = _resolve_vmid_for_device_in_proxmox(proxmox, node, child_device_id)
|
|
5197
|
+
_ensure_container_managed(proxmox, node, vmid, device_id=child_device_id)
|
|
5198
|
+
|
|
5199
|
+
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
5200
|
+
start_status, start_elapsed = _start_container(proxmox, node, vmid)
|
|
5201
|
+
_update_container_record(vmid, {"status": "running"})
|
|
5202
|
+
|
|
5203
|
+
forwarding_ip_updated = False
|
|
5204
|
+
try:
|
|
5205
|
+
from .cloudflare_forwarding import reconcile_container_forwarding_ip
|
|
5206
|
+
|
|
5207
|
+
forwarding_ip_updated = reconcile_container_forwarding_ip(
|
|
5208
|
+
child_device_id,
|
|
5209
|
+
proxmox=proxmox,
|
|
5210
|
+
node=node,
|
|
5211
|
+
lease_attempts=6,
|
|
5212
|
+
lease_retry_seconds=1.0,
|
|
5213
|
+
)
|
|
5214
|
+
except Exception:
|
|
5215
|
+
logger.warning(
|
|
5216
|
+
"Unable to reconcile exposed IP after restarting container device_id=%s vmid=%s",
|
|
5217
|
+
child_device_id,
|
|
5218
|
+
vmid,
|
|
5219
|
+
exc_info=True,
|
|
5220
|
+
)
|
|
5221
|
+
|
|
5222
|
+
return {
|
|
5223
|
+
"event": "proxmox_container_action",
|
|
5224
|
+
"action": "restart",
|
|
5225
|
+
"success": True,
|
|
5226
|
+
"ctid": str(vmid),
|
|
5227
|
+
"message": f"Restarted container {vmid} in {stop_elapsed + start_elapsed:.1f}s.",
|
|
5228
|
+
"details": {
|
|
5229
|
+
"stop_exitstatus": stop_status.get("exitstatus"),
|
|
5230
|
+
"start_exitstatus": start_status.get("exitstatus"),
|
|
5231
|
+
"forwarding_ip_updated": forwarding_ip_updated,
|
|
5232
|
+
},
|
|
5233
|
+
"status": start_status.get("status") or "running",
|
|
5234
|
+
"infra": get_infra_snapshot(),
|
|
5235
|
+
}
|
|
5236
|
+
|
|
5237
|
+
|
|
5168
5238
|
class RemoveProxmoxContainerHandler(SyncHandler):
|
|
5169
5239
|
"""Delete a managed container via the Proxmox API."""
|
|
5170
5240
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Command registry for managing handler dispatch."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
from typing import Dict, Type, Any, Optional, List, TYPE_CHECKING
|
|
5
6
|
from portacode.utils.ntp_clock import ntp_clock
|
|
@@ -24,6 +25,7 @@ class CommandRegistry:
|
|
|
24
25
|
self.control_channel = control_channel
|
|
25
26
|
self.context = context
|
|
26
27
|
self._handlers: Dict[str, "BaseHandler"] = {}
|
|
28
|
+
self._inflight_dispatches: Dict[str, asyncio.Task[bool]] = {}
|
|
27
29
|
|
|
28
30
|
def register(self, handler_class: Type["BaseHandler"]) -> None:
|
|
29
31
|
"""Register a command handler.
|
|
@@ -99,6 +101,35 @@ class CommandRegistry:
|
|
|
99
101
|
logger.warning("registry: No handler found for command: %s", command_name)
|
|
100
102
|
return False
|
|
101
103
|
|
|
104
|
+
# System information is requested by periodic refreshes, dashboard
|
|
105
|
+
# joins, and explicit probes. All of those sources must share one
|
|
106
|
+
# collection; Proxmox discovery is too expensive to run concurrently.
|
|
107
|
+
if command_name == "system_info":
|
|
108
|
+
existing = self._inflight_dispatches.get(command_name)
|
|
109
|
+
if existing is not None and not existing.done():
|
|
110
|
+
logger.info("registry: Coalesced concurrent system_info request")
|
|
111
|
+
return True
|
|
112
|
+
task = asyncio.create_task(
|
|
113
|
+
self._dispatch_handler(command_name, handler, message, reply_channel)
|
|
114
|
+
)
|
|
115
|
+
self._inflight_dispatches[command_name] = task
|
|
116
|
+
|
|
117
|
+
def _clear_inflight(completed: asyncio.Task[bool]) -> None:
|
|
118
|
+
if self._inflight_dispatches.get(command_name) is completed:
|
|
119
|
+
self._inflight_dispatches.pop(command_name, None)
|
|
120
|
+
|
|
121
|
+
task.add_done_callback(_clear_inflight)
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
return await self._dispatch_handler(command_name, handler, message, reply_channel)
|
|
125
|
+
|
|
126
|
+
async def _dispatch_handler(
|
|
127
|
+
self,
|
|
128
|
+
command_name: str,
|
|
129
|
+
handler: "BaseHandler",
|
|
130
|
+
message: Dict[str, Any],
|
|
131
|
+
reply_channel: Optional[str],
|
|
132
|
+
) -> bool:
|
|
102
133
|
try:
|
|
103
134
|
await handler.handle(message, reply_channel)
|
|
104
135
|
logger.info("registry: Successfully dispatched command '%s'", command_name)
|
|
@@ -632,7 +632,7 @@ class SystemInfoHandler(SyncHandler):
|
|
|
632
632
|
info["protocol_capabilities"] = {
|
|
633
633
|
"guest_host_requests": {
|
|
634
634
|
"protocol_version": 1,
|
|
635
|
-
"request_types": ["expose_ports"],
|
|
635
|
+
"request_types": ["expose_ports", "power", "delete", "provision"],
|
|
636
636
|
"message_types": [
|
|
637
637
|
"guest_host_request",
|
|
638
638
|
"guest_host_ack",
|
{portacode-2.0.0.dev0 → portacode-2.0.2}/portacode/connection/handlers/test_proxmox_infra.py
RENAMED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from unittest import TestCase
|
|
2
|
+
import threading
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from tempfile import TemporaryDirectory
|
|
4
5
|
from unittest.mock import MagicMock, patch
|
|
5
6
|
|
|
6
7
|
from portacode.connection.handlers.proxmox_infra import (
|
|
7
8
|
RemoveProxmoxContainerHandler,
|
|
9
|
+
RestartProxmoxContainerHandler,
|
|
8
10
|
_build_full_container_summary,
|
|
9
11
|
_compose_managed_containers_summary,
|
|
10
12
|
_build_bootstrap_steps,
|
|
@@ -36,6 +38,31 @@ from portacode.connection.handlers.proxmox_infra import (
|
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
class ProxmoxInfraHandlerTests(TestCase):
|
|
41
|
+
@patch("portacode.connection.handlers.proxmox_infra.get_infra_snapshot", return_value={})
|
|
42
|
+
@patch("portacode.connection.handlers.proxmox_infra._update_container_record")
|
|
43
|
+
@patch("portacode.connection.handlers.proxmox_infra._start_container")
|
|
44
|
+
@patch("portacode.connection.handlers.proxmox_infra._stop_container")
|
|
45
|
+
@patch("portacode.connection.handlers.proxmox_infra._ensure_container_managed")
|
|
46
|
+
@patch("portacode.connection.handlers.proxmox_infra._resolve_vmid_for_device", return_value=101)
|
|
47
|
+
@patch("portacode.connection.handlers.proxmox_infra._get_node_from_config", return_value="pve")
|
|
48
|
+
@patch("portacode.connection.handlers.proxmox_infra._connect_proxmox")
|
|
49
|
+
@patch("portacode.connection.handlers.proxmox_infra._ensure_infra_configured", return_value={})
|
|
50
|
+
def test_restart_validates_child_then_stops_and_starts_once(
|
|
51
|
+
self, _config, connect, _node, _resolve, ensure_managed, stop, start, update, _snapshot
|
|
52
|
+
):
|
|
53
|
+
stop.return_value = ({"exitstatus": "OK", "status": "stopped"}, 1.0)
|
|
54
|
+
start.return_value = ({"exitstatus": "OK", "status": "running"}, 2.0)
|
|
55
|
+
handler = RestartProxmoxContainerHandler(MagicMock(), {})
|
|
56
|
+
|
|
57
|
+
result = handler.execute({"child_device_id": "42"})
|
|
58
|
+
|
|
59
|
+
ensure_managed.assert_called_once_with(connect.return_value, "pve", 101, device_id="42")
|
|
60
|
+
stop.assert_called_once_with(connect.return_value, "pve", 101)
|
|
61
|
+
start.assert_called_once_with(connect.return_value, "pve", 101)
|
|
62
|
+
update.assert_called_once_with(101, {"status": "running"})
|
|
63
|
+
self.assertEqual(result["action"], "restart")
|
|
64
|
+
self.assertTrue(result["success"])
|
|
65
|
+
|
|
39
66
|
def test_container_config_memory_is_always_mib_even_above_10000(self):
|
|
40
67
|
self.assertEqual(
|
|
41
68
|
_pick_container_ram_mib("lxc", {"memory": 32768}, {"maxmem": 34359738368}),
|
|
@@ -211,6 +238,50 @@ class ProxmoxInfraHandlerTests(TestCase):
|
|
|
211
238
|
self.assertEqual(result["cleaned_vmids"], [])
|
|
212
239
|
self.assertEqual(result["cleanup_errors"][0]["vmid"], "144")
|
|
213
240
|
|
|
241
|
+
@patch("portacode.connection.handlers.proxmox_infra._load_config", return_value={"node": "pve"})
|
|
242
|
+
def test_reconciliation_does_not_hold_state_lock_during_forwarding_cleanup(self, _config):
|
|
243
|
+
from portacode.connection.handlers import proxmox_infra
|
|
244
|
+
|
|
245
|
+
entered_cleanup = threading.Event()
|
|
246
|
+
release_cleanup = threading.Event()
|
|
247
|
+
state = {
|
|
248
|
+
"initialized": True, "base_summary": {}, "initial_totals": {},
|
|
249
|
+
"records": {"144": {"vmid": 144, "device_id": "42"}},
|
|
250
|
+
"pending": {}, "revision": 2,
|
|
251
|
+
}
|
|
252
|
+
refreshed = {
|
|
253
|
+
"inventory_updated_at": "now",
|
|
254
|
+
"missing_containers": [{"vmid": "144", "device_id": "42"}],
|
|
255
|
+
"total_ram_mib": 0, "total_disk_gib": 0, "total_cpu_share": 0,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
def blocking_cleanup(_device_id, _rules):
|
|
259
|
+
entered_cleanup.set()
|
|
260
|
+
release_cleanup.wait(timeout=1)
|
|
261
|
+
|
|
262
|
+
with TemporaryDirectory() as root, patch(
|
|
263
|
+
"portacode.connection.handlers.proxmox_infra.CONTAINERS_DIR", Path(root)
|
|
264
|
+
), patch(
|
|
265
|
+
"portacode.connection.handlers.proxmox_infra._MANAGED_CONTAINERS_STATE", state
|
|
266
|
+
), patch(
|
|
267
|
+
"portacode.connection.handlers.proxmox_infra._build_full_container_summary",
|
|
268
|
+
return_value=refreshed,
|
|
269
|
+
), patch(
|
|
270
|
+
"portacode.connection.handlers.cloudflare_forwarding.set_container_forwarding_rules",
|
|
271
|
+
side_effect=blocking_cleanup,
|
|
272
|
+
):
|
|
273
|
+
worker = threading.Thread(target=reconcile_managed_containers_inventory)
|
|
274
|
+
worker.start()
|
|
275
|
+
self.assertTrue(entered_cleanup.wait(timeout=0.2))
|
|
276
|
+
acquired = proxmox_infra._MANAGED_CONTAINERS_STATE_LOCK.acquire(timeout=0.2)
|
|
277
|
+
if acquired:
|
|
278
|
+
proxmox_infra._MANAGED_CONTAINERS_STATE_LOCK.release()
|
|
279
|
+
release_cleanup.set()
|
|
280
|
+
worker.join(timeout=1)
|
|
281
|
+
|
|
282
|
+
self.assertTrue(acquired)
|
|
283
|
+
self.assertFalse(worker.is_alive())
|
|
284
|
+
|
|
214
285
|
@patch("portacode.connection.handlers.proxmox_infra._load_config", return_value={"node": "pve"})
|
|
215
286
|
def test_reconciliation_discards_scan_when_managed_state_changes(self, _config):
|
|
216
287
|
state = {
|
|
@@ -74,6 +74,7 @@ from .handlers import (
|
|
|
74
74
|
StartPortacodeServiceHandler,
|
|
75
75
|
StartProxmoxContainerHandler,
|
|
76
76
|
StopProxmoxContainerHandler,
|
|
77
|
+
RestartProxmoxContainerHandler,
|
|
77
78
|
RemoveProxmoxContainerHandler,
|
|
78
79
|
CloudflareTunnelSetupHandler,
|
|
79
80
|
CloudflareForwardingHandler,
|
|
@@ -110,6 +111,7 @@ from portacode.tunneling.privileged import read_text, run, write_text
|
|
|
110
111
|
logger = logging.getLogger(__name__)
|
|
111
112
|
EXPOSED_SERVICES_MONITOR_PATH = "/etc/portacode/exposed_services.json"
|
|
112
113
|
EXPOSED_SERVICES_MONITOR_INTERVAL_S = 2.0
|
|
114
|
+
SYSTEM_INFO_INTERVAL_S = 10.0
|
|
113
115
|
EXPOSED_SERVICES_MISSING_SIGNATURE = "__missing__"
|
|
114
116
|
EXPOSED_SERVICES_INVALID_SIGNATURE = "__invalid__"
|
|
115
117
|
EXPOSED_SERVICES_ENV_PATH = Path("/etc/portacode/exposed_services.env")
|
|
@@ -595,6 +597,7 @@ class TerminalManager:
|
|
|
595
597
|
self._command_registry.register(StartPortacodeServiceHandler)
|
|
596
598
|
self._command_registry.register(StartProxmoxContainerHandler)
|
|
597
599
|
self._command_registry.register(StopProxmoxContainerHandler)
|
|
600
|
+
self._command_registry.register(RestartProxmoxContainerHandler)
|
|
598
601
|
self._command_registry.register(RemoveProxmoxContainerHandler)
|
|
599
602
|
self._command_registry.register(RevertProxmoxInfraHandler)
|
|
600
603
|
self._command_registry.register(CloudflareTunnelSetupHandler)
|
|
@@ -776,12 +779,14 @@ class TerminalManager:
|
|
|
776
779
|
"""Send system_info event every 10 seconds when clients are connected."""
|
|
777
780
|
while True:
|
|
778
781
|
try:
|
|
779
|
-
await asyncio.sleep(
|
|
780
|
-
if self._client_session_manager.has_interested_clients():
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
782
|
+
await asyncio.sleep(SYSTEM_INFO_INTERVAL_S)
|
|
783
|
+
if not self._client_session_manager.has_interested_clients():
|
|
784
|
+
continue
|
|
785
|
+
# CommandRegistry owns the single-flight invariant shared by
|
|
786
|
+
# periodic ticks, dashboard joins, and explicit probes.
|
|
787
|
+
await self._command_registry.dispatch("system_info", {}, None)
|
|
788
|
+
except asyncio.CancelledError:
|
|
789
|
+
raise
|
|
785
790
|
except Exception as exc:
|
|
786
791
|
logger.exception("Error in periodic system info: %s", exc)
|
|
787
792
|
continue
|
|
@@ -30,6 +30,21 @@ class Registry:
|
|
|
30
30
|
return self.handler if command == self.handler.command_name else None
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
class CommandRecordingHandler(SyncHandler):
|
|
34
|
+
def __init__(self, control_channel, session, command):
|
|
35
|
+
super().__init__(control_channel, session)
|
|
36
|
+
self._command = command
|
|
37
|
+
self.messages = []
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def command_name(self):
|
|
41
|
+
return self._command
|
|
42
|
+
|
|
43
|
+
def execute(self, message):
|
|
44
|
+
self.messages.append(message)
|
|
45
|
+
return {"event": "handled", "command": self._command}
|
|
46
|
+
|
|
47
|
+
|
|
33
48
|
def request(request_id="ghr-1", target="42"):
|
|
34
49
|
return {
|
|
35
50
|
"type": GUEST_HOST_REQUEST,
|
|
@@ -76,11 +91,36 @@ class GuestHostTerminalTests(IsolatedAsyncioTestCase):
|
|
|
76
91
|
await self.manager._handle_guest_host_request(envelope)
|
|
77
92
|
self.assertEqual(self.manager._control_channel.sent, [])
|
|
78
93
|
|
|
79
|
-
async def
|
|
94
|
+
async def test_enabled_request_without_registered_handler_is_rejected(self):
|
|
80
95
|
envelope = request()
|
|
81
96
|
envelope["command"] = "stop_proxmox_container"
|
|
82
97
|
envelope["authorization"]["operation"] = "power.stop"
|
|
83
98
|
await self.manager._handle_guest_host_request(envelope)
|
|
84
99
|
ack, result = self.manager._control_channel.sent
|
|
85
|
-
self.
|
|
86
|
-
self.assertEqual(result["data"]["error_code"], "
|
|
100
|
+
self.assertTrue(ack["data"]["accepted"])
|
|
101
|
+
self.assertEqual(result["data"]["error_code"], "handler_unavailable")
|
|
102
|
+
|
|
103
|
+
async def test_each_advertised_operation_delegates_to_existing_handler(self):
|
|
104
|
+
cases = (
|
|
105
|
+
("start_proxmox_container", "power.start"),
|
|
106
|
+
("stop_proxmox_container", "power.stop"),
|
|
107
|
+
("restart_proxmox_container", "power.reboot"),
|
|
108
|
+
("remove_proxmox_container", "delete"),
|
|
109
|
+
("create_proxmox_container", "provision"),
|
|
110
|
+
)
|
|
111
|
+
for command, operation in cases:
|
|
112
|
+
with self.subTest(command=command):
|
|
113
|
+
handler = CommandRecordingHandler(self.manager._control_channel, {}, command)
|
|
114
|
+
self.manager._command_registry = Registry(handler)
|
|
115
|
+
self.manager._control_channel.sent.clear()
|
|
116
|
+
envelope = request(request_id=f"ghr-{command}")
|
|
117
|
+
envelope["command"] = command
|
|
118
|
+
envelope["authorization"]["operation"] = operation
|
|
119
|
+
|
|
120
|
+
await self.manager._handle_guest_host_request(envelope)
|
|
121
|
+
|
|
122
|
+
ack, result = self.manager._control_channel.sent
|
|
123
|
+
self.assertTrue(ack["data"]["accepted"])
|
|
124
|
+
self.assertTrue(result["data"]["success"])
|
|
125
|
+
self.assertEqual(len(handler.messages), 1)
|
|
126
|
+
self.assertEqual(handler.messages[0]["child_device_id"], "42")
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from types import SimpleNamespace
|
|
3
|
+
from unittest.mock import AsyncMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from portacode.connection import terminal
|
|
8
|
+
from portacode.connection.handlers.registry import CommandRegistry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.asyncio
|
|
12
|
+
async def test_registry_coalesces_concurrent_system_info_dispatches():
|
|
13
|
+
started = asyncio.Event()
|
|
14
|
+
release = asyncio.Event()
|
|
15
|
+
calls = 0
|
|
16
|
+
|
|
17
|
+
class BlockingHandler:
|
|
18
|
+
async def handle(self, _message, _reply_channel):
|
|
19
|
+
nonlocal calls
|
|
20
|
+
calls += 1
|
|
21
|
+
started.set()
|
|
22
|
+
await release.wait()
|
|
23
|
+
|
|
24
|
+
registry = CommandRegistry(AsyncMock(), {})
|
|
25
|
+
registry._handlers["system_info"] = BlockingHandler()
|
|
26
|
+
|
|
27
|
+
assert await registry.dispatch("system_info", {}, None)
|
|
28
|
+
await asyncio.wait_for(started.wait(), timeout=0.1)
|
|
29
|
+
assert await registry.dispatch("system_info", {}, None)
|
|
30
|
+
assert calls == 1
|
|
31
|
+
|
|
32
|
+
inflight = registry._inflight_dispatches["system_info"]
|
|
33
|
+
release.set()
|
|
34
|
+
await asyncio.wait_for(inflight, timeout=0.1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_periodic_system_info_uses_shared_registry(monkeypatch):
|
|
39
|
+
manager = object.__new__(terminal.TerminalManager)
|
|
40
|
+
manager._client_session_manager = SimpleNamespace(
|
|
41
|
+
has_interested_clients=lambda: True
|
|
42
|
+
)
|
|
43
|
+
manager._command_registry = SimpleNamespace(dispatch=AsyncMock(return_value=True))
|
|
44
|
+
monkeypatch.setattr(terminal, "SYSTEM_INFO_INTERVAL_S", 0.001)
|
|
45
|
+
periodic = asyncio.create_task(manager._periodic_system_info())
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
async def wait_for_dispatch():
|
|
49
|
+
while not manager._command_registry.dispatch.await_count:
|
|
50
|
+
await asyncio.sleep(0.001)
|
|
51
|
+
|
|
52
|
+
await asyncio.wait_for(wait_for_dispatch(), timeout=0.1)
|
|
53
|
+
manager._command_registry.dispatch.assert_awaited_with(
|
|
54
|
+
"system_info", {}, None
|
|
55
|
+
)
|
|
56
|
+
finally:
|
|
57
|
+
periodic.cancel()
|
|
58
|
+
with pytest.raises(asyncio.CancelledError):
|
|
59
|
+
await periodic
|