Audience: Developers Type: API Reference
The Azure CLI detection system consists of three core modules that work together to detect WSL2 + Windows CLI issues and fix them automatically.
Detects the runtime environment (WSL2, native Linux, Windows) and identifies Azure CLI installation type (Windows or Linux).
Data class containing detection results:
@dataclass
class EnvironmentInfo:
environment: Environment # WSL2, LINUX_NATIVE, WINDOWS, UNKNOWN
cli_type: CLIType # WINDOWS, LINUX, NONE
cli_path: Optional[Path] # Path to az command
has_problem: bool # True if WSL2 + Windows CLI
problem_description: Optional[str] # Human-readable problem descriptionMain detection class:
class CLIDetector:
def detect() -> EnvironmentInfo:
"""
Perform complete detection of environment and CLI.
Returns:
EnvironmentInfo with all detection results
"""
def get_linux_cli_path() -> Optional[Path]:
"""
Get explicit path to Linux Azure CLI if installed.
Returns:
Path to Linux CLI binary, or None if not found
"""Environment Detection:
- Check
platform.system()for OS type - If Linux, check for WSL2 indicators:
/proc/versioncontains "microsoft" or "wsl2"/run/WSLdirectory existsWSL_DISTRO_NAMEorWSL_INTEROPenvironment variables present
CLI Detection:
- Use
shutil.which("az")to find az command - Check if path indicates Windows installation:
- Starts with
/mnt/c/or/mnt/d/ - Contains "Program Files" (case-insensitive)
- Has
.exeextension
- Starts with
Problem Detection:
- Problem exists if:
environment == WSL2ANDcli_type == WINDOWS
from azlin.modules.cli_detector import CLIDetector
# Detect environment and CLI
detector = CLIDetector()
env_info = detector.detect()
if env_info.has_problem:
print(f"Problem detected: {env_info.problem_description}")
print(f"Windows CLI at: {env_info.cli_path}")
# Check if Linux CLI is available
linux_cli = detector.get_linux_cli_path()
if linux_cli:
print(f"Linux CLI available at: {linux_cli}")
else:
print("Linux CLI not installed")Handles interactive installation of Linux Azure CLI in WSL2.
Data class containing installation outcome:
@dataclass
class InstallResult:
status: InstallStatus # SUCCESS, CANCELLED, FAILED, ALREADY_INSTALLED
cli_path: Optional[Path] # Path to installed CLI (if successful)
error_message: Optional[str] # Error details (if failed)Installation orchestrator:
class CLIInstaller:
def prompt_install() -> bool:
"""
Prompt user for installation consent.
Returns:
True if user consents, False otherwise
"""
def install() -> InstallResult:
"""
Execute Linux Azure CLI installation.
Returns:
InstallResult with installation outcome
"""-
Check Existing Installation:
- Query
CLIDetector.get_linux_cli_path() - If found, return
ALREADY_INSTALLED
- Query
-
Prompt User:
- Display problem explanation
- Show installation details (URL, requirements, time)
- Get user consent (y/N)
-
Download Script:
curl -sL https://aka.ms/InstallAzureCLIDeb- Timeout: 300 seconds (5 minutes)
-
Execute Installation:
sudo bash -c <script>- User prompted for sudo password
- Timeout: 300 seconds
-
Verify Installation:
- Check
get_linux_cli_path()again - Return
SUCCESSorFAILED
- Check
from azlin.modules.cli_installer import CLIInstaller
installer = CLIInstaller()
# Execute installation (prompts user)
result = installer.install()
if result.status.value == "success":
print(f"Installed at: {result.cli_path}")
elif result.status.value == "cancelled":
print("User cancelled installation")
else:
print(f"Installation failed: {result.error_message}")Executes subprocesses with deadlock prevention through pipe draining.
Data class containing execution result:
@dataclass
class SubprocessResult:
returncode: int # Process exit code
stdout: str # Captured stdout
stderr: str # Captured stderr
timed_out: bool # True if timeout occurredExecute subprocess safely:
def safe_run(
cmd: List[str],
cwd: Optional[Path] = None,
timeout: Optional[int] = 30,
env: Optional[dict] = None
) -> SubprocessResult:
"""
Execute subprocess with pipe deadlock prevention.
Uses background threads to drain stdout/stderr pipes,
preventing buffer overflow that causes deadlocks.
Args:
cmd: Command and arguments
cwd: Working directory
timeout: Timeout in seconds (None = no timeout)
env: Environment variables
Returns:
SubprocessResult with output and exit code
"""Problem: Azure CLI commands write continuous output. If stdout/stderr pipes aren't drained, the 64KB buffer fills up and the process blocks.
Solution:
- Spawn Process: Create
subprocess.PopenwithPIPEfor stdout/stderr - Start Drain Threads: Background threads continuously read from pipes
- Wait for Process: Call
process.wait(timeout=timeout) - Collect Output: Gather drained output from queues
- Return Result: Exit code + captured output
from azlin.modules.subprocess_helper import safe_run
# Execute Azure CLI command
result = safe_run(
cmd=["az", "network", "bastion", "tunnel", "--name", "my-bastion"],
timeout=None # No timeout for long-running tunnel
)
if result.timed_out:
print("Command timed out")
elif result.returncode != 0:
print(f"Command failed: {result.stderr}")
else:
print("Command succeeded")
# Tunnel is running, result.stdout has outputdef startup_checks():
"""Perform startup environment checks."""
from .modules.cli_detector import CLIDetector
from .modules.cli_installer import CLIInstaller
detector = CLIDetector()
env_info = detector.detect()
if env_info.has_problem:
print(f"\n⚠️ {env_info.problem_description}\n")
installer = CLIInstaller()
result = installer.install()
if result.status.value == "success":
print("\n✓ Azure CLI fixed! Continuing...\n")
elif result.status.value == "cancelled":
print("\n⚠️ Installation cancelled.\n")
else:
print(f"\n❌ Installation failed: {result.error_message}\n")
def main():
"""Main CLI entry point."""
startup_checks() # Add before argument parsing
# ... rest of CLI logic ...from ..modules.subprocess_helper import safe_run
from ..modules.cli_detector import CLIDetector
class BastionManager:
def __init__(self):
self._cli_path = self._get_cli_path()
def _get_cli_path(self) -> str:
"""Get explicit path to Azure CLI."""
detector = CLIDetector()
env_info = detector.detect()
if env_info.environment.value == "wsl2":
linux_cli = detector.get_linux_cli_path()
if linux_cli:
return str(linux_cli)
return "az" # Fallback to PATH
def create_tunnel(self, ...):
"""Create bastion tunnel using safe subprocess execution."""
cmd = [self._cli_path, "network", "bastion", "tunnel", ...]
result = safe_run(cmd, timeout=None)
if result.timed_out or result.returncode != 0:
return False
return True- No exceptions raised, always returns
EnvironmentInfo
Returns InstallResult with status:
SUCCESS: Installation completed successfullyCANCELLED: User chose not to installFAILED: Installation failed (seeerror_message)ALREADY_INSTALLED: Linux CLI already present
Returns SubprocessResult with:
returncode == 0: Successreturncode == 127: Command not foundreturncode == -1: Timeout occurredreturncode > 0: Command failedtimed_out == True: Timeout flag
See test files in tests/ directory:
test_cli_detector.py: Unit tests for detection logictest_cli_installer.py: Integration tests for installationtest_subprocess_helper.py: Unit tests for subprocess execution