diff --git a/src/Commands/TailCommand.php b/src/Commands/TailCommand.php index e160d462..ae766502 100644 --- a/src/Commands/TailCommand.php +++ b/src/Commands/TailCommand.php @@ -3,11 +3,20 @@ namespace Native\Mobile\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; +use Native\Mobile\Traits\PromptsAndroidTarget; use Symfony\Component\Process\Process; +use function Laravel\Prompts\select; + class TailCommand extends Command { - protected $signature = 'native:tail'; + use PromptsAndroidTarget; + + const LOG_DIR = 'app_storage/persisted_data/storage/logs'; + + protected $signature = 'native:tail {udid?}'; protected $description = 'Tail Laravel logs from the Android app'; @@ -30,9 +39,13 @@ private function tailAndroid(string $appId): void $this->info("🤖 Tailing Android logs for app: $appId"); $this->line("Press Ctrl+C to stop...\n"); + $target = $this->argument('udid') ?? $this->promptForAndroidTarget(); + + $logFile = $this->promptForLogFile($target, $appId); + $command = [ - 'adb', 'shell', 'run-as', $appId, 'tail', '-f', - 'app_storage/persisted_data/storage/logs/laravel.log', + 'adb', '-s', $target, 'shell', 'run-as', $appId, 'tail', '-f', + self::LOG_DIR.'/'.$logFile, ]; $process = new Process($command); @@ -56,4 +69,25 @@ private function tailAndroid(string $appId): void $this->line('• The app is installed and running'); } } + + private function promptForLogFile(string $target, $appId): string + { + $command = [ + 'adb', '-s', $target, 'shell', 'run-as', $appId, 'ls', self::LOG_DIR, + ]; + + $process = new Process($command); + $process->run(); + $output = $process->getOutput(); + + /** @var Collection $logFiles */ + $logFiles = collect(explode(PHP_EOL, $output)) + ->filter(fn (string $line) => Str::endsWith($line, '.log')); + + if ($logFiles->count() === 1) { + return $logFiles->first(); + } + + return select('Select a log file', $logFiles); + } } diff --git a/src/Traits/PromptsAndroidTarget.php b/src/Traits/PromptsAndroidTarget.php new file mode 100644 index 00000000..d28239c5 --- /dev/null +++ b/src/Traits/PromptsAndroidTarget.php @@ -0,0 +1,166 @@ +getConnectedAndroidDevices(); + + if (empty($devices)) { + error('No connected Android devices or emulators found.'); + exit(1); + } + + if (count($devices) === 1) { + return array_key_first($devices); + } + + return select('Select a device or emulator', $devices); + } + + private function getConnectedAndroidDevices(): array + { + $adbCommand = PHP_OS_FAMILY === 'Windows' ? 'adb.exe' : 'adb'; + $devices = $this->parseAdbDevices($adbCommand); + + if (! empty($devices)) { + return $devices; + } + + note('No devices found. Attempting to launch an emulator...'); + + $emulatorBinary = $this->resolveAndroidEmulatorPath(); + + if (! $emulatorBinary) { + error('Could not locate the Android emulator binary.'); + exit(1); + } + + $listCommand = sprintf('"%s" -list-avds', $emulatorBinary); + $listProcess = SymfonyProcess::fromShellCommandline($listCommand); + $listProcess->run(); + + if (! $listProcess->isSuccessful()) { + error('Failed to list Android emulators.'); + exit(1); + } + + $avds = array_filter(explode("\n", trim($listProcess->getOutput()))); + + if (empty($avds)) { + error('No AVDs found.'); + exit(1); + } + + $selected = select( + label: 'Select an emulator to launch', + options: $avds + ); + + $escapedBinary = escapeshellarg($emulatorBinary); + $escapedAvd = escapeshellarg($selected); + + if (PHP_OS_FAMILY === 'Windows') { + $launchCommand = "start /B \"\" \"{$emulatorBinary}\" -avd \"{$selected}\""; + } else { + $launchCommand = "nohup $escapedBinary -avd $escapedAvd > /tmp/emulator.log 2>&1 &"; + } + + SymfonyProcess::fromShellCommandline($launchCommand)->start(); + + $this->components->task('Waiting for emulator to boot', function () use ($adbCommand) { + for ($i = 0; $i < 100; $i++) { + $bootCompleted = $this->adbGetProp($adbCommand, 'sys.boot_completed'); + $bootAnim = $this->adbGetProp($adbCommand, 'init.svc.bootanim'); + + if ($bootCompleted === '1' && $bootAnim === 'stopped') { + return true; + } + + usleep(250000); + } + + return false; + }); + + return $this->parseAdbDevices($adbCommand); + } + + private function adbGetProp(string $adbCommand, string $property): string + { + $cmd = "$adbCommand shell getprop $property"; + + $descriptorSpec = [ + 1 => ['pipe', 'w'], // stdout + 2 => ['file', 'NUL', 'a'], // stderr (Windows) + ]; + + $process = proc_open($cmd, $descriptorSpec, $pipes); + if (is_resource($process)) { + $output = stream_get_contents($pipes[1]); + fclose($pipes[1]); + proc_close($process); + + return trim($output); + } + + return ''; + } + + private function parseAdbDevices(string $adbCommand): array + { + $output = shell_exec("{$adbCommand} devices") ?: ''; + + return collect(explode("\n", $output)) + ->filter(fn ($line) => Str::contains($line, "\tdevice")) + ->mapWithKeys(fn ($line) => [explode("\t", $line)[0] => explode("\t", $line)[0]]) + ->all(); + } + + private function resolveAndroidEmulatorPath(): ?string + { + // 1. Allow override from config or .env + $customPath = config('nativephp.android.emulator_path') ?? env('ANDROID_EMULATOR'); + if ($customPath && file_exists($customPath)) { + return $customPath; + } + + // 2. Check SDK paths from env vars + $sdk = env('ANDROID_HOME') ?: env('ANDROID_SDK_ROOT'); + + $candidates = []; + + if ($sdk) { + $candidates[] = $sdk.DIRECTORY_SEPARATOR.'emulator'.DIRECTORY_SEPARATOR.(PHP_OS_FAMILY === 'Windows' ? 'emulator.exe' : 'emulator'); + } + + // 3. Fallback defaults per OS + if (PHP_OS_FAMILY === 'Windows') { + $username = getenv('USERNAME') ?: 'user'; + $candidates[] = "C:\\Users\\{$username}\\AppData\\Local\\Android\\Sdk\\emulator\\emulator.exe"; + $candidates[] = getenv('LOCALAPPDATA').'\\Android\\Sdk\\emulator\\emulator.exe'; + } elseif (PHP_OS_FAMILY === 'Darwin') { + $candidates[] = getenv('HOME').'/Library/Android/sdk/emulator/emulator'; + } else { // Linux + $candidates[] = getenv('HOME').'/Android/Sdk/emulator/emulator'; + } + + // 4. Return first found + foreach ($candidates as $path) { + if ($path && file_exists($path)) { + return $path; + } + } + + return null; + } +} diff --git a/src/Traits/RunsAndroid.php b/src/Traits/RunsAndroid.php index 544d38a9..dcf1afd8 100644 --- a/src/Traits/RunsAndroid.php +++ b/src/Traits/RunsAndroid.php @@ -4,22 +4,19 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Str; use Native\Mobile\Plugins\Compilers\AndroidPluginCompiler; use Native\Mobile\Plugins\PluginHookRunner; use Native\Mobile\Plugins\PluginRegistry; use Native\Mobile\Plugins\PluginSecretsValidator; -use Symfony\Component\Process\Process as SymfonyProcess; use function Laravel\Prompts\error; use function Laravel\Prompts\note; use function Laravel\Prompts\outro; -use function Laravel\Prompts\select; use function Laravel\Prompts\warning; trait RunsAndroid { - use PreparesBuild, WatchesAndroid; + use PreparesBuild, PromptsAndroidTarget, WatchesAndroid; protected string $androidLogPath = 'nativephp'.DIRECTORY_SEPARATOR.'android-build.log'; @@ -547,159 +544,6 @@ private function runTheAndroidBuild(?string $targetDeviceId): void } } - private function promptForAndroidTarget(): string - { - $devices = $this->getConnectedAndroidDevices(); - - if (empty($devices)) { - error('No connected Android devices or emulators found.'); - exit(1); - } - - if (count($devices) === 1) { - return array_key_first($devices); - } - - return select('Select a device or emulator', $devices); - } - - private function getConnectedAndroidDevices(): array - { - $adbCommand = PHP_OS_FAMILY === 'Windows' ? 'adb.exe' : 'adb'; - $devices = $this->parseAdbDevices($adbCommand); - - if (! empty($devices)) { - return $devices; - } - - note('No devices found. Attempting to launch an emulator...'); - - $emulatorBinary = $this->resolveAndroidEmulatorPath(); - - if (! $emulatorBinary) { - error('Could not locate the Android emulator binary.'); - exit(1); - } - - $listCommand = sprintf('"%s" -list-avds', $emulatorBinary); - $listProcess = SymfonyProcess::fromShellCommandline($listCommand); - $listProcess->run(); - - if (! $listProcess->isSuccessful()) { - error('Failed to list Android emulators.'); - exit(1); - } - - $avds = array_filter(explode("\n", trim($listProcess->getOutput()))); - - if (empty($avds)) { - error('No AVDs found.'); - exit(1); - } - - $selected = select( - label: 'Select an emulator to launch', - options: $avds - ); - - $escapedBinary = escapeshellarg($emulatorBinary); - $escapedAvd = escapeshellarg($selected); - - if (PHP_OS_FAMILY === 'Windows') { - $launchCommand = "start /B \"\" \"{$emulatorBinary}\" -avd \"{$selected}\""; - } else { - $launchCommand = "nohup $escapedBinary -avd $escapedAvd > /tmp/emulator.log 2>&1 &"; - } - - SymfonyProcess::fromShellCommandline($launchCommand)->start(); - - $this->components->task('Waiting for emulator to boot', function () use ($adbCommand) { - for ($i = 0; $i < 100; $i++) { - $bootCompleted = $this->adbGetProp($adbCommand, 'sys.boot_completed'); - $bootAnim = $this->adbGetProp($adbCommand, 'init.svc.bootanim'); - - if ($bootCompleted === '1' && $bootAnim === 'stopped') { - return true; - } - - usleep(250000); - } - - return false; - }); - - return $this->parseAdbDevices($adbCommand); - } - - private function adbGetProp(string $adbCommand, string $property): string - { - $cmd = "$adbCommand shell getprop $property"; - - $descriptorSpec = [ - 1 => ['pipe', 'w'], // stdout - 2 => ['file', 'NUL', 'a'], // stderr (Windows) - ]; - - $process = proc_open($cmd, $descriptorSpec, $pipes); - if (is_resource($process)) { - $output = stream_get_contents($pipes[1]); - fclose($pipes[1]); - proc_close($process); - - return trim($output); - } - - return ''; - } - - private function parseAdbDevices(string $adbCommand): array - { - $output = shell_exec("{$adbCommand} devices") ?: ''; - - return collect(explode("\n", $output)) - ->filter(fn ($line) => Str::contains($line, "\tdevice")) - ->mapWithKeys(fn ($line) => [explode("\t", $line)[0] => explode("\t", $line)[0]]) - ->all(); - } - - private function resolveAndroidEmulatorPath(): ?string - { - // 1. Allow override from config or .env - $customPath = config('nativephp.android.emulator_path') ?? env('ANDROID_EMULATOR'); - if ($customPath && file_exists($customPath)) { - return $customPath; - } - - // 2. Check SDK paths from env vars - $sdk = env('ANDROID_HOME') ?: env('ANDROID_SDK_ROOT'); - - $candidates = []; - - if ($sdk) { - $candidates[] = $sdk.DIRECTORY_SEPARATOR.'emulator'.DIRECTORY_SEPARATOR.(PHP_OS_FAMILY === 'Windows' ? 'emulator.exe' : 'emulator'); - } - - // 3. Fallback defaults per OS - if (PHP_OS_FAMILY === 'Windows') { - $username = getenv('USERNAME') ?: 'user'; - $candidates[] = "C:\\Users\\{$username}\\AppData\\Local\\Android\\Sdk\\emulator\\emulator.exe"; - $candidates[] = getenv('LOCALAPPDATA').'\\Android\\Sdk\\emulator\\emulator.exe'; - } elseif (PHP_OS_FAMILY === 'Darwin') { - $candidates[] = getenv('HOME').'/Library/Android/sdk/emulator/emulator'; - } else { // Linux - $candidates[] = getenv('HOME').'/Android/Sdk/emulator/emulator'; - } - - // 4. Return first found - foreach ($candidates as $path) { - if ($path && file_exists($path)) { - return $path; - } - } - - return null; - } - private function findReleaseApk(): ?string { $apkDir = base_path('nativephp/android/app/build/outputs/apk/release');