defmodule DalaDev.Utils do @moduledoc """ Shared utility functions used across the dala_dev codebase. This module centralizes common operations to reduce duplication and ensure consistent behavior across modules. """ @doc """ Compiles a regex pattern with the given options. Centralizes regex compilation to avoid duplicating `Regex.compile!/2` calls and provides a single place to handle compilation errors. ## Examples iex> DalaDev.Utils.compile_regex("hello\\s+world") ~r/hello\\s+world/ """ @spec compile_regex(String.t(), String.t()) :: Regex.t() def compile_regex(pattern, opts \\ "") do case Regex.compile(pattern, opts) do {:ok, regex} -> regex {:error, {reason, position}} -> raise "Invalid regex pattern: #{inspect(pattern)}, " <> "error: #{reason} at position #{position}" end end @doc """ Safely runs an ADB command with timeout protection. Returns `{:ok, output}` on success, `{:error, reason}` on failure. ## Options - `:timeout` - timeout in milliseconds (default: 8000) - `:stderr_to_stdout` - whether to merge stderr (default: true) """ @spec run_adb_with_timeout(list(String.t()), keyword()) :: {:ok, String.t()} | {:error, term()} def run_adb_with_timeout(args, opts \\ []) do timeout = Keyword.get(opts, :timeout, 8000) stderr_to_stdout = Keyword.get(opts, :stderr_to_stdout, true) cmd = Enum.join(["adb" | args], " ") if timeout_available?() do run_with_timeout(cmd, timeout, stderr_to_stdout) else IO.puts( "#{IO.ANSI.yellow()}[dala_dev] Warning: 'timeout' command not found. Running without timeout protection.\n" <> " Commands may hang indefinitely on unresponsive devices.#{IO.ANSI.reset()}" ) run_without_timeout(cmd, stderr_to_stdout) end end defp run_with_timeout(cmd, timeout, stderr_to_stdout) do case System.cmd("sh", ["-c", "timeout #{div(timeout, 1000)} #{cmd}"], stderr_to_stdout: stderr_to_stdout ) do {output, 0} -> {:ok, String.trim(output)} {_output, 124} -> {:error, :timeout} {output, _code} -> {:error, String.trim(output)} end end defp run_without_timeout(cmd, stderr_to_stdout) do case System.cmd("sh", ["-c", cmd], stderr_to_stdout: stderr_to_stdout) do {output, 0} -> {:ok, String.trim(output)} {output, _code} -> {:error, String.trim(output)} end end defp timeout_available? do System.find_executable("timeout") != nil end @doc """ Runs an ADB command for a specific device with timeout. Convenience wrapper that prepends `-s ` to the arguments. """ @spec run_adb_for_device(String.t(), list(String.t()), keyword()) :: {:ok, String.t()} | {:error, term()} def run_adb_for_device(serial, args, opts \\ []) do run_adb_with_timeout(["-s", serial | args], opts) end @doc """ Checks if ADB is available in the system PATH. """ @spec adb_available?() :: boolean() def adb_available? do command_available?("adb") end @doc """ Parses ADB devices output into a list of device identifiers. Expects output from `adb devices` command. """ @spec parse_adb_devices_output(String.t()) :: [String.t()] def parse_adb_devices_output(output) do output |> String.split("\n") |> Enum.drop(1) |> Enum.reject(&(String.trim(&1) == "")) |> Enum.map(&String.split(&1, "\t")) |> Enum.map(&List.first/1) |> Enum.reject(&is_nil/1) |> Enum.reject(&(&1 == "")) end @doc """ Checks if a command is available in the system PATH. """ @spec command_available?(String.t()) :: boolean() def command_available?(cmd) do System.find_executable(cmd) != nil end @doc """ Ensures a directory exists, creating it if necessary. Returns `:ok` on success, `{:error, reason}` on failure. """ @spec ensure_dir(String.t()) :: :ok | {:error, term()} def ensure_dir(path) do case File.mkdir_p(path) do :ok -> :ok {:error, _} = err -> err end end @doc """ Formats a byte size into a human-readable string. """ @spec format_bytes(non_neg_integer()) :: String.t() def format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" def format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 1)} KB" def format_bytes(bytes) when bytes < 1024 * 1024 * 1024, do: "#{Float.round(bytes / (1024 * 1024), 1)} MB" def format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024 * 1024), 1)} GB" end