diff --git a/lib/esbuild.ex b/lib/esbuild.ex index 6ab26fe..dd4f01a 100644 --- a/lib/esbuild.ex +++ b/lib/esbuild.ex @@ -89,7 +89,7 @@ defmodule Esbuild do :ok end - Supervisor.start_link([], strategy: :one_for_one) + Supervisor.start_link([], strategy: :one_for_one, name: __MODULE__.Supervisor) end @doc false @@ -182,15 +182,30 @@ defmodule Esbuild do |> elem(1) end + defp start_unique_install_worker() do + ref = + __MODULE__.Supervisor + |> Supervisor.start_child( + Supervisor.child_spec({Task, &install/0}, restart: :transient, id: __MODULE__.Installer) + ) + |> case do + {:ok, pid} -> pid + {:error, {:already_started, pid}} -> pid + end + |> Process.monitor() + + receive do + {:DOWN, ^ref, _, _, _} -> :ok + end + end + @doc """ Installs, if not available, and then runs `esbuild`. Returns the same as `run/2`. """ def install_and_run(profile, args) do - unless File.exists?(bin_path()) do - install() - end + File.exists?(bin_path()) || start_unique_install_worker() run(profile, args) end diff --git a/test/esbuild_test.exs b/test/esbuild_test.exs index 12be6e8..073abd1 100644 --- a/test/esbuild_test.exs +++ b/test/esbuild_test.exs @@ -32,4 +32,29 @@ defmodule EsbuildTest do assert Esbuild.run(:default, ["--version"]) == 0 end) =~ @version end + + test "install and run multiple concurrently" do + bin_path = Esbuild.bin_path() + + assert :ok = File.exists?(bin_path) && File.rm!(bin_path) + + results = + [:extra1, :extra2, :extra3] + |> Enum.map(fn profile -> + Application.put_env(:esbuild, profile, args: ["--version"]) + + Task.async(fn -> + ret_code = Esbuild.install_and_run(profile, []) + # Let the first finished task set the binary file to read and execute only, + # so that the others will fail if they try to overwrite it. + File.chmod!(bin_path, 0o500) + ret_code == 0 + end) + end) + |> Task.await_many(:infinity) + + File.chmod!(bin_path, 0o700) + + assert results |> Enum.all?() + end end