amnu3387

amnu3387

Issues with opening port for bash script and automatically closing the process

Hey, perhaps someone ran into this before and can help me out. Basically I’m opening a port that runs a bash script for launching chrome. It works fine on my local mac, chrome gets started, when the Port is sent Process.exit(pid, :normal), the chrome instance is killed.

Now I’ve deployed this umbrella app to ubuntu, it’s working, but chrome instances are not being killed when I .exit() the process. Any ideas?

#!/bin/sh
google-chrome --attrs --flag-switches-begin --headless --disable-gpu --remote-debugging-port="$1" --user-agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36" --user-data-dir=/dev/null --disable-extensions --disable-internal-flash --disable-bundled-ppapi-flash --incognito --ignore-certificate-errors --noerrdialogs --enable-internal-media-session --flag-switches-end & pid=$!
while read line ; do
  :
done
kill -KILL $pid

And what calls that script:

def handle_cast({:start_session, reference, %{"pageUrl" => page_url}}, %{session: false} = state) do
    IO.puts("Hound :start_session for reference: #{inspect page_url}")
    port = find_available_port()
    path = Path.absname(chrome_path(), Application.app_dir(:app_chrome))
    chrome = Port.open({:spawn_executable, path},
      [:binary, :use_stdio, :stderr_to_stdout, :exit_status, args: ["#{port}"]])
    full_page_url = base_url() <> page_url
    {:os_pid, pid} = Port.info(chrome, :os_pid)
    case Regex.named_captures(~r/listening\son\s?(?<url>.*)\s/, handle_output(chrome)) do
       %{"url" => url} ->
        GenServer.cast(ChannelerServer, {:start_socket, %{port: port, reference: reference, page_url: full_page_url}})
        GenServer.call(HoundKeeper, {:update, reference, chrome, pid, port, url}, 60_000)

      _ -> {:error, "Not Active"}
    end
    {:noreply, state}
  end

Ubuntu is 16.04
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:10] [hipe] [kernel-poll:false]
Elixir 1.5.2

Any ideas? Thanks

Most Liked

amnu3387

amnu3387

Sharing what I found while researching and solving my issues with this, In case anybody needs some solutions for dealing with misbehaving ports/processes outside the beam - it’s a bit hacky, but hey, gets the work done on linux ubuntu and chromium instances.

First, use “kill” on the bash script itself, then have a function that aggregates recursively all the PIDS, kill them, checks the cleanup and as last resort “kill -9” them. kill -9 on chromium processes on linux is a bad idea, although sometimes it won’t help to just do the standard kill, as the processes will remain running, so you’re basically left with having to make sure you catch them all. In my particular case I can’t pgrep/grep on the process name, as I need to kill specific instances of chromium at certain times, and not all running instances of chromium.

For the bash script

commandline blablablabla &
pid=$!
while read line ; do
  :
done
ps -ef | grep $pid | grep -v grep | awk '{print $2}' | xargs kill
kill -KILL $pid

This will grep all processes that mention the &background command PID (so the main process itself and any other forked processes that reference it as the Parent PID). Sends them a kill signal, and a -9 to the main process alone. Xargs is parallel, so all processes grepped there will receive a regular kill before the parent receives a -9.

This should work most of the time, but chromium sometimes is nasty. So I added a new layer at the application level. Since I always pass an arbitrary non-repeated debugger port to the chromium instance I can use that to identify the processes I’m interested in finding. You can use whatever, and even pass non-existing flags to identify your chromium instances, since they keep it as part of the “command” attribute that was ran to create the process:

Task.start(fn -> try_port_kill(ws_port) end)

def try_port_kill(port) do
    pids = "ps aux | grep -ie port=#{port} | grep -v grep | awk '{print $2}'" |> String.to_charlist |> :os.cmd |> List.to_string |> String.split
    port_kill(pids)
  end

  def port_kill([]), do: :ok
  def port_kill([h|t]) do
    pids = "ps -ef | grep #{h} | grep -v grep | awk '{print $2}'" |> String.to_charlist |> :os.cmd |> List.to_string |> String.split
    
    Task.start(fn -> port_kill(pids -- [h]) end)
    System.cmd("kill", ["#{h}"])
    Task.start(fn -> brutal_kill(h) end)
    
    port_kill(t)
  end

  def brutal_kill(h) do
    case "kill -0 #{h}" |> String.to_charlist |> :os.cmd |> List.to_string do
      "" ->
        :timer.sleep 5_000
        System.cmd("kill", ["-9", "#{h}"])
      _ ->
        :ok
    end
  end

