Update Firmware IO logs

Update Firmware IO logs

defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
  @moduledoc """
  This asset worker does not get restarted. It inistead responds to GenServer
  calls.
  """

  use GenServer
  require Logger
  require FarmbotCore.Logger
  alias FarmbotCeleryScript.AST
  alias FarmbotCore.{Asset.FbosConfig, BotState, Config}
  import FarmbotFirmware.PackageUtils, only: [package_to_string: 1]

  @firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold]
  @firmware_flash_timeout Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_timeout] || 5000
  @disable_firmware_io_logs_timeout Application.get_env(:farmbot_core, __MODULE__)[:disable_firmware_io_logs_timeout] || 300000
  @firmware_flash_attempt_threshold || Mix.raise """
  Firmware open attempt threshold not configured:

  config :farmbot_core, #{__MODULE__}, [
    firmware_flash_attempt_threshold: :infinity
  ]
  """

  @impl FarmbotCore.AssetWorker
  def preload(%FbosConfig{}), do: []

  @impl FarmbotCore.AssetWorker
  def tracks_changes?(%FbosConfig{}), do: true

  @impl FarmbotCore.AssetWorker
  def start_link(%FbosConfig{} = fbos_config, _args) do
    GenServer.start_link(__MODULE__, %FbosConfig{} = fbos_config)
  end

  @impl GenServer
  def init(%FbosConfig{} = fbos_config) do
    if Config.get_config_value(:bool, "settings", "firmware_needs_flash") do
      Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    end
    state = %{
      fbos_config: fbos_config,
      firmware_io_timer: nil,
      firmware_flash_attempts: 0,
      firmware_flash_attempt_threshold: @firmware_flash_attempt_threshold,
      firmware_flash_timeout: @firmware_flash_timeout,
      firmware_flash_in_progress: false
    }
    {:ok, state, 0}
  end

  @impl GenServer
  def handle_info({:step_complete, _, :ok}, state) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    {:noreply, %{state | firmware_flash_in_progress: false}}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{firmware_flash_attempts: tries, firmware_flash_attempt_threshold: thresh} = state)
  when tries >= thresh do
    FarmbotCore.Logger.error 1, """
    Failed flashing firmware: #{reason}
    Tried #{tries} times. Not retrying
    """
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    {:noreply, %{state | firmware_flash_attempts: 0, firmware_flash_in_progress: false}}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", true)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)

    firmware_flash_timeout = state.firmware_flash_timeout
    firmware_flash_attempts = state.firmware_flash_attempts

    new_state = %{
      state |
      firmware_flash_attempts: firmware_flash_attempts + 1,
      firmware_flash_timeout: @firmware_flash_timeout * (firmware_flash_attempts + 1),
      firmware_flash_in_progress: false
    }
    FarmbotCore.Logger.error 1, """
    Error flashing firmware: #{reason}
    Trying again in #{state.firmware_flash_timeout / 1000} seconds
    """
    Process.send_after(self(), {:maybe_flash_firmware, fbos_config}, firmware_flash_timeout)
    {:noreply, new_state}
  end

  def handle_info(:timeout, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    FarmbotCore.Logger.debug 3, "Got initial fbos config"
    set_config_to_state(fbos_config)
    send self(), {:maybe_flash_firmware, fbos_config}
    send self(), {:maybe_start_io_log_timer, fbos_config}
    {:noreply, state}
  end

  def handle_info({:maybe_flash_firmware, old_fbos_config}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    unless state.firmware_flash_in_progress do
      _ = maybe_flash_firmware(state, fbos_config.firmware_hardware, old_fbos_config.firmware_hardware)
    end
    {:noreply, state}
  end

  def handle_info({:maybe_start_io_log_timer, old_fbos_config}, %{fbos_config: fbos_config, firmware_io_timer: nil} = state) do
    out_logs = {old_fbos_config.firmware_output_log, fbos_config.firmware_output_log}
    in_logs = {old_fbos_config.firmware_input_log, fbos_config.firmware_input_log}
    # if either of the log types were enabled, start a timer
    recently_enabled? = match?({_, true}, out_logs) || match?({_, true}, in_logs)
    # if both of the log types are disabled, cancel the timer
    recently_disabled? = match?({_, false}, out_logs) && match?({_, false}, in_logs)
    cond do
      recently_enabled? ->
        FarmbotCore.Logger.info 2, "Firmware logs will be disabled after 5 minutes"
        firmware_io_timer = Process.send_after(self(), :disable_firmware_io_logs, @disable_firmware_io_logs_timeout)
        {:noreply, %{state | firmware_io_timer: firmware_io_timer}}
      recently_disabled? ->
        state.firmware_io_timer && Process.cancel_timer(state.firmware_io_timer)
        {:noreply, %{state | firmware_io_timer: nil}}
      true ->
        {:noreply, state}
    end
  end

  # the timer is already started
  def handle_info({:maybe_start_io_log_timer, _}, state) do
    {:noreply, state}
  end

  def handle_info(:disable_firmware_io_logs, state) do
    new_fbos_config = FarmbotCore.Asset.update_fbos_config!(state.fbos_config, %{
      firmware_output_log: false,
      firmware_input_log: false
    })
    _ = FarmbotCore.Asset.Private.mark_dirty!(new_fbos_config)
    FarmbotCore.Logger.info 2, "Automatically disabling firmware IO logs (5 minutes have elapsed)"
    {:noreply, %{state | fbos_config: new_fbos_config, firmware_io_timer: nil}}
  end

  @impl GenServer
  def handle_cast({:new_data, new_fbos_config}, %{fbos_config: %FbosConfig{} = old_fbos_config} = state) do
    _ = set_config_to_state(new_fbos_config, old_fbos_config)
    send self(), {:maybe_flash_firmware, old_fbos_config}
    send self(), {:maybe_start_io_log_timer, old_fbos_config}
    {:noreply, %{state | fbos_config: new_fbos_config}}
  end

  def maybe_flash_firmware(_state, "none", _old_hardware) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    :ok
  end

  def maybe_flash_firmware(_state, nil, _old_hardware) do
    FarmbotCore.Logger.warn 1, "Firmware hardware unset. Not flashing"
    :ok
  end

  def maybe_flash_firmware(_state, new_hardware, old_hardware) do
    force? = Config.get_config_value(:bool, "settings", "firmware_needs_flash")
    cond do
      force? ->
        FarmbotCore.Logger.warn 1, "Firmware hardware forced flash"
        Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      new_hardware != old_hardware ->
        FarmbotCore.Logger.warn 1, "Firmware hardware change from #{package_to_string(old_hardware)} to #{package_to_string(new_hardware)}"
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      true ->
        # Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
        :ok
    end
  end

  def set_config_to_state(new_fbos_config, old_fbos_config) do
    interesting_params = [
      :arduino_debug_messages,
      :firmware_input_log,
      :firmware_output_log,
      :auto_sync,
      :beta_opt_in,
      :disable_factory_reset,
      :network_not_found_timer,
      :os_auto_update,
      :sequence_body_log,
      :sequence_complete_log,
      :sequence_init_log,
      :update_channel
    ]
    new_interesting_fbos_config = Map.take(new_fbos_config, interesting_params) |> MapSet.new()
    old_interesting_fbos_config = Map.take(old_fbos_config, interesting_params) |> MapSet.new()
    difference = MapSet.difference(new_interesting_fbos_config, old_interesting_fbos_config)
    Enum.each(difference, fn
      {:arduino_debug_messages, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino debug messages to #{bool}"

      {:firmware_input_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino input logs to #{bool}"

      {:firmware_output_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino output logs to #{bool}"

      {:auto_sync, bool} ->
        FarmbotCore.Logger.success 1, "Set auto sync to #{bool}"

      {:beta_opt_in, true} ->
        FarmbotCore.Logger.success 1, "Opting into beta updates"

      {:beta_opt_in, false} ->
        FarmbotCore.Logger.success 1, "Opting out of beta updates"

      {:update_channel, channel} ->
        FarmbotCore.Logger.success 1, "Set OS update channel to #{channel}"

      {:os_auto_update, bool} ->
        FarmbotCore.Logger.success 1, "Set OS auto update to #{bool}"

      {:disable_factory_reset, bool} ->
        FarmbotCore.Logger.success 1, "Set automatic factory reset to #{!bool}"

      {:network_not_found_timer, minutes} ->
        FarmbotCore.Logger.success 1, "Set connection attempt period to #{minutes} minutes"

      {:sequence_body_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence step log messages to #{bool}"

      {:sequence_complete_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence complete log messages to #{bool}"

      {:sequence_init_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence init log messages to #{bool}"

      {param, value} ->
        FarmbotCore.Logger.success 1, "Set #{param} to #{value}"
    end)
    set_config_to_state(new_fbos_config)
  end

  def set_config_to_state(fbos_config) do
    # firmware
    :ok = BotState.set_config_value(:arduino_debug_messages, fbos_config.arduino_debug_messages)
    :ok = BotState.set_config_value(:firmware_input_log, fbos_config.firmware_input_log)
    :ok = BotState.set_config_value(:firmware_output_log, fbos_config.firmware_output_log)
    # firmware_hardware is set by FarmbotFirmware.SideEffects

    :ok = BotState.set_config_value(:auto_sync, fbos_config.auto_sync)
    :ok = BotState.set_config_value(:beta_opt_in, fbos_config.beta_opt_in)
    :ok = BotState.set_config_value(:disable_factory_reset, fbos_config.disable_factory_reset)
    :ok = BotState.set_config_value(:network_not_found_timer, fbos_config.network_not_found_timer)
    :ok = BotState.set_config_value(:os_auto_update, fbos_config.os_auto_update)

    # CeleryScript
    :ok = BotState.set_config_value(:sequence_body_log, fbos_config.sequence_body_log)
    :ok = BotState.set_config_value(:sequence_complete_log, fbos_config.sequence_complete_log)
    :ok = BotState.set_config_value(:sequence_init_log, fbos_config.sequence_init_log)
  end

  def fbos_config_to_flash_firmware_rpc(firmware_hardware) do
    AST.Factory.new()
    |> AST.Factory.rpc_request("FbosConfig")
    |> AST.Factory.flash_firmware(firmware_hardware)
  end
end

defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
  @moduledoc """
  This asset worker does not get restarted. It inistead responds to GenServer
  calls.
  """

  use GenServer
  require Logger
  require FarmbotCore.Logger
  alias FarmbotCeleryScript.AST
  alias FarmbotCore.{Asset.FbosConfig, BotState, Config}
  import FarmbotFirmware.PackageUtils, only: [package_to_string: 1]

  @firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold]
  @firmware_flash_timeout Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_timeout] || 5000
  @disable_firmware_io_logs_timeout Application.get_env(:farmbot_core, __MODULE__)[:disable_firmware_io_logs_timeout] || 300000
  @firmware_flash_attempt_threshold || Mix.raise """
  Firmware open attempt threshold not configured:

  config :farmbot_core, #{__MODULE__}, [
    firmware_flash_attempt_threshold: :infinity
  ]
  """

  @impl FarmbotCore.AssetWorker
  def preload(%FbosConfig{}), do: []

  @impl FarmbotCore.AssetWorker
  def tracks_changes?(%FbosConfig{}), do: true

  @impl FarmbotCore.AssetWorker
  def start_link(%FbosConfig{} = fbos_config, _args) do
    GenServer.start_link(__MODULE__, %FbosConfig{} = fbos_config)
  end

  @impl GenServer
  def init(%FbosConfig{} = fbos_config) do
    if Config.get_config_value(:bool, "settings", "firmware_needs_flash") do
      Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    end
    state = %{
      fbos_config: fbos_config,
      firmware_io_timer: nil,
      firmware_flash_attempts: 0,
      firmware_flash_attempt_threshold: @firmware_flash_attempt_threshold,
      firmware_flash_timeout: @firmware_flash_timeout,
      firmware_flash_in_progress: false
    }
    {:ok, state, 0}
  end

  @impl GenServer
  def handle_info({:step_complete, _, :ok}, state) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    {:noreply, %{state | firmware_flash_in_progress: false}}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{firmware_flash_attempts: tries, firmware_flash_attempt_threshold: thresh} = state)
  when tries >= thresh do
    FarmbotCore.Logger.error 1, """
    Failed flashing firmware: #{reason}
    Tried #{tries} times. Not retrying
    """
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    {:noreply, %{state | firmware_flash_attempts: 0, firmware_flash_in_progress: false}}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", true)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)

    firmware_flash_timeout = state.firmware_flash_timeout
    firmware_flash_attempts = state.firmware_flash_attempts

    new_state = %{
      state |
      firmware_flash_attempts: firmware_flash_attempts + 1,
      firmware_flash_timeout: @firmware_flash_timeout * (firmware_flash_attempts + 1),
      firmware_flash_in_progress: false
    }
    FarmbotCore.Logger.error 1, """
    Error flashing firmware: #{reason}
    Trying again in #{state.firmware_flash_timeout / 1000} seconds
    """
    Process.send_after(self(), {:maybe_flash_firmware, fbos_config}, firmware_flash_timeout)
    {:noreply, new_state}
  end

  def handle_info(:timeout, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    FarmbotCore.Logger.debug 3, "Got initial fbos config"
    set_config_to_state(fbos_config)
    send self(), {:maybe_flash_firmware, fbos_config}
    send self(), {:maybe_start_io_log_timer, fbos_config}
    {:noreply, state}
  end

  def handle_info({:maybe_flash_firmware, old_fbos_config}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    unless state.firmware_flash_in_progress do
      _ = maybe_flash_firmware(state, fbos_config.firmware_hardware, old_fbos_config.firmware_hardware)
    end
    {:noreply, state}
  end

  def handle_info({:maybe_start_io_log_timer, old_fbos_config}, %{fbos_config: fbos_config, firmware_io_timer: nil} = state) do
    out_logs = {old_fbos_config.firmware_output_log, fbos_config.firmware_output_log}
    in_logs = {old_fbos_config.firmware_input_log, fbos_config.firmware_input_log}
    # if either of the log types were enabled, start a timer
    recently_enabled? = match?({_, true}, out_logs) || match?({_, true}, in_logs)
    # if both of the log types are disabled, cancel the timer
    recently_disabled? = match?({_, false}, out_logs) && match?({_, false}, in_logs)
    cond do
      recently_enabled? ->
        FarmbotCore.Logger.debug 3, "Firmware logs will be disabled after 5 minutes"
        firmware_io_timer = Process.send_after(self(), :disable_firmware_io_logs, @disable_firmware_io_logs_timeout)
        {:noreply, %{state | firmware_io_timer: firmware_io_timer}}
      recently_disabled? ->
        state.firmware_io_timer && Process.cancel_timer(state.firmware_io_timer)
        {:noreply, %{state | firmware_io_timer: nil}}
      true ->
        {:noreply, state}
    end
  end

  # the timer is already started
  def handle_info({:maybe_start_io_log_timer, _}, state) do
    {:noreply, state}
  end

  def handle_info(:disable_firmware_io_logs, state) do
    new_fbos_config = FarmbotCore.Asset.update_fbos_config!(state.fbos_config, %{
      firmware_output_log: false,
      firmware_input_log: false
    })
    _ = FarmbotCore.Asset.Private.mark_dirty!(new_fbos_config)
    {:noreply, %{state | fbos_config: new_fbos_config, firmware_io_timer: nil}}
  end

  @impl GenServer
  def handle_cast({:new_data, new_fbos_config}, %{fbos_config: %FbosConfig{} = old_fbos_config} = state) do
    _ = set_config_to_state(new_fbos_config, old_fbos_config)
    send self(), {:maybe_flash_firmware, old_fbos_config}
    send self(), {:maybe_start_io_log_timer, old_fbos_config}
    {:noreply, %{state | fbos_config: new_fbos_config}}
  end

  def maybe_flash_firmware(_state, "none", _old_hardware) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    :ok
  end

  def maybe_flash_firmware(_state, nil, _old_hardware) do
    FarmbotCore.Logger.warn 1, "Firmware hardware unset. Not flashing"
    :ok
  end

  def maybe_flash_firmware(_state, new_hardware, old_hardware) do
    force? = Config.get_config_value(:bool, "settings", "firmware_needs_flash")
    cond do
      force? ->
        FarmbotCore.Logger.warn 1, "Firmware hardware forced flash"
        Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      new_hardware != old_hardware ->
        FarmbotCore.Logger.warn 1, "Firmware hardware change from #{package_to_string(old_hardware)} to #{package_to_string(new_hardware)}"
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      true ->
        # Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
        :ok
    end
  end

  def set_config_to_state(new_fbos_config, old_fbos_config) do
    interesting_params = [
      :arduino_debug_messages,
      :firmware_input_log,
      :firmware_output_log,
      :auto_sync,
      :beta_opt_in,
      :disable_factory_reset,
      :network_not_found_timer,
      :os_auto_update,
      :sequence_body_log,
      :sequence_complete_log,
      :sequence_init_log,
      :update_channel
    ]
    new_interesting_fbos_config = Map.take(new_fbos_config, interesting_params) |> MapSet.new()
    old_interesting_fbos_config = Map.take(old_fbos_config, interesting_params) |> MapSet.new()
    difference = MapSet.difference(new_interesting_fbos_config, old_interesting_fbos_config)
    Enum.each(difference, fn
      {:arduino_debug_messages, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino debug messages to #{bool}"

      {:firmware_input_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino input logs to #{bool}"

      {:firmware_output_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino output logs to #{bool}"

      {:auto_sync, bool} ->
        FarmbotCore.Logger.success 1, "Set auto sync to #{bool}"

      {:beta_opt_in, true} ->
        FarmbotCore.Logger.success 1, "Opting into beta updates"

      {:beta_opt_in, false} ->
        FarmbotCore.Logger.success 1, "Opting out of beta updates"

      {:update_channel, channel} ->
        FarmbotCore.Logger.success 1, "Set OS update channel to #{channel}"

      {:os_auto_update, bool} ->
        FarmbotCore.Logger.success 1, "Set OS auto update to #{bool}"

      {:disable_factory_reset, bool} ->
        FarmbotCore.Logger.success 1, "Set automatic factory reset to #{!bool}"

      {:network_not_found_timer, minutes} ->
        FarmbotCore.Logger.success 1, "Set connection attempt period to #{minutes} minutes"

      {:sequence_body_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence step log messages to #{bool}"

      {:sequence_complete_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence complete log messages to #{bool}"

      {:sequence_init_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence init log messages to #{bool}"

      {param, value} ->
        FarmbotCore.Logger.success 1, "Set #{param} to #{value}"
    end)
    set_config_to_state(new_fbos_config)
  end

  def set_config_to_state(fbos_config) do
    # firmware
    :ok = BotState.set_config_value(:arduino_debug_messages, fbos_config.arduino_debug_messages)
    :ok = BotState.set_config_value(:firmware_input_log, fbos_config.firmware_input_log)
    :ok = BotState.set_config_value(:firmware_output_log, fbos_config.firmware_output_log)
    # firmware_hardware is set by FarmbotFirmware.SideEffects

    :ok = BotState.set_config_value(:auto_sync, fbos_config.auto_sync)
    :ok = BotState.set_config_value(:beta_opt_in, fbos_config.beta_opt_in)
    :ok = BotState.set_config_value(:disable_factory_reset, fbos_config.disable_factory_reset)
    :ok = BotState.set_config_value(:network_not_found_timer, fbos_config.network_not_found_timer)
    :ok = BotState.set_config_value(:os_auto_update, fbos_config.os_auto_update)

    # CeleryScript
    :ok = BotState.set_config_value(:sequence_body_log, fbos_config.sequence_body_log)
    :ok = BotState.set_config_value(:sequence_complete_log, fbos_config.sequence_complete_log)
    :ok = BotState.set_config_value(:sequence_init_log, fbos_config.sequence_init_log)
  end

  def fbos_config_to_flash_firmware_rpc(firmware_hardware) do
    AST.Factory.new()
    |> AST.Factory.rpc_request("FbosConfig")
    |> AST.Factory.flash_firmware(firmware_hardware)
  end
end


Update logs in fbos_config_worker again

Update logs in fbos_config_worker again

* Add log for update channel
* Update log for auto factory reset to match frontend

defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
  @moduledoc """
  This asset worker does not get restarted. It inistead responds to GenServer
  calls.
  """

  use GenServer
  require Logger
  require FarmbotCore.Logger
  alias FarmbotCeleryScript.AST
  alias FarmbotCore.{Asset.FbosConfig, BotState, Config}
  import FarmbotFirmware.PackageUtils, only: [package_to_string: 1]

  @firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold]
  @firmware_flash_attempt_threshold || Mix.raise """
  Firmware open attempt threshold not configured:

  config :farmbot_core, #{__MODULE__}, [
    firmware_flash_attempt_threshold: :infinity
  ]
  """

  @impl FarmbotCore.AssetWorker
  def preload(%FbosConfig{}), do: []

  @impl FarmbotCore.AssetWorker
  def tracks_changes?(%FbosConfig{}), do: true

  @impl FarmbotCore.AssetWorker
  def start_link(%FbosConfig{} = fbos_config, _args) do
    GenServer.start_link(__MODULE__, %FbosConfig{} = fbos_config)
  end

  @impl GenServer
  def init(%FbosConfig{} = fbos_config) do
    if Config.get_config_value(:bool, "settings", "firmware_needs_flash") do
      Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    end
    state = %{
      fbos_config: fbos_config,
      firmware_flash_attempts: 0,
      firmware_flash_attempt_threshold: @firmware_flash_attempt_threshold
    }
    {:ok, state, 0}
  end

  @impl GenServer
  def handle_info({:step_complete, _, :ok}, state) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    {:noreply, state}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{firmware_flash_attempts: tries, firmware_flash_attempt_threshold: thresh} = state)
  when tries >= thresh do
    FarmbotCore.Logger.error 1, """
    Failed flashing firmware: #{reason}
    Tried #{tries} times. Not retrying
    """
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    {:noreply, %{state | firmware_flash_attempts: 0}}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    FarmbotCore.Logger.error 1, """
    Error flashing firmware: #{reason}
    Trying again in 5 seconds
    """
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", true)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    Process.sleep(5000)
    _ = maybe_flash_firmware(state, fbos_config.firmware_hardware, fbos_config.firmware_hardware)
    {:noreply, %{state | firmware_flash_attempts: state.firmware_flash_attempts + 1}}
  end

  def handle_info(:timeout, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    FarmbotCore.Logger.debug 3, "Got initial fbos config"
    set_config_to_state(fbos_config)
    _ = maybe_flash_firmware(state, fbos_config.firmware_hardware, fbos_config.firmware_hardware)
    {:noreply, state}
  end

  @impl GenServer
  def handle_cast({:new_data, new_fbos_config}, %{fbos_config: %FbosConfig{} = old_fbos_config} = state) do
    _ = set_config_to_state(new_fbos_config, old_fbos_config)
    _ = maybe_flash_firmware(state, new_fbos_config.firmware_hardware, old_fbos_config.firmware_hardware)
    {:noreply, %{state | fbos_config: new_fbos_config}}
  end

  def maybe_flash_firmware(_state, "none", _old_hardware) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    :ok
  end

  def maybe_flash_firmware(_state, nil, _old_hardware) do
    FarmbotCore.Logger.warn 1, "Firmware hardware unset. Not flashing"
    :ok
  end

  def maybe_flash_firmware(_state, new_hardware, old_hardware) do
    force? = Config.get_config_value(:bool, "settings", "firmware_needs_flash")
    cond do
      force? ->
        FarmbotCore.Logger.warn 1, "Firmware hardware forced flash"
        Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      new_hardware != old_hardware ->
        FarmbotCore.Logger.warn 1, "Firmware hardware change from #{package_to_string(old_hardware)} to #{package_to_string(new_hardware)}"
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      true ->
        # Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
        :ok
    end
  end

  def set_config_to_state(new_fbos_config, old_fbos_config) do
    interesting_params = [
      :arduino_debug_messages,
      :firmware_input_log,
      :firmware_output_log,
      :auto_sync,
      :beta_opt_in,
      :disable_factory_reset,
      :network_not_found_timer,
      :os_auto_update,
      :sequence_body_log,
      :sequence_complete_log,
      :sequence_init_log,
      :update_channel
    ]
    new_interesting_fbos_config = Map.take(new_fbos_config, interesting_params) |> MapSet.new()
    old_interesting_fbos_config = Map.take(old_fbos_config, interesting_params) |> MapSet.new()
    difference = MapSet.difference(new_interesting_fbos_config, old_interesting_fbos_config)
    Enum.each(difference, fn
      {:arduino_debug_messages, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino debug messages to #{bool}"

      {:firmware_input_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino input logs to #{bool}"

      {:firmware_output_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino output logs to #{bool}"

      {:auto_sync, bool} ->
        FarmbotCore.Logger.success 1, "Set auto sync to #{bool}"

      {:beta_opt_in, true} ->
        FarmbotCore.Logger.success 1, "Opting into beta updates"

      {:beta_opt_in, false} ->
        FarmbotCore.Logger.success 1, "Opting out of beta updates"

      {:update_channel, channel} ->
        FarmbotCore.Logger.success 1, "Set OS update channel to #{channel}"

      {:os_auto_update, bool} ->
        FarmbotCore.Logger.success 1, "Set OS auto update to #{bool}"

      {:disable_factory_reset, bool} ->
        FarmbotCore.Logger.success 1, "Set automatic factory reset to #{!bool}"

      {:network_not_found_timer, minutes} ->
        FarmbotCore.Logger.success 1, "Set connection attempt period to #{minutes} minutes"

      {:sequence_body_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence step log messages to #{bool}"

      {:sequence_complete_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence complete log messages to #{bool}"

      {:sequence_init_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence init log messages to #{bool}"

      {param, value} ->
        FarmbotCore.Logger.success 1, "Set #{param} to #{value}"
    end)
    set_config_to_state(new_fbos_config)
  end

  def set_config_to_state(fbos_config) do
    # firmware
    :ok = BotState.set_config_value(:arduino_debug_messages, fbos_config.arduino_debug_messages)
    :ok = BotState.set_config_value(:firmware_input_log, fbos_config.firmware_input_log)
    :ok = BotState.set_config_value(:firmware_output_log, fbos_config.firmware_output_log)
    # firmware_hardware is set by FarmbotFirmware.SideEffects

    :ok = BotState.set_config_value(:auto_sync, fbos_config.auto_sync)
    :ok = BotState.set_config_value(:beta_opt_in, fbos_config.beta_opt_in)
    :ok = BotState.set_config_value(:disable_factory_reset, fbos_config.disable_factory_reset)
    :ok = BotState.set_config_value(:network_not_found_timer, fbos_config.network_not_found_timer)
    :ok = BotState.set_config_value(:os_auto_update, fbos_config.os_auto_update)

    # CeleryScript
    :ok = BotState.set_config_value(:sequence_body_log, fbos_config.sequence_body_log)
    :ok = BotState.set_config_value(:sequence_complete_log, fbos_config.sequence_complete_log)
    :ok = BotState.set_config_value(:sequence_init_log, fbos_config.sequence_init_log)
  end

  def fbos_config_to_flash_firmware_rpc(firmware_hardware) do
    AST.Factory.new()
    |> AST.Factory.rpc_request("FbosConfig")
    |> AST.Factory.flash_firmware(firmware_hardware)
  end
end

defimpl FarmbotCore.AssetWorker, for: FarmbotCore.Asset.FbosConfig do
  @moduledoc """
  This asset worker does not get restarted. It inistead responds to GenServer
  calls.
  """

  use GenServer
  require Logger
  require FarmbotCore.Logger
  alias FarmbotCeleryScript.AST
  alias FarmbotCore.{Asset.FbosConfig, BotState, Config}
  import FarmbotFirmware.PackageUtils, only: [package_to_string: 1]

  @firmware_flash_attempt_threshold Application.get_env(:farmbot_core, __MODULE__)[:firmware_flash_attempt_threshold]
  @firmware_flash_attempt_threshold || Mix.raise """
  Firmware open attempt threshold not configured:

  config :farmbot_core, #{__MODULE__}, [
    firmware_flash_attempt_threshold: :infinity
  ]
  """

  @impl FarmbotCore.AssetWorker
  def preload(%FbosConfig{}), do: []

  @impl FarmbotCore.AssetWorker
  def tracks_changes?(%FbosConfig{}), do: true

  @impl FarmbotCore.AssetWorker
  def start_link(%FbosConfig{} = fbos_config, _args) do
    GenServer.start_link(__MODULE__, %FbosConfig{} = fbos_config)
  end

  @impl GenServer
  def init(%FbosConfig{} = fbos_config) do
    if Config.get_config_value(:bool, "settings", "firmware_needs_flash") do
      Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    end
    state = %{
      fbos_config: fbos_config,
      firmware_flash_attempts: 0,
      firmware_flash_attempt_threshold: @firmware_flash_attempt_threshold
    }
    {:ok, state, 0}
  end

  @impl GenServer
  def handle_info({:step_complete, _, :ok}, state) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    {:noreply, state}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{firmware_flash_attempts: tries, firmware_flash_attempt_threshold: thresh} = state)
  when tries >= thresh do
    FarmbotCore.Logger.error 1, """
    Failed flashing firmware: #{reason}
    Tried #{tries} times. Not retrying
    """
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    {:noreply, %{state | firmware_flash_attempts: 0}}
  end

  def handle_info({:step_complete, _, {:error, reason}}, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    FarmbotCore.Logger.error 1, """
    Error flashing firmware: #{reason}
    Trying again in 5 seconds
    """
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", true)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
    Process.sleep(5000)
    _ = maybe_flash_firmware(state, fbos_config.firmware_hardware, fbos_config.firmware_hardware)
    {:noreply, %{state | firmware_flash_attempts: state.firmware_flash_attempts + 1}}
  end

  def handle_info(:timeout, %{fbos_config: %FbosConfig{} = fbos_config} = state) do
    FarmbotCore.Logger.debug 3, "Got initial fbos config"
    set_config_to_state(fbos_config)
    _ = maybe_flash_firmware(state, fbos_config.firmware_hardware, fbos_config.firmware_hardware)
    {:noreply, state}
  end

  @impl GenServer
  def handle_cast({:new_data, new_fbos_config}, %{fbos_config: %FbosConfig{} = old_fbos_config} = state) do
    _ = set_config_to_state(new_fbos_config, old_fbos_config)
    _ = maybe_flash_firmware(state, new_fbos_config.firmware_hardware, old_fbos_config.firmware_hardware)
    {:noreply, %{state | fbos_config: new_fbos_config}}
  end

  def maybe_flash_firmware(_state, "none", _old_hardware) do
    Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
    Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
    :ok
  end

  def maybe_flash_firmware(_state, nil, _old_hardware) do
    FarmbotCore.Logger.warn 1, "Firmware hardware unset. Not flashing"
    :ok
  end

  def maybe_flash_firmware(_state, new_hardware, old_hardware) do
    force? = Config.get_config_value(:bool, "settings", "firmware_needs_flash")
    cond do
      force? ->
        FarmbotCore.Logger.warn 1, "Firmware hardware forced flash"
        Config.update_config_value(:bool, "settings", "firmware_needs_flash", false)
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      new_hardware != old_hardware ->
        FarmbotCore.Logger.warn 1, "Firmware hardware change from #{package_to_string(old_hardware)} to #{package_to_string(new_hardware)}"
        new_hardware
        |> fbos_config_to_flash_firmware_rpc()
        |> FarmbotCeleryScript.execute(make_ref())

      true ->
        # Config.update_config_value(:bool, "settings", "firmware_needs_open", true)
        :ok
    end
  end

  def set_config_to_state(new_fbos_config, old_fbos_config) do
    interesting_params = [
      :arduino_debug_messages,
      :firmware_input_log,
      :firmware_output_log,
      :auto_sync,
      :beta_opt_in,
      :disable_factory_reset,
      :network_not_found_timer,
      :os_auto_update,
      :sequence_body_log,
      :sequence_complete_log,
      :sequence_init_log,
    ]
    new_interesting_fbos_config = Map.take(new_fbos_config, interesting_params) |> MapSet.new()
    old_interesting_fbos_config = Map.take(old_fbos_config, interesting_params) |> MapSet.new()
    difference = MapSet.difference(new_interesting_fbos_config, old_interesting_fbos_config)
    Enum.each(difference, fn
      {:arduino_debug_messages, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino debug messages to #{bool}"

      {:firmware_input_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino input logs to #{bool}"

      {:firmware_output_log, bool} ->
        FarmbotCore.Logger.success 1, "Set arduino output logs to #{bool}"

      {:auto_sync, bool} ->
        FarmbotCore.Logger.success 1, "Set auto sync to #{bool}"

      {:beta_opt_in, true} ->
        FarmbotCore.Logger.success 1, "Opting into beta updates"

      {:beta_opt_in, false} ->
        FarmbotCore.Logger.success 1, "Opting out of beta updates"

      {:os_auto_update, bool} ->
        FarmbotCore.Logger.success 1, "Set OS auto update to #{bool}"

      {:disable_factory_reset, bool} ->
        FarmbotCore.Logger.success 1, "Set disable factory reset to #{bool}"

      {:network_not_found_timer, seconds} ->
        FarmbotCore.Logger.success 1, "Set connection attempt period to #{seconds}"

      {:sequence_body_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence step log messages to #{bool}"

      {:sequence_complete_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence complete log messages to #{bool}"

      {:sequence_init_log, bool} ->
        FarmbotCore.Logger.success 1, "Set sequence init log messages to #{bool}"

      {param, value} ->
        FarmbotCore.Logger.success 1, "Set #{param} to #{value}"
    end)
    set_config_to_state(new_fbos_config)
  end

  def set_config_to_state(fbos_config) do
    # firmware
    :ok = BotState.set_config_value(:arduino_debug_messages, fbos_config.arduino_debug_messages)
    :ok = BotState.set_config_value(:firmware_input_log, fbos_config.firmware_input_log)
    :ok = BotState.set_config_value(:firmware_output_log, fbos_config.firmware_output_log)
    # firmware_hardware is set by FarmbotFirmware.SideEffects

    :ok = BotState.set_config_value(:auto_sync, fbos_config.auto_sync)
    :ok = BotState.set_config_value(:beta_opt_in, fbos_config.beta_opt_in)
    :ok = BotState.set_config_value(:disable_factory_reset, fbos_config.disable_factory_reset)
    :ok = BotState.set_config_value(:network_not_found_timer, fbos_config.network_not_found_timer)
    :ok = BotState.set_config_value(:os_auto_update, fbos_config.os_auto_update)

    # CeleryScript
    :ok = BotState.set_config_value(:sequence_body_log, fbos_config.sequence_body_log)
    :ok = BotState.set_config_value(:sequence_complete_log, fbos_config.sequence_complete_log)
    :ok = BotState.set_config_value(:sequence_init_log, fbos_config.sequence_init_log)
  end

  def fbos_config_to_flash_firmware_rpc(firmware_hardware) do
    AST.Factory.new()
    |> AST.Factory.rpc_request("FbosConfig")
    |> AST.Factory.flash_firmware(firmware_hardware)
  end
end


0x bug

0x bug

defmodule Neoscan.Blocks do
  @moduledoc false

  @moduledoc """
  The boundary for the Blocks system.
  """

  import Ecto.Query, warn: true
  alias Neoscan.Repo
  alias Neoscan.Blocks.Block
  alias Neoscan.Transactions.Transaction
  alias Neoscan.Addresses
  alias NeoscanMonitor.Api

  @doc """
  Returns the list of blocks.

  ## Examples

      iex> list_blocks()
      [%Block{}, ...]

  """
  def list_blocks do
    Repo.all(Block)
  end

  @doc """
  Returns the list of blocks in the home page.

  ## Examples

      iex> home_blocks()
      [%Block{}, ...]

  """
  def home_blocks do
    block_query = from e in Block,
      where: e.index > 1200000,
      order_by: [desc: e.index],
      select: %{:index => e.index, :time => e.time, :tx_count => e.tx_count, :hash => e.hash},
      limit: 15

    Repo.all(block_query)
  end

  @doc """
  Gets a single block.

  Raises `Ecto.NoResultsError` if the Block does not exist.

  ## Examples

      iex> get_block!(123)
      %Block{}

      iex> get_block!(456)
      ** (Ecto.NoResultsError)

  """
  def get_block!(id), do: Repo.get!(Block, id)

  @doc """
  Gets a single block by its hash value

  ## Examples

      iex> get_block_by_hash(123)
      %Block{}

      iex> get_block_by_hash(456)
      nil

  """
  def get_block_by_hash(hash) do
   query = from e in Block,
     where: e.hash == ^hash,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its hash value for blocks page

  ## Examples

      iex> get_block_by_hash_for_view(hash)
      %Block{}

      iex> get_block_by_hash_for_view(hash)
      nil

  """
  def get_block_by_hash_for_view(hash) do
    trans_query = from t in Transaction,
      select: %{
        type: t.type,
        txid: t.txid
      }
   query = from e in Block,
     where: e.hash == ^hash,
     preload: [transactions: ^trans_query],
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its heigh value

  ## Examples

      iex> get_block_by_height(123)
      %Block{}

      iex> get_block_by_height(456)
      nill

  """
  def get_block_by_height(height) do
   query = from e in Block,
     where: e.index == ^height,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Creates a block.

  ## Examples

      iex> create_block(%{field: value})
      {:ok, %Block{}}

      iex> create_block(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_block(attrs \\ %{}) do
    %Block{}
    |> Block.changeset(attrs)
    |> Repo.insert!()
    |> update_blocks_state
  end
  def update_blocks_state(block) do
    Api.add_block(block)
    block
  end

  @doc """
  Updates a block.

  ## Examples

      iex> update_block(block, %{field: new_value})
      {:ok, %Block{}}

      iex> update_block(block, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_block(%Block{} = block, attrs) do
    block
    |> Block.changeset(attrs)
    |> Repo.update!()
  end

  @doc """
  Deletes a Block.

  ## Examples

      iex> delete_block(block)
      {:ok, %Block{}}

      iex> delete_block(block)
      {:error, %Ecto.Changeset{}}

  """
  def delete_block(%Block{:updated_at => time} = block) do
    Addresses.rollback_addresses(time)
    Repo.delete!(block)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking block changes.

  ## Examples

      iex> change_block(block)
      %Ecto.Changeset{source: %Block{}}

  """
  def change_block(%Block{} = block) do
    Block.changeset(block, %{})
  end

  @doc """
  Returns the heighest block in the database

  ## Examples

      iex> get_highest_block_in_db()
      {:ok, %Block{}}

  """
  def get_highest_block_in_db() do
    query = from e in Block,
      select: e.index,
      where: e.index > 1200000,  #force postgres to use index
      order_by: [desc: e.index],
      limit: 1

    case Repo.all(query) do
      [index] ->
        {:ok , index}
      [] ->
        {:ok , -1 }
    end
  end

  @doc """
  get all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def get_higher_than(index) do
    query = from e in Block,
      where: e.index > ^index,
      select: e
    Repo.all(query)
  end

  @doc """
  delete all blocks in list

  ## Examples

      iex> delete_blocks([%Block{}, ...])
      { :ok, "deleted"}

  """
  def delete_blocks([ block | tail ]), do: [ delete_block(block) | delete_blocks(tail)]
  def delete_blocks([]), do: {:ok , "Deleted" }

  @doc """
  delete all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def delete_higher_than(height) do
    get_higher_than(height)
    |> delete_blocks
  end

  def check_if_transaction_blocks_are_missing(transactions) do
    Enum.map(transactions, fn {:ok, %{"blockhash" => block_hash} = transaction } -> check_if_block_exists(String.slice(block_hash, -64..-1), transaction) end)
  end

  def check_if_block_exists(hash, transaction) do
    query = from e in Block,
      where: e.hash == ^hash,
      select: e
    case Repo.all(query) |> List.first() do
      nil ->
        {:block_missing , transaction}
      block ->
        {:transaction_missing, {block, transaction}}
    end
  end

  def get_missing_blocks(transaction_tuples) do
    Enum.filter(transaction_tuples, fn { key, _transaction} -> key == :block_missing end)
    |> Enum.group_by( fn { _key, transaction} -> transaction["blockhash"] end)
    |> Map.keys
    |> Enum.each(fn hash -> get_missing_block(hash) end)
  end

  def get_missing_block(hash) do
    {:ok, block} = NeoscanSync.Blockchain.get_block_by_hash(NeoscanSync.HttpCalls.url(1), hash)
    NeoscanSync.Consumer.add_block(block)
  end

end

defmodule Neoscan.Blocks do
  @moduledoc false

  @moduledoc """
  The boundary for the Blocks system.
  """

  import Ecto.Query, warn: true
  alias Neoscan.Repo
  alias Neoscan.Blocks.Block
  alias Neoscan.Transactions.Transaction
  alias Neoscan.Addresses
  alias NeoscanMonitor.Api

  @doc """
  Returns the list of blocks.

  ## Examples

      iex> list_blocks()
      [%Block{}, ...]

  """
  def list_blocks do
    Repo.all(Block)
  end

  @doc """
  Returns the list of blocks in the home page.

  ## Examples

      iex> home_blocks()
      [%Block{}, ...]

  """
  def home_blocks do
    block_query = from e in Block,
      where: e.index > 1200000,
      order_by: [desc: e.index],
      select: %{:index => e.index, :time => e.time, :tx_count => e.tx_count, :hash => e.hash},
      limit: 15

    Repo.all(block_query)
  end

  @doc """
  Gets a single block.

  Raises `Ecto.NoResultsError` if the Block does not exist.

  ## Examples

      iex> get_block!(123)
      %Block{}

      iex> get_block!(456)
      ** (Ecto.NoResultsError)

  """
  def get_block!(id), do: Repo.get!(Block, id)

  @doc """
  Gets a single block by its hash value

  ## Examples

      iex> get_block_by_hash(123)
      %Block{}

      iex> get_block_by_hash(456)
      nil

  """
  def get_block_by_hash(hash) do
   query = from e in Block,
     where: e.hash == ^hash,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its hash value for blocks page

  ## Examples

      iex> get_block_by_hash_for_view(hash)
      %Block{}

      iex> get_block_by_hash_for_view(hash)
      nil

  """
  def get_block_by_hash_for_view(hash) do
    trans_query = from t in Transaction,
      select: %{
        type: t.type,
        txid: t.txid
      }
   query = from e in Block,
     where: e.hash == ^hash,
     preload: [transactions: ^trans_query],
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its heigh value

  ## Examples

      iex> get_block_by_height(123)
      %Block{}

      iex> get_block_by_height(456)
      nill

  """
  def get_block_by_height(height) do
   query = from e in Block,
     where: e.index == ^height,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Creates a block.

  ## Examples

      iex> create_block(%{field: value})
      {:ok, %Block{}}

      iex> create_block(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_block(attrs \\ %{}) do
    %Block{}
    |> Block.changeset(attrs)
    |> Repo.insert!()
    |> update_blocks_state
  end
  def update_blocks_state(block) do
    Api.add_block(block)
    block
  end

  @doc """
  Updates a block.

  ## Examples

      iex> update_block(block, %{field: new_value})
      {:ok, %Block{}}

      iex> update_block(block, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_block(%Block{} = block, attrs) do
    block
    |> Block.changeset(attrs)
    |> Repo.update!()
  end

  @doc """
  Deletes a Block.

  ## Examples

      iex> delete_block(block)
      {:ok, %Block{}}

      iex> delete_block(block)
      {:error, %Ecto.Changeset{}}

  """
  def delete_block(%Block{:updated_at => time} = block) do
    Addresses.rollback_addresses(time)
    Repo.delete!(block)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking block changes.

  ## Examples

      iex> change_block(block)
      %Ecto.Changeset{source: %Block{}}

  """
  def change_block(%Block{} = block) do
    Block.changeset(block, %{})
  end

  @doc """
  Returns the heighest block in the database

  ## Examples

      iex> get_highest_block_in_db()
      {:ok, %Block{}}

  """
  def get_highest_block_in_db() do
    query = from e in Block,
      select: e.index,
      where: e.index > 1200000,  #force postgres to use index
      order_by: [desc: e.index],
      limit: 1

    case Repo.all(query) do
      [index] ->
        {:ok , index}
      [] ->
        {:ok , -1 }
    end
  end

  @doc """
  get all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def get_higher_than(index) do
    query = from e in Block,
      where: e.index > ^index,
      select: e
    Repo.all(query)
  end

  @doc """
  delete all blocks in list

  ## Examples

      iex> delete_blocks([%Block{}, ...])
      { :ok, "deleted"}

  """
  def delete_blocks([ block | tail ]), do: [ delete_block(block) | delete_blocks(tail)]
  def delete_blocks([]), do: {:ok , "Deleted" }

  @doc """
  delete all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def delete_higher_than(height) do
    get_higher_than(height)
    |> delete_blocks
  end

  def check_if_transaction_blocks_are_missing(transactions) do
    Enum.map(transactions, fn {:ok, %{"blockhash" => block_hash} = transaction } -> check_if_block_exists(block_hash, transaction) end)
  end

  def check_if_block_exists(hash, transaction) do
    query = from e in Block,
      where: e.hash == ^hash,
      select: e
    case Repo.all(query) |> List.first() do
      nil ->
        {:block_missing , transaction}
      block ->
        {:transaction_missing, {block, transaction}}
    end
  end

  def get_missing_blocks(transaction_tuples) do
    Enum.filter(transaction_tuples, fn { key, _transaction} -> key == :block_missing end)
    |> Enum.group_by( fn { _key, transaction} -> transaction["blockhash"] end)
    |> Map.keys
    |> Enum.each(fn hash -> get_missing_block(hash) end)
  end

  def get_missing_block(hash) do
    {:ok, block} = NeoscanSync.Blockchain.get_block_by_hash(NeoscanSync.HttpCalls.url(1), hash)
    NeoscanSync.Consumer.add_block(block)
  end

end


Add warning about :math.pow to elixir grains

Code.load_file("grains.exs")
ExUnit.start

# NOTE: :math.pow/2 doesn't do what you'd expect:
# `:math.pow(2, 64) == :math.pow(2, 64) - 1` is true.
#
# It's best to avoid functions operating on floating point numbers for very
# large numbers.

defmodule GrainsTest do
  use ExUnit.Case

  test "square 1" do
    assert 1 == Grains.square(1)
  end

  test "square 2" do
    assert 2 == Grains.square(2)
  end

  test "square 3" do
    assert 4 == Grains.square(3)
  end

  test "square 4" do
    assert 8 == Grains.square(4)
  end

  test "square 16" do
    assert 32768 == Grains.square(16)
  end

  test "square 32" do
    assert 2147483648 == Grains.square(32)
  end

  test "square 64" do
    assert 9223372036854775808 == Grains.square(64)
  end

  test "total grains" do
    assert 18446744073709551615 == Grains.total
  end
end

Code.load_file("grains.exs")
ExUnit.start

defmodule GrainsTest do
  use ExUnit.Case

  test "square 1" do
    assert 1 == Grains.square(1)
  end

  test "square 2" do
    assert 2 == Grains.square(2)
  end

  test "square 3" do
    assert 4 == Grains.square(3)
  end

  test "square 4" do
    assert 8 == Grains.square(4)
  end

  test "square 16" do
    assert 32768 == Grains.square(16)
  end

  test "square 32" do
    assert 2147483648 == Grains.square(32)
  end

  test "square 64" do
    assert 9223372036854775808 == Grains.square(64)
  end

  test "total grains" do
    assert 18446744073709551615 == Grains.total
  end
end


fix transfer page bugs

fix transfer page bugs

defmodule Neoscan.Transfers do
  @moduledoc false
  import Ecto.Query, warn: false
  alias Neoscan.Repo
  alias Neoscan.Transfers.Transfer
  alias Neoscan.Addresses
  alias Neoscan.Stats
  alias NeoscanMonitor.Api

  require Logger

  @doc """
  Returns the list of transfers in the home page.

  ## Examples

      iex> home_transfers()
      [%Transfer{}, ...]

  """
  def home_transfers do
    transfer_query =
      from(
        transfer in Transfer,
        order_by: [
          desc: transfer.id
        ],
        select: %{
          :id => transfer.id,
          :address_from => transfer.address_from,
          :address_to => transfer.address_to,
          :amount => transfer.amount,
          :block_height => transfer.block_height,
          :txid => transfer.txid,
          :contract => transfer.contract,
          :time => transfer.time
        },
        limit: 15
      )

    Repo.all(transfer_query)
  end

  @doc """
  Returns the list of paginated transfers.

  ## Examples

      iex> paginate_transfers(page)
      [%Transfer{}, ...]

  """
  def paginate_transfers(pag) do
    transfer_query =
      from(
        transfer in Transfer,
        order_by: [
          desc: transfer.id
        ],
        select: %{
          :id => transfer.id,
          :address_from => transfer.address_from,
          :address_to => transfer.address_to,
          :amount => transfer.amount,
          :block_height => transfer.block_height,
          :txid => transfer.txid,
          :contract => transfer.contract,
          :time => transfer.time
        },
        limit: 15
      )

    Repo.paginate(transfer_query, page: pag, page_size: 15)
  end

  @doc """
  Creates a transfer

  ## Examples

      iex> create_transfer(%{field: value})
      {:ok, %Transfer{}}

      iex> create_transfer(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_transfer(transfer, time, block) do
    attrs =
      transfer
      |> Map.merge(%{
        "txid" => transfer["tx"],
        "block_height" => transfer["block"],
        "address_from" => transfer["addr_from"],
        "address_to" => transfer["addr_to"],
        "time" => time,
        "contract" => String.slice(to_string(transfer["contract"]), -40..-1),
      })

    Transfer.changeset(block, attrs)
    |> Repo.insert!()
    |> update_transfer_state
  end

  def update_transfer_state(transfer) do
    Api.add_transfer(transfer)
    Stats.add_transfer_to_table(transfer)
    transfer
  end

  @doc """
  Count total transfers in DB.

  ## Examples

      iex> count_transfers()
      50

  """
  def count_transfers do
    Repo.aggregate(Transfer, :count, :id)
  end

  def add_block_transfers({_block, []}, _time) do
    {:ok, "all operations were succesfull"}
  end

  def add_block_transfers({block, transfers}, time) do
    get_transfers_addresses(transfers, time)
    |> Addresses.update_all_addresses(transfers, time, block)
    |> Addresses.update_multiple_addresses()
  end

  def get_transfers_addresses(transfers, time) do
    transfers
    |> Enum.reduce([], fn %{"addr_from" => from, "addr_to" => to}, acc ->
      acc ++ [from, to]
    end)
    |> Enum.uniq
    |> Addresses.get_transfer_addresses(time)
  end
end

defmodule Neoscan.Transfers do
  @moduledoc false
  import Ecto.Query, warn: false
  alias Neoscan.Repo
  alias Neoscan.Transfers.Transfer
  alias Neoscan.Addresses
  alias Neoscan.Stats
  alias NeoscanMonitor.Api

  require Logger

  @doc """
  Returns the list of transfers in the home page.

  ## Examples

      iex> home_transfers()
      [%Transfer{}, ...]

  """
  def home_transfers do
    transfer_query =
      from(
        transfer in Transfer,
        order_by: [
          desc: transfer.id
        ],
        select: %{
          :id => transfer.id,
          :address_from => transfer.address_from,
          :address_to => transfer.address_to,
          :amount => transfer.amount,
          :block_height => transfer.block_height,
          :txid => transfer.txid,
          :contract => transfer.contract,
          :time => transfer.time
        },
        limit: 15
      )

    Repo.all(transfer_query)
  end

  @doc """
  Returns the list of paginated transfers.

  ## Examples

      iex> paginate_transfers(page)
      [%Transfer{}, ...]

  """
  def paginate_transfers(pag) do
    transfer_query =
      from(
        transfer in Transfer,
        order_by: [
          desc: transfer.id
        ],
        select: %{
          :id => transfer.id,
          :address_from => transfer.address_from,
          :address_to => transfer.address_to,
          :amount => transfer.amount,
          :block_height => transfer.block_height,
          :txid => transfer.txid,
          :contract => transfer.contract,
          :time => transfer.time
        },
        limit: 15
      )

    Repo.paginate(transfer_query, page: pag, page_size: 15)
  end

  @doc """
  Creates a transfer

  ## Examples

      iex> create_transfer(%{field: value})
      {:ok, %Transfer{}}

      iex> create_transfer(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_transfer(transfer, time, block) do
    attrs =
      transfer
      |> Map.merge(%{
        "txid" => transfer["tx"],
        "block_height" => transfer["block"],
        "address_from" => transfer["addr_from"],
        "address_to" => transfer["addr_to"],
        "time" => time
      })

    Transfer.changeset(block, attrs)
    |> Repo.insert!()
    |> update_transfer_state
  end

  def update_transfer_state(transfer) do
    Api.add_transfer(transfer)
    Stats.add_transfer_to_table(transfer)
    transfer
  end

  @doc """
  Count total transfers in DB.

  ## Examples

      iex> count_transfers()
      50

  """
  def count_transfers do
    Repo.aggregate(Transfer, :count, :id)
  end

  def add_block_transfers({_block, []}, _time) do
    {:ok, "all operations were succesfull"}
  end

  def add_block_transfers({block, transfers}, time) do
    get_transfers_addresses(transfers, time)
    |> Addresses.update_all_addresses(transfers, time, block)
    |> Addresses.update_multiple_addresses()
  end

  def get_transfers_addresses(transfers, time) do
    transfers
    |> Enum.reduce([], fn %{"addr_from" => from, "addr_to" => to}, acc ->
      acc ++ [from, to]
    end)
    |> Enum.uniq
    |> Addresses.get_transfer_addresses(time)
  end
end


:+1: Implement parse_args.

:+1: Implement parse_args.

defmodule Issues.CLI do

  @default_count 4

  @doc """
  Handle the command line parsing and the dispach to
  the verious functions that end up generating a
  table of the last _n_ issues in a github project.
  """

  def run(argv) do
    parse_args(argv)
  end

  @doc """
  `argv` can be -h or --help, which returns :help.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format.
  Return a tuple of `{user, project, count}`, or `:help` if help was given.
  """

  def parse_args(argv) do
    parse = OptionParser.parse(argv, switches: [ help: :boolean ],
                                     aliases:  [ h: :help       ])

    case parse do

    { [ help: true ], _, _ }
      -> :help

    { _, [ user, project, count ], _ }
      -> { user, project, count }

    { [ user, project ], _ }
      -> {user, project, @default_count }

    _ -> :help
    end
  end
end

defmodule Issues.CLI do

  @default_count 4

  @doc """
  Handle the command line parsing and the dispach to
  the verious functions that end up generating a
  table of the last _n_ issues in a github project.
  """

  def run(argv) do
    parse_args(argv)
  end

  @doc """
  `argv` can be -h or --help, which returns :help.

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format.
  Return a tuple of `{user, project, count}`, or `:help` if help was given.
  """

  def parse_args(argv) do
  end
end


Ensure the same pid is not queued twice in the parallel compiler

defmodule Kernel.ParallelCompiler do
  @moduledoc """
  A module responsible for compiling files in parallel.
  """

  @doc """
  Compiles the given files.

  Those files are compiled in parallel and can automatically
  detect dependencies between them. Once a dependency is found,
  the current file stops being compiled until the dependency is
  resolved.

  If there is an error during compilation or if `warnings_as_errors`
  is set to `true` and there is a warning, this function will fail
  with an exception.

  This function receives a set of callbacks as options:

  * `:each_file` - for each file compiled, invokes the callback passing the file
  * `:each_module` - for each module compiled, invokes the callback
                     passing the file, module and the module bytecode

  The compiler doesn't care about the return values of the callbacks.
  Returns the modules generated by each compiled file.
  """
  def files(files, callbacks \\ [])

  def files(files, callbacks) when is_list(callbacks) do
    spawn_compilers(files, nil, callbacks)
  end

  @doc """
  Compiles the given files to the given path.
  Read `files/2` for more information.
  """
  def files_to_path(files, path, callbacks \\ [])

  def files_to_path(files, path, callbacks) when is_binary(path) and is_list(callbacks) do
    spawn_compilers(files, path, callbacks)
  end

  defp spawn_compilers(files, path, callbacks) do
    Code.ensure_loaded(Kernel.ErrorHandler)
    compiler_pid = self()
    :elixir_code_server.cast({ :reset_warnings, compiler_pid })
    schedulers = max(:erlang.system_info(:schedulers_online), 2)

    result = spawn_compilers(files, files, path, callbacks, [], [], schedulers, [])

    # In case --warning-as-errors is enabled and there was a warning,
    # compilation status will be set to error and we fail with CompileError
    case :elixir_code_server.call({ :compilation_status, compiler_pid }) do
      :ok    -> result
      :error -> raise CompileError, [], []
    end
  end

  # We already have 4 currently running, don't spawn new ones
  defp spawn_compilers(entries, original, output, callbacks, waiting, queued, schedulers, result) when
      length(queued) - length(waiting) >= schedulers do
    wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result)
  end

  # Release waiting processes
  defp spawn_compilers([h|t], original, output, callbacks, waiting, queued, schedulers, result) when is_pid(h) do
    { _kind, ^h, ref, _module } = List.keyfind(waiting, h, 1)
    send h, { ref, :ready }
    waiting = List.keydelete(waiting, h, 1)
    spawn_compilers(t, original, output, callbacks, waiting, queued, schedulers, result)
  end

  # Spawn a compiler for each file in the list until we reach the limit
  defp spawn_compilers([h|t], original, output, callbacks, waiting, queued, schedulers, result) do
    parent = self()

    { pid, ref } =
      :erlang.spawn_monitor fn ->
        # Notify Code.ensure_compiled/2 that we should
        # attempt to compile the module by doing a dispatch.
        :erlang.put(:elixir_ensure_compiled, true)

        # Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
        :erlang.put(:elixir_compiler_pid, parent)
        :erlang.process_flag(:error_handler, Kernel.ErrorHandler)

        exit(try do
          if output do
            :elixir_compiler.file_to_path(h, output)
          else
            :elixir_compiler.file(h)
          end
          { :compiled, h }
        catch
          kind, reason ->
            { :failure, kind, reason, System.stacktrace }
        end)
      end

    spawn_compilers(t, original, output, callbacks, waiting,
                    [{ pid, ref, h }|queued], schedulers, result)
  end

  # No more files, nothing waiting, queue is empty, we are done
  defp spawn_compilers([], _original, _output, _callbacks, [], [], _schedulers, result) do
    for { :module, mod } <- result, do: mod
  end

  # Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
  defp spawn_compilers([], original, output, callbacks, waiting, queued, schedulers, result) when length(waiting) == length(queued) do
    Enum.each queued, fn { child, _, _ } ->
      { _kind, ^child, ref, _module } = List.keyfind(waiting, child, 1)
      send child, { ref, :release }
    end
    wait_for_messages([], original, output, callbacks, waiting, queued, schedulers, result)
  end

  # No more files, but queue and waiting are not full or do not match
  defp spawn_compilers([], original, output, callbacks, waiting, queued, schedulers, result) do
    wait_for_messages([], original, output, callbacks, waiting, queued, schedulers, result)
  end

  # Wait for messages from child processes
  defp wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result) do
    receive do
      { :struct_available, module } ->
        available = for { :struct, pid, _, waiting_module } <- waiting,
                        module == waiting_module,
                        not pid in entries,
                        do: pid

        spawn_compilers(available ++ entries, original, output, callbacks,
                        waiting, queued, schedulers, [{ :struct, module }|result])

      { :module_available, child, ref, file, module, binary } ->
        if callback = Keyword.get(callbacks, :each_module) do
          callback.(file, module, binary)
        end

        # Release the module loader which is waiting for an ack
        send child, { ref, :ack }

        available = for { _kind, pid, _, waiting_module } <- waiting,
                        module == waiting_module,
                        not pid in entries,
                        do: pid

        spawn_compilers(available ++ entries, original, output, callbacks,
                        waiting, queued, schedulers, [{ :module, module }|result])

      { :waiting, kind, child, ref, on } ->
        defined = fn { k, m } -> on == m and k in [kind, :module] end

        # Oops, we already got it, do not put it on waiting.
        if :lists.any(defined, result) do
          send child, { ref, :ready }
        else
          waiting = [{ kind, child, ref, on }|waiting]
        end

        spawn_compilers(entries, original, output, callbacks, waiting, queued, schedulers, result)

      { :DOWN, _down_ref, :process, down_pid, { :compiled, file } } ->
        if callback = Keyword.get(callbacks, :each_file) do
          callback.(file)
        end

        # Sometimes we may have spurious entries in the waiting
        # list because someone invoked try/rescue UndefinedFunctionError
        new_entries = List.delete(entries, down_pid)
        new_queued  = List.keydelete(queued, down_pid, 0)
        new_waiting = List.keydelete(waiting, down_pid, 1)
        spawn_compilers(new_entries, original, output, callbacks, new_waiting, new_queued, schedulers, result)

      { :DOWN, down_ref, :process, _down_pid, { :failure, kind, reason, stacktrace } } ->
        handle_failure(down_ref, kind, reason, stacktrace, entries, waiting, queued)
        wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result)

      { :DOWN, down_ref, :process, _down_pid, other } ->
        handle_failure(down_ref, :exit, other, [], entries, waiting, queued)
        wait_for_messages(entries, original, output, callbacks, waiting, queued, schedulers, result)
    end
  end

  defp handle_failure(ref, kind, reason, stacktrace, entries, waiting, queued) do
    case List.keyfind(queued, ref, 1) do
      { child, ^ref, file } ->
        if many_missing?(child, entries, waiting, queued) do
          IO.puts "== Compilation failed =="
          IO.puts "Compilation failed on the following files:\n"

          Enum.each Enum.reverse(queued), fn { pid, _ref, file } ->
            case List.keyfind(waiting, pid, 1) do
              { _, ^pid, _, mod } -> IO.puts "* #{file} is missing module #{inspect mod}"
              _ -> :ok
            end
          end

          IO.puts "\nOne of the failures is shown below..."
        end

        IO.puts "== Compilation error on file #{file} =="
        :erlang.raise(kind, reason, stacktrace)
      _ ->
        :ok
    end
  end

  defp many_missing?(child, entries, waiting, queued) do
    waiting_length = length(waiting)

    match?({ _, ^child, _, _ }, List.keyfind(waiting, child, 1)) and
      waiting_length > 1 and entries == [] and
      waiting_length == length(queued)
  end
end

defmodule Kernel.ParallelCompiler do
  @moduledoc """
  A module responsible for compiling files in parallel.
  """

  @doc """
  Compiles the given files.

  Those files are compiled in parallel and can automatically
  detect dependencies between them. Once a dependency is found,
  the current file stops being compiled until the dependency is
  resolved.

  If there is an error during compilation or if `warnings_as_errors`
  is set to `true` and there is a warning, this function will fail
  with an exception.

  This function receives a set of callbacks as options:

  * `:each_file` - for each file compiled, invokes the callback passing the file
  * `:each_module` - for each module compiled, invokes the callback
                     passing the file, module and the module bytecode

  The compiler doesn't care about the return values of the callbacks.
  Returns the modules generated by each compiled file.
  """
  def files(files, callbacks \\ [])

  def files(files, callbacks) when is_list(callbacks) do
    spawn_compilers(files, nil, callbacks)
  end

  @doc """
  Compiles the given files to the given path.
  Read `files/2` for more information.
  """
  def files_to_path(files, path, callbacks \\ [])

  def files_to_path(files, path, callbacks) when is_binary(path) and is_list(callbacks) do
    spawn_compilers(files, path, callbacks)
  end

  defp spawn_compilers(files, path, callbacks) do
    Code.ensure_loaded(Kernel.ErrorHandler)
    compiler_pid = self()
    :elixir_code_server.cast({ :reset_warnings, compiler_pid })
    schedulers = max(:erlang.system_info(:schedulers_online), 2)

    result = spawn_compilers(files, files, path, callbacks, [], [], schedulers, [])

    # In case --warning-as-errors is enabled and there was a warning,
    # compilation status will be set to error and we fail with CompileError
    case :elixir_code_server.call({ :compilation_status, compiler_pid }) do
      :ok    -> result
      :error -> raise CompileError, [], []
    end
  end

  # We already have 4 currently running, don't spawn new ones
  defp spawn_compilers(files, original, output, callbacks, waiting, queued, schedulers, result) when
      length(queued) - length(waiting) >= schedulers do
    wait_for_messages(files, original, output, callbacks, waiting, queued, schedulers, result)
  end

  # Release waiting processes
  defp spawn_compilers([h|t], original, output, callbacks, waiting, queued, schedulers, result) when is_pid(h) do
    { _kind, ^h, ref, _module } = List.keyfind(waiting, h, 1)
    send h, { ref, :ready }
    waiting = List.keydelete(waiting, h, 1)
    spawn_compilers(t, original, output, callbacks, waiting, queued, schedulers, result)
  end

  # Spawn a compiler for each file in the list until we reach the limit
  defp spawn_compilers([h|t], original, output, callbacks, waiting, queued, schedulers, result) do
    parent = self()

    { pid, ref } =
      :erlang.spawn_monitor fn ->
        # Notify Code.ensure_compiled/2 that we should
        # attempt to compile the module by doing a dispatch.
        :erlang.put(:elixir_ensure_compiled, true)

        # Set the elixir_compiler_pid used by our custom Kernel.ErrorHandler.
        :erlang.put(:elixir_compiler_pid, parent)
        :erlang.process_flag(:error_handler, Kernel.ErrorHandler)

        exit(try do
          if output do
            :elixir_compiler.file_to_path(h, output)
          else
            :elixir_compiler.file(h)
          end
          { :compiled, h }
        catch
          kind, reason ->
            { :failure, kind, reason, System.stacktrace }
        end)
      end

    spawn_compilers(t, original, output, callbacks, waiting,
                    [{ pid, ref, h }|queued], schedulers, result)
  end

  # No more files, nothing waiting, queue is empty, we are done
  defp spawn_compilers([], _original, _output, _callbacks, [], [], _schedulers, result) do
    for { :module, mod } <- result, do: mod
  end

  # Queued x, waiting for x: POSSIBLE ERROR! Release processes so we get the failures
  defp spawn_compilers([], original, output, callbacks, waiting, queued, schedulers, result) when length(waiting) == length(queued) do
    Enum.each queued, fn { child, _, _ } ->
      { _kind, ^child, ref, _module } = List.keyfind(waiting, child, 1)
      send child, { ref, :release }
    end
    wait_for_messages([], original, output, callbacks, waiting, queued, schedulers, result)
  end

  # No more files, but queue and waiting are not full or do not match
  defp spawn_compilers([], original, output, callbacks, waiting, queued, schedulers, result) do
    wait_for_messages([], original, output, callbacks, waiting, queued, schedulers, result)
  end

  # Wait for messages from child processes
  defp wait_for_messages(files, original, output, callbacks, waiting, queued, schedulers, result) do
    receive do
      { :struct_available, module } ->
        available = for { :struct, pid, _, waiting_module } <- waiting,
                        waiting_module == module,
                        do: pid

        spawn_compilers(available ++ files, original, output, callbacks,
                        waiting, queued, schedulers, [{ :struct, module }|result])

      { :module_available, child, ref, file, module, binary } ->
        if callback = Keyword.get(callbacks, :each_module) do
          callback.(file, module, binary)
        end

        # Release the module loader which is waiting for an ack
        send child, { ref, :ack }

        available  = for { _kind, pid, _, waiting_module } <- waiting,
                         waiting_module == module,
                         do: pid

        spawn_compilers(available ++ files, original, output, callbacks,
                        waiting, queued, schedulers, [{ :module, module }|result])

      { :waiting, kind, child, ref, on } ->
        defined = fn { k, m } -> on == m and k in [kind, :module] end

        # Oops, we already got it, do not put it on waiting.
        if :lists.any(defined, result) do
          send child, { ref, :ready }
        else
          waiting = [{ kind, child, ref, on }|waiting]
        end

        spawn_compilers(files, original, output, callbacks, waiting, queued, schedulers, result)

      { :DOWN, _down_ref, :process, down_pid, { :compiled, file } } ->
        if callback = Keyword.get(callbacks, :each_file) do
          callback.(file)
        end

        # Sometimes we may have spurious entries in the waiting
        # list because someone invoked try/rescue UndefinedFunctionError
        new_files   = List.delete(files, down_pid)
        new_queued  = List.keydelete(queued, down_pid, 0)
        new_waiting = List.keydelete(waiting, down_pid, 1)
        spawn_compilers(new_files, original, output, callbacks, new_waiting, new_queued, schedulers, result)

      { :DOWN, down_ref, :process, _down_pid, { :failure, kind, reason, stacktrace } } ->
        handle_failure(down_ref, kind, reason, stacktrace, files, waiting, queued)
        wait_for_messages(files, original, output, callbacks, waiting, queued, schedulers, result)

      { :DOWN, down_ref, :process, _down_pid, other } ->
        handle_failure(down_ref, :exit, other, [], files, waiting, queued)
        wait_for_messages(files, original, output, callbacks, waiting, queued, schedulers, result)
    end
  end

  defp handle_failure(ref, kind, reason, stacktrace, files, waiting, queued) do
    case List.keyfind(queued, ref, 1) do
      { child, ^ref, file } ->
        if many_missing?(child, files, waiting, queued) do
          IO.puts "== Compilation failed =="
          IO.puts "Compilation failed on the following files:\n"

          Enum.each Enum.reverse(queued), fn { pid, _ref, file } ->
            case List.keyfind(waiting, pid, 1) do
              { _, ^pid, _, mod } -> IO.puts "* #{file} is missing module #{inspect mod}"
              _ -> :ok
            end
          end

          IO.puts "\nThe first failure is shown below..."
        end

        IO.puts "== Compilation error on file #{file} =="
        :erlang.raise(kind, reason, stacktrace)
      _ ->
        :ok
    end
  end

  defp many_missing?(child, files, waiting, queued) do
    waiting_length = length(waiting)

    match?({ _, ^child, _, _ }, List.keyfind(waiting, child, 1)) and
      waiting_length > 1 and files == [] and
      waiting_length == length(queued)
  end
end


Filter out social networks with nil URL values

defmodule WhoWeb.MemberPresenter do
  @moduledoc """
    Creates a data object containing all the
    important information regarding a Member.
  """
  alias WhoWeb.MemberView

  @type t :: %__MODULE__{
    name: String.t(),
    title: String.t(),
    state: String.t(),
    party: String.t(),
    chamber: String.t(),
    district: non_neg_integer(),

    start_date: String.t(),
    end_date: String. t(),
    member_id: String.t(),
    committees: list(String.t()),
    votes_with_party: float(),
    votes: list(map),

    social_urls: list(map),
    phone_url: String.t(),
    office: String.t(),
    map_src_url: String.t,

    profile_link: String.t()
  }

  defstruct [
    name: nil,
    title: nil,
    state: nil,
    party: nil,
    chamber: nil,
    district: nil,

    start_date: nil,
    end_date: nil,
    member_id: nil,
    committees: [],
    votes_with_party: nil,
    votes: [],

    social_urls: [],
    phone_url: nil,
    office: nil,
    map_src_url: nil,

    profile_link: nil
  ]

  @doc """
    Creates a new %MemberPresenter{} named struct.
  """
  @spec new(map()) :: t()
  def new(nil), do: nil
  def new(member, member_votes_map) do
    chamber = MemberView.parse_member_field(member, "roles", "chamber")
    vote_list = MemberView.parse_member_field(member_votes_map, "votes")
    member_id = MemberView.parse_member_field(member, "member_id")
    office = MemberView.parse_member_field(member, "roles", "office")

    website_url = MemberView.parse_member_field(member, "domain")
    facebook_url =  member
                    |> MemberView.parse_member_social_account("facebook")
                    |> MemberView.build_social_profile_url("facebook")
    twitter_url =   member
                    |> MemberView.parse_member_social_account("twitter")
                    |> MemberView.build_social_profile_url("twitter")
    youtube_url =   member
                    |> MemberView.parse_member_social_account("youtube")
                    |> MemberView.build_social_profile_url("youtube")

    social_urls = [
      %{ network: "website", url: website_url },
      %{ network: "facebook", url: facebook_url },
      %{ network: "twitter", url: twitter_url },
      %{ network: "youtube", url: youtube_url }
    ]

    %__MODULE__{
      name: MemberView.parse_member_name(member),
      title: MemberView.build_member_title(chamber),
      state: MemberView.build_state_name(member),
      party: MemberView.build_member_party(member),
      chamber: chamber,
      district: MemberView.parse_member_district(member, chamber),

      start_date: MemberView.parse_member_start_date(member),
      end_date: MemberView.parse_member_end_date(member),
      member_id: member_id,
      committees: MemberView.build_current_session_committee_list(member),
      votes_with_party: MemberView.build_aggregate_party_vote_pct(member),
      votes: MemberView.build_votes_list(vote_list) |> Enum.slice(1..10),

      social_urls: Enum.filter(social_urls, fn(x) -> x.url != nil end),
      phone_url: "tel:1-" <> MemberView.parse_member_field(member, "roles", "phone"),
      office: office,
      map_src_url: Who.GoogleMapsAPI.Base.build_embed_src_url(office),

      profile_link: ~s(/member/#{member_id})
    }
  end
end

defmodule WhoWeb.MemberPresenter do
  @moduledoc """
    Creates a data object containing all the
    important information regarding a Member.
  """
  alias WhoWeb.MemberView

  @type t :: %__MODULE__{
    name: String.t(),
    title: String.t(),
    state: String.t(),
    party: String.t(),
    chamber: String.t(),
    district: non_neg_integer(),

    start_date: String.t(),
    end_date: String. t(),
    member_id: String.t(),
    committees: list(String.t()),
    votes_with_party: float(),
    votes: list(map),

    social_urls: list(map),
    phone_url: String.t(),
    office: String.t(),
    map_src_url: String.t,

    profile_link: String.t()
  }

  defstruct [
    name: nil,
    title: nil,
    state: nil,
    party: nil,
    chamber: nil,
    district: nil,

    start_date: nil,
    end_date: nil,
    member_id: nil,
    committees: [],
    votes_with_party: nil,
    votes: [],

    social_urls: [],
    phone_url: nil,
    office: nil,
    map_src_url: nil,

    profile_link: nil
  ]

  @doc """
    Creates a new %MemberPresenter{} named struct.
  """
  @spec new(map()) :: t()
  def new(nil), do: nil
  def new(member, member_votes_map) do
    chamber = MemberView.parse_member_field(member, "roles", "chamber")
    vote_list = MemberView.parse_member_field(member_votes_map, "votes")
    member_id = MemberView.parse_member_field(member, "member_id")
    office = MemberView.parse_member_field(member, "roles", "office")

    website_url = MemberView.parse_member_field(member, "domain")
    facebook_url =  member
                    |> MemberView.parse_member_social_account("facebook")
                    |> MemberView.build_social_profile_url("facebook")
    twitter_url =   member
                    |> MemberView.parse_member_social_account("twitter")
                    |> MemberView.build_social_profile_url("twitter")
    youtube_url =   member
                    |> MemberView.parse_member_social_account("youtube")
                    |> MemberView.build_social_profile_url("youtube")

    %__MODULE__{
      name: MemberView.parse_member_name(member),
      title: MemberView.build_member_title(chamber),
      state: MemberView.build_state_name(member),
      party: MemberView.build_member_party(member),
      chamber: chamber,
      district: MemberView.parse_member_district(member, chamber),

      start_date: MemberView.parse_member_start_date(member),
      end_date: MemberView.parse_member_end_date(member),
      member_id: member_id,
      committees: MemberView.build_current_session_committee_list(member),
      votes_with_party: MemberView.build_aggregate_party_vote_pct(member),
      votes: MemberView.build_votes_list(vote_list) |> Enum.slice(1..10),

      social_urls: [
        %{ network: "website", url: website_url },
        %{ network: "facebook", url: facebook_url },
        %{ network: "twitter", url: twitter_url },
        %{ network: "youtube", url: youtube_url }
      ],
      phone_url: "tel:1-" <> MemberView.parse_member_field(member, "roles", "phone"),
      office: office,
      map_src_url: Who.GoogleMapsAPI.Base.build_embed_src_url(office),

      profile_link: ~s(/member/#{member_id})
    }
  end
end


Demonitor Farmware Process to prevent false failures

defmodule FarmbotOS.SysCalls.Farmware do
  require Logger
  alias FarmbotCeleryScript.AST
  alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime}
  alias FarmbotExt.API.ImageUploader

  def update_farmware(farmware_name) do
    with {:ok, installation} <- lookup_installation(farmware_name) do
      AssetSupervisor.cast_child(installation, :update)
    else
      {:error, reason} when is_binary(reason) ->
        {:error, reason}
    end
  end

  def lookup_manifest(farmware_name) do
    case Asset.get_farmware_manifest(farmware_name) do
      nil -> {:error, "#{farmware_name} farmware not installed"}
      manifest -> {:ok, manifest}
    end
  end

  def lookup_installation(farmware_name) do
    case Asset.get_farmware_installation(farmware_name) do
      nil -> {:error, "#{farmware_name} farmware not installed"}
      farmware -> {:ok, farmware}
    end
  end

  # Entry point to starting a farmware
  def execute_script(farmware_name, env) do
    # NOTE Connor:
    # this is a really big hammer to fix a bug i don't fully understand.
    # for some reason even tho we call `if Process.alive?....` before every call,
    # there is still a possibility of the genserver being down for some reason.
    # catching the genserver call failure "fixes" it.
    try do
      with {:ok, manifest} <- lookup_manifest(farmware_name),
           {:ok, runtime} <- FarmwareRuntime.start_link(manifest, env),
           monitor <- Process.monitor(runtime),
           :ok <- loop(farmware_name, runtime, monitor, {nil, nil}),
           _ <- Process.demonitor(monitor),
           _ <- empty_mailbox(),
           :ok <- ImageUploader.force_checkup() do
        :ok
      else
        {:error, {:already_started, pid}} ->
          Logger.warn("Farmware #{farmware_name} is already running")
          _ = FarmwareRuntime.stop(pid)
          execute_script(farmware_name, env)

        {:error, reason} when is_binary(reason) ->
          _ = ImageUploader.force_checkup()
          {:error, reason}
      end
    catch
      error, reason ->
        Logger.debug("farmware catchall #{inspect(error)}: #{inspect(reason)}")
        :ok
    end
  end

  defp empty_mailbox() do
    receive do
      msg ->
        Logger.debug("dropping message after farmware: #{inspect(msg)}")
        empty_mailbox()
    after
      100 -> :ok
    end
  end

  defp loop(farmware_name, runtime, monitor, {ref, label}) do
    receive do
      {:EXIT, ^runtime, :normal} ->
        Logger.debug("Farmware monitor down: :normal state: #{inspect(label)}")
        :ok

      {:DOWN, ^monitor, :process, ^runtime, :normal} ->
        Logger.debug("Farmware monitor down: :normal state: #{inspect(label)}")
        :ok

      {:EXIT, ^runtime, error} ->
        Logger.debug("Farmware monitor down: #{inspect(error)} state: #{inspect(label)}")
        {:error, inspect(error)}

      {:DOWN, ^monitor, :process, ^runtime, error} ->
        Logger.debug("Farmware monitor down: #{inspect(error)} state: #{inspect(label)}")
        {:error, inspect(error)}

      {:step_complete, ^ref, :ok} ->
        Logger.debug("ok for #{label}")
        response = %AST{kind: :rpc_ok, args: %{label: label}, body: []}

        if Process.alive?(runtime) do
          true = FarmwareRuntime.rpc_processed(runtime, response)
          loop(farmware_name, runtime, monitor, {nil, nil})
        else
          :ok
        end

      {:step_complete, ^ref, {:error, reason}} ->
        Logger.debug("error for #{label}")
        explanation = %AST{kind: :explanation, args: %{message: reason}}
        response = %AST{kind: :rpc_error, args: %{label: label}, body: [explanation]}

        if Process.alive?(runtime) do
          true = FarmwareRuntime.rpc_processed(runtime, response)
          loop(farmware_name, runtime, monitor, {nil, nil})
        else
          :ok
        end

      msg ->
        if Process.alive?(runtime) do
          _ = FarmwareRuntime.stop(runtime)
        end

        {:error, "unhandled message: #{inspect(msg)} in state: #{inspect({ref, label})}"}
    after
      500 ->
        cond do
          # already have a request processing
          is_reference(ref) ->
            Logger.info("Already processing a celeryscript request: #{label}")
            loop(farmware_name, runtime, monitor, {ref, label})

          # check to see if it's alive just in case?
          Process.alive?(runtime) ->
            process(farmware_name, runtime, monitor, {ref, label})

          # No other conditions: Process stopped, but missed the message?
          true ->
            if Process.alive?(runtime) do
              _ = FarmwareRuntime.stop(runtime)
            end

            :ok
        end
    end
  end

  defp process(farmware_name, runtime, monitor, {ref, label}) do
    if Process.alive?(runtime) do
      case FarmwareRuntime.process_rpc(runtime) do
        {:ok, %{args: %{label: label}} = rpc} ->
          ref = make_ref()
          Logger.debug("executing rpc: #{inspect(rpc)}")
          FarmbotCeleryScript.execute(rpc, ref)
          loop(farmware_name, runtime, monitor, {ref, label})

        {:error, :no_rpc} ->
          loop(farmware_name, runtime, monitor, {ref, label})
      end
    else
      :ok
    end
  end
end

defmodule FarmbotOS.SysCalls.Farmware do
  require Logger
  alias FarmbotCeleryScript.AST
  alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime}
  alias FarmbotExt.API.ImageUploader

  def update_farmware(farmware_name) do
    with {:ok, installation} <- lookup_installation(farmware_name) do
      AssetSupervisor.cast_child(installation, :update)
    else
      {:error, reason} when is_binary(reason) ->
        {:error, reason}
    end
  end

  def lookup_manifest(farmware_name) do
    case Asset.get_farmware_manifest(farmware_name) do
      nil -> {:error, "#{farmware_name} farmware not installed"}
      manifest -> {:ok, manifest}
    end
  end

  def lookup_installation(farmware_name) do
    case Asset.get_farmware_installation(farmware_name) do
      nil -> {:error, "#{farmware_name} farmware not installed"}
      farmware -> {:ok, farmware}
    end
  end

  # Entry point to starting a farmware
  def execute_script(farmware_name, env) do
    # NOTE Connor:
    # this is a really big hammer to fix a bug i don't fully understand.
    # for some reason even tho we call `if Process.alive?....` before every call,
    # there is still a possibility of the genserver being down for some reason.
    # catching the genserver call failure "fixes" it.
    try do
      with {:ok, manifest} <- lookup_manifest(farmware_name),
           {:ok, runtime} <- FarmwareRuntime.start_link(manifest, env),
           monitor <- Process.monitor(runtime),
           :ok <- loop(farmware_name, runtime, monitor, {nil, nil}),
           :ok <- ImageUploader.force_checkup() do
        :ok
      else
        {:error, {:already_started, pid}} ->
          Logger.warn("Farmware #{farmware_name} is already running")
          _ = FarmwareRuntime.stop(pid)
          execute_script(farmware_name, env)

        {:error, reason} when is_binary(reason) ->
          _ = ImageUploader.force_checkup()
          {:error, reason}
      end
    catch
      error, reason ->
        Logger.debug("farmware catchall #{inspect(error)}: #{inspect(reason)}")
        :ok
    end
  end

  defp loop(farmware_name, runtime, monitor, {ref, label}) do
    receive do
      {:EXIT, ^runtime, :normal} ->
        Logger.debug("Farmware monitor down: :normal state: #{inspect(label)}")
        :ok

      {:DOWN, ^monitor, :process, ^runtime, :normal} ->
        Logger.debug("Farmware monitor down: :normal state: #{inspect(label)}")
        :ok

      {:EXIT, ^runtime, error} ->
        Logger.debug("Farmware monitor down: #{inspect(error)} state: #{inspect(label)}")
        {:error, inspect(error)}

      {:DOWN, ^monitor, :process, ^runtime, error} ->
        Logger.debug("Farmware monitor down: #{inspect(error)} state: #{inspect(label)}")
        {:error, inspect(error)}

      {:step_complete, ^ref, :ok} ->
        Logger.debug("ok for #{label}")
        response = %AST{kind: :rpc_ok, args: %{label: label}, body: []}

        if Process.alive?(runtime) do
          true = FarmwareRuntime.rpc_processed(runtime, response)
          loop(farmware_name, runtime, monitor, {nil, nil})
        else
          :ok
        end

      {:step_complete, ^ref, {:error, reason}} ->
        Logger.debug("error for #{label}")
        explanation = %AST{kind: :explanation, args: %{message: reason}}
        response = %AST{kind: :rpc_error, args: %{label: label}, body: [explanation]}

        if Process.alive?(runtime) do
          true = FarmwareRuntime.rpc_processed(runtime, response)
          loop(farmware_name, runtime, monitor, {nil, nil})
        else
          :ok
        end

      msg ->
        if Process.alive?(runtime) do
          _ = FarmwareRuntime.stop(runtime)
        end

        {:error, "unhandled message: #{inspect(msg)} in state: #{inspect({ref, label})}"}
    after
      500 ->
        cond do
          # already have a request processing
          is_reference(ref) ->
            Logger.info("Already processing a celeryscript request: #{label}")
            loop(farmware_name, runtime, monitor, {ref, label})

          # check to see if it's alive just in case?
          Process.alive?(runtime) ->
            process(farmware_name, runtime, monitor, {ref, label})

          # No other conditions: Process stopped, but missed the message?
          true ->
            if Process.alive?(runtime) do
              _ = FarmwareRuntime.stop(runtime)
            end

            :ok
        end
    end
  end

  defp process(farmware_name, runtime, monitor, {ref, label}) do
    if Process.alive?(runtime) do
      case FarmwareRuntime.process_rpc(runtime) do
        {:ok, %{args: %{label: label}} = rpc} ->
          ref = make_ref()
          Logger.debug("executing rpc: #{inspect(rpc)}")
          FarmbotCeleryScript.execute(rpc, ref)
          loop(farmware_name, runtime, monitor, {ref, label})

        {:error, :no_rpc} ->
          loop(farmware_name, runtime, monitor, {ref, label})
      end
    else
      :ok
    end
  end
end


Test the capability checks

Test the capability checks

defmodule Baumeister.WorkerTest do
  use ExUnit.Case

  require Logger

  alias Baumeister.Coordinator
  alias Baumeister.Worker
  alias Baumeister.BaumeisterFile

  # Ensures that the coordinator is running and puts the pid in the environment
  setup do
    Application.ensure_started(:baumeister)
    m = wait_for_coordinator
    {:ok, coordinator: m}
  end

  def wait_for_coordinator(wait\\ 5)
  def wait_for_coordinator(0), do: assert "Coordinator is not available :-("
  def wait_for_coordinator(wait) do
    m = GenServer.whereis(Coordinator.name)
    if not is_pid(m) do
      # only 1 ms, to let the scheduler recover. This is to
      # prevent a concurrency issue on Travis-CI
      Process.sleep(1)
      wait_for_coordinator(wait - 1)
    else
      m
    end
  end

  @tag timeout: 1_000
  test "Start a worker", _env do
    {:ok, worker} = Worker.start_link()
    Logger.debug "Worker is started"
    assert is_pid(worker)
    assert Process.alive?(worker)
    all_workers = Coordinator.workers()
    Logger.debug "all workers: #{inspect all_workers}"
    assert Enum.any?(all_workers, fn(w) -> w.pid == worker end)
    Process.exit(worker, :normal)
    remaining_workers = Coordinator.workers()
    assert not Enum.member?(remaining_workers, worker)
  end

  @tag timeout: 1_000
  test "Kill the coordinator", env do
    Process.flag(:trap_exit, true)
    {:ok, worker} = Worker.start_link()
    assert Process.alive?(worker)
    ref = Process.monitor(GenServer.whereis(Coordinator.name))
    ref_worker = Process.monitor(worker)

    Logger.info "env is: #{inspect env}"
    Logger.debug "Registered workers: #{inspect Coordinator.workers}"
    Logger.debug "Killing Coordinator"
    Process.exit(env[:coordinator], :kill)

    # Coordinator is down
    assert_receive({:DOWN, ^ref, :process, _, _})
    # Worker goes down
    assert_receive({:DOWN, ^ref_worker, :process, _, _})
    refute Process.alive?(worker)
  end

  test "Check for the capabilities" do
    l = Worker.detect_capabilities()
    assert is_map(l)
    assert Map.fetch!(l, :os) != nil
  end

  test "Find suitable workers for Elixir on macOS" do
    {_, local_os} = :os.type()
    local_os = local_os |> Atom.to_string
    bmf = """
      os: #{local_os}
      language: elixir
    """ |> BaumeisterFile.parse!
    {:ok, _worker} = Worker.start_link()
    Logger.debug "Worker is started"
    capa = Worker.detect_capabilities
    capa_expected = %{:os => BaumeisterFile.canonized_values(local_os, :os), :mix => true}

    # check that at least all values of capa_expected are set in capa
    for {k, v} <- capa_expected do
      assert capa[k] == v, "key: #{inspect k}, expect: #{inspect v}, got: #{inspect capa[k]}"
    end
    assert Coordinator.match_worker?(capa, bmf)
  end

end

defmodule Baumeister.WorkerTest do
  use ExUnit.Case

  require Logger

  alias Baumeister.Coordinator
  alias Baumeister.Worker
  alias Baumeister.BaumeisterFile

  # Ensures that the coordinator is running and puts the pid in the environment
  setup do
    Application.ensure_started(:baumeister)
    m = wait_for_coordinator
    {:ok, coordinator: m}
  end

  def wait_for_coordinator(wait\\ 5)
  def wait_for_coordinator(0), do: assert "Coordinator is not available :-("
  def wait_for_coordinator(wait) do
    m = GenServer.whereis(Coordinator.name)
    if not is_pid(m) do
      # only 1 ms, to let the scheduler recover. This is to
      # prevent a concurrency issue on Travis-CI
      Process.sleep(1)
      wait_for_coordinator(wait - 1)
    else
      m
    end
  end

  @tag timeout: 1_000
  test "Start a worker", _env do
    {:ok, worker} = Worker.start_link()
    Logger.debug "Worker is started"
    assert is_pid(worker)
    assert Process.alive?(worker)
    all_workers = Coordinator.workers()
    Logger.debug "all workers: #{inspect all_workers}"
    assert Enum.any?(all_workers, fn(w) -> w.pid == worker end)
    Process.exit(worker, :normal)
    remaining_workers = Coordinator.workers()
    assert not Enum.member?(remaining_workers, worker)
  end

  @tag timeout: 1_000
  test "Kill the coordinator", env do
    Process.flag(:trap_exit, true)
    {:ok, worker} = Worker.start_link()
    assert Process.alive?(worker)
    ref = Process.monitor(GenServer.whereis(Coordinator.name))
    ref_worker = Process.monitor(worker)

    Logger.info "env is: #{inspect env}"
    Logger.debug "Registered workers: #{inspect Coordinator.workers}"
    Logger.debug "Killing Coordinator"
    Process.exit(env[:coordinator], :kill)

    # Coordinator is down
    assert_receive({:DOWN, ^ref, :process, _, _})
    # Worker goes down
    assert_receive({:DOWN, ^ref_worker, :process, _, _})
    refute Process.alive?(worker)
  end

  test "Check for the capabilities" do
    l = Worker.detect_capabilities()
    assert is_map(l)
    assert Map.fetch!(l, :os) != nil
  end

  test "Find suitable workers for Elixir on macOS" do
    {_, local_os} = :os.type()
    local_os = local_os |> Atom.to_string
    bmf = """
      os: #{local_os}
      language: elixir
    """ |> BaumeisterFile.parse!
    {:ok, worker} = Worker.start_link()
    Logger.debug "Worker is started"
    capa = Worker.detect_capabilities
    capa_manual = %{:os => BaumeisterFile.canonized_values(local_os, :os), :mix => true}
    assert capa_manual = capa
    assert Coordinator.match_worker?(capa, bmf)
  end

end