A console on your running app
A console lets you type Gleam into your app while it runs: look up a user,
grant a role, sign someone out, or call a function you just wrote to see what
it does. GSH is a Gleam REPL that can attach to a
running Erlang node, and howdy/console gives it the values your main built,
so what you type works on the same database connection, auth configuration
and caches as the server's own requests.
Unlike rails console, which starts a second copy of the app, this runs inside
the server itself. A role you grant is seen by the next request, and
processes, ETS tables and caches are the live ones.
Install GSH
GSH is a development tool, so add it as a dev dependency:
gleam add gsh --dev
List the modules you want imported in every session in gleam.toml:
[tools.gsh]
imports = ["console", "gleam/list", "gleam/option", "gleam/result"]
Expose what main builds
Code typed into the console can call any module, but the values main
built, such as the database connection, exist only as its local variables.
Put the ones a console needs in a record, declare a key for it, and expose it
before starting the server:
import howdy/console
pub type Services {
Services(db: Repo, identity: auth.Auth, permissions: access.Authorization)
}
pub fn services_key() -> console.Key(Services) {
console.key("my_app")
}
pub fn main() {
let db = database.connect()
let assert Ok(identity) = auth.new(repo: db, ...)
let assert Ok(permissions) = access.new(db)
console.expose(services_key(), Services(db:, identity:, permissions:))
let assert Ok(_) = howdy.start(app(db, identity, permissions))
process.sleep_forever()
}
Declare each key once, in a function with its type written out. Two keys with the same name and different types would read each other's values as the wrong type.
howdy dev starts your app from dev/my_app_dev.gleam rather than main,
so expose the services there too if you want a console on the development
server.
Exposing a value costs almost nothing to read later, but replacing one makes the VM scan every process. Expose values once at startup, never per request.
Write your shortcuts
The console is more pleasant with a module of shortcuts for the jobs you do
often. Keep it in src/console.gleam, so its name is short to type:
import howdy/auth/user
import howdy/authorization as access
import howdy/console as exposed
import my_app
// Recorded as the client of every audit event the console causes.
const actor = user.SystemFrom(client: "console")
pub fn services() -> my_app.Services {
let assert Ok(services) = exposed.get(my_app.services_key())
as "the server is not running on this node"
services
}
pub fn grant(user_id: String, role: String) {
access.assign(services().permissions, user_id, role, access.Global, by: actor)
}
Operations like these are privileged and check nothing themselves. That is right for a console, because only the people who run the app can reach it.
Run the app inside the console
The quickest start runs the server and the console in one VM. Pass the
module whose main starts your app:
gleam run -m gsh -- my_app
GSH starts main in the background and opens a prompt. The server is on its
usual port, so you can use the site in a browser and the console at once:
gsh(1)> console.grant("s8nOmB5I6M8Q...", "reader")
Ok(Nil) : Result(Nil, Error)
After you change the code, :cc recompiles it and reloads it into the
running VM.
Attach to a server that is already running
To attach to a server started on its own, start it as a named Erlang node.
ERL_FLAGS passes the flags to the VM without changing your code, and works
the same with gleam run and howdy dev:
ERL_EPMD_ADDRESS=127.0.0.1 \
ERL_FLAGS="-sname my_app@localhost -setcookie dev -kernel inet_dist_use_interface {127,0,0,1}" \
gleam run
Then attach from a second terminal in the same project:
ERL_FLAGS="-kernel inet_dist_use_interface {127,0,0,1}" \
gleam run -m gsh -- --remsh my_app@localhost --cookie dev
GSH prints Connected to remote node: my_app@localhost, and everything you
type from then on runs inside the server. It compiles each line on your side,
so the project must be built from the same code the server is running.
If it cannot connect, GSH carries on with a local session, and your shortcuts report that the server is not running on this node. Check that both sides use the same node name and cookie.
Keep the node private
Anyone who can connect to an Erlang node can run any code in it, with the same access as your app. The cookie is the only thing that stops them, so the node must not be reachable by anyone who should not have that power.
The commands above keep everything on your own machine:
-
@localhostin the node name makes GSH connect over loopback. A bare-sname my_appuses your hostname, which can resolve to a network address. -
inet_dist_use_interfacemakes the VM listen for other nodes on127.0.0.1only, rather than on every interface. -
ERL_EPMD_ADDRESSdoes the same for EPMD, the port mapper nodes use to find each other. It only applies when it starts EPMD: one already running from an earlier node keeps listening where it was started, until you stop it withepmd -kill.
Never expose Erlang distribution to the internet. To reach a server on another machine, use a long random cookie kept out of source control, and connect through a private network or an SSH tunnel. Treat console access like root access to the database.
Pause a request and look inside
GSH can also stop a process at a point in your code and let you evaluate
expressions inside it, with its variables in scope. Declare its pry
function and call it where you want to stop:
@external(erlang, "gsh_pry", "pry")
fn pry(value: a, label: String) -> a
fn show(ctx) {
let current = pry(ctx.guard.user, "current")
controller.json(ctx, user.to_json(current))
}
When a request reaches it, the console announces it. :pry attaches,
and :continue lets the request carry on. A pry call only pauses while a GSH
session runs in the same VM, so leave none in code you deploy.
More examples
The authentication examples
expose their services this way and include a console module for users,
roles and sessions.
