1 |
|
-module(chrme_launcher). |
2 |
|
-behaviour(gen_server). |
3 |
|
|
4 |
|
-export([start/1, start_link/1, await_start/1, stop/1]). |
5 |
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]). |
6 |
|
-export_type([ |
7 |
|
options/0 |
8 |
|
, state/0 |
9 |
|
, name/0 |
10 |
|
]). |
11 |
|
|
12 |
|
-type name() :: term(). |
13 |
|
|
14 |
|
-type options() :: #{ |
15 |
|
name := name() |
16 |
|
, executable => klsn:binstr() |
17 |
|
, remote_port => 0..65535 |
18 |
|
, user_data_dir => klsn:binstr() |
19 |
|
, extra_args => [klsn:binstr()] |
20 |
|
, headless => boolean() |
21 |
|
}. |
22 |
|
|
23 |
|
-type state() :: #{ |
24 |
|
port := port() |
25 |
|
, opts := options() |
26 |
|
}. |
27 |
|
|
28 |
|
-spec start(options()) -> {ok, pid()} | {error, term()}. |
29 |
|
start(Options) -> |
30 |
:-( |
Name = maps:get(name, Options), |
31 |
:-( |
gen_server:start({global, Name}, ?MODULE, Options, []). |
32 |
|
|
33 |
|
-spec start_link(options()) -> {ok, pid()} | {error, term()}. |
34 |
|
start_link(Options) -> |
35 |
4 |
Name = maps:get(name, Options), |
36 |
4 |
gen_server:start_link({global, Name}, ?MODULE, Options, []). |
37 |
|
|
38 |
|
-spec await_start(name()) -> ok. |
39 |
|
await_start(Name) -> |
40 |
|
% This also makes sure server exists. |
41 |
17 |
Options = gen_server:call({global, Name}, get_options), |
42 |
17 |
Host = <<"localhost">>, |
43 |
17 |
Port = maps:get(remote_port, Options), |
44 |
17 |
case chrme_http_apic:version(Host, Port) of |
45 |
|
{ok, _} -> |
46 |
4 |
ok; |
47 |
|
_Error -> |
48 |
13 |
timer:sleep(100), |
49 |
13 |
await_start(Name) |
50 |
|
end. |
51 |
|
|
52 |
|
-spec stop(name()) -> ok. |
53 |
|
stop(Name) -> |
54 |
4 |
gen_server:stop({global, Name}). |
55 |
|
|
56 |
|
-spec init(options()) -> {ok, state()}. |
57 |
|
init(Options0) -> |
58 |
4 |
Options = normalize_options(Options0), |
59 |
4 |
PathBin = maps:get(executable, Options), |
60 |
4 |
Path = binary_to_list(PathBin), |
61 |
4 |
RemotePort = maps:get(remote_port, Options), |
62 |
4 |
RemotePortStr = integer_to_list(RemotePort), |
63 |
4 |
DataDirBin = maps:get(user_data_dir, Options), |
64 |
4 |
DataDir = binary_to_list(DataDirBin), |
65 |
4 |
Headless = maps:get(headless, Options), |
66 |
4 |
ExtraArgsBin = maps:get(extra_args, Options), |
67 |
4 |
ExtraArgs = [binary_to_list(B) || B <- ExtraArgsBin], |
68 |
4 |
BaseArgs = [ |
69 |
|
"--remote-debugging-port=" ++ RemotePortStr, |
70 |
|
"--user-data-dir=" ++ DataDir |
71 |
|
], |
72 |
4 |
HeadlessArgs = case Headless of |
73 |
4 |
true -> ["--headless"]; |
74 |
:-( |
false -> [] |
75 |
|
end, |
76 |
4 |
CmdArgs = BaseArgs ++ HeadlessArgs ++ ExtraArgs, |
77 |
4 |
process_flag(trap_exit, true), |
78 |
4 |
Port = open_port({spawn_executable, Path}, [ |
79 |
|
binary, |
80 |
|
exit_status, |
81 |
|
use_stdio, |
82 |
|
stderr_to_stdout, |
83 |
|
{args, CmdArgs} |
84 |
|
]), |
85 |
4 |
{ok, #{port => Port, opts => Options}}. |
86 |
|
|
87 |
|
% Internal: merge user options with defaults |
88 |
|
-spec normalize_options(map()) -> options(). |
89 |
|
normalize_options(Options) -> |
90 |
4 |
#{ |
91 |
|
name => maps:get(name, Options) |
92 |
|
, executable => maps:get(executable, Options, <<"/usr/bin/google-chrome">>) |
93 |
|
, remote_port => maps:get(remote_port, Options, 9222) |
94 |
|
, user_data_dir => maps:get(user_data_dir, Options, <<"/tmp/chrme_default_user_data_dir">>) |
95 |
|
, extra_args => maps:get(extra_args, Options, []) |
96 |
|
, headless => maps:get(headless, Options, true) |
97 |
|
}. |
98 |
|
|
99 |
|
handle_call(get_options, _From, State) -> |
100 |
17 |
{reply, maps:get(opts, State), State}. |
101 |
|
|
102 |
|
handle_cast(_Msg, State) -> |
103 |
:-( |
{noreply, State}. |
104 |
|
|
105 |
|
handle_info({Port, {data, _Data}}, State = #{port := Port}) -> |
106 |
54 |
{noreply, State}; |
107 |
|
handle_info({Port, {exit_status, Status}}, State = #{port := Port}) -> |
108 |
:-( |
{stop, {chrome_exit, Status}, State}; |
109 |
|
handle_info({'EXIT', Port, Reason}, State = #{port := Port}) -> |
110 |
:-( |
{stop, {port_exit, Reason}, State}; |
111 |
|
handle_info(_Info, State) -> |
112 |
:-( |
{noreply, State}. |
113 |
|
|
114 |
|
terminate(_Reason, #{port := Port}) -> |
115 |
4 |
catch port_close(Port), |
116 |
4 |
ok. |