portacode 1.4.35.dev4__tar.gz → 1.4.37.dev0__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-1.4.35.dev4 → portacode-1.4.37.dev0}/PKG-INFO +1 -1
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/_version.py +2 -2
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/client.py +20 -3
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/terminal.py +198 -0
- portacode-1.4.37.dev0/portacode/exit_codes.py +7 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/service.py +52 -10
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/SOURCES.txt +2 -1
- portacode-1.4.37.dev0/todo/issues/websocket_client_silently_dead.md +51 -0
- portacode-1.4.35.dev4/portacode/test_updater.py +0 -100
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.claude/agents/communication-manager.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.claude/settings.local.json +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.gitignore +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/.gitmodules +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/LICENSE +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/MANIFEST.in +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/Makefile +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/backup.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/build_android.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/connect.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/connect.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docker-compose.yaml +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/cloudflared-domain-connect-containers-audit.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/creative-team-brief-portacode.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/devops-messaging-ab-tests.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/homepage-dashboard-positioning-fixes.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/device-transfer-button.png +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/device-transfer-modal.png +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/pair-device-button.png +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/pairing-request.png +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/images/student-workspace.png +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/docs/template-guide.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/Dockerfile +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/data/device-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/simple_device/docker-compose.yaml +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/Dockerfile +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/.local/share/portacode/run/gateway.pid +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/.gitignore +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/db.sqlite3 +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/settings.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/__pycache__/wsgi.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/admin.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/apps.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/menu.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/models.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/urls.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/__pycache__/views.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/migrations/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-01/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-02/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-03/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-04/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-05/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-06/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-07/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-08/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-09/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/.local/share/portacode/keys/id_portacode.pub +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/data/student-10/workspace/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/docker-compose.yaml +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/asgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/settings.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/galactic_bakeshop/wsgi.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/manage.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/templates/treats/home.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/admin.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/apps.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/menu.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/migrations/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/urls.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/initial_content/treats/views.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/examples/workshop_fleet/instructions/WELCOME.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/__main__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/cli.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/automation_v2_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/base.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/chunked_content.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/cloudflare_forwarding.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/cloudflare_tunnel.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/diff_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_aware_file_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/file_system_watcher.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/git_manager.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/manager.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/models.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state/utils.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/project_state_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/proxmox_infra.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/registry.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/session.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/tab_factory.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/test_proxmox_infra.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/handlers/update_handler.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/connection/multiplex.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/data.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/keypair.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/__pycache__/__init__.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/__pycache__/link_capture_wrapper.cpython-311.pyc +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/elinks +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/gio-open +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/gnome-open +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/gvfs-open +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/kde-open +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/kfmclient +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/link_capture_exec.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/link_capture_wrapper.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/links +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/links2 +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/lynx +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/mate-open +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/netsurf +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/sensible-browser +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/w3m +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/x-www-browser +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/link_capture/bin/xdg-open +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/logging_categories.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/pairing.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/restart.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/static/js/test-ntp-clock.html +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/static/js/utils/ntp-clock.js +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/cloudflared_login.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/ensure_cloudflared.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/ensure_pyyaml.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/forwarding_state.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/get_domain.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/privileged.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/service_install.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/tunneling/state.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/updater.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/NTP_ARCHITECTURE.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/diff_apply.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/diff_renderer.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode/utils/ntp_clock.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/requires.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/portacode.egg-info/top_level.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/pyproject.toml +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/restore.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/run_tests.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/setup.cfg +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/setup.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_device_online.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_file_operations.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_git_status_ui.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_login_flow.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_navigate_testing_folder.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_play_store_screenshots.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_buffer_performance.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_interaction.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_loading_race_condition.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_modules/test_terminal_start.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/test_request_id.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/.env.example +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/README.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/cli.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/__init__.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/base_test.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/cli_manager.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/hierarchical_runner.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/playwright_manager.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/runner.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/shared_cli_manager.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/core/test_discovery.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/testing_framework/requirements.txt +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/UI_UX/opening_a_file_on_desktop_results_in_nothing.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/UI_UX/server_occasionally_stops_communicating_with_all_devices.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/agent_context_management.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/django_server_time_sync.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/device_performance_degradation.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/git_data_not_captured_in_proxmox.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/indefinite_resource_loading.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/portacode_service_silently_down.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/premature_terminal_exit.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/project_cpu_hotspots.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/terminals_exit_upon_starting.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/issues/wrong_item_classification_on_client_side.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/todo/smartphone_terminal_input_frustrations.md +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/generate_play_store_assets.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/pairing_tester.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/run_screenshot_suite.sh +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/tools/test_python_ntp_clock.py +0 -0
- {portacode-1.4.35.dev4 → portacode-1.4.37.dev0}/validate.sh +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4,
|
|
31
|
+
__version__ = version = '1.4.37.dev0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 37, 'dev0')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -17,9 +17,13 @@ from websockets import WebSocketClientProtocol
|
|
|
17
17
|
from ..keypair import KeyPair
|
|
18
18
|
from .multiplex import Multiplexer
|
|
19
19
|
from ..logging_categories import get_categorized_logger, LogCategory
|
|
20
|
+
from ..exit_codes import AUTH_REJECTED_EXIT_CODE
|
|
20
21
|
|
|
21
22
|
logger = get_categorized_logger(__name__)
|
|
22
23
|
|
|
24
|
+
class AuthenticationRejectedError(RuntimeError):
|
|
25
|
+
"""Raised when gateway authentication is rejected for this device key."""
|
|
26
|
+
|
|
23
27
|
|
|
24
28
|
class ConnectionManager:
|
|
25
29
|
"""Maintain a persistent connection to the Portacode gateway.
|
|
@@ -43,6 +47,8 @@ class ConnectionManager:
|
|
|
43
47
|
CLOCK_SYNC_INITIAL_REQUESTS = 5
|
|
44
48
|
CLOCK_SYNC_TIMEOUT = 20.0
|
|
45
49
|
CLOCK_SYNC_MAX_FAILURES = 3
|
|
50
|
+
WS_PING_INTERVAL = 60.0
|
|
51
|
+
WS_PING_TIMEOUT = 20.0
|
|
46
52
|
|
|
47
53
|
def __init__(self, gateway_url: str, keypair: KeyPair, reconnect_delay: float = 1.0, max_retries: int = None, debug: bool = False):
|
|
48
54
|
self.gateway_url = gateway_url
|
|
@@ -85,7 +91,11 @@ class ConnectionManager:
|
|
|
85
91
|
logger.warning("Reconnecting in %.1f s (attempt %d)…", LogCategory.CONNECTION, delay, attempt)
|
|
86
92
|
await asyncio.sleep(delay)
|
|
87
93
|
logger.info("Connecting to gateway at %s", LogCategory.CONNECTION, self.gateway_url)
|
|
88
|
-
async with websockets.connect(
|
|
94
|
+
async with websockets.connect(
|
|
95
|
+
self.gateway_url,
|
|
96
|
+
ping_interval=self.WS_PING_INTERVAL,
|
|
97
|
+
ping_timeout=self.WS_PING_TIMEOUT,
|
|
98
|
+
) as ws:
|
|
89
99
|
# Reset attempt counter after successful connection
|
|
90
100
|
attempt = 0
|
|
91
101
|
|
|
@@ -118,6 +128,9 @@ class ConnectionManager:
|
|
|
118
128
|
logger.warning("Connection error: %s", LogCategory.CONNECTION, exc)
|
|
119
129
|
# Remove the max_retries limit - keep trying indefinitely
|
|
120
130
|
# The service manager (systemd) will handle any necessary restarts
|
|
131
|
+
except AuthenticationRejectedError as exc:
|
|
132
|
+
logger.error("Authentication rejected by gateway: %s", LogCategory.CONNECTION, exc)
|
|
133
|
+
sys.exit(AUTH_REJECTED_EXIT_CODE)
|
|
121
134
|
except Exception as exc:
|
|
122
135
|
# For truly fatal errors (like authentication failures),
|
|
123
136
|
# log and exit cleanly so systemd can restart the service
|
|
@@ -137,7 +150,7 @@ class ConnectionManager:
|
|
|
137
150
|
challenge = data["challenge"]
|
|
138
151
|
except Exception:
|
|
139
152
|
# Not a challenge, must be an error
|
|
140
|
-
raise
|
|
153
|
+
raise AuthenticationRejectedError(f"Gateway rejected authentication: {response}")
|
|
141
154
|
# Step 3: Sign challenge and send signature
|
|
142
155
|
signature = self.keypair.sign_challenge(challenge)
|
|
143
156
|
signature_b64 = base64.b64encode(signature).decode()
|
|
@@ -145,7 +158,7 @@ class ConnectionManager:
|
|
|
145
158
|
# Step 4: Receive final status
|
|
146
159
|
status = await self.websocket.recv()
|
|
147
160
|
if status != "ok":
|
|
148
|
-
raise
|
|
161
|
+
raise AuthenticationRejectedError(f"Gateway rejected authentication: {status}")
|
|
149
162
|
# Print success message in green and show close instructions
|
|
150
163
|
try:
|
|
151
164
|
import click
|
|
@@ -206,6 +219,7 @@ class ConnectionManager:
|
|
|
206
219
|
self._clock_sync_task.cancel()
|
|
207
220
|
self._clock_sync_task = asyncio.create_task(self._clock_sync_loop())
|
|
208
221
|
self._remaining_initial_syncs = self.CLOCK_SYNC_INITIAL_REQUESTS
|
|
222
|
+
self._clock_sync_failures = 0
|
|
209
223
|
|
|
210
224
|
async def _stop_clock_sync_task(self) -> None:
|
|
211
225
|
if self._clock_sync_task:
|
|
@@ -228,6 +242,9 @@ class ConnectionManager:
|
|
|
228
242
|
self._remaining_initial_syncs -= 1
|
|
229
243
|
if self._remaining_initial_syncs > 0:
|
|
230
244
|
await asyncio.sleep(self.CLOCK_SYNC_FAST_INTERVAL)
|
|
245
|
+
while not self._stop_event.is_set():
|
|
246
|
+
await asyncio.sleep(self.CLOCK_SYNC_INTERVAL)
|
|
247
|
+
await self._perform_clock_sync()
|
|
231
248
|
except asyncio.CancelledError:
|
|
232
249
|
pass
|
|
233
250
|
|
|
@@ -14,9 +14,12 @@ in the handlers directory.
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
|
+
import faulthandler
|
|
17
18
|
import json
|
|
18
19
|
import logging
|
|
19
20
|
import os
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
20
23
|
import time
|
|
21
24
|
from datetime import datetime, timezone
|
|
22
25
|
from dataclasses import asdict
|
|
@@ -389,11 +392,185 @@ class TerminalManager:
|
|
|
389
392
|
self.mux = mux
|
|
390
393
|
self.debug = debug
|
|
391
394
|
self._last_exposed_services_signature = "__unset__"
|
|
395
|
+
self._diag_lock = threading.Lock()
|
|
396
|
+
now_mono = time.monotonic()
|
|
397
|
+
self._diag_state: Dict[str, Any] = {
|
|
398
|
+
"heartbeat_monotonic": now_mono,
|
|
399
|
+
"heartbeat_epoch_s": time.time(),
|
|
400
|
+
"loop_phase": "init",
|
|
401
|
+
"active_cmd": None,
|
|
402
|
+
"active_cmd_started_monotonic": None,
|
|
403
|
+
"active_cmd_reply_channel": None,
|
|
404
|
+
"last_completed_cmd": None,
|
|
405
|
+
"last_completed_cmd_duration_s": None,
|
|
406
|
+
"queue_stats": {},
|
|
407
|
+
}
|
|
408
|
+
self._diag_monitor_enabled = os.getenv("PORTACODE_EVENT_LOOP_BLOCK_MONITOR", "1").lower() not in ("0", "false", "no")
|
|
409
|
+
self._diag_threshold_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_THRESHOLD_S", "5"))
|
|
410
|
+
self._diag_monitor_interval_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_MONITOR_INTERVAL_S", "0.5"))
|
|
411
|
+
self._diag_report_interval_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_REPORT_INTERVAL_S", "30"))
|
|
412
|
+
self._diag_heartbeat_interval_s = float(os.getenv("PORTACODE_EVENT_LOOP_BLOCK_HEARTBEAT_INTERVAL_S", "0.5"))
|
|
413
|
+
self._diag_dump_threads = os.getenv("PORTACODE_EVENT_LOOP_BLOCK_DUMP_THREADS", "1").lower() not in ("0", "false", "no")
|
|
414
|
+
self._diag_monitor_stop = threading.Event()
|
|
415
|
+
self._diag_monitor_thread: Optional[threading.Thread] = None
|
|
416
|
+
self._diag_last_report_monotonic = 0.0
|
|
417
|
+
self._diag_last_report_key: Optional[str] = None
|
|
392
418
|
self._session_manager = None # Initialize as None first
|
|
393
419
|
self._client_session_manager = ClientSessionManager() # Initialize client session manager
|
|
394
420
|
self._client_session_manager.set_terminal_manager(self) # Set reference for cleanup
|
|
421
|
+
self._start_block_monitor_thread_if_enabled()
|
|
395
422
|
self._set_mux(mux, is_initial=True)
|
|
396
423
|
|
|
424
|
+
def _start_block_monitor_thread_if_enabled(self) -> None:
|
|
425
|
+
if not self._diag_monitor_enabled or self._diag_monitor_thread:
|
|
426
|
+
return
|
|
427
|
+
self._diag_monitor_thread = threading.Thread(
|
|
428
|
+
target=self._event_loop_block_monitor_thread,
|
|
429
|
+
name="portacode-event-loop-block-monitor",
|
|
430
|
+
daemon=True,
|
|
431
|
+
)
|
|
432
|
+
self._diag_monitor_thread.start()
|
|
433
|
+
|
|
434
|
+
def _set_diag_phase(self, phase: str) -> None:
|
|
435
|
+
now_mono = time.monotonic()
|
|
436
|
+
now_epoch = time.time()
|
|
437
|
+
with self._diag_lock:
|
|
438
|
+
self._diag_state["loop_phase"] = phase
|
|
439
|
+
self._diag_state["heartbeat_monotonic"] = now_mono
|
|
440
|
+
self._diag_state["heartbeat_epoch_s"] = now_epoch
|
|
441
|
+
|
|
442
|
+
def _set_diag_active_cmd(self, cmd: str, reply_channel: Optional[str]) -> None:
|
|
443
|
+
now_mono = time.monotonic()
|
|
444
|
+
now_epoch = time.time()
|
|
445
|
+
with self._diag_lock:
|
|
446
|
+
self._diag_state["active_cmd"] = cmd
|
|
447
|
+
self._diag_state["active_cmd_reply_channel"] = reply_channel
|
|
448
|
+
self._diag_state["active_cmd_started_monotonic"] = now_mono
|
|
449
|
+
self._diag_state["heartbeat_monotonic"] = now_mono
|
|
450
|
+
self._diag_state["heartbeat_epoch_s"] = now_epoch
|
|
451
|
+
|
|
452
|
+
def _clear_diag_active_cmd(self, completed_cmd: str, duration_s: float) -> None:
|
|
453
|
+
now_mono = time.monotonic()
|
|
454
|
+
now_epoch = time.time()
|
|
455
|
+
with self._diag_lock:
|
|
456
|
+
self._diag_state["active_cmd"] = None
|
|
457
|
+
self._diag_state["active_cmd_reply_channel"] = None
|
|
458
|
+
self._diag_state["active_cmd_started_monotonic"] = None
|
|
459
|
+
self._diag_state["last_completed_cmd"] = completed_cmd
|
|
460
|
+
self._diag_state["last_completed_cmd_duration_s"] = round(duration_s, 4)
|
|
461
|
+
self._diag_state["heartbeat_monotonic"] = now_mono
|
|
462
|
+
self._diag_state["heartbeat_epoch_s"] = now_epoch
|
|
463
|
+
|
|
464
|
+
def _snapshot_diag_state_for_monitor(self) -> Dict[str, Any]:
|
|
465
|
+
with self._diag_lock:
|
|
466
|
+
return dict(self._diag_state)
|
|
467
|
+
|
|
468
|
+
def _event_loop_block_monitor_thread(self) -> None:
|
|
469
|
+
while not self._diag_monitor_stop.wait(self._diag_monitor_interval_s):
|
|
470
|
+
snapshot = self._snapshot_diag_state_for_monitor()
|
|
471
|
+
now = time.monotonic()
|
|
472
|
+
last_hb = float(snapshot.get("heartbeat_monotonic") or now)
|
|
473
|
+
blocked_for_s = max(now - last_hb, 0.0)
|
|
474
|
+
|
|
475
|
+
if blocked_for_s < self._diag_threshold_s:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
active_cmd = snapshot.get("active_cmd")
|
|
479
|
+
queue_stats = snapshot.get("queue_stats") if isinstance(snapshot.get("queue_stats"), dict) else {}
|
|
480
|
+
control_q = queue_stats.get("control_channel_queue_size")
|
|
481
|
+
total_mux_q = queue_stats.get("mux_total_queued_frames")
|
|
482
|
+
has_backlog = (
|
|
483
|
+
(isinstance(control_q, int) and control_q > 0)
|
|
484
|
+
or (isinstance(total_mux_q, int) and total_mux_q > 0)
|
|
485
|
+
)
|
|
486
|
+
if not active_cmd and not has_backlog:
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
active_cmd_started = snapshot.get("active_cmd_started_monotonic")
|
|
490
|
+
active_cmd_age_s = None
|
|
491
|
+
if active_cmd and isinstance(active_cmd_started, (int, float)):
|
|
492
|
+
active_cmd_age_s = max(now - float(active_cmd_started), 0.0)
|
|
493
|
+
|
|
494
|
+
report_key = f"{snapshot.get('loop_phase')}:{active_cmd}:{snapshot.get('heartbeat_epoch_s')}"
|
|
495
|
+
if report_key == self._diag_last_report_key and (now - self._diag_last_report_monotonic) < self._diag_report_interval_s:
|
|
496
|
+
continue
|
|
497
|
+
|
|
498
|
+
self._diag_last_report_key = report_key
|
|
499
|
+
self._diag_last_report_monotonic = now
|
|
500
|
+
|
|
501
|
+
payload = {
|
|
502
|
+
"event": "event_loop_blocking_detected",
|
|
503
|
+
"blocked_for_s": round(blocked_for_s, 3),
|
|
504
|
+
"threshold_s": self._diag_threshold_s,
|
|
505
|
+
"cause_hint": {
|
|
506
|
+
"loop_phase": snapshot.get("loop_phase"),
|
|
507
|
+
"active_cmd": active_cmd,
|
|
508
|
+
"active_cmd_reply_channel": snapshot.get("active_cmd_reply_channel"),
|
|
509
|
+
"active_cmd_age_s": round(active_cmd_age_s, 3) if active_cmd_age_s is not None else None,
|
|
510
|
+
"last_completed_cmd": snapshot.get("last_completed_cmd"),
|
|
511
|
+
"last_completed_cmd_duration_s": snapshot.get("last_completed_cmd_duration_s"),
|
|
512
|
+
},
|
|
513
|
+
"queue_stats": queue_stats,
|
|
514
|
+
"last_heartbeat_epoch_s": snapshot.get("heartbeat_epoch_s"),
|
|
515
|
+
"monitor_thread_epoch_s": time.time(),
|
|
516
|
+
"pid": os.getpid(),
|
|
517
|
+
}
|
|
518
|
+
logger.error("Event loop stall monitor: %s", json.dumps(payload, sort_keys=True))
|
|
519
|
+
|
|
520
|
+
if self._diag_dump_threads:
|
|
521
|
+
try:
|
|
522
|
+
faulthandler.dump_traceback(file=sys.stderr, all_threads=True)
|
|
523
|
+
except Exception as exc:
|
|
524
|
+
logger.error("Event loop stall monitor failed to dump thread traceback: %s", exc)
|
|
525
|
+
|
|
526
|
+
def _collect_loop_queue_stats(self) -> Dict[str, Any]:
|
|
527
|
+
control_q_size = None
|
|
528
|
+
try:
|
|
529
|
+
control_q = getattr(self._control_channel, "_incoming", None)
|
|
530
|
+
if control_q is not None:
|
|
531
|
+
control_q_size = control_q.qsize()
|
|
532
|
+
except Exception:
|
|
533
|
+
control_q_size = None
|
|
534
|
+
|
|
535
|
+
mux_channels = getattr(self.mux, "_channels", {}) if self.mux else {}
|
|
536
|
+
total_queued = 0
|
|
537
|
+
top_channels = []
|
|
538
|
+
for channel_id, channel in mux_channels.items():
|
|
539
|
+
try:
|
|
540
|
+
incoming_q = getattr(channel, "_incoming", None)
|
|
541
|
+
size = incoming_q.qsize() if incoming_q is not None else None
|
|
542
|
+
except Exception:
|
|
543
|
+
size = None
|
|
544
|
+
if isinstance(size, int):
|
|
545
|
+
total_queued += size
|
|
546
|
+
top_channels.append((str(channel_id), size))
|
|
547
|
+
|
|
548
|
+
top_channels.sort(key=lambda item: item[1], reverse=True)
|
|
549
|
+
return {
|
|
550
|
+
"control_channel_queue_size": control_q_size,
|
|
551
|
+
"mux_channel_count": len(mux_channels),
|
|
552
|
+
"mux_total_queued_frames": total_queued,
|
|
553
|
+
"mux_top_queues": [{"channel_id": cid, "size": size} for cid, size in top_channels[:5]],
|
|
554
|
+
"client_sessions_count": len(self._client_session_manager.get_sessions()),
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async def _diag_heartbeat_loop(self) -> None:
|
|
558
|
+
while True:
|
|
559
|
+
try:
|
|
560
|
+
await asyncio.sleep(self._diag_heartbeat_interval_s)
|
|
561
|
+
queue_stats = self._collect_loop_queue_stats()
|
|
562
|
+
now_mono = time.monotonic()
|
|
563
|
+
now_epoch = time.time()
|
|
564
|
+
with self._diag_lock:
|
|
565
|
+
self._diag_state["heartbeat_monotonic"] = now_mono
|
|
566
|
+
self._diag_state["heartbeat_epoch_s"] = now_epoch
|
|
567
|
+
self._diag_state["queue_stats"] = queue_stats
|
|
568
|
+
except asyncio.CancelledError:
|
|
569
|
+
return
|
|
570
|
+
except Exception as exc:
|
|
571
|
+
logger.debug("Event loop heartbeat diagnostics error: %s", exc)
|
|
572
|
+
await asyncio.sleep(self._diag_heartbeat_interval_s)
|
|
573
|
+
|
|
397
574
|
# ------------------------------------------------------------------
|
|
398
575
|
# Mux attach/detach helpers (for reconnection resilience)
|
|
399
576
|
# ------------------------------------------------------------------
|
|
@@ -469,6 +646,14 @@ class TerminalManager:
|
|
|
469
646
|
except Exception:
|
|
470
647
|
pass
|
|
471
648
|
self._exposed_services_task = asyncio.create_task(self._watch_exposed_services())
|
|
649
|
+
|
|
650
|
+
# Start passive event-loop diagnostics heartbeat (asyncio thread only).
|
|
651
|
+
if getattr(self, "_diag_heartbeat_task", None):
|
|
652
|
+
try:
|
|
653
|
+
self._diag_heartbeat_task.cancel()
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
self._diag_heartbeat_task = asyncio.create_task(self._diag_heartbeat_loop())
|
|
472
657
|
|
|
473
658
|
# For initial connections, request client sessions after control loop starts
|
|
474
659
|
if is_initial:
|
|
@@ -531,7 +716,9 @@ class TerminalManager:
|
|
|
531
716
|
logger.info("terminal_manager: Starting control loop")
|
|
532
717
|
while True:
|
|
533
718
|
try:
|
|
719
|
+
self._set_diag_phase("control_loop_waiting_for_message")
|
|
534
720
|
message = await self._control_channel.recv()
|
|
721
|
+
self._set_diag_phase("control_loop_received_message")
|
|
535
722
|
logger.debug("terminal_manager: Received message: %s", message)
|
|
536
723
|
|
|
537
724
|
# Older parts of the system may send *raw* str. Ensure dict.
|
|
@@ -554,6 +741,8 @@ class TerminalManager:
|
|
|
554
741
|
logger.warning("terminal_manager: Missing 'cmd' in control frame: %s", message)
|
|
555
742
|
continue
|
|
556
743
|
reply_chan = message.get("reply_channel")
|
|
744
|
+
cmd_start = time.monotonic()
|
|
745
|
+
self._set_diag_active_cmd(cmd, reply_chan)
|
|
557
746
|
|
|
558
747
|
logger.info("terminal_manager: Processing command '%s' with reply_channel=%s", cmd, reply_chan)
|
|
559
748
|
logger.debug("terminal_manager: Full message: %s", message)
|
|
@@ -573,6 +762,8 @@ class TerminalManager:
|
|
|
573
762
|
asyncio.create_task(self._send_initial_data_to_clients(newly_added_sessions))
|
|
574
763
|
else:
|
|
575
764
|
logger.info("terminal_manager: ℹ️ No new sessions to send data to")
|
|
765
|
+
self._clear_diag_active_cmd(cmd, time.monotonic() - cmd_start)
|
|
766
|
+
self._set_diag_phase("control_loop_completed_client_sessions_update")
|
|
576
767
|
continue
|
|
577
768
|
|
|
578
769
|
# Dispatch command through registry
|
|
@@ -580,9 +771,16 @@ class TerminalManager:
|
|
|
580
771
|
if not handled:
|
|
581
772
|
logger.warning("terminal_manager: Command '%s' was not handled by any handler", cmd)
|
|
582
773
|
await self._send_error(f"Unknown cmd: {cmd}", reply_chan)
|
|
774
|
+
self._clear_diag_active_cmd(cmd, time.monotonic() - cmd_start)
|
|
775
|
+
self._set_diag_phase(f"control_loop_completed_{cmd}")
|
|
583
776
|
|
|
584
777
|
except Exception as exc:
|
|
585
778
|
logger.exception("terminal_manager: Error in control loop: %s", exc)
|
|
779
|
+
with self._diag_lock:
|
|
780
|
+
self._diag_state["active_cmd"] = None
|
|
781
|
+
self._diag_state["active_cmd_started_monotonic"] = None
|
|
782
|
+
self._diag_state["active_cmd_reply_channel"] = None
|
|
783
|
+
self._set_diag_phase("control_loop_exception")
|
|
586
784
|
# Continue processing other messages
|
|
587
785
|
continue
|
|
588
786
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Shared process exit codes used by the Portacode runtime and service managers."""
|
|
2
|
+
|
|
3
|
+
# Process exited because the gateway rejected device authentication (for example,
|
|
4
|
+
# an unknown/deleted device key). Service supervisors should not auto-restart on
|
|
5
|
+
# this code to avoid endless auth failure loops.
|
|
6
|
+
AUTH_REJECTED_EXIT_CODE = 86
|
|
7
|
+
|
|
@@ -29,6 +29,8 @@ import shutil
|
|
|
29
29
|
import pwd
|
|
30
30
|
import tempfile
|
|
31
31
|
|
|
32
|
+
from .exit_codes import AUTH_REJECTED_EXIT_CODE
|
|
33
|
+
|
|
32
34
|
__all__ = [
|
|
33
35
|
"ServiceManager",
|
|
34
36
|
"get_manager",
|
|
@@ -117,8 +119,6 @@ class _SystemdUserService:
|
|
|
117
119
|
current_shell = os.getenv("SHELL", "/bin/bash")
|
|
118
120
|
|
|
119
121
|
if self.system_mode:
|
|
120
|
-
sudo_needed = os.geteuid() != 0
|
|
121
|
-
prefix = ["sudo"] if sudo_needed else []
|
|
122
122
|
unit = textwrap.dedent(f"""
|
|
123
123
|
[Unit]
|
|
124
124
|
Description=Portacode persistent connection (system-wide)
|
|
@@ -131,6 +131,7 @@ class _SystemdUserService:
|
|
|
131
131
|
Environment=SHELL={current_shell}
|
|
132
132
|
ExecStart={self.python} -m portacode connect --non-interactive
|
|
133
133
|
Restart=on-failure
|
|
134
|
+
RestartPreventExitStatus={AUTH_REJECTED_EXIT_CODE}
|
|
134
135
|
RestartSec=5
|
|
135
136
|
|
|
136
137
|
[Install]
|
|
@@ -147,6 +148,7 @@ class _SystemdUserService:
|
|
|
147
148
|
Environment=SHELL={current_shell}
|
|
148
149
|
ExecStart={self.python} -m portacode.cli connect --non-interactive
|
|
149
150
|
Restart=on-failure
|
|
151
|
+
RestartPreventExitStatus={AUTH_REJECTED_EXIT_CODE}
|
|
150
152
|
RestartSec=5
|
|
151
153
|
|
|
152
154
|
[Install]
|
|
@@ -256,8 +258,6 @@ class _OpenRCService:
|
|
|
256
258
|
pidfile="/run/portacode.pid"
|
|
257
259
|
directory="{self.home}"
|
|
258
260
|
supervisor=supervise-daemon
|
|
259
|
-
respawn_delay=5
|
|
260
|
-
respawn_max=0
|
|
261
261
|
|
|
262
262
|
depend() {{
|
|
263
263
|
need net
|
|
@@ -289,7 +289,18 @@ class _OpenRCService:
|
|
|
289
289
|
script = textwrap.dedent(f"""
|
|
290
290
|
#!/bin/sh
|
|
291
291
|
cd "{self.home}"
|
|
292
|
-
|
|
292
|
+
while true; do
|
|
293
|
+
"{self.python}" -m portacode connect --non-interactive >> "{self.log_path}" 2>&1
|
|
294
|
+
rc="$?"
|
|
295
|
+
if [ "$rc" -eq {AUTH_REJECTED_EXIT_CODE} ]; then
|
|
296
|
+
echo "Portacode authentication rejected; stopping service restart loop." >> "{self.log_path}"
|
|
297
|
+
exit 0
|
|
298
|
+
fi
|
|
299
|
+
if [ "$rc" -eq 0 ]; then
|
|
300
|
+
exit 0
|
|
301
|
+
fi
|
|
302
|
+
sleep 5
|
|
303
|
+
done
|
|
293
304
|
""").lstrip()
|
|
294
305
|
tmp_path = Path(tempfile.gettempdir()) / f"portacode-wrapper-{os.getpid()}"
|
|
295
306
|
tmp_path.write_text(script)
|
|
@@ -362,6 +373,8 @@ class _LaunchdService:
|
|
|
362
373
|
/ "Library/LaunchAgents"
|
|
363
374
|
/ f"{self.LABEL}.plist"
|
|
364
375
|
)
|
|
376
|
+
self.script_path = Path.home() / ".local" / "share" / "portacode" / "connect_service.sh"
|
|
377
|
+
self.log_path = Path.home() / ".local" / "share" / "portacode" / "connect.log"
|
|
365
378
|
|
|
366
379
|
def _run(self, *args: str) -> subprocess.CompletedProcess[str]:
|
|
367
380
|
cmd = ["launchctl", *args]
|
|
@@ -369,6 +382,27 @@ class _LaunchdService:
|
|
|
369
382
|
|
|
370
383
|
def install(self) -> None:
|
|
371
384
|
self.plist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
self.script_path.parent.mkdir(parents=True, exist_ok=True)
|
|
386
|
+
script = textwrap.dedent(
|
|
387
|
+
f"""
|
|
388
|
+
#!/bin/sh
|
|
389
|
+
cd "$HOME" || exit 1
|
|
390
|
+
while true; do
|
|
391
|
+
"{sys.executable}" -m portacode connect --non-interactive >> "{self.log_path}" 2>&1
|
|
392
|
+
rc="$?"
|
|
393
|
+
if [ "$rc" -eq {AUTH_REJECTED_EXIT_CODE} ]; then
|
|
394
|
+
echo "Portacode authentication rejected; stopping service restart loop." >> "{self.log_path}"
|
|
395
|
+
exit 0
|
|
396
|
+
fi
|
|
397
|
+
if [ "$rc" -eq 0 ]; then
|
|
398
|
+
exit 0
|
|
399
|
+
fi
|
|
400
|
+
sleep 5
|
|
401
|
+
done
|
|
402
|
+
"""
|
|
403
|
+
).lstrip()
|
|
404
|
+
self.script_path.write_text(script)
|
|
405
|
+
self.script_path.chmod(0o755)
|
|
372
406
|
plist = textwrap.dedent(
|
|
373
407
|
f"""
|
|
374
408
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -378,13 +412,13 @@ class _LaunchdService:
|
|
|
378
412
|
<key>Label</key><string>{self.LABEL}</string>
|
|
379
413
|
<key>ProgramArguments</key>
|
|
380
414
|
<array>
|
|
381
|
-
<string>{
|
|
382
|
-
<string>-m</string><string>portacode</string>
|
|
383
|
-
<string>connect</string>
|
|
384
|
-
<string>--non-interactive</string>
|
|
415
|
+
<string>{self.script_path}</string>
|
|
385
416
|
</array>
|
|
386
417
|
<key>RunAtLoad</key><true/>
|
|
387
|
-
<key>KeepAlive</key
|
|
418
|
+
<key>KeepAlive</key>
|
|
419
|
+
<dict>
|
|
420
|
+
<key>SuccessfulExit</key><false/>
|
|
421
|
+
</dict>
|
|
388
422
|
</dict>
|
|
389
423
|
</plist>
|
|
390
424
|
"""
|
|
@@ -396,6 +430,8 @@ class _LaunchdService:
|
|
|
396
430
|
self._run("unload", "-w", str(self.plist_path))
|
|
397
431
|
if self.plist_path.exists():
|
|
398
432
|
self.plist_path.unlink()
|
|
433
|
+
if self.script_path.exists():
|
|
434
|
+
self.script_path.unlink()
|
|
399
435
|
|
|
400
436
|
def start(self) -> None:
|
|
401
437
|
self._run("start", self.LABEL)
|
|
@@ -451,7 +487,13 @@ class _WindowsTask:
|
|
|
451
487
|
script = (
|
|
452
488
|
"@echo off\r\n"
|
|
453
489
|
"cd /d %USERPROFILE%\r\n"
|
|
490
|
+
":loop\r\n"
|
|
454
491
|
f"{py_cmd} -m portacode connect --non-interactive >> \"%USERPROFILE%\\.local\\share\\portacode\\connect.log\" 2>>&1\r\n"
|
|
492
|
+
"set \"RC=%ERRORLEVEL%\"\r\n"
|
|
493
|
+
f"if \"%RC%\"==\"{AUTH_REJECTED_EXIT_CODE}\" exit /b 0\r\n"
|
|
494
|
+
"if \"%RC%\"==\"0\" exit /b 0\r\n"
|
|
495
|
+
"timeout /t 5 /nobreak >nul\r\n"
|
|
496
|
+
"goto loop\r\n"
|
|
455
497
|
)
|
|
456
498
|
self._script_path.write_text(script)
|
|
457
499
|
|
|
@@ -277,12 +277,12 @@ portacode/__main__.py
|
|
|
277
277
|
portacode/_version.py
|
|
278
278
|
portacode/cli.py
|
|
279
279
|
portacode/data.py
|
|
280
|
+
portacode/exit_codes.py
|
|
280
281
|
portacode/keypair.py
|
|
281
282
|
portacode/logging_categories.py
|
|
282
283
|
portacode/pairing.py
|
|
283
284
|
portacode/restart.py
|
|
284
285
|
portacode/service.py
|
|
285
|
-
portacode/test_updater.py
|
|
286
286
|
portacode/updater.py
|
|
287
287
|
portacode.egg-info/PKG-INFO
|
|
288
288
|
portacode.egg-info/SOURCES.txt
|
|
@@ -396,6 +396,7 @@ todo/issues/portacode_service_silently_down.md
|
|
|
396
396
|
todo/issues/premature_terminal_exit.md
|
|
397
397
|
todo/issues/project_cpu_hotspots.md
|
|
398
398
|
todo/issues/terminals_exit_upon_starting.md
|
|
399
|
+
todo/issues/websocket_client_silently_dead.md
|
|
399
400
|
todo/issues/wrong_item_classification_on_client_side.md
|
|
400
401
|
tools/generate_play_store_assets.py
|
|
401
402
|
tools/pairing_tester.py
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Issue summary
|
|
2
|
+
|
|
3
|
+
The SDK is supposed to keep trying to reconnect whenever the websocket connection to the server is dead, and that is what it do most of the time, but in some rare instances, the connection drops in a way where the server shows the the device is offline, but the device logs show that the SDK assums that it is connected. Result is that a manual restart is needed as the device SDK never detects that is it offline/disconnected so the reconnect loop never triggers.
|
|
4
|
+
|
|
5
|
+
# Issue details and findings
|
|
6
|
+
|
|
7
|
+
• Most likely root cause: the client got stuck in a half-closed TCP state (CLOSE-WAIT) where the websocket reader stopped consuming bytes, so reconnect logic never
|
|
8
|
+
triggered.
|
|
9
|
+
|
|
10
|
+
What I found (read-only):
|
|
11
|
+
|
|
12
|
+
1. Live process is still running since 2026-03-04 08:15:27 UTC with no restart.
|
|
13
|
+
2. Last Portacode log line is 2026-03-04 08:28:58 UTC; no logs after that.
|
|
14
|
+
3. The only Portacode network socket is:
|
|
15
|
+
- 10.10.0.136:41718 -> 94.204.184.54:443 in CLOSE-WAIT
|
|
16
|
+
- kernel Recv-Q is 177 bytes (unread data still queued)
|
|
17
|
+
- FD is 6 in process 7061
|
|
18
|
+
4. Restart policy is Restart=on-failure, but this process never exits, so systemd won’t restart it.
|
|
19
|
+
|
|
20
|
+
Code correlation (installed portacode==1.4.35 matches tag v1.4.35 exactly):
|
|
21
|
+
|
|
22
|
+
1. Receive loop only reconnects when recv() raises websockets.ConnectionClosed:
|
|
23
|
+
- /home/user/debo/portacode/portacode/connection/client.py:172:172
|
|
24
|
+
- /home/user/debo/portacode/portacode/connection/client.py:203:203
|
|
25
|
+
2. It wraps recv() in asyncio.wait_for(..., timeout=1.0) and cancels it every second on idle:
|
|
26
|
+
- /home/user/debo/portacode/portacode/connection/client.py:176:176
|
|
27
|
+
3. Clock-sync-based forced close is only initial burst (5 times), not continuous:
|
|
28
|
+
- /home/user/debo/portacode/portacode/connection/client.py:231:231
|
|
29
|
+
|
|
30
|
+
Inference:
|
|
31
|
+
|
|
32
|
+
- You hit a rare wedge where the websocket path stopped draining/closing after peer-side close (or close frame), leaving FD in CLOSE-WAIT.
|
|
33
|
+
- Because the process didn’t crash and no close exception propagated, reconnect loop never ran, and systemd didn’t intervene.
|
|
34
|
+
- Limited ongoing heartbeat (only initial clock sync) removes a second safety net once this wedge happens.
|
|
35
|
+
|
|
36
|
+
Confidence: medium-high on “stuck reader in CLOSE-WAIT without reconnect”; medium on exact trigger being the per-second wait_for(recv) cancellation race vs a lower-level
|
|
37
|
+
websockets/SSL edge case.
|
|
38
|
+
|
|
39
|
+
# Suggestions
|
|
40
|
+
|
|
41
|
+
1. WebSocket protocol keepalive (Ping/Pong)
|
|
42
|
+
Enable client-side idle ping at a low frequency (for example every 60-180s only when no traffic). This uses WebSocket control frames, not JSON payloads, and a
|
|
43
|
+
compliant server stack auto-replies with Pong without custom server code. If pong is missed, client force-closes and reconnects.
|
|
44
|
+
2. TCP keepalive probes
|
|
45
|
+
Enable and tune socket keepalive (SO_KEEPALIVE, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT) on the client connection. This is kernel-level liveness detection with
|
|
46
|
+
minimal overhead and no app-channel traffic. If peer becomes unreachable/half-dead, TCP eventually errors, allowing client reconnect logic to trigger.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Resolution
|
|
50
|
+
|
|
51
|
+
Suggestion 1 has been implemented in the commit on which this issue file was added and the issue file is kept to further monitor the issue in case of any future reoccurence. The file may be removed in 3 month later if the issue never reoccured. Scheduled file detetion is 4th of Jun 2026
|