fixos 2.2.51__tar.gz → 2.2.53__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.
- {fixos-2.2.51 → fixos-2.2.53}/MANIFEST.in +1 -0
- {fixos-2.2.51 → fixos-2.2.53}/PKG-INFO +2 -2
- {fixos-2.2.51 → fixos-2.2.53}/README.md +1 -1
- fixos-2.2.53/VERSION +1 -0
- fixos-2.2.53/fixos/cli/__main__.py +6 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_space.py +36 -4
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/cleanup_cmd.py +23 -5
- {fixos-2.2.51 → fixos-2.2.53}/fixos/config.py +24 -2
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/native_scan.py +15 -3
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/quick_snapshot.py +31 -2
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/service_cleanup.py +51 -6
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/service_scanner.py +61 -2
- {fixos-2.2.51 → fixos-2.2.53}/fixos/interactive/cleanup_planner.py +26 -28
- {fixos-2.2.51 → fixos-2.2.53}/fixos.egg-info/PKG-INFO +2 -2
- {fixos-2.2.51 → fixos-2.2.53}/fixos.egg-info/SOURCES.txt +5 -0
- {fixos-2.2.51 → fixos-2.2.53}/pyproject.toml +1 -1
- {fixos-2.2.51 → fixos-2.2.53}/setup.py +10 -2
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_effectiveness.py +93 -0
- fixos-2.2.53/tests/unit/test_cleanup_planner.py +41 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_tmp.py +43 -0
- fixos-2.2.53/tests/unit/test_cli_main.py +42 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_native_scan.py +26 -0
- fixos-2.2.53/tests/unit/test_ollama_endpoint.py +46 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_quick_snapshot.py +33 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_service_cleanup.py +75 -0
- {fixos-2.2.51 → fixos-2.2.53}/.env.example +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/CHANGELOG.md +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/LICENSE +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/README.md +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/TEST_RESULTS.md +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/TEST_RESULTS_V2.md +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/alpine/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/arch/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/base/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/broken-audio/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/broken-full/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/broken-network/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/broken-thumbnails/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/debian/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/docker-compose.multi-system.yml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/docker-compose.yml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/fedora/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/test-multi-system.sh +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/test-scenarios.sh +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/ubuntu/Dockerfile +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docker/validate-scenario.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docs/examples/advanced_usage.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/docs/examples/quickstart.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/__main__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/autonomous.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/autonomous_session.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/hitl.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/hitl_session.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/session_core.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/session_handlers.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/agent/session_io.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/anonymizer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_flatpak.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_home.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_snap.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_system.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_utils.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/_cleanup_venvs.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/ask_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/config_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/dashboard.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/features_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/fix_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/history_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/jetbrains_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/main.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/orchestrate_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/output_formatter.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/profile_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/projects_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/provider_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/quick_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/quickfix_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/report_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/rollback_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/scan_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/shared.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/shell_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/token_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli/watch_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/cli.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/config_interactive.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/constants.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/_flatpak_analysis_mixin.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/_flatpak_execution_mixin.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/_flatpak_recommendations_mixin.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/_storage_container_mixin.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/_storage_system_mixin.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/_storage_user_mixin.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/cache_discovery.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/_shared.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/audio.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/file_analysis.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/hardware.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/packages.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/resources.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/security.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/storage_optimization.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/system_core.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/checks/thumbnails.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/dev_project_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/disk_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/docker_network_cleanup.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/docker_startup_optimizer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/flatpak_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/jetbrains_ai.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/jetbrains_recovery.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/orphaned_workloads.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/process_chains.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/project_scanner.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/service_details.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/storage_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/system_checks.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/utils.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/diagnostics/venv_cleanup.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/endpoint_refresh.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/auditor.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/catalog.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/data/packages.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/data/profiles/developer.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/data/profiles/office.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/data/profiles/sysadmin.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/installer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/profiles.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/features/renderer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/fixes/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/interactive/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/llm_shell.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/orchestrator/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/orchestrator/executor.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/orchestrator/graph.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/orchestrator/orchestrator.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/orchestrator/rollback.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/orphan_pins.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/platform_utils.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/base.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/audio.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/disk.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/hardware.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/resources.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/security.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/builtin/thumbnails.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/plugins/registry.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/profiles/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/profiles/desktop.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/profiles/developer.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/profiles/minimal.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/profiles/server.yaml +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/providers/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/providers/gemini_native.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/providers/llm.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/providers/llm_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/providers/schemas.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/system_checks.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/utils/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/utils/anonymizer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/utils/terminal.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/utils/timeout.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/utils/web_search.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos/watch.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos.egg-info/dependency_links.txt +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos.egg-info/entry_points.txt +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos.egg-info/requires.txt +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/fixos.egg-info/top_level.txt +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/pytest.ini +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/requirements-dev.txt +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/requirements.txt +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/scripts/pyqual-calibrate.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/setup.cfg +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/conftest.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_anonymization_layers.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_audio_broken.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_cli.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_diagnostics_integration.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_executor.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_multi_system.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_network_broken.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/e2e/test_thumbnails_broken.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/test_fixos.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/__init__.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_anonymizer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_ask_and_nl_group.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cache_discovery.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_retention.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_safety.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_space.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_cleanup_utils.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_core.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_dashboard.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_dev_project_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_disk_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_docker_network_cleanup.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_docker_startup_optimizer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_endpoint_refresh.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_executor.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_gemini_native_client.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_jetbrains_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_jetbrains_recovery.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_llm_auth_stop.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_llm_provider.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_orchestrator.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_orphan_pins.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_orphaned_workloads.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_packaging_data.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_process_chains.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_project_scanner.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_projects_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_quick_cmd.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_resources_quick.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_security_quick.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_service_scanner.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_shell_ux.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_storage_analyzer.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_system_checks_parallel.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_venv_cleanup.py +0 -0
- {fixos-2.2.51 → fixos-2.2.53}/tests/unit/test_version_parity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fixos
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.53
|
|
4
4
|
Summary: AI-powered Linux/Windows diagnostics and repair – audio, hardware, system issues
|
|
5
5
|
Home-page: https://github.com/wronai/fixos
|
|
6
6
|
Author: fixos contributors
|
|
@@ -63,7 +63,7 @@ AI-powered OS Diagnostics
|
|
|
63
63
|
|
|
64
64
|
## AI Cost Tracking
|
|
65
65
|
|
|
66
|
-
    