This doesn’t rely on the PID from the bashscript - I’m also grepping on something I’m sure is unique in my use case “port=an_arbitrary_port_that_I_had_set” can only match for what I want so I’m fine with the hackiness of it. You could also start chromium with a flag such as --my-marker-flag=something and then grepping on this. I call this method after I “close” the port.

Lastly since it involves long-lived, always running processes and I don’t want to risk having any of them creep, and even although it seems what I’ve added before is enough to keep it tamed and under control, I went a bit further and added a sweeper based on time that will kill any chromium/chrome processes running more than 40min, and I recurrently send_after to call this:

def sweeper do
    {all, _} = System.cmd("ps", ["-eo", "pid,lstart,comm"])
    [header | proc_lines] = String.split(all, "\n")
    Enum.each(proc_lines, fn(proc) -> sweep_zombie(proc) end)
    {:noreply, state}
end
def sweep_zombie(proc_line) do
    split = String.split(proc_line)
    {[pid, _wday, month, day, hhmmss, year], all_comm} = Enum.split(split, 6)
    comm = Enum.join(all_comm, " ")
    
   case Regex.match?(~r/chrom/, comm) || Regex.match?(~r/Chrome/, comm) do
     true ->
       {:ok, datetime, 0} = DateTime.from_iso8601("#{year}-#{n_month(month)}-#{String.pad_leading(day, 2, "0")}T#{hhmmss}Z")
      dt = DateTime.to_unix(datetime)
    tn = System.system_time(:second)
    case (tn - dt) > (40*60) do
      true ->
        IO.puts("DT: #{inspect dt} TN: #{inspect tn} COMM: #{inspect comm}")
        System.cmd("kill", ["-9", "#{pid}"])  
      false ->
        :ok
    end
  false ->
    :ok
  end
end

def n_month("Jan"), do: "01"
def n_month("Feb"), do: "02"
def n_month("Mar"), do: "03"
def n_month("Apr"), do: "04"
def n_month("May"), do: "05"
def n_month("Jun"), do: "06"
def n_month("Jul"), do: "07"
def n_month("Aug"), do: "08"
def n_month("Sep"), do: "09"
def n_month("Oct"), do: "10"
def n_month("Nov"), do: "11"
def n_month("Dec"), do: "12"
OvermindDL1

OvermindDL1

The problem is not killing off an OS process, kill is just fine for that, it is that chrome often forks itself to new PID’s so the one you tried to kill actually does not belong to chrome any longer (it might even belong to something else).

So yes, for a generic program that script is fine, however chrome is not a generic program, most specifically when running with --remote-debugging-port=... as it will try to stay persistant. Thus to stop chrome properly you should not kill it (as it may often not work due to it forking away into new processes), but rather you need to send the ‘close’ signal to that port that you set up via --remote-debugging-port argument.

https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md

Honestly you should probably write either a node script using the javascript API that google made to interact with it, or write a C++ program and use the C++ library that google made to interact with it, whichever you use ‘that’ is what you’d call from a Port in elixir. Yes chrome is weird (it should just handle it on stdin/stdout to be honest), but you have to use it’s API that you exposed at that port to properly shut it down, killing chrome does not always work as you see.

amnu3387

amnu3387

Hi keathley, I did try wallaby and hound, I ended up not using either because I was unable to keep multiple sessions alive (say 20 sessions sitting for an arbitrary number of minutes on a page), with individual devtools debugger for each session (so 20 different pages, independent sessions, each one with an individual debugger attached for anywhere between 5min to 15min, sometimes more)? If you know that that can be achieved with wallaby, running chrome, I’m all to use what you guys already did.

I will indeed take a look at how you got it working, I was thinking “Browser.close” through the wire would suffice, but “devtools” is telling me: %{"error" => %{"code" => -32601, "message" => "'Browser.close' wasn't found"}, "id" => 999}
Thanks

Where Next?

Popular in Questions Top

belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New

Other popular topics Top

freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New

We're in Beta

About us Mission Statement