Current section
Files
Jump to
Current section
Files
lib/webengine_kiosk.ex
defmodule WebengineKiosk do
use GenServer
alias WebengineKiosk.{Message, Options}
require Logger
def child_spec({opts, genserver_opts}) do
id = genserver_opts[:id] || __MODULE__
%{
id: id,
start: {__MODULE__, :start_link, [opts, genserver_opts]}
}
end
def child_spec(opts) do
child_spec({opts, []})
end
@moduledoc """
Control a fullscreen web browser using Elixir for use in a kiosk
"""
@doc """
Start the kiosk.
The kiosk starts fullscreen and goes to a default local web page. To change
this, set one or more options:
* `background_color: color` - specify a background color as #RRGGBB or by name
* `blank_image: path` - specify a path to an image for when the screen is blanked
* `data_dir: path` - specify a writable path for data files
* `debug_keys: boolean` - enable key combinations useful for debugging
* `fullscreen: boolean` - show fullscreen
* `gid: gid` - run the browser with this group id
* `homepage: url` - load this page first. For local files, specify `file:///path/to/index.html`
* `monitor: index` - select the monitor for the web browser (0, 1, etc.)
* `opengl: "gl" | "gles" | "software" | "auto"` - specify the OpenGL backend. This is only a hint.
* `progress: boolean` - show a progress bar when loading pages
* `run_as_root: boolean` - set to true if you really want to run Chromium as root
* `sounds: boolean` - play sounds on clicks
* `uid: uid` - run the browser as this user
Untested:
* `clear_cache: boolean`
* `proxy_enable: boolean` - enable/disable proxy support
* `proxy_system: ` -
* `proxy_host: hostname` - the host to connect to for using a proxy
* `proxy_port: port` - the port to connect to on the proxy
* `proxy_username: username` - a username for the proxy
* `proxy_password: password` - a password for the proxy
* `stay_on_top: boolean` -
* `window_clicked_sound: url` - a sound to play when the window is clicked
* `link_clicked_sound: url` - a sound to play when a link is clicked
* `hide_cursor: boolean` - show or hide the mouse pointer
* `javascript: boolean` - enable or disable Javascript support
* `javascript_can_open_windows: boolean` - allow Javascript to open windows
* `width: pixels` - when not fullscreen, the window is this width
* `height: pixels` - when not fullscreen, the window is this height
"""
@spec start_link(Keyword.t(), GenServer.options()) :: {:ok, pid} | {:error, term}
def start_link(args, genserver_opts \\ []) do
with :ok <- Options.check_args(args) do
GenServer.start_link(__MODULE__, args, genserver_opts)
end
end
@doc """
Stop the kiosk
"""
@spec stop(GenServer.server()) :: :ok
def stop(server) do
GenServer.stop(server)
end
@doc """
Blank the screen
The web browser will be replaced by a screen with the `blank_image`. If
someone clicks or taps on the screen then a wakeup message will be sent.
While the screen is in the blank state, it can still accept requests to go to
other URLs.
"""
@spec blank(GenServer.server()) :: :ok
def blank(server) do
GenServer.call(server, {:blank, true})
end
@doc """
Unblank the screen
Show the web browser again.
"""
@spec unblank(GenServer.server()) :: :ok
def unblank(server) do
GenServer.call(server, {:blank, false})
end
@doc """
Request that the browser go to the homepage.
"""
@spec go_home(GenServer.server()) :: :ok | {:error, term}
def go_home(server) do
GenServer.call(server, :go_home)
end
@doc """
Request that the browser go to the specified URL.
"""
@spec go_to_url(GenServer.server(), String.t()) :: :ok | {:error, term}
def go_to_url(server, url) do
GenServer.call(server, {:go_to_url, url})
end
@doc """
Run Javascript in the browser.
"""
@spec run_javascript(GenServer.server(), String.t()) :: :ok | {:error, term}
def run_javascript(server, code) do
GenServer.call(server, {:run_javascript, code})
end
@doc """
Reload the current page.
"""
@spec reload(GenServer.server()) :: :ok | {:error, term()}
def reload(server), do: GenServer.call(server, :reload)
@doc """
Go to the previously visited page.
"""
@spec back(GenServer.server()) :: :ok | {:error, term()}
def back(server), do: GenServer.call(server, :back)
@doc """
Go forward in history.
"""
@spec forward(GenServer.server()) :: :ok | {:error, term()}
def forward(server), do: GenServer.call(server, :forward)
@doc """
Stop loading the current page.
"""
@spec stop_loading(GenServer.server()) :: :ok | {:error, term()}
def stop_loading(server), do: GenServer.call(server, :stop_loading)
@doc """
Set the zoom factor for displaying the page.
"""
@spec set_zoom(GenServer.server(), number()) :: :ok | {:error, term()}
def set_zoom(server, factor) when is_number(factor) and factor > 0 do
GenServer.call(server, {:set_zoom, factor})
end
def init(args) do
priv_dir = :code.priv_dir(:webengine_kiosk)
cmd = Path.join(priv_dir, "kiosk")
if !File.exists?(cmd) do
_ = Logger.error("Kiosk port application is missing. It should be at #{cmd}.")
raise "Kiosk port missing"
end
all_options = Options.add_defaults(args)
cmd_args =
all_options
|> Enum.flat_map(fn {key, value} -> ["--#{key}", to_string(value)] end)
set_permissions(all_options)
homepage = Keyword.get(all_options, :homepage)
port =
Port.open({:spawn_executable, cmd}, [
{:args, cmd_args},
{:cd, priv_dir},
{:packet, 2},
:use_stdio,
:binary,
:exit_status
])
{:ok, %{port: port, homepage: homepage}}
end
def handle_call(:go_home, _from, state) do
send_port(state, Message.go_to_url(state.homepage))
{:reply, :ok, state}
end
def handle_call({:go_to_url, url}, _from, state) do
send_port(state, Message.go_to_url(url))
{:reply, :ok, state}
end
def handle_call({:run_javascript, code}, _from, state) do
send_port(state, Message.run_javascript(code))
{:reply, :ok, state}
end
def handle_call({:blank, yes}, _from, state) do
send_port(state, Message.blank(yes))
{:reply, :ok, state}
end
def handle_call(:reload, _from, state) do
send_port(state, Message.reload())
{:reply, :ok, state}
end
def handle_call(:back, _from, state) do
send_port(state, Message.go_back())
{:reply, :ok, state}
end
def handle_call(:forward, _from, state) do
send_port(state, Message.go_forward())
{:reply, :ok, state}
end
def handle_call(:stop_loading, _from, state) do
send_port(state, Message.stop_loading())
{:reply, :ok, state}
end
def handle_call({:set_zoom, factor}, _from, state) do
send_port(state, Message.set_zoom(factor))
{:reply, :ok, state}
end
def handle_info({_, {:data, raw_message}}, state) do
raw_message
|> Message.decode()
|> handle_browser_message(state)
end
def handle_info({port, {:exit_status, 0}}, %{port: port} = state) do
_ = Logger.info("webengine_kiosk: normal exit from port")
{:stop, :normal, state}
end
def handle_info({port, {:exit_status, status}}, %{port: port} = state) do
_ = Logger.error("webengine_kiosk: unexpected exit from port: #{status}")
{:stop, :unexpected_exit, state}
end
defp handle_browser_message({:browser_crashed, reason, _exit_status}, state) do
_ =
Logger.error(
"webengine_kiosk: browser crashed: #{inspect(reason)}. Going home and hoping..."
)
# Try to recover by going back home
send_port(state, Message.go_to_url(state.homepage))
{:noreply, state}
end
defp handle_browser_message({:console_log, log}, state) do
_ = Logger.warn("webengine_kiosk(stderr): #{log}")
{:noreply, state}
end
defp handle_browser_message(message, state) do
_ = Logger.debug("webengine_kiosk: received #{inspect(message)}")
{:noreply, state}
end
defp send_port(state, message) do
send(state.port, {self(), {:command, message}})
end
defp set_permissions(opts) do
# Check if we are on a raspberry pi
if File.exists?("/dev/vchiq") do
File.chgrp("/dev/vchiq", 28)
File.chmod("/dev/vchiq", 0o660)
end
if data_dir = Keyword.get(opts, :data_dir) do
File.mkdir_p(data_dir)
if uid = Keyword.get(opts, :uid) do
chown(data_dir, uid)
end
end
end
defp chown(file, uid) when is_binary(uid) do
case System.cmd("id", ["-u", uid]) do
{uid, 0} ->
uid =
String.trim(uid)
|> String.to_integer()
chown(file, uid)
_ ->
:error
end
end
defp chown(file, uid) when is_integer(uid) do
File.chown(file, uid)
end
end