1%%
2%%  wings_console.erl --
3%%
4%%     Console for Wings.
5%%
6%%  Copyright (c) 2004-2011 Raimo Niskanen
7%%
8%%  See the file "license.terms" for information on usage and redistribution
9%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
10%%
11%%     $Id$
12%%
13
14-module(wings_console).
15
16%% I/O server and console server
17-export([start_link/1,init/2,get_pid/0,stop/0,stop/1,
18	 setopts/1,getopts/1]).
19
20%% Also duplicates as event_handler/logger_handler for process crashes
21-export([init/1, handle_event/2, handle_info/2]).
22-export([log/2]).
23
24%% Wings window
25-export([window/0,window/4,popup_window/0]).
26
27-define(SERVER_NAME, ?MODULE).
28-define(WIN_NAME, console).
29-define(DIRTY_TIME, 250).
30
31-define(NEED_OPENGL, 1).
32-define(NEED_ESDL, 1).
33-include("wings.hrl").
34-import(lists, [reverse/1]).
35
36%% Debug exports
37-export([code_change/0,get_state/0]).
38
39%% Internal exports
40-export([do_code_change/3]).
41
42%% Slim Event handler for error logging and forwarding events to wings process.
43init(_Type) ->
44    {ok, #{state=>normal}}.
45
46handle_event({info_report,_,{_,progress,_}}, St) ->
47    {ok, St};
48handle_event(_, #{state:=shutdown}=St) ->
49    {ok, St};
50handle_event({error_report,_GL,{_,supervisor_report,Report}}, #{state:=normal}=St) ->
51    Reason = proplists:get_value(reason, Report),
52    Context = proplists:get_value(errorContext, Report),
53    if Reason =:= shutdown  -> {ok, #{state=>shutdown}};
54       Reason =:= normal    -> {ok, #{state=>shutdown}};
55       Context =:= shutdown -> {ok, #{state=>shutdown}};
56       Context =:= child_terminated -> {ok, #{state=>shutdown}};
57       true ->
58            Off = proplists:get_value(offender, Report),
59            case proplists:get_value(restart_type, Off) of
60                permanent ->
61                    log_error(Off, Reason, St);
62                _Other ->
63                    Pid = proplists:get_value(pid, Off),
64                    wings ! {'EXIT', Pid, Reason},
65                    log_error(Off, Reason, St)
66            end
67    end;
68handle_event({error_report,_GL,{_Pid,crash_report,[Report,[]]}}, #{state:=normal}=St) ->
69    Error = proplists:get_value(error_info, Report),
70    log_error(Report, Error, St);
71handle_event({error_report,_GL,{Pid,crash_report,Report}}, #{state:=normal}=St) ->
72    log_error(Pid, Report, St),
73    {ok, St#{error=>Pid}};
74handle_event({_Type, _GL, _Msg}, State) ->
75    %% io:format("~p:~p:~p ~p ~p~n", [?MODULE, ?LINE, State, _Msg, Type]),
76    {ok, State}.
77
78handle_info(_, State)  ->
79    {ok, State}.
80
81log_error(_Off, _, #{error:=_} = St) ->
82    %% Already wrote one crash dump
83    {ok, St};
84log_error(Off, {exit, {Reason, Stacktrace}, [_|_]}, St) ->
85    log_error(Off, {Reason, Stacktrace}, St);
86log_error(Off, {Reason, [_|_]=Stacktrace}, St) ->
87    {Pid, Who} = who(Off),
88    LogName = wings_u:crash_log(Who, Reason, Stacktrace),
89    catch wings_wm:psend(geom, {crash_in_other_window,LogName}),
90    {ok, St#{error=>Pid}};
91log_error(Off, {Reason, stack, Stacktrace}, St) ->
92    {Pid, Who} = who(Off),
93    LogName = wings_u:crash_log(Who, Reason, Stacktrace),
94    catch wings_wm:psend(geom, {crash_in_other_window,LogName}),
95    {ok, St#{error=>Pid}};
96log_error(Off, Reason, St) ->
97    {Pid, Who} = who(Off),
98    LogName = wings_u:crash_log(Who, Reason, []),
99    catch wings_wm:psend(geom, {crash_in_other_window,LogName}),
100    {ok, St#{error=>Pid}}.
101
102who(Pid) when is_pid(Pid) ->
103    {Pid, Pid};
104who([_|_]=PL) ->
105    Pid = proplists:get_value(pid, PL),
106    Id  = proplists:get_value(id, PL, undefined),
107    Name = proplists:get_value(registered_name, PL, []),
108    {Mod,_,_} = proplists:get_value(initial_call, PL, undefined),
109    Info = case {Id, Name, Mod} of
110               {undefined, [], undefined} -> Pid;
111               {undefined, [], Mod} -> [Mod, Pid];
112               {undefined, Name, undefined} -> [Name, Pid];
113               {undefined, Name, Mod} -> [Name, Mod, Pid];
114               {Id, [], Mod} -> [Id, Mod, Pid];
115               {Id, Name, Mod} -> [Id, Name, Mod, Pid]
116           end,
117    {Pid, Info}.
118
119%% logger callback
120log(#{msg:={report, #{label:={supervisor,Ignore}}}}, _)
121  when Ignore =:= shutdown; Ignore =:= shutdown_error; Ignore =:= child_terminated ->
122    ok;
123log(#{msg:={report, #{label:={proc_lib,_},report:=[Data|_]}}}=_Log, #{config:=Config}=St) ->
124    Error = proplists:get_value(error_info, Data),
125    case log_error(Data,Error,Config) of
126        {ok, Config} -> ok;
127        {ok, NewConfig} -> logger:set_handler_config(wings_logger, St#{config:=NewConfig})
128    end;
129log(LogEvent, #{formatter := {FModule, FConfig}}) ->
130    io:put_chars(FModule:format(LogEvent, FConfig)).
131
132
133%%% I/O server state record ---------------------------------------------------
134
135-record(state, {gmon,			% Monitor ref of original group leader
136		group_leader,		% pid()
137		win,
138		ctrl,
139		save_lines=200,		% -"-
140		cnt=1,			% Queued lines incl last
141		lines=queue:new(),	% Queue of binaries, head is oldest
142		last = <<>>             % Last line without eol
143	       }).
144
145-define(STATE, {state,Gmon,GroupLeader,Win,Ctrl,SaveLines,Cnt,Lines,Last}).
146
147%%% API -----------------------------------------------------------------------
148
149start_link(Env) ->
150    GroupLeader = group_leader(),
151    proc_lib:start_link(?MODULE, init, [Env, GroupLeader]).
152
153init(Env, GroupLeader) ->
154    process_flag(trap_exit, true),
155    {_,_,VsnStr} = lists:keyfind(kernel, 1, application:loaded_applications()),
156    case string:to_float(VsnStr) of
157        {Vsn, _} when Vsn > 6.0 ->  %% Be backwards compatible
158            logger:set_primary_config(level, warning),
159            logger:remove_handler(default),
160            logger:add_handler(wings_logger, ?MODULE, #{}),
161            logger:update_formatter_config(wings_logger, single_line, false),
162            logger:update_formatter_config(wings_logger, depth, 20),
163            logger:update_formatter_config(wings_logger, max_size, 500),
164            logger:set_handler_config(wings_logger, config, #{});
165            %% case logger:get_handler_config(wings_logger) of
166            %%     {error, _} -> ok;
167            %%     {ok,Config} ->
168            %%         logger:set_handler_config(wings_logger, maps:remove(error, Config))
169            %% end;
170        _ ->
171            error_logger:add_report_handler(?MODULE)
172    end,
173    case catch register(?SERVER_NAME, self()) of
174        true ->
175            wx:set_env(Env),
176            Gmon = erlang:monitor(process, GroupLeader),
177            group_leader(self(), whereis(wings_sup)),
178            proc_lib:init_ack({ok, self()}),
179            server_loop(#state{gmon=Gmon, group_leader=GroupLeader});
180        _ ->
181            exit(already_started)
182    end.
183
184get_pid() ->
185    case whereis(?SERVER_NAME) of
186	Server when is_pid(Server) ->
187	    Server;
188	undefined ->
189	    exit(not_started)
190    end.
191
192stop() -> req({stop,shutdown}).
193stop(Reason) -> req({stop,Reason}).
194
195setopts(Opts) when is_list(Opts) -> req({setopts,Opts}).
196
197getopts(Opts) when is_list(Opts) -> req({getopts,Opts}).
198
199window() ->
200    popup_window().
201
202window(Name, Pos, Size, Ps) ->
203    do_window(Name, [{pos,Pos},{size,Size}|Ps]).
204
205popup_window() ->
206    case wings_wm:is_window(?WIN_NAME) of
207	true ->
208	    wings_wm:show(?WIN_NAME);
209	false ->
210	    do_window(?WIN_NAME, [])
211    end.
212
213%%%
214%%% Debug API
215%%%
216
217get_state() -> req(get_state).
218
219code_change() -> req(code_change).
220
221%%% End of API ----------------------------------------------------------------
222
223%%%
224%%% Scrollable console window.
225%%%
226
227do_window(Name, Opts) ->
228    Title = ?STR(wc_open_window,1,"Wings3D Log"),
229    Font = ?GET(console_font_wx),
230    Size = case proplists:get_value(size, Opts) of
231	       undefined ->
232		   Width0  = wings_pref:get_value(console_width),
233		   Height0 = wings_pref:get_value(console_height),
234		   {CW,CH,_,_} = wxWindow:getTextExtent(?GET(top_frame), "W", [{theFont,Font}]),
235		   W = max(3 + (Width0*CW) + 3, 400),
236		   H = max(1 + (Height0*CH) + 4, 100),
237		   {W,H};
238	       SavedSize ->
239		   SavedSize
240	   end,
241    Pos = case proplists:get_value(pos, Opts) of
242	      undefined -> {-1, -1};
243	      SavedPos -> SavedPos
244	  end,
245    {Win, Ps} = wings_frame:make_win(Title, [{size, Size}, {pos, Pos}|Opts]),
246    {ok, Window} = req({window, wings_io:get_process_option(), Win, Font}),
247    wings_wm:toplevel(Name, Window, Ps, {push, fun(Ev) -> req({event, Ev}), keep end}).
248
249%%% I/O server ----------------------------------------------------------------
250
251req(Request) ->
252    case whereis(?SERVER_NAME) of
253	Server when is_pid(Server) ->
254	    req(Server, Request);
255	_ ->
256	    exit(not_started)
257    end.
258
259req(Server, Request) ->
260    Mref = erlang:monitor(process, Server),
261    Server ! {wings_console_request,self(),Mref,Request},
262    receive
263	{wings_console_reply,Mref,Reply} ->
264	    console_demonitor(Mref),
265	    Reply;
266	{'DOWN',Mref,_,_,Reason} ->
267	    exit(Reason)
268    end.
269
270console_demonitor(Mref) ->
271    demonitor(Mref),
272    receive {'DOWN',Mref,_,_,_} -> ok after 0 -> ok end.
273
274server_loop(#state{gmon=Gmon, win=Win}=State) ->
275    receive
276	{io_request,From,ReplyAs,Request}=Msg when is_pid(From) ->
277	    case io_request(State, Request) of
278		{NewState,_,forward} ->
279		    forward(Msg),
280		    server_loop(NewState);
281		{NewState,Reply,_} ->
282		    io_reply(From, ReplyAs, Reply),
283		    server_loop(NewState)
284	    end;
285	{wings_console_request,From,ReplyAs,{stop,Reason}} when is_pid(From) ->
286	    Win =:= undefined orelse wxFrame:destroy(Win),
287	    wings_console_reply(From, ReplyAs, State#state.group_leader),
288            error_logger:delete_report_handler(?MODULE),
289	    exit(Reason);
290	{wings_console_request,From,ReplyAs,code_change} when is_pid(From) ->
291	    %% Code change exit point from old module
292	    ?MODULE:do_code_change(State, From, ReplyAs);
293	{wings_console_request,From,ReplyAs,Request} when is_pid(From) ->
294	    {NewState,Reply} = wings_console_request(State, Request),
295	    wings_console_reply(From, ReplyAs, Reply),
296	    server_loop(NewState);
297	{'DOWN',Gmon,_,_,Reason} ->
298	    %% Group leader is down - die
299            error_logger:delete_report_handler(?MODULE),
300	    exit(Reason);
301	#wx{} = WxEvent ->
302	    NewState = wings_console_event(State, WxEvent),
303	    server_loop(NewState);
304	{'EXIT', _, _} ->
305	    %% Wings main process down die
306            error_logger:delete_report_handler(?MODULE),
307	    exit(shutdown);
308	Unknown ->
309	    io:format(?MODULE_STRING++?STR(server_loop,1,":~w Received unknown: ~p~n"),
310		      [?LINE,Unknown]),
311	    server_loop(State)
312    end.
313
314io_reply(From, ReplyAs, Reply) ->
315    From ! {io_reply,ReplyAs,Reply}.
316
317wings_console_reply(From, ReplyAs, Reply) ->
318    From ! {wings_console_reply,ReplyAs,Reply}.
319
320forward({io_request,From,ReplyAs,Req0}) ->
321    Req = forward_1(Req0),
322    group_leader() ! {io_request,From,ReplyAs,Req}.
323
324forward_1({put_chars,unicode,Chars}) when is_binary(Chars) ->
325    {put_chars,Chars};
326forward_1({put_chars,unicode,Chars}) when is_list(Chars) ->
327    try
328	{put_chars,list_to_binary(Chars)}
329    catch
330	error:badarg ->
331	    {put_chars,filter_chars(Chars)}
332    end;
333forward_1({put_chars,Chars}) when is_list(Chars) ->
334    try
335	{put_chars,list_to_binary(Chars)}
336    catch
337	error:badarg ->
338	    {put_chars,filter_chars(Chars)}
339    end;
340forward_1({put_chars,Chars}=Req) when is_binary(Chars) ->
341    Req;
342forward_1({put_chars,unicode,Mod,Func,Args}) ->
343    forward_1({put_chars,unicode,apply(Mod, Func, Args)});
344forward_1({put_chars,Mod,Func,Args}) ->
345    forward_1({put_chars,apply(Mod, Func, Args)}).
346
347filter_chars([H|T]) when is_list(H) ->
348    [filter_chars(H)|filter_chars(T)];
349filter_chars([H|T]) when is_integer(H), 255 < H ->
350    [$?,filter_chars(T)];
351filter_chars([H|T]) ->
352    [H|filter_chars(T)];
353filter_chars([]) -> [].
354
355%%%
356%%% I/O requests
357%%%
358
359io_request(State, {put_chars,Chars}) ->
360    {put_chars(State, Chars),ok,forward};
361io_request(State, {put_chars,unicode,Chars}) ->
362    {put_chars(State, Chars),ok,forward};
363io_request(State, {put_chars,unicode,Mod,Func,Args}) ->
364    case catch apply(Mod, Func, Args) of
365	Chars when is_list(Chars); is_binary(Chars) ->
366	    io_request(State, {put_chars,unicode,Chars});
367	_ ->
368	    {State,{error,Func},error}
369    end;
370io_request(State, {put_chars,Mod,Func,Args}) ->
371    case catch apply(Mod, Func, Args) of
372	Chars when is_list(Chars); is_binary(Chars) ->
373	    io_request(State, {put_chars,Chars});
374	_ ->
375	    {State,{error,Func},error}
376    end;
377io_request(State, {requests,Requests}) when is_list(Requests) ->
378    io_request_loop(Requests, {State,ok,ok});
379io_request(State, {setopts,Opts}) when is_list(Opts) ->
380    {State,{error,badarg},error};
381io_request(State, Request) ->
382    %% Probably a new version of Erlang/OTP with extensions to the
383    %% I/O protocol. We could generate an error here, but the
384    %% stack dump in wings_crash.dump would generate the caller of
385    %% io:format/2 in the main Wings process with no indication that
386    %% this module is the culprit. Therefore, we choose to ignore
387    %% the request, but write a message to the console to point out
388    %% the problem.
389    S = io_lib:format("Internal error in Console - unknown I/O request:\n~P\n",
390		      [Request,10]),
391    {put_chars(State, iolist_to_binary(S)),ok,forward}.
392
393io_request_loop([], Result) ->
394    Result;
395io_request_loop([_|_], {_,_,error}=Result) ->
396    Result;
397io_request_loop([Request|Requests], {State,_,ok}) ->
398    io_request_loop(Requests, io_request(State, Request)).
399
400put_chars(#state{ctrl=Ctrl} = State, Chars) when is_binary(Chars) ->
401    is_tuple(Ctrl) andalso wxTextCtrl:appendText(Ctrl, [Chars]),
402    put_chars_1(State, Chars);
403put_chars(#state{ctrl=Ctrl} = State, Chars) when is_list(Chars) ->
404    is_tuple(Ctrl) andalso wxTextCtrl:appendText(Ctrl, Chars),
405    put_chars_1(State, unicode:characters_to_binary(Chars)).
406
407put_chars_1(#state{cnt=Cnt0, lines=Lines0,
408		   last=Last0, save_lines=Save}=State, IoBin0) ->
409    IoBin = erlang:iolist_to_binary([Last0, IoBin0]),
410    NewLines = binary:split(IoBin, <<"\n">>, [global]),
411    {Lines, Cnt, Last} = put_chars_1(NewLines, Lines0, Cnt0, Save),
412    State#state{cnt=Cnt, lines=Lines, last=Last}.
413
414put_chars_1([<<>>], Lines, Cnt, _Save) ->
415    {Lines, Cnt, <<>>};
416put_chars_1([LastWOeol], Lines, Cnt, _) ->
417    {Lines, Cnt, LastWOeol};
418put_chars_1([Line|NLs], Lines, Cnt, Save) when Cnt < Save ->
419    put_chars_1(NLs, queue:in(Line, Lines), Cnt+1, Save);
420put_chars_1([Line|NLs], Lines, Cnt, Save) ->
421    put_chars_1(NLs, queue:in(Line, queue:drop(Lines)), Cnt, Save).
422
423%%%
424%%% Wings console requests
425%%%
426
427wings_console_event(State, #wx{event=#wxWindowDestroy{}}) ->
428    wings ! {wm, {delete, ?WIN_NAME}},
429    State#state{win=undefined, ctrl=undefined};
430wings_console_event(#state{ctrl=Ctrl} = State, #wx{event=#wxSize{size={W0,H0}}}) ->
431    {CW,CH,_,_} = wxWindow:getTextExtent(Ctrl, "W"),
432    W=W0-6, H=H0-5,
433    wings_pref:set_value(console_width, W div CW),
434    wings_pref:set_value(console_height, H div CH),
435    State;
436wings_console_event(State, #wx{event=#wxMouse{}}=Ev) ->
437    wings_frame ! Ev,
438    State.
439
440wings_console_request(State0, {window, WxEnv, Win, Font}) ->
441    wings_io:set_process_option(WxEnv),
442    wc_open_window(State0, Win, Font);
443wings_console_request(State, {setopts,Opts}) ->
444    wc_setopts(State, Opts);
445wings_console_request(State, {getopts,Opts}) ->
446    wc_getopts(State, Opts, []);
447wings_console_request(State, get_state) ->
448    {State,State};
449wings_console_request(State, {event, Ev}) ->
450    case Ev of
451	close -> wings ! {wm, {delete, ?WIN_NAME}};
452	_ -> %% io:format("~p: Got ~p~n",[?MODULE, Ev]),
453	    ignore
454    end,
455    {State,State};
456wings_console_request(State, Request) ->
457    {State,{error,{request,Request}}}.
458
459wc_setopts(#state{save_lines=SaveLines0}=State,Opts) ->
460    SaveLines = proplists:get_value(save_lines, Opts, SaveLines0),
461    if is_integer(SaveLines), SaveLines >= 0 ->
462	    {State#state{save_lines=SaveLines},ok};
463       true ->
464	    {State,{error,badarg}}
465    end.
466
467wc_getopts(State, [], R) ->  {State,reverse(R)};
468wc_getopts(#state{save_lines=SaveLines}=State, [save_lines|Opts], R) ->
469    wc_getopts(State, Opts, [{save_lines,SaveLines}|R]);
470wc_getopts(State, _, _) ->
471    {State,{error,badarg}}.
472
473wc_open_window(#state{lines=Lines}=State, Win, Font) ->
474    TStyle = ?wxTE_MULTILINE bor ?wxTE_READONLY bor ?wxTE_RICH2 bor wings_frame:get_border(),
475    Ctrl = wxTextCtrl:new(Win, ?wxID_ANY, [{style, TStyle}]),
476
477    wxWindow:setFont(Ctrl, Font),
478    wxWindow:setBackgroundColour(Ctrl, wings_color:rgb4bv(wings_pref:get_value(console_color))),
479    wxWindow:setForegroundColour(Ctrl, wings_color:rgb4bv(wings_pref:get_value(console_text_color))),
480    wxTextCtrl:appendText(Ctrl, [[Line,$\n] || Line <- queue:to_list(Lines)]),
481    wxWindow:connect(Ctrl, destroy, [{skip, true}]),
482    wxWindow:connect(Ctrl, size, [{skip, true}]),
483    wxWindow:connect(Ctrl, enter_window, [{userData, {win, Ctrl}}]),
484    {State#state{win=Win, ctrl=Ctrl}, {ok, Ctrl}}.
485
486%%% Other support functions
487%%%
488
489%% Code change entry point in the new module. Called in the new module
490%% with the state from the old module.
491%%
492%% Intended for development.
493%%
494do_code_change(?STATE, From, ReplyAs) ->
495    wings_console_reply(From, ReplyAs, ok),
496    server_loop(#state{gmon=Gmon,group_leader=GroupLeader,
497		       win=Win, ctrl=Ctrl,
498		       save_lines=SaveLines,last=Last,
499		       cnt=Cnt,lines=Lines}).
500