Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add consistency to output handling in runtime code execution #583

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions docs/extensibility/release_tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
Once your release is built, you can execute a number of helpful commands from the entry script.

If you've built a release for `#!elixir :myapp`, you can run `bin/myapp` to see the full list of commands.

## Release tasks

| Task | Description |
|:--------------------|:--------------------------------------------------------------------------------|
| start | Start myapp as a daemon |
| start_boot <file> | Start myapp as a daemon, but supply a custom .boot file |
| foreground | Start myapp in the foreground<br>This is similar to running `mix run --no-halt` |
| console | Start myapp with a console attached<br>This is similar to running `iex -S mix` |
| console_clean | Start a console with code paths set but no apps loaded/started |
| console_boot <file> | Start myapp with a console attached, but supply a custom .boot file |
| stop | Stop the myapp daemon |
| restart | Restart the myapp daemon without shutting down the VM |
| reboot | Restart the myapp daemon |
| upgrade <version> | Upgrade myapp to <version> |
| downgrade <version> | Downgrade myapp to <version> |
| attach | Attach the current TTY to myapp's console |
| remote_console | Remote shell to myapp's console |
| reload_config | Reload the current system's configuration from disk |
| pid | Get the pid of the running myapp instance |
| ping | Checks if myapp is running, pong is returned if successful |
| pingpeer <peer> | Check if a peer node is running, pong is returned if successful |
| escript | Execute an escript |
| rpc | Execute Elixir code on the running node |
| eval | Execute Elixir code locally |
| describe | Print useful information about the myapp release |

## Running code in releases

Distillery comes with two very useful tasks for working with your release.

* Both will take some Elixir code and run it for you and show you the result.
* Both tasks use the exact same syntax, the only difference is the context your code is run in.

### Running code with `rpc`

This task executes code on the running node, and is what you'd want to use to interact with your application when _it's already running_.

!!! example "Using Module.fun/arity"
```bash tab="Command"
$ ./bin/myapp rpc --mfa "Application.loaded_applications/0"
```

```elixir tab="Output"
[
{:lz_string,
'Elixir implementation of pieroxy\'s lz-string compression algorithm.',
'0.0.7'},
{:phoenix_html,
...snip
```

??? example "Using Module.fun/arity with arguments"
```bash tab="Commands"
$ ./bin/myapp rpc --mfa "Application.put_env/3" -- myapp token supersecret
$ ./bin/myapp rpc --mfa "Application.get_env/2" -- myapp token
```

```bash tab="Output"
$ ./bin/myapp rpc --mfa "Application.put_env/3" -- myapp token supersecret
:ok
$ ./bin/myapp rpc --mfa "Application.get_env/2" -- myapp token
"supersecret"
```

??? example "Using Module.fun/1 with arguments as a single list"
Here, the `--argv` option can be used to construct a list before passing your arguments to the function specified.

```bash tab="Command"
$ ./bin/myapp rpc --mfa "Enum.join/1" --argv -- foo bar baz
```

```elixir tab="Output"
"foobarbaz"
```

??? example "Using an expression, getting application version"
You can also use an expression, but you'll need to be mindful of shell quoting.

```bash tab="Command"
$ ./bin/myapp rpc 'Application.spec(:myapp, :vsn)'
```

```elixir tab="Output"
'0.0.1'
```

??? example "Using an expression, broadcasting to a Phoenix channel"
```bash
$ ./bin/myapp rpc 'MyappWeb.Endpoint.broadcast!("channel:lobby", "status", %{current_status: "oopsy"})'
```

### Running code with `eval`

This task executes code locally in a clean instance. Although execution will be within the context of your release, no applications will have been started. This is very useful for things like migrations, where you'll want to start only some applications (e.g. Ecto) manually before doing some work.

!!! example "Using Module.fun/arity"
Assuming that you've created a `Myapp.ReleaseTasks` module, you can call it into like so:
```
$ ./bin/myapp eval --mfa 'Myapp.ReleaseTasks.migrate/0'
```

??? example "Using Module.fun/arity with arguments"
Like with `rpc`, arguments can be specified (but are generally treated as strings).

```bash
$ ./bin/myapp eval --mfa 'File.touch/1' -- /tmp/foo
:ok
```

??? example "Using an expression"
Like with `rpc`, an expression can be used.

```bash tab="Command"
$ ./bin/myapp eval 'Timex.Duration.now |> Timex.format_duration(:humanized)'
```

