defmodule Nerves.Utils.WSL do @moduledoc """ This module contains utility functions to assist in detecting a Windows Subsystem for Linux environment as well as functions to convert paths between the Windows host and Linux. """ @doc """ Returns a two item tuple where the first item is a command and the second is the argument list to run a powershell command as administrator in Windows """ def admin_powershell_command(command, args) do { "powershell.exe", [ "-Command", "Start-Process #{command} -Verb runAs -Wait -ArgumentList \"#{args}\"" ] } end @doc """ Returns true if inside a WSL shell environment """ @spec running_on_wsl?(String.t()) :: boolean def running_on_wsl?(osrelease_path \\ "/proc/sys/kernel/osrelease") do with true <- File.exists?(osrelease_path), {content, _} <- System.cmd("cat", [osrelease_path]) do Regex.match?(~r/Microsoft/, content) else _ -> false end end @doc """ Gets a list of fwup devices on a Windows host. This function can be run from within WSL, as it runs a powershell command to get the list and writes it to a temporary file that WSL can access. """ def get_fwup_devices() do {win_path, _} = make_file_accessible("fwup_devs.txt", running_on_wsl?(), has_wslpath?()) {_, wsl_path} = get_wsl_paths(win_path, has_wslpath?()) powershell_args = "fwup.exe -D | set-content -encoding UTF8 #{win_path}" with {command, args} <- admin_powershell_command("powershell.exe", powershell_args), {"", 0} <- System.cmd(command, args), {:ok, devs} <- File.read(wsl_path) do devs = Regex.replace(~r/[\x{200B}\x{200C}\x{200D}\x{FEFF}]/u, devs, "") |> String.replace("\r", "") File.rm(wsl_path) {devs, 0} else {:error, :enoent} -> # fwup didn't find any devices and no tmp file was generated # behave as fwup does normally and return an empty string result {"", 0} error -> error end end @doc """ Returns true if the WSL utility `wslpath` is available """ @spec has_wslpath?() :: boolean def has_wslpath?() do System.find_executable("wslpath") != nil end @doc """ Returns true if the path is accessible in Windows """ @spec path_accessible_in_windows?(String.t(), boolean) :: boolean def path_accessible_in_windows?(file, _use_wslpath = true) do {_path, exitcode} = System.cmd("wslpath", ["-w", "-a", file], stderr_to_stdout: true) exitcode == 0 end def path_accessible_in_windows?(file, _use_wslpath) do Regex.match?(~r/(\/mnt\/\w{1})\//, file) end @doc """ Returns a path to the base file name a temporary location in Windows """ @spec get_temp_file_location(String.t()) :: String.t() def get_temp_file_location(file) do {win_path, 0} = System.cmd("cmd.exe", ["/c", "echo %TEMP%"]) "#{String.trim(win_path)}\\#{Path.basename(file)}" end @doc """ Returns true when the path starts with a drive letter, colon and either single backslash or double backslash """ @spec valid_windows_path?(String.t()) :: boolean def valid_windows_path?(path) do # Match : then \ or \\ and then one or more characters except line breaks Regex.match?(~r/(^\w{1}:)(\\\\|\\)(.+)/, path) end @doc """ Returns true if the path is not a Windows path """ @spec valid_wsl_path?(String.t()) :: boolean def valid_wsl_path?(path) do valid_windows_path?(path) === false end @doc """ Returns a two item tuple containing the Windows host path for a file and its WSL counterpart. If the path is not available in either Windows or WSL, nil will replace the item ## Examples iex> Nerves.Utils.WSL.get_wsl_paths("mix.exs", Nerves.Utils.WSL.has_wslpath?()) {"C:\\Users\\username\\src\\nerves\\mix.exs", "/mnt/c/Users/username/src/nerves/mix.exs"} """ @spec get_wsl_paths(String.t(), boolean) :: {String.t() | nil, String.t() | nil} def get_wsl_paths(file, _use_wslpath = true) do # Use wslpath, available from Windows 10 1803 # https://superuser.com/questions/1113385/convert-windows-path-for-windows-ubuntu-bash # -a force result to absolute path format # -u translate from a Windows path to a WSL path (default) # -w translate from a WSL path to a Windows path # -m translate from a WSL path to a Windows path, with ‘/’ instead of ‘\\’ win_path = if valid_windows_path?(file) do file else execute_wslpath(file, ["-w", "-a"]) end wsl_path = if valid_wsl_path?(file) do Path.expand(file) else execute_wslpath(file, ["-u", "-a"]) end {win_path, wsl_path} end def get_wsl_paths(file, _use_wslpath) do # Maintain support for Windows builds before 1803 fullpath = if valid_wsl_path?(file) do Path.expand(file) else file end # Check if the full path is accessible form Windows win_path = if Regex.match?(~r/(\/mnt\/\w{1})\//, fullpath) do # extract drive letter from path %{"drive" => drive_letter} = Regex.named_captures(~r/\/mnt\/(?\w{1})\//, fullpath) # replace /mnt// with windows version C:/ win_path = Regex.replace(~r/(\/mnt\/\w{1})\//, fullpath, "#{String.upcase(drive_letter)}:/") # replace forward slashes with backslashes Regex.replace(~r/\//, win_path, "\\\\") else # If path is already a windows path, return it if valid_windows_path?(fullpath) do fullpath else nil end end # Check if full path is a windows path wsl_path = if valid_windows_path?(fullpath) do # extract drive letter %{"drive" => drive_letter} = Regex.named_captures(~r/(?^.{1}):/, fullpath) # replace : with /mnt/ wsl_path = Regex.replace(~r/^.{1}:/, fullpath, "/mnt/#{String.downcase(drive_letter)}") # replace \\ or \ with forward slashes Regex.replace(~r/\\\\|\\/, wsl_path, "/") else # if we are already a wsl path just return it fullpath end {win_path, wsl_path} end @doc """ Executes wslpath with the file and arguments. When a valid WSL path is passed through to `wslpath` asking for a valid path an "Invalid argument" error is returned. This function catches this error and returns the valid path. """ @spec execute_wslpath(String.t(), list) :: String.t() | nil def execute_wslpath(file, arguments) do with {path, 0} <- System.cmd("wslpath", arguments ++ [file], stderr_to_stdout: true) do String.trim(path) else {error, _} -> if String.contains?(error, "Invalid argument") do Path.expand(file) else nil end end end @doc """ Returns an item tuple with the Windows accessible path and whether the path is a temporary location or original location """ @spec make_file_accessible(String.t(), boolean, boolean) :: {String.t(), :original_location} | {String.t(), :temporary_location} def make_file_accessible(file, _is_wsl = true, has_wslpath) do if path_accessible_in_windows?(file, has_wslpath) do {win_path, _wsl_path} = get_wsl_paths(file, has_wslpath) {win_path, :original_location} else # Create a temporary .fw file that fwup.exe is able to access temp_file_location = get_temp_file_location(file) {win_path, wsl_path} = get_wsl_paths(temp_file_location, has_wslpath) File.copy(file, wsl_path) {win_path, :temporary_location} end end def make_file_accessible(file, _is_wsl, _has_wslpath) do {file, :original_location} end @doc """ If the file was created in a temporary location, get the WSL path and delete it. Otherwise return `:ok` """ @spec cleanup_file(String.t(), :temporary_location | :original_location) :: :ok | {:error, atom} def cleanup_file(file, :temporary_location) do {_win_path, wsl_path} = get_wsl_paths(file, has_wslpath?()) File.rm(wsl_path) end def cleanup_file(_, _), do: :ok end