Skip to content

Commit c36db0a

Browse files
committed
1.2.0 - more options for commands, and shell running
1 parent 299f36a commit c36db0a

File tree

5 files changed

+134
-26
lines changed

5 files changed

+134
-26
lines changed

lib/hemdal/config/command.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ defmodule Hemdal.Config.Command do
44
each alert. The data used is:
55
66
- `name` the name of the script.
7-
- `type` the type of the script could be `line` or `script`.
7+
- `type` the type of the script could be `line`, `shell` or `script`.
88
The first one let us define a command in a single line,
99
the last one let us define a script, a multi-line code which will be
1010
copied and executed in the host, and the last one let us run a command
1111
interacting with a process for providing information in runtime.
12+
The type `shell` is different to the others because it has no command
13+
to run and it should be `interactive`, indeed it configures interactive
14+
by default as `true`.
1215
- `interactive` specify if the command is handled in an interactive way
1316
(default: false).
1417
- `output` specify if the output should be stored (default: true).
18+
- `idle_timeout` specify the time between interactions that the interactive
19+
execution will be kept open in milliseconds (default: 60_000).
20+
- `decode` says if we should decode (JSON) the output or not (default: true).
1521
- `command` as the command line to be executed. It could be only one line
1622
or a multi-line script.
1723
"""
@@ -21,6 +27,8 @@ defmodule Hemdal.Config.Command do
2127
field(:type, :string)
2228
field(:interactive, :boolean, default: false)
2329
field(:output, :boolean, default: true)
24-
field(:command, :string)
30+
field(:idle_timeout, :integer, default: :timer.minutes(1))
31+
field(:decode, :boolean, default: true)
32+
field(:command, :string, default: nil)
2533
end
2634
end

lib/hemdal/host.ex

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ defmodule Hemdal.Host do
417417
defp run_in_background(caller, cmd, args, send_result, %__MODULE__{host: %_{module: mod} = host}) do
418418
mod.transaction(host, fn handler ->
419419
with {:ok, errorlevel, output} <- exec_cmd(handler, mod, caller, cmd, args),
420-
{:ok, %{"status" => status} = data} <- decode(output) do
420+
{:ok, %{"status" => status} = data} <- decode(output, errorlevel, cmd.decode) do
421421
Logger.debug("command exit(#{errorlevel}) output: #{inspect(data)}")
422422
run_result(data, errorlevel, status)
423423
else
@@ -430,16 +430,38 @@ defmodule Hemdal.Host do
430430
:ok
431431
end
432432

433-
defp decode(output) do
433+
defp decode(nil, 0, _), do: {:ok, %{"status" => "OK", "errorlevel" => 0}}
434+
435+
defp decode(nil, errorlevel, _) do
436+
{:error, %{"status" => "UNKNOWN", "errorlevel" => errorlevel}}
437+
end
438+
439+
defp decode(output, 0, false) when is_binary(output) do
440+
{:ok, %{"status" => "OK", "message" => output}}
441+
end
442+
443+
defp decode(output, 0, false) do
444+
{:ok, %{"status" => "OK", "message" => "#{inspect(output)}"}}
445+
end
446+
447+
defp decode(output, errorlevel, false) when is_binary(output) do
448+
{:ok, %{"status" => "OK", "message" => output, "errorlevel" => errorlevel}}
449+
end
450+
451+
defp decode(output, errorlevel, false) do
452+
{:ok, %{"status" => "OK", "message" => "#{inspect(output)}", "errorlevel" => errorlevel}}
453+
end
454+
455+
defp decode(output, errorlevel, true) do
434456
case Jason.decode(output) do
435457
{:ok, [status, message]} ->
436-
{:ok, %{"status" => status, "message" => message}}
458+
{:ok, %{"status" => status, "message" => message, "errorlevel" => errorlevel}}
437459

438460
{:ok, status} when is_binary(status) ->
439-
{:ok, %{"status" => status}}
461+
{:ok, %{"status" => status, "errorlevel" => errorlevel}}
440462

441463
{:error, %Jason.DecodeError{data: error}} ->
442-
{:error, %{"message" => error, "status" => "UNKNOWN"}}
464+
{:error, %{"message" => error, "status" => "UNKNOWN", "errorlevel" => errorlevel}}
443465

444466
other_resp ->
445467
other_resp
@@ -453,12 +475,18 @@ defmodule Hemdal.Host do
453475
|> String.pad_leading(7, "0")
454476
end
455477

478+
defp exec_cmd(handler, mod, caller, %Command{type: "shell"} = cmd, _args) do
479+
opts = [output: cmd.output, timeout: cmd.idle_timeout, command: cmd.command]
480+
mod.shell(handler, caller, opts)
481+
end
482+
456483
defp exec_cmd(handler, mod, _caller, %Command{type: "line", command: command, interactive: false}, _args) do
457484
mod.exec(handler, command)
458485
end
459486

460-
defp exec_cmd(handler, mod, caller, %Command{type: "line", command: command}, _args) do
461-
mod.exec_interactive(handler, command, caller)
487+
defp exec_cmd(handler, mod, caller, %Command{type: "line", command: command} = cmd, _args) do
488+
opts = [output: cmd.output, timeout: cmd.idle_timeout]
489+
mod.exec_interactive(handler, command, caller, opts)
462490
end
463491

464492
defp exec_cmd(handler, mod, _caller, %Command{type: "script", command: script, interactive: false}, args) do
@@ -479,7 +507,7 @@ defmodule Hemdal.Host do
479507
end
480508
end
481509

482-
defp exec_cmd(handler, mod, caller, %Command{type: "script", command: script}, args) do
510+
defp exec_cmd(handler, mod, caller, %Command{type: "script", command: script} = command, args) do
483511
tmp_file = Path.join([@default_temporal_dir, random_string()])
484512

485513
sh =
@@ -490,7 +518,8 @@ defmodule Hemdal.Host do
490518

491519
mod.write_file(handler, tmp_file, script)
492520
cmd = Enum.join([sh, tmp_file | args], " ")
493-
mod.exec_interactive(handler, cmd, caller)
521+
opts = [output: command.output, timeout: command.idle_timeout]
522+
mod.exec_interactive(handler, cmd, caller, opts)
494523
end
495524

496525
@typedoc """
@@ -552,13 +581,31 @@ defmodule Hemdal.Host do
552581
"""
553582
@callback exec(handler(), command()) :: {:ok, errorlevel(), output()} | {:error, reason()}
554583

584+
@typedoc """
585+
The options for `exec_interactive/4` and `shell/2` let us define if we want to
586+
accumulate the output (it's usually not desirable for shell sessions) and the
587+
timeout for idle. It means the time it wait between interactions to close the
588+
communication.
589+
"""
590+
@type exec_mod_opts() :: [
591+
output: boolean(),
592+
timeout: timeout()
593+
]
594+
555595
@doc """
556596
Exec an interactive command implemented by the module where it's
557597
implemented. The `c:exec_interactive/3` command is getting a handler from the
558598
transaction and the command to be executed as a string.
559599
"""
560-
@callback exec_interactive(handler(), command(), pid()) ::
561-
{:ok, errorlevel(), output()} | {:error, reason()}
600+
@callback exec_interactive(handler(), command(), pid(), exec_mod_opts()) ::
601+
{:ok, pid()} | {:ok, errorlevel(), output()} | {:error, reason()}
602+
603+
@doc """
604+
Start a shell and connect to it. The shell usually has specific features that let
605+
us to do more than in normal or usual exec commands.
606+
"""
607+
@callback shell(handler(), pid(), exec_mod_opts()) ::
608+
{:ok, pid()} | {:ok, errorlevel(), output()} | {:error, reason()}
562609

563610
@doc """
564611
Write a file in the remote (or local) host. It's intended to write the

lib/hemdal/host/local.ex

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ defmodule Hemdal.Host.Local do
88
"""
99
use Hemdal.Host
1010

11+
@default_idle_timeout :timer.minutes(1)
12+
1113
@impl Hemdal.Host
1214
@doc """
1315
Run locally a command. It's using `System.shell/2` for achieving that.
@@ -18,33 +20,51 @@ defmodule Hemdal.Host.Local do
1820
end
1921

2022
@impl Hemdal.Host
21-
def exec_interactive(_opts, command, caller) when is_pid(caller) do
23+
def exec_interactive(_opts, command, caller, opts) when is_pid(caller) do
2224
port = Port.open({:spawn, command}, [:binary])
2325
send(caller, {:start, self()})
24-
get_and_send_all(port, caller, "")
26+
output = if(opts[:output], do: "")
27+
opts = [{:echo, false} | opts]
28+
get_and_send_all(port, caller, output, opts)
29+
end
30+
31+
@impl Hemdal.Host
32+
def shell(_opts, caller, opts) when is_pid(caller) do
33+
port = Port.open({:spawn, opts[:command]}, [:binary])
34+
send(caller, {:start, self()})
35+
output = if(opts[:output], do: "")
36+
opts = [{:echo, true} | opts]
37+
get_and_send_all(port, caller, output, opts)
2538
end
2639

27-
defp get_and_send_all(port, pid, output) do
40+
defp get_and_send_all(port, caller, output, opts) do
2841
receive do
2942
{:data, data} ->
3043
send(port, {self(), {:command, data}})
31-
get_and_send_all(port, pid, output)
3244

33-
:close ->
34-
send(port, {self(), :close})
35-
get_and_send_all(port, pid, output)
45+
if opts[:echo] do
46+
send(caller, {:continue, data})
47+
get_and_send_all(port, caller, output <> data, opts)
48+
else
49+
get_and_send_all(port, caller, output, opts)
50+
end
3651

3752
{^port, {:data, data}} ->
38-
send(pid, {:continue, data})
39-
get_and_send_all(port, pid, output <> data)
53+
send(caller, {:continue, data})
54+
output = if(output, do: output <> data)
55+
get_and_send_all(port, caller, output, opts)
56+
57+
:close ->
58+
send(port, {self(), :close})
59+
get_and_send_all(port, caller, output, opts)
4060

4161
{^port, :closed} ->
42-
send(pid, :closed)
62+
send(caller, :closed)
4363
{:ok, 0, output}
4464
after
45-
60_000 ->
65+
opts[:timeout] || @default_idle_timeout ->
4666
Port.close(port)
47-
send(pid, :closed)
67+
send(caller, :closed)
4868
{:ok, 127, output}
4969
end
5070
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Hemdal.MixProject do
22
use Mix.Project
33

4-
@version "1.1.0"
4+
@version "1.2.0"
55

66
def project do
77
[

test/hemdal/host_test.exs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,37 @@ defmodule Hemdal.HostTest do
237237

238238
assert_receive {:ok, %{"message" => "hello world!"}}, 5_000
239239
end
240+
241+
test "run shell" do
242+
assert :ok = Hemdal.Host.reload_all()
243+
assert [host_id, _] = Hemdal.Host.get_all()
244+
245+
echo = %Hemdal.Config.Command{
246+
name: "hello world!",
247+
type: "shell",
248+
interactive: true,
249+
command: "cat"
250+
}
251+
252+
assert {:ok, runner} = Hemdal.Host.exec_background(host_id, echo)
253+
assert is_pid(runner)
254+
255+
assert_receive {:start, worker}
256+
assert is_pid(worker)
257+
refute runner != worker
258+
259+
send(worker, {:data, ~s|{"status": "OK",\n|})
260+
assert_receive {:continue, ~s|{"status": "OK",\n|}
261+
send(worker, {:data, ~s| "message": "hello world!"}\n|})
262+
assert_receive {:continue, ~s| "message": "hello world!"}\n|}
263+
send(worker, {:data, <<4>>})
264+
assert_receive {:continue, "\x04"}
265+
assert_receive {:continue, ~s|{"status": "OK",\n "message": "hello world!"}\n\x04|}
266+
send(worker, :close)
267+
assert_receive :closed
268+
269+
message = ~s|{"status": "OK",\n "message": "hello world!"}\n\x04|
270+
received_message = message <> message
271+
assert_receive {:error, %{"message" => ^received_message}}, 5_000
272+
end
240273
end

0 commit comments

Comments
 (0)