Compare commits
2 commits
e180aa8248
...
01d9c54023
Author | SHA1 | Date | |
---|---|---|---|
01d9c54023 | |||
b452dc87d3 |
5 changed files with 292 additions and 76 deletions
|
@ -4,68 +4,81 @@ defmodule Freedive.Api.Service.Cli do
|
||||||
"""
|
"""
|
||||||
require Logger
|
require Logger
|
||||||
import Freedive.Api.Command
|
import Freedive.Api.Command
|
||||||
|
import Freedive.Api.Service.Icons
|
||||||
|
|
||||||
@service_bin "/usr/sbin/service"
|
@service_bin "/usr/sbin/service"
|
||||||
|
@skip_service_names ["DAEMON", "FILESYSTEMS", "LOGIN", "NETWORKING", "SERVERS"]
|
||||||
|
|
||||||
def list_services() do
|
def list_services!() do
|
||||||
|
all_service_names = all_service_names()
|
||||||
|
enabled_service_names = enabled_service_names()
|
||||||
|
|
||||||
|
all_service_names
|
||||||
|
|> Enum.map(fn name -> service_details(name, Enum.member?(enabled_service_names, name)) end)
|
||||||
|
|> Enum.into(%{}, &{&1[:name], &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_command!(name, command, args \\ []) do
|
||||||
|
case service(name, command, args) do
|
||||||
|
{:ok, stdout} ->
|
||||||
|
{:ok, stdout |> String.trim()}
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def all_service_names() do
|
||||||
case execute(@service_bin, ["-l"]) do
|
case execute(@service_bin, ["-l"]) do
|
||||||
{:ok, stdout} ->
|
{:ok, stdout} ->
|
||||||
service_names =
|
|
||||||
stdout
|
stdout
|
||||||
|> String.split("\n")
|
|> String.split("\n")
|
||||||
|> Enum.map(&String.trim/1)
|
|> Enum.map(&String.trim/1)
|
||||||
|> Enum.reject(
|
|> Enum.reject(&Enum.member?(@skip_service_names, &1))
|
||||||
&Enum.member?(["DAEMON", "FILESYSTEMS", "LOGIN", "NETWORKING", "SERVERS"], &1)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("List services: #{reason}")
|
||||||
|
raise "Failed to list services."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enabled_service_names() do
|
||||||
case execute(@service_bin, ["-e"]) do
|
case execute(@service_bin, ["-e"]) do
|
||||||
{:ok, stdout} ->
|
{:ok, stdout} ->
|
||||||
enabled_service_names =
|
|
||||||
stdout
|
stdout
|
||||||
|> String.split("\n")
|
|> String.split("\n")
|
||||||
|> Enum.map(&String.trim/1)
|
|> Enum.map(&String.trim/1)
|
||||||
|> Enum.map(&Path.basename/1)
|
|> Enum.map(&Path.basename/1)
|
||||||
|> Enum.into(%{}, &{&1, true})
|
|
||||||
|
|
||||||
services =
|
{:error, reason} ->
|
||||||
service_names
|
Logger.error("List enabled services: #{reason}")
|
||||||
|> Enum.map(fn name ->
|
raise "Failed to list enabled services."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_details(name, enabled \\ nil) do
|
||||||
|
enabled = if(enabled != nil, do: enabled, else: service_is_enabled?(name))
|
||||||
|
|
||||||
%{
|
%{
|
||||||
name: name,
|
name: name,
|
||||||
icon: "puzzle",
|
icon: icon_for_service(name),
|
||||||
enabled: Map.has_key?(enabled_service_names, name),
|
enabled: enabled,
|
||||||
running:
|
running: if(enabled, do: service_is_running?(name), else: nil),
|
||||||
if Map.has_key?(enabled_service_names, name) do
|
description: if(enabled, do: service_description!(name), else: nil),
|
||||||
service_is_running?(name)
|
commands: if(enabled, do: service_commands!(name), else: nil),
|
||||||
else
|
log: [],
|
||||||
nil
|
busy: false
|
||||||
end,
|
|
||||||
description:
|
|
||||||
if Map.has_key?(enabled_service_names, name) do
|
|
||||||
case service_description(name) do
|
|
||||||
{:ok, desc} -> desc
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end,
|
|
||||||
commands: nil,
|
|
||||||
rcvars: nil
|
|
||||||
}
|
}
|
||||||
end)
|
|
||||||
|> Enum.into(%{}, &{&1[:name], &1})
|
|
||||||
|
|
||||||
{:ok, services}
|
|
||||||
|
|
||||||
{:error, {stderr, _code}} ->
|
|
||||||
Logger.error("list_services enabled, log: #{inspect(stderr)}")
|
|
||||||
{:error, stderr}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:error, {stderr, _code}} ->
|
def refresh(service) do
|
||||||
Logger.error("list_services, log: #{inspect(stderr)}")
|
name = service.name
|
||||||
{:error, stderr}
|
|
||||||
end
|
%{
|
||||||
|
service
|
||||||
|
| running: service_is_running?(name),
|
||||||
|
enabled: service_is_enabled?(name)
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_is_running?(name, args \\ []) do
|
def service_is_running?(name, args \\ []) do
|
||||||
|
@ -88,24 +101,44 @@ defmodule Freedive.Api.Service.Cli do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def service_description(name, args \\ []) do
|
def service_status!(name, args \\ []) do
|
||||||
|
case service(name, "onestatus", args) do
|
||||||
|
{:ok, stdout} ->
|
||||||
|
stdout |> String.trim()
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_description!(name, args \\ []) do
|
||||||
case service(name, "onedescribe", args) do
|
case service(name, "onedescribe", args) do
|
||||||
{:ok, stdout} ->
|
{:ok, stdout} ->
|
||||||
stdout = String.trim(stdout)
|
stdout |> String.trim()
|
||||||
{:ok, stdout}
|
|
||||||
|
|
||||||
{:error, {stderr, _code}} ->
|
error ->
|
||||||
{:error, stderr}
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp service(name, action, args) do
|
def service_commands!(name, args \\ []) do
|
||||||
case execute(@service_bin, [name, action] ++ args, doas: true) do
|
case service(name, "oneextracommands", args) do
|
||||||
|
{:ok, stdout} ->
|
||||||
|
stdout |> String.split(" ") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == ""))
|
||||||
|
|
||||||
|
error ->
|
||||||
|
error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp service(name, command, args) do
|
||||||
|
case execute(@service_bin, [name, command] ++ args, doas: true) do
|
||||||
{:ok, stdout} ->
|
{:ok, stdout} ->
|
||||||
# Logger.debug("service, log: #{inspect(stdout)}")
|
# Logger.debug("service, log: #{inspect(stdout)}")
|
||||||
{:ok, stdout}
|
{:ok, stdout}
|
||||||
|
|
||||||
{:error, {stderr, code}} ->
|
{:error, {stderr, code}} ->
|
||||||
|
# Logger.warning("service #{name} #{command}: #{String.trim(stderr)}")
|
||||||
{:error, {stderr, code}}
|
{:error, {stderr, code}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,16 +5,50 @@ defmodule Freedive.Api.Service do
|
||||||
use GenServer
|
use GenServer
|
||||||
require Logger
|
require Logger
|
||||||
import Freedive.Api.Service.Cli
|
import Freedive.Api.Service.Cli
|
||||||
import Freedive.Api.Service.Icons
|
|
||||||
|
@topic "system:service"
|
||||||
|
@events [
|
||||||
|
:command,
|
||||||
|
:refreshed,
|
||||||
|
:stdlog
|
||||||
|
]
|
||||||
|
|
||||||
def start_link(opts) do
|
def start_link(opts) do
|
||||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def topic do
|
||||||
|
@topic
|
||||||
|
end
|
||||||
|
|
||||||
|
def events do
|
||||||
|
@events
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribe do
|
||||||
|
Phoenix.PubSub.subscribe(Freedive.PubSub, @topic)
|
||||||
|
end
|
||||||
|
|
||||||
def list() do
|
def list() do
|
||||||
GenServer.call(__MODULE__, {:list})
|
GenServer.call(__MODULE__, {:list})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def start(name) do
|
||||||
|
GenServer.cast(__MODULE__, {:command, name, "start"})
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop(name) do
|
||||||
|
GenServer.cast(__MODULE__, {:command, name, "stop"})
|
||||||
|
end
|
||||||
|
|
||||||
|
def restart(name) do
|
||||||
|
GenServer.cast(__MODULE__, {:command, name, "restart"})
|
||||||
|
end
|
||||||
|
|
||||||
|
def command(name, command) do
|
||||||
|
GenServer.cast(__MODULE__, {:command, name, command})
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
state = %{opts: opts, services: []}
|
state = %{opts: opts, services: []}
|
||||||
|
@ -24,13 +58,7 @@ defmodule Freedive.Api.Service do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_continue(_opts, state) do
|
def handle_continue(_opts, state) do
|
||||||
{:ok, services} = list_services()
|
state = %{state | services: list_services!()}
|
||||||
services = services
|
|
||||||
|> Enum.map(fn {name, service} ->
|
|
||||||
{name, Map.put(service, :icon, icon_for_service(name))}
|
|
||||||
end)
|
|
||||||
|> Enum.into(%{})
|
|
||||||
state = %{state | services: services}
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -39,4 +67,45 @@ defmodule Freedive.Api.Service do
|
||||||
services = state[:services]
|
services = state[:services]
|
||||||
{:reply, services, state}
|
{:reply, services, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:command, name, command}, state) do
|
||||||
|
broadcast(:command, %{name: name, command: command})
|
||||||
|
broadcast(:stdlog, %{name: name, log: std_to_log("# service #{name} #{command}")})
|
||||||
|
|
||||||
|
case service_command!(name, command) do
|
||||||
|
{:ok, result} ->
|
||||||
|
broadcast(:stdlog, %{name: name, log: std_to_log(result)})
|
||||||
|
|
||||||
|
{:error, {reason, _code}} ->
|
||||||
|
broadcast(:stdlog, %{name: name, log: std_to_log(reason)})
|
||||||
|
end
|
||||||
|
|
||||||
|
if command in ["start", "stop", "restart"] do
|
||||||
|
broadcast(:refreshed, service_details(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast(event, payload) do
|
||||||
|
if event in @events do
|
||||||
|
Phoenix.PubSub.broadcast(
|
||||||
|
Freedive.PubSub,
|
||||||
|
@topic,
|
||||||
|
{event,
|
||||||
|
Map.merge(payload, %{
|
||||||
|
hostname: "unknown"
|
||||||
|
})}
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Logger.error("Service.Server broadcast: unknown event: #{event}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp std_to_log(result) do
|
||||||
|
(result <> "\n")
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,6 +13,10 @@ defmodule FreediveWeb.HomeLive do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_mount_connected(socket) do
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def items() do
|
def items() do
|
||||||
%{
|
%{
|
||||||
"services" => %{
|
"services" => %{
|
||||||
|
|
|
@ -6,6 +6,8 @@ defmodule FreediveWeb.LiliformLive do
|
||||||
defmacro __using__(opts) do
|
defmacro __using__(opts) do
|
||||||
quote location: :keep, bind_quoted: [opts: opts] do
|
quote location: :keep, bind_quoted: [opts: opts] do
|
||||||
use FreediveWeb, :live_view
|
use FreediveWeb, :live_view
|
||||||
|
require Logger
|
||||||
|
|
||||||
@behaviour FreediveWeb.LiliformLive
|
@behaviour FreediveWeb.LiliformLive
|
||||||
@filters_all [
|
@filters_all [
|
||||||
%{
|
%{
|
||||||
|
@ -36,8 +38,13 @@ defmodule FreediveWeb.LiliformLive do
|
||||||
|
|
||||||
socket = assign(socket, :filters, @filters_all ++ filters)
|
socket = assign(socket, :filters, @filters_all ++ filters)
|
||||||
socket = assign(socket, :query, "")
|
socket = assign(socket, :query, "")
|
||||||
|
|
||||||
|
if connected?(socket) do
|
||||||
|
apply(__MODULE__, :on_mount_connected, [socket])
|
||||||
|
else
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def(handle_event("search", %{"value" => query}, socket)) do
|
def(handle_event("search", %{"value" => query}, socket)) do
|
||||||
filtered_items =
|
filtered_items =
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule FreediveWeb.ServiceLive do
|
defmodule FreediveWeb.ServiceLive do
|
||||||
use FreediveWeb.LiliformLive
|
use FreediveWeb.LiliformLive
|
||||||
alias Freedive.Api.Service
|
alias Freedive.Api.Service
|
||||||
|
require Logger
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
@ -14,6 +15,12 @@ defmodule FreediveWeb.ServiceLive do
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def on_mount_connected(socket) do
|
||||||
|
Logger.warning("Mounting ServiceLive")
|
||||||
|
Service.subscribe()
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def items() do
|
def items() do
|
||||||
Service.list()
|
Service.list()
|
||||||
end
|
end
|
||||||
|
@ -42,6 +49,78 @@ defmodule FreediveWeb.ServiceLive do
|
||||||
end)
|
end)
|
||||||
|> Enum.into(%{}, fn {name, item} -> {name, item} end)
|
|> Enum.into(%{}, fn {name, item} -> {name, item} end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("action:" <> action, %{"name" => service_name}, socket)
|
||||||
|
when action in ["start", "stop", "restart"] do
|
||||||
|
case action do
|
||||||
|
"start" -> Service.start(service_name)
|
||||||
|
"stop" -> Service.stop(service_name)
|
||||||
|
"restart" -> Service.restart(service_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("command:" <> command, %{"name" => service_name}, socket) do
|
||||||
|
Service.command(service_name, command)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({event, payload}, socket) do
|
||||||
|
case event do
|
||||||
|
:command ->
|
||||||
|
# Logger.info("ServiceLive: Command: #{inspect(payload)}")
|
||||||
|
|
||||||
|
socket =
|
||||||
|
if socket.assigns.selected_item && payload.name == socket.assigns.selected_item.name do
|
||||||
|
socket |> assign(:selected_item, %{socket.assigns.selected_item | busy: true})
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
:refreshed ->
|
||||||
|
# Logger.info("ServiceLive: Refreshed: #{inspect(payload)}")
|
||||||
|
|
||||||
|
socket =
|
||||||
|
if socket.assigns.selected_item && payload.name == socket.assigns.selected_item.name do
|
||||||
|
socket
|
||||||
|
|> assign(:selected_item, %{
|
||||||
|
socket.assigns.selected_item
|
||||||
|
| running: payload.running,
|
||||||
|
enabled: payload.enabled,
|
||||||
|
busy: false
|
||||||
|
})
|
||||||
|
|> assign(:items, socket.assigns.items |> Map.put(payload.name, payload))
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
:stdlog ->
|
||||||
|
# Logger.info("ServiceLive: Stdlog: #{inspect(payload)}")
|
||||||
|
|
||||||
|
socket =
|
||||||
|
if socket.assigns.selected_item && payload.name == socket.assigns.selected_item.name do
|
||||||
|
socket
|
||||||
|
|> assign(:selected_item, %{
|
||||||
|
socket.assigns.selected_item
|
||||||
|
| log: socket.assigns.selected_item.log ++ payload.log,
|
||||||
|
busy: false
|
||||||
|
})
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Logger.warning("ServiceLive: Unknown event: #{event}, payload: #{inspect(payload)}")
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule FreediveWeb.ServiceLive.Components do
|
defmodule FreediveWeb.ServiceLive.Components do
|
||||||
|
@ -101,6 +180,16 @@ defmodule FreediveWeb.ServiceLive.Components do
|
||||||
</:actions>
|
</:actions>
|
||||||
</.panel_media>
|
</.panel_media>
|
||||||
|
|
||||||
|
<%= if Map.has_key?(@selected_item, :log) and @selected_item.log != [] do %>
|
||||||
|
<.panel_media>
|
||||||
|
<:icon>
|
||||||
|
<.icon for="terminal" color="gray" aria-hidden="true" />
|
||||||
|
</:icon>
|
||||||
|
<:actions></:actions>
|
||||||
|
<pre><code><%= Enum.join(@selected_item.log, "\n") %></code></pre>
|
||||||
|
</.panel_media>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<.panel_media>
|
<.panel_media>
|
||||||
<:icon>
|
<:icon>
|
||||||
<%!-- <.icon for="terminal" color="auto" size="3rem" aria-hidden="true" /> --%>
|
<%!-- <.icon for="terminal" color="auto" size="3rem" aria-hidden="true" /> --%>
|
||||||
|
@ -112,18 +201,31 @@ defmodule FreediveWeb.ServiceLive.Components do
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<button
|
<button
|
||||||
class="button is-warning"
|
class="button is-warning"
|
||||||
|
phx-click="action:restart"
|
||||||
|
phx-value-name={@selected_item.name}
|
||||||
|
{if @selected_item.busy, do: [disabled: true], else: []}
|
||||||
>
|
>
|
||||||
Restart
|
Restart
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="button is-danger">
|
<button
|
||||||
|
class="button is-danger"
|
||||||
|
phx-click="action:stop"
|
||||||
|
phx-value-name={@selected_item.name}
|
||||||
|
{if @selected_item.busy, do: [disabled: true], else: []}
|
||||||
|
>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<button class="button is-success">
|
<button
|
||||||
|
class="button is-success"
|
||||||
|
phx-click="action:start"
|
||||||
|
phx-value-name={@selected_item.name}
|
||||||
|
{if @selected_item.busy, do: [disabled: true], else: []}
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,9 +237,10 @@ defmodule FreediveWeb.ServiceLive.Components do
|
||||||
<%= for command <- @selected_item.commands do %>
|
<%= for command <- @selected_item.commands do %>
|
||||||
<button
|
<button
|
||||||
class="button is-info"
|
class="button is-info"
|
||||||
phx-click="command"
|
phx-disable-with="..."
|
||||||
|
phx-click={"command:#{command}"}
|
||||||
phx-value-name={@selected_item.name}
|
phx-value-name={@selected_item.name}
|
||||||
phx-value-command={command}
|
{if @selected_item.busy, do: [disabled: true], else: []}
|
||||||
>
|
>
|
||||||
<%= command %>
|
<%= command %>
|
||||||
</button>
|
</button>
|
||||||
|
|
Loading…
Reference in a new issue