```elixir tab="Output"
"48 years, 10 months, 2 weeks, 2 days, 4 hours, 8 minutes, 52 seconds, 883.16 milliseconds"
```
3 changes: 1 addition & 2 deletions docs/tooling/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ entry point, i.e. the `bin/myapp` script.

There are numerous tasks available, you have already seen a few of them:

* `foreground` - run the release in the foreground, like `mix run --no-halt`
* `console` - run the release with a shell attached, like `iex -S mix`
* `start` - run the release in the background

Expand All @@ -40,4 +39,4 @@ There are a few other important tasks:
* `remote_console` - attach a shell to a running release
* `describe` - print metadata about the release

To see a full listing of tasks available, run `bin/myapp` with no arguments.
To see a full listing of tasks available, run `bin/myapp` with no arguments, or refer to the [Release Tasks](/extensibility/release_tasks.md) reference page.
12 changes: 9 additions & 3 deletions lib/mix/lib/releases/runtime/control.ex
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,9 @@ defmodule Mix.Releases.Runtime.Control do
Executes an expression or a file locally (i.e. not on the running node)
"""
def eval(_argv, %{file: file}) do
Code.eval_file(file)
file
|> Code.eval_file()
|> IO.inspect()
rescue
err in [Code.LoadError] ->
Console.error("""
Expand Down Expand Up @@ -714,7 +716,9 @@ defmodule Mix.Releases.Runtime.Control do
[argv]
end

apply(module, fun, args)
module
|> apply(fun, args)
|> IO.inspect()

{:ok, [_module, _fun, _arity]} when use_argv? ->
Console.error("""
Expand All @@ -736,7 +740,9 @@ defmodule Mix.Releases.Runtime.Control do
but the function has a different arity!
""")
else
apply(module, fun, args)
module
|> apply(fun, args)
|> IO.inspect()
end

{:ok, _parts} ->
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ nav:
- 'Release Plugins': 'extensibility/release_plugins.md'
- 'Config Providers': 'extensibility/config_providers.md'
- 'Custom Commands': 'extensibility/custom_commands.md'
- 'Release Tasks': 'extensibility/release_tasks.md'
- 'Overlays': 'extensibility/overlays.md'
- 'Boot Hooks': 'extensibility/boot_hooks.md'
- 'Shell Scripts': 'extensibility/shell_scripts.md'
Expand Down
18 changes: 18 additions & 0 deletions test/cases/cli_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ defmodule Distillery.Test.CliTest do
end) =~ "[\"foo\", \"bar\"]"
end

test "eval --mfa --argv outputs result" do
assert is_success(fn ->
Control.main(["eval", "--mfa", "Enum.join/1", "--argv", "--", "foo", "bar"])
end) =~ "foobar"
end

test "eval --mfa correct args" do
assert is_success(fn ->
Control.main(["eval", "--mfa", "Distillery.Test.Tasks.run/2", "--", "foo", "bar"])
Expand All @@ -75,12 +81,24 @@ defmodule Distillery.Test.CliTest do
end) =~ "function has a different arity!"
end

test "eval --mfa outputs result" do
assert is_success(fn ->
Control.main(["eval", "--mfa", "Distillery.Test.Tasks.eval/2", "--", "foo", "bar"])
end) =~ "{\"foo\", \"bar\"}"
end

test "eval --file" do
assert is_success(fn ->
Control.main(["eval", "--file", Path.join([@fixtures_path, "files", "eval_file_example.exs"]) |> Path.expand])
end) =~ "ok from [email protected]\n"
end

test "eval --file outputs result" do
assert is_success(fn ->
Control.main(["eval", "--file", Path.join([@fixtures_path, "files", "eval_file_example_noout.exs"]) |> Path.expand])
end) =~ "{{:ok, \"done\"}, []}\n"
end

test "eval syntax error produces friendly error" do
assert is_failure(fn ->
Control.main(["eval", "div(2, 0)"])
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/files/eval_file_example_noout.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule FooTuple do
def do_something, do: {:ok, "done"}
end

FooTuple.do_something
4 changes: 4 additions & 0 deletions test/support/tasks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ defmodule Distillery.Test.Tasks do
def run(arg1, arg2) do
IO.inspect(arg1: arg1, arg2: arg2)
end

def eval(arg1, arg2) do
{arg1, arg2}
end
end