Skip to content

Instantly share code, notes, and snippets.

@parndt
Last active July 22, 2024 07:56

Revisions

  1. parndt revised this gist Jul 22, 2024. 1 changed file with 16 additions and 0 deletions.
    16 changes: 16 additions & 0 deletions Caddyfile
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,16 @@
    app.localhost {
    reverse_proxy http://127.0.0.1:9393
    tls ./app.localhost+5.pem ./app.localhost+5-key.pem
    }

    admin.app.localhost {
    reverse_proxy http://127.0.0.1:9292
    }

    api.app.localhost {
    reverse_proxy http://127.0.0.1:3456
    }

    assets.app.localhost {
    reverse_proxy http://127.0.0.1:8080
    }
  2. parndt created this gist Jul 22, 2024.
    75 changes: 75 additions & 0 deletions application.ex
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,75 @@
    defmodule App.Application do
    # See https://hexdocs.pm/elixir/Application.html
    # for more information on OTP Applications
    @moduledoc false

    use Application

    @impl true
    def start(_type, _args) do
    children =
    ecto_repos(App.config_env()) ++
    [
    # Start the PubSub system
    {Phoenix.PubSub, name: App.PubSub}
    # Start a worker by calling: App.Worker.start_link(arg)
    # {App.Worker, arg}
    ] ++
    extra_children(
    App.config_env(),
    (System.get_env("API_PROD_ENDPOINT") || "") |> String.split(".") |> List.last()
    )

    Supervisor.start_link(children, strategy: :one_for_one, name: App.Supervisor)
    end

    def ecto_repos(:test), do: [App.Repo]
    def ecto_repos(_), do: [App.Repo, App.Repo.Replica]

    defp extra_children(:dev, _), do: all_extra_children()
    defp extra_children(:prod, "localhost"), do: all_extra_children()

    defp extra_children(_, _), do: []

    defp all_extra_children do
    [
    Supervisor.child_spec(
    {App.Ports.Commander,
    [
    command: "../bin/monitor.sh ./node_modules/.bin/ts-node ./scripts/build development",
    args: [
    {:cd, "web"},
    {:env, [{'TS_NODE_PROJECT', 'tsconfig.webpack.json'}, {'PORT', '8080'}]}
    ]
    ]},
    id: :build_assets
    ),
    Supervisor.child_spec(
    {App.Ports.Commander,
    [
    command: "../bin/monitor.sh ./node_modules/.bin/serve -C ./tmp",
    args: [
    {:cd, "web"},
    {:env, [{'PORT', '8080'}]}
    ]
    ]},
    id: :assets
    ),
    Supervisor.child_spec(
    {App.Ports.Commander,
    [
    command: "bin/monitor.sh caddy run --watch"
    ]},
    id: :caddy
    ),
    Supervisor.child_spec(
    {App.Ports.Commander,
    [
    args: [{:cd, "web"}],
    command: "../bin/monitor.sh bin/puma"
    ]},
    id: :puma
    )
    ]
    end
    end
    61 changes: 61 additions & 0 deletions commander.ex
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,61 @@
    # lib/app/ports/commander.ex
    defmodule App.Ports.Commander do
    use GenServer
    require Logger

    # GenServer API
    def start_link(args \\ [], opts \\ []) do
    GenServer.start_link(__MODULE__, args, opts)
    end

    def init(args \\ []) do
    Process.flag(:trap_exit, true)

    port = Port.open({:spawn, args[:command]}, [:binary, :exit_status] ++ (args[:args] || []))
    Port.monitor(port)

    {:ok, %{port: port, latest_output: nil, exit_status: nil}}
    end

    # This callback handles data incoming from the command's STDOUT
    def handle_info({_port, {:data, text_line}}, state) do
    Logger.info("Latest output: #{inspect(text_line)}")

    {:noreply, %{state | latest_output: String.trim(text_line)}}
    end

    # This callback tells us when the process exits
    def handle_info({_port, {:exit_status, status}}, state) do
    Logger.info("External exit: :exit_status: #{status}")

    {:noreply, %{state | exit_status: status}}
    end

    def handle_info({:DOWN, _ref, :port, port, :normal}, state) do
    Logger.info("Handled :DOWN message from port: #{inspect(port)}")

    {:noreply, state}
    end

    # no-op catch-all callback for unhandled messages
    def handle_info(msg, state) do
    Logger.info("Unhandled message: #{inspect(msg)}")

    {:noreply, state}
    end

    def terminate(reason, %{port: port} = state) do
    Logger.info(
    "** TERMINATE: #{inspect(reason)}. This is the last chance to clean up after this process."
    )

    Logger.info("Final state: #{inspect(state)}")

    port_info = Port.info(port)
    os_pid = port_info[:os_pid]

    Logger.warn("Orphaned OS process: #{os_pid}")

    :normal
    end
    end
    23 changes: 23 additions & 0 deletions monitor.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    # bin/monitor.sh
    #!/usr/bin/env bash

    # 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