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