|
|
67
67
|
  
|
|
68
68
|
|
|
69
69
|
- 🤖 **LLM usage:** $4.9010 (154 commits)
|
|
@@ -20,7 +20,7 @@ AI-powered OS Diagnostics
|
|
|
20
20
|
|
|
21
21
|
## AI Cost Tracking
|
|
22
22
|
|
|
23
|
-
    
|
|
24
24
|
  
|
|
25
25
|
|
|
26
26
|
- 🤖 **LLM usage:** $4.9010 (154 commits)
|
fixos-2.2.53/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.2.53
|
|
@@ -52,6 +52,31 @@ def _run_cmd(argv: list[str], timeout: int = 60) -> subprocess.CompletedProcess:
|
|
|
52
52
|
return subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
_DU_BATCH = 512
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _dir_sizes(paths: list[Path]) -> dict[str, int]:
|
|
59
|
+
"""Sizes in bytes for many directories with one ``du`` per batch.
|
|
60
|
+
|
|
61
|
+
``--count-links`` keeps each directory independent of the others, as a
|
|
62
|
+
separate ``du`` would be; NUL-terminated output survives any file name.
|
|
63
|
+
Paths missing from the result must be measured individually.
|
|
64
|
+
"""
|
|
65
|
+
sizes: dict[str, int] = {}
|
|
66
|
+
for start in range(0, len(paths), _DU_BATCH):
|
|
67
|
+
batch = [str(path) for path in paths[start : start + _DU_BATCH]]
|
|
68
|
+
try:
|
|
69
|
+
result = _run_cmd(["du", "-sbl0", "--", *batch], timeout=300)
|
|
70
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
71
|
+
continue
|
|
72
|
+
# du exits 1 when some subtree is unreadable but still prints totals.
|
|
73
|
+
for record in result.stdout.split("\0"):
|
|
74
|
+
size, sep, name = record.partition("\t")
|
|
75
|
+
if sep and size.isdigit():
|
|
76
|
+
sizes[name] = int(size)
|
|
77
|
+
return sizes
|
|
78
|
+
|
|
79
|
+
|
|
55
80
|
def _dir_size(path: Path) -> int:
|
|
56
81
|
"""Return directory size in bytes (du -sb with a Python fallback)."""
|
|
57
82
|
try:
|
|
@@ -876,6 +901,7 @@ def _scan_tmp(
|
|
|
876
901
|
cutoff_seconds = days * 86400
|
|
877
902
|
tmp_path = Path(tmp_dir) if tmp_dir is not None else Path("/tmp")
|
|
878
903
|
candidates = []
|
|
904
|
+
directories = []
|
|
879
905
|
|
|
880
906
|
if tmp_path.is_dir():
|
|
881
907
|
try:
|
|
@@ -893,23 +919,29 @@ def _scan_tmp(
|
|
|
893
919
|
mtime = stat_val.st_mtime
|
|
894
920
|
age_seconds = now - mtime
|
|
895
921
|
if age_seconds >= cutoff_seconds:
|
|
896
|
-
|
|
897
|
-
stat_val.st_size if not item.is_dir() else _dir_size(item)
|
|
898
|
-
)
|
|
922
|
+
is_dir = stat_module.S_ISDIR(stat_val.st_mode)
|
|
899
923
|
age_days = age_seconds / 86400
|
|
900
924
|
candidates.append({
|
|
901
925
|
"description": f"{name} (wiek: {age_days:.1f} dni)",
|
|
902
926
|
"path": item,
|
|
903
927
|
"name": name,
|
|
904
|
-
"size_bytes":
|
|
928
|
+
"size_bytes": 0 if is_dir else stat_val.st_size,
|
|
905
929
|
"note": f"{age_days:.1f} dni",
|
|
906
930
|
"age_days": age_days,
|
|
907
931
|
})
|
|
932
|
+
if is_dir:
|
|
933
|
+
directories.append(candidates[-1])
|
|
908
934
|
except (OSError, PermissionError):
|
|
909
935
|
continue
|
|
910
936
|
except (OSError, PermissionError):
|
|
911
937
|
pass
|
|
912
938
|
|
|
939
|
+
# One `du` per directory meant thousands of processes on a busy /tmp.
|
|
940
|
+
sizes = _dir_sizes([c["path"] for c in directories])
|
|
941
|
+
for candidate in directories:
|
|
942
|
+
size = sizes.get(str(candidate["path"]))
|
|
943
|
+
candidate["size_bytes"] = size if size is not None else _dir_size(candidate["path"])
|
|
944
|
+
|
|
913
945
|
return candidates
|
|
914
946
|
|
|
915
947
|
|
|
@@ -129,11 +129,7 @@ def _clean_terminal_input(value: str) -> str:
|
|
|
129
129
|
|
|
130
130
|
def _display_service_item(svc: dict) -> None:
|
|
131
131
|
"""Display a single service item with details."""
|
|
132
|
-
size_str = (
|
|
133
|
-
f"{svc['size_gb']:.2f} GB"
|
|
134
|
-
if svc["size_gb"] >= 1
|
|
135
|
-
else f"{svc['size_mb']:.0f} MB"
|
|
136
|
-
)
|
|
132
|
+
size_str = _size_str(svc)
|
|
137
133
|
risk = svc.get("risk_level") or ("safe" if svc["safe_to_cleanup"] else "review")
|
|
138
134
|
safe_text, safe_color = _RISK_LABELS.get(risk, _RISK_LABELS["review"])
|
|
139
135
|
|
|
@@ -164,6 +160,8 @@ def _display_service_item(svc: dict) -> None:
|
|
|
164
160
|
)
|
|
165
161
|
else:
|
|
166
162
|
click.echo(f" Komponenty: {svc['details']['components']}")
|
|
163
|
+
elif svc["service_type"] == "docker" and svc["details"].get("fast_inventory"):
|
|
164
|
+
_display_docker_inventory(svc["details"]["fast_inventory"])
|
|
167
165
|
elif svc["service_type"] == "ollama" and svc["details"].get("models"):
|
|
168
166
|
models = svc["details"]["models"]
|
|
169
167
|
if models:
|
|
@@ -173,6 +171,26 @@ def _display_service_item(svc: dict) -> None:
|
|
|
173
171
|
click.echo()
|
|
174
172
|
|
|
175
173
|
|
|
174
|
+
def _display_docker_inventory(inventory: dict) -> None:
|
|
175
|
+
"""Show what a timed-out `docker system df` could not size."""
|
|
176
|
+
click.echo(" Docker — szybki spis (bez rozmiarów wolumenów):")
|
|
177
|
+
pairs = (
|
|
178
|
+
("obrazy", "images", "dangling_images", "wiszących"),
|
|
179
|
+
("kontenery", "containers", "stopped_containers", "zatrzymanych"),
|
|
180
|
+
("wolumeny", "volumes", "dangling_volumes", "nieużywanych — dane, tylko ręcznie"),
|
|
181
|
+
)
|
|
182
|
+
for label, total_key, part_key, part_label in pairs:
|
|
183
|
+
if total_key in inventory:
|
|
184
|
+
part = inventory.get(part_key)
|
|
185
|
+
suffix = f" ({part} {part_label})" if part is not None else ""
|
|
186
|
+
click.echo(f" • {label}: {inventory[total_key]}{suffix}")
|
|
187
|
+
if "build_cache_total_gb" in inventory:
|
|
188
|
+
click.echo(
|
|
189
|
+
f" • cache buildów: {inventory['build_cache_total_gb']:.2f} GB "
|
|
190
|
+
f"(odzyskiwalne {inventory.get('build_cache_reclaimable_gb', 0):.2f} GB)"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
176
194
|
def _error_message(result: dict) -> str:
|
|
177
195
|
"""Human-readable failure reason, even when the command's own stderr was
|
|
178
196
|
empty (e.g. suppressed with 2>/dev/null inside the cleanup command
|
|
@@ -128,7 +128,8 @@ PROVIDER_DEFAULTS = {
|
|
|
128
128
|
"description": "Cerebras – najszybsze wnioskowanie na świecie, darmowy tier",
|
|
129
129
|
},
|
|
130
130
|
"ollama": {
|
|
131
|
-
|
|
131
|
+
# Resolved from OLLAMA_HOST at load time, see ollama_host_url().
|
|
132
|
+
"base_url": None,
|
|
132
133
|
"model": "llama3.2",
|
|
133
134
|
"key_env": None,
|
|
134
135
|
"key_url": "https://ollama.com/download",
|
|
@@ -142,6 +143,22 @@ _OPENROUTER_MODEL_MIGRATIONS = {
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
|
|
146
|
+
DEFAULT_OLLAMA_HOST = "127.0.0.1:11434"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def ollama_host_url() -> str:
|
|
150
|
+
"""Ollama server URL from ``OLLAMA_HOST`` (the variable Ollama itself reads).
|
|
151
|
+
|
|
152
|
+
Single source for every fixOS component talking to Ollama: the LLM
|
|
153
|
+
provider and model cleanup previously disagreed when OLLAMA_HOST pointed
|
|
154
|
+
to another host.
|
|
155
|
+
"""
|
|
156
|
+
host = os.environ.get("OLLAMA_HOST", "").strip() or DEFAULT_OLLAMA_HOST
|
|
157
|
+
if "://" not in host:
|
|
158
|
+
host = f"http://{host}"
|
|
159
|
+
return host.rstrip("/")
|
|
160
|
+
|
|
161
|
+
|
|
145
162
|
def _normalize_model_id(provider: str, model: str) -> str:
|
|
146
163
|
"""Normalize provider-specific model IDs without changing valid choices."""
|
|
147
164
|
normalized = model.strip()
|
|
@@ -293,7 +310,12 @@ class FixOsConfig:
|
|
|
293
310
|
|
|
294
311
|
# Base URL
|
|
295
312
|
url_env_key = f"{cfg.provider.upper()}_BASE_URL"
|
|
296
|
-
cfg.base_url =
|
|
313
|
+
cfg.base_url = (
|
|
314
|
+
base_url
|
|
315
|
+
or os.environ.get(url_env_key)
|
|
316
|
+
or pdef["base_url"]
|
|
317
|
+
or (f"{ollama_host_url()}/v1" if cfg.provider == "ollama" else None)
|
|
318
|
+
)
|
|
297
319
|
|
|
298
320
|
# Transport Gemini: natywne API Google albo endpoint OpenAI-compatible
|
|
299
321
|
cfg.gemini_transport = os.environ.get("GEMINI_TRANSPORT", "native").lower()
|
|
@@ -69,9 +69,19 @@ def measure_tree(
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def measure_batch(
|
|
72
|
-
paths: list[Path],
|
|
72
|
+
paths: list[Path],
|
|
73
|
+
exclude: tuple[str, ...] = (),
|
|
74
|
+
*,
|
|
75
|
+
native: bool = True,
|
|
76
|
+
timeout: float = 120,
|
|
77
|
+
python_fallback: bool = True,
|
|
73
78
|
) -> dict[str, dict]:
|
|
74
|
-
"""Measure multiple paths, accelerating with Rust batch measurement if available.
|
|
79
|
+
"""Measure multiple paths, accelerating with Rust batch measurement if available.
|
|
80
|
+
|
|
81
|
+
Results are keyed by the resolved path. ``python_fallback=False`` returns
|
|
82
|
+
``{}`` instead of an unbounded Python walk when the native batch is
|
|
83
|
+
missing, fails or exceeds ``timeout``; time-budgeted callers use it.
|
|
84
|
+
"""
|
|
75
85
|
if not paths:
|
|
76
86
|
return {}
|
|
77
87
|
if native:
|
|
@@ -84,7 +94,7 @@ def measure_batch(
|
|
|
84
94
|
[str(executable), "batch-measure", *abs_paths],
|
|
85
95
|
capture_output=True,
|
|
86
96
|
text=True,
|
|
87
|
-
timeout=
|
|
97
|
+
timeout=timeout,
|
|
88
98
|
check=True,
|
|
89
99
|
)
|
|
90
100
|
data = json.loads(result.stdout)
|
|
@@ -103,6 +113,8 @@ def measure_batch(
|
|
|
103
113
|
return res
|
|
104
114
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
105
115
|
pass
|
|
116
|
+
if not python_fallback:
|
|
117
|
+
return {}
|
|
106
118
|
return {str(p.resolve()): measure_python(p, exclude) for p in paths}
|
|
107
119
|
|
|
108
120
|
|
|
@@ -348,11 +348,15 @@ def _measure_caches(
|
|
|
348
348
|
from fixos.diagnostics.native_scan import measure_batch
|
|
349
349
|
|
|
350
350
|
unique_paths = list({path for _, path in jobs})
|
|
351
|
-
|
|
351
|
+
# The native batch had a 120 s limit and an unbounded Python
|
|
352
|
+
# fallback; on a busy disk that turned "quick" into minutes.
|
|
353
|
+
batch_result = measure_batch(
|
|
354
|
+
unique_paths, timeout=timeout * 2, python_fallback=False
|
|
355
|
+
)
|
|
352
356
|
if isinstance(batch_result, dict) and batch_result:
|
|
353
357
|
unresolved_jobs: list[tuple[CacheRule, Path]] = []
|
|
354
358
|
for rule, path in jobs:
|
|
355
|
-
m = batch_result.get(str(path))
|
|
359
|
+
m = batch_result.get(str(path.resolve()))
|
|
356
360
|
if isinstance(m, dict) and "bytes" in m:
|
|
357
361
|
kib = (m["bytes"] + 1023) // 1024
|
|
358
362
|
measured.setdefault(rule.id, []).append((path, kib))
|
|
@@ -569,6 +573,28 @@ def _history_row(
|
|
|
569
573
|
}
|
|
570
574
|
|
|
571
575
|
|
|
576
|
+
def _fill_from_history(
|
|
577
|
+
items: list[dict[str, Any]], history: list[dict[str, Any]]
|
|
578
|
+
) -> None:
|
|
579
|
+
"""Show the last complete size for caches that missed the time budget.
|
|
580
|
+
|
|
581
|
+
The item stays ``complete: False`` so growth reports never compare it.
|
|
582
|
+
"""
|
|
583
|
+
for item in items:
|
|
584
|
+
if item.get("complete", True):
|
|
585
|
+
continue
|
|
586
|
+
for row in reversed(history):
|
|
587
|
+
if row.get("cache_complete", {}).get(item["id"]) and item["id"] in row.get(
|
|
588
|
+
"cache_sizes", {}
|
|
589
|
+
):
|
|
590
|
+
size = int(row["cache_sizes"][item["id"]])
|
|
591
|
+
if size > item["size_bytes"]:
|
|
592
|
+
item["size_bytes"] = size
|
|
593
|
+
item["size_gb"] = round(size / 1024**3, 3)
|
|
594
|
+
item["last_complete_at"] = row.get("generated_at")
|
|
595
|
+
break
|
|
596
|
+
|
|
597
|
+
|
|
572
598
|
def _parse_timestamp(row: dict[str, Any]) -> datetime | None:
|
|
573
599
|
try:
|
|
574
600
|
parsed = datetime.fromisoformat(str(row["generated_at"]))
|
|
@@ -836,6 +862,9 @@ def collect_quick_snapshot(
|
|
|
836
862
|
history_path = _state_dir(state_dir) / "quick-history.json"
|
|
837
863
|
history = _load_history(history_path)
|
|
838
864
|
current_row = _history_row(generated_at, resources, safe_items, review_items)
|
|
865
|
+
_fill_from_history(safe_items + review_items, history)
|
|
866
|
+
safe_items.sort(key=lambda item: item["size_bytes"], reverse=True)
|
|
867
|
+
review_items.sort(key=lambda item: item["size_bytes"], reverse=True)
|
|
839
868
|
growth = _growth_report(history, current_row, now, max(1, hours))
|
|
840
869
|
alerts = _alerts(resources, growth, thresholds)
|
|
841
870
|
|
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import re
|
|
9
9
|
import shlex
|
|
10
10
|
import shutil
|
|
11
|
+
import stat
|
|
11
12
|
import subprocess
|
|
12
13
|
import urllib.error
|
|
13
14
|
import urllib.request
|
|
@@ -341,10 +342,9 @@ class ServiceCleaner:
|
|
|
341
342
|
|
|
342
343
|
@staticmethod
|
|
343
344
|
def _ollama_base_url() -> str:
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return host.rstrip("/")
|
|
345
|
+
from ..config import ollama_host_url
|
|
346
|
+
|
|
347
|
+
return ollama_host_url()
|
|
348
348
|
|
|
349
349
|
@staticmethod
|
|
350
350
|
def _parse_ollama_modified_at(value: str) -> datetime:
|
|
@@ -1480,6 +1480,16 @@ class ServiceCleaner:
|
|
|
1480
1480
|
result["estimated_max_gb"] = estimated_gb
|
|
1481
1481
|
return result
|
|
1482
1482
|
|
|
1483
|
+
planned_paths = service.details.get("paths") or [service.path]
|
|
1484
|
+
# uv/pnpm/conda caches hardlink files into environments; deleting
|
|
1485
|
+
# the cache link frees nothing while another link survives.
|
|
1486
|
+
measure_shared = service_enum != ServiceType.DOCKER
|
|
1487
|
+
shared_before_gb = (
|
|
1488
|
+
self._shared_hardlink_bytes(planned_paths) / 1024**3
|
|
1489
|
+
if measure_shared
|
|
1490
|
+
else 0.0
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1483
1493
|
# Execute cleanup
|
|
1484
1494
|
cleanup_result = subprocess.run(
|
|
1485
1495
|
service.cleanup_command,
|
|
@@ -1492,7 +1502,6 @@ class ServiceCleaner:
|
|
|
1492
1502
|
|
|
1493
1503
|
# Check new size
|
|
1494
1504
|
measure = getattr(self.scanner, "measure_service_size_mb", None)
|
|
1495
|
-
planned_paths = service.details.get("paths") or [service.path]
|
|
1496
1505
|
if callable(measure):
|
|
1497
1506
|
new_size_mb = sum(
|
|
1498
1507
|
measure(service_enum, path, refresh=True) for path in planned_paths
|
|
@@ -1502,7 +1511,14 @@ class ServiceCleaner:
|
|
|
1502
1511
|
self.scanner._get_path_size_mb(path) for path in planned_paths
|
|
1503
1512
|
)
|
|
1504
1513
|
new_size_gb = new_size_mb / 1024
|
|
1505
|
-
|
|
1514
|
+
shared_after_gb = (
|
|
1515
|
+
self._shared_hardlink_bytes(planned_paths) / 1024**3
|
|
1516
|
+
if measure_shared
|
|
1517
|
+
else 0.0
|
|
1518
|
+
)
|
|
1519
|
+
freed_gb = max(
|
|
1520
|
+
0, (initial_size - shared_before_gb) - (new_size_gb - shared_after_gb)
|
|
1521
|
+
)
|
|
1506
1522
|
|
|
1507
1523
|
result["success"] = cleanup_result.returncode == 0
|
|
1508
1524
|
result["output"] = cleanup_result.stdout
|
|
@@ -1511,6 +1527,9 @@ class ServiceCleaner:
|
|
|
1511
1527
|
result["space_freed_gb"] = round(freed_gb, 3)
|
|
1512
1528
|
result["initial_size_gb"] = initial_size
|
|
1513
1529
|
result["remaining_size_gb"] = round(new_size_gb, 3)
|
|
1530
|
+
result["shared_hardlink_gb"] = round(
|
|
1531
|
+
max(0.0, shared_before_gb - shared_after_gb), 3
|
|
1532
|
+
)
|
|
1514
1533
|
|
|
1515
1534
|
except Exception as e: # noqa: BLE001 - stored in result["error"] for the caller to display
|
|
1516
1535
|
result["error"] = str(e)
|
|
@@ -1528,6 +1547,32 @@ class ServiceCleaner:
|
|
|
1528
1547
|
== "docker builder prune --force --filter until=168h"
|
|
1529
1548
|
)
|
|
1530
1549
|
|
|
1550
|
+
@staticmethod
|
|
1551
|
+
def _shared_hardlink_bytes(paths: list[str]) -> int:
|
|
1552
|
+
"""Allocated bytes under ``paths`` whose inode also has links elsewhere.
|
|
1553
|
+
|
|
1554
|
+
Removing such a link does not return blocks to the filesystem, so it
|
|
1555
|
+
must not count as freed space. Links inside the measured trees are
|
|
1556
|
+
counted together; only an inode with more links than seen is shared.
|
|
1557
|
+
"""
|
|
1558
|
+
inodes: dict[tuple[int, int], list[int]] = {}
|
|
1559
|
+
for root in paths:
|
|
1560
|
+
if not root or os.path.islink(root) or not os.path.isdir(root):
|
|
1561
|
+
continue
|
|
1562
|
+
for dirpath, _dirnames, filenames in os.walk(root):
|
|
1563
|
+
for name in filenames:
|
|
1564
|
+
try:
|
|
1565
|
+
st = os.lstat(os.path.join(dirpath, name))
|
|
1566
|
+
except OSError:
|
|
1567
|
+
continue
|
|
1568
|
+
if st.st_nlink < 2 or not stat.S_ISREG(st.st_mode):
|
|
1569
|
+
continue
|
|
1570
|
+
entry = inodes.setdefault(
|
|
1571
|
+
(st.st_dev, st.st_ino), [st.st_nlink, 0, st.st_blocks * 512]
|
|
1572
|
+
)
|
|
1573
|
+
entry[1] += 1
|
|
1574
|
+
return sum(size for nlink, seen, size in inodes.values() if seen < nlink)
|
|
1575
|
+
|
|
1531
1576
|
@staticmethod
|
|
1532
1577
|
def _estimated_cleanup_gb(service) -> float:
|
|
1533
1578
|
"""Best available upper bound for a dry-run cleanup.
|
|
@@ -330,7 +330,12 @@ class ServiceDataScanner:
|
|
|
330
330
|
for path in matching_paths:
|
|
331
331
|
if os.path.exists(path):
|
|
332
332
|
info = self._analyze_service_path(service_type, path)
|
|
333
|
-
|
|
333
|
+
# An unmeasurable entry (slow Docker daemon) has no size to
|
|
334
|
+
# compare; dropping it hid hundreds of GB from the report.
|
|
335
|
+
if info and (
|
|
336
|
+
info.size_mb >= self.threshold_mb
|
|
337
|
+
or info.details.get("size_unknown")
|
|
338
|
+
):
|
|
334
339
|
results.append(info)
|
|
335
340
|
if len(results) > 1:
|
|
336
341
|
return self._merge_by_risk(results)
|
|
@@ -543,10 +548,64 @@ class ServiceDataScanner:
|
|
|
543
548
|
preview_command=ServiceCleaner.get_preview_command(ServiceType.DOCKER, path),
|
|
544
549
|
safe_to_cleanup=False,
|
|
545
550
|
impact="high",
|
|
546
|
-
details={
|
|
551
|
+
details={
|
|
552
|
+
"size_unknown": True,
|
|
553
|
+
"reason": "docker_system_df_timeout",
|
|
554
|
+
"fast_inventory": self._docker_fast_inventory(),
|
|
555
|
+
},
|
|
547
556
|
risk_level=ServiceCleaner.get_risk_level(ServiceType.DOCKER, path),
|
|
548
557
|
)
|
|
549
558
|
|
|
559
|
+
@staticmethod
|
|
560
|
+
def _docker_fast_inventory(timeout_s: float = 10.0) -> dict[str, Any]:
|
|
561
|
+
"""Counts and build-cache size from Docker commands that stay fast.
|
|
562
|
+
|
|
563
|
+
``docker system df`` walks every volume to size it; with hundreds of
|
|
564
|
+
volumes it can take minutes even on an idle host. Listing objects and
|
|
565
|
+
``docker builder du`` answer in well under a second, so a timed-out
|
|
566
|
+
scan can still show where Docker holds data. Volume sizes stay
|
|
567
|
+
unknown; orphaned volumes are data and are only counted.
|
|
568
|
+
"""
|
|
569
|
+
|
|
570
|
+
def lines(*args: str) -> list[str] | None:
|
|
571
|
+
try:
|
|
572
|
+
result = subprocess.run(
|
|
573
|
+
["docker", *args],
|
|
574
|
+
capture_output=True,
|
|
575
|
+
text=True,
|
|
576
|
+
timeout=timeout_s,
|
|
577
|
+
check=False,
|
|
578
|
+
)
|
|
579
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
580
|
+
return None
|
|
581
|
+
if result.returncode != 0:
|
|
582
|
+
return None
|
|
583
|
+
return [line for line in result.stdout.splitlines() if line.strip()]
|
|
584
|
+
|
|
585
|
+
inventory: dict[str, Any] = {}
|
|
586
|
+
queries = {
|
|
587
|
+
"images": ("image", "ls", "-q", "--no-trunc"),
|
|
588
|
+
"dangling_images": ("image", "ls", "-q", "--no-trunc", "-f", "dangling=true"),
|
|
589
|
+
"containers": ("ps", "-aq"),
|
|
590
|
+
"stopped_containers": ("ps", "-aq", "-f", "status=exited"),
|
|
591
|
+
"volumes": ("volume", "ls", "-q"),
|
|
592
|
+
"dangling_volumes": ("volume", "ls", "-q", "-f", "dangling=true"),
|
|
593
|
+
}
|
|
594
|
+
for key, args in queries.items():
|
|
595
|
+
found = lines(*args)
|
|
596
|
+
if found is not None:
|
|
597
|
+
inventory[key] = len(set(found))
|
|
598
|
+
|
|
599
|
+
builder = lines("builder", "du")
|
|
600
|
+
if builder is not None:
|
|
601
|
+
for line in builder:
|
|
602
|
+
label, _, value = line.partition(":")
|
|
603
|
+
label = label.strip().lower()
|
|
604
|
+
if label in ("total", "reclaimable") and value.strip():
|
|
605
|
+
size_mb = ServiceDataScanner._parse_human_size_to_mb(value.strip())
|
|
606
|
+
inventory[f"build_cache_{label}_gb"] = round(size_mb / 1024, 3)
|
|
607
|
+
return inventory
|
|
608
|
+
|
|
550
609
|
def _get_docker_daemon_size_mb(self, *, refresh: bool = False) -> float | None:
|
|
551
610
|
"""Total Docker disk usage as reported by the daemon itself."""
|
|
552
611
|
usage = self._get_docker_daemon_usage(refresh=refresh)
|