Commit 2e8eba448388437b7275ce1a060ed9d1d2cbccec

Michael Crumm 2021-07-30T14:29:52

Wrap sass execution in a bash script to address zombie processes (#1) * Simplify path functions * Add wrapper bash script to address zombie processes when --watch is given

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 375a94e..01a3d99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # CHANGELOG
 
+## v0.1.1 (Unreleased)
+
+  * Add wrapper script to address zombie processes
+
 ## v0.1.0 (2021-07-25)
 
   * First release
diff --git a/lib/dart_sass.ex b/lib/dart_sass.ex
index 6e4d158..db2bca7 100644
--- a/lib/dart_sass.ex
+++ b/lib/dart_sass.ex
@@ -90,16 +90,6 @@ defmodule DartSass do
   end
 
   @doc """
-  Returns the path to the executable.
-
-  The executable may not be available if it was not yet installed.
-  """
-  def bin_path do
-    {path, _snapshot} = bin_paths()
-    path
-  end
-
-  @doc """
   Returns the path to the executable and optional snapshot.
 
   Depending on your environment, sass may be invoked through a
@@ -130,6 +120,12 @@ defmodule DartSass do
     Path.join(Path.dirname(Mix.Project.build_path()), "dart")
   end
 
+  # TODO: Remove when dart-sass will exit when stdin is closed.
+  @doc false
+  def script_path() do
+    Path.join(:code.priv_dir(:dart_sass), "dart_sass.bash")
+  end
+
   @doc """
   Returns the version of the dart_sass executable.
 
@@ -148,9 +144,20 @@ defmodule DartSass do
   end
 
   defp sass(args) do
-    case bin_paths() do
-      {sass, nil} -> {sass, args}
-      {vm, snapshot} -> {vm, [snapshot] ++ args}
+    {path, args} =
+      case bin_paths() do
+        {sass, nil} -> {sass, args}
+        {vm, snapshot} -> {vm, [snapshot] ++ args}
+      end
+
+    # TODO: Remove when dart-sass will exit when stdin is closed.
+    # Link: https://github.com/sass/dart-sass/pull/1411
+    cond do
+      "--watch" in args and not match?({:win32, _}, :os.type()) ->
+        {script_path(), [path] ++ args}
+
+      true ->
+        {path, args}
     end
   end
 
@@ -172,9 +179,9 @@ defmodule DartSass do
       stderr_to_stdout: true
     ]
 
-    {sass_path, args} = sass(args ++ extra_args)
+    {path, args} = sass(args ++ extra_args)
 
-    sass_path
+    path
     |> System.cmd(args, opts)
     |> elem(1)
   end
@@ -210,7 +217,7 @@ defmodule DartSass do
       other -> raise "couldn't unpack archive: #{inspect(other)}"
     end
 
-    bin_path = DartSass.bin_path()
+    sass_path = DartSass.sass_path()
     snapshot_path = DartSass.snapshot_path()
     vm_path = DartSass.vm_path()
 
@@ -224,7 +231,7 @@ defmodule DartSass do
         File.cp!(Path.join([tmp_dir, "dart-sass", "src", "sass.snapshot"]), snapshot_path)
 
       _ ->
-        File.cp!(Path.join([tmp_dir, "dart-sass", "sass"]), bin_path)
+        File.cp!(Path.join([tmp_dir, "dart-sass", "sass"]), sass_path)
     end
   end
 
diff --git a/priv/dart_sass.bash b/priv/dart_sass.bash
new file mode 100755
index 0000000..0f97f90
--- /dev/null
+++ b/priv/dart_sass.bash
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+# This script was taken from the Elixir Port guide. It is used to ensure
+# graceful termination of the `sass` process when it detects that stdin
+# has been closed.
+# Link: https://hexdocs.pm/elixir/Port.html#module-zombie-operating-system-processes
+#
+# This script is required until dart-sass supports listening on stdin and
+# gracefully terminating when stdin is closed. There is currently a PR for
+# this behaviour: https://github.com/sass/dart-sass/pull/1411
+#
+# Start the program in the background
+exec "$@" &
+pid1=$!
+
+# Silence warnings from here on
+exec >/dev/null 2>&1
+
+# Read from stdin in the background and
+# kill running program when stdin closes
+exec 0<&0 $(
+  while read; do :; done
+  kill -KILL $pid1
+) &
+pid2=$!
+
+# Clean up
+wait $pid1
+ret=$?
+kill -KILL $pid2
+exit $ret