Compare commits

...

2 commits

Author SHA1 Message Date
01d9c54023 Add service commands 2024-05-18 12:28:34 +05:30
b452dc87d3 Refactor list_services() for clarity 2024-05-18 08:16:27 +05:30
5 changed files with 292 additions and 76 deletions

View file

@ -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

View file

@ -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

View file

@ -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" => %{

View file

@ -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 =

View file

@ -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>