1 /*
2 FXiTe - The Free eXtensIble Text Editor
3 Copyright (c) 2009-2011 Jeffrey Pohlmeyer <yetanothergeek@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify it
6 under the terms of the GNU General Public License version 3 as
7 published by the Free Software Foundation.
8
9 This software is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include <unistd.h>
20 #include <lua.hpp>
21 #include <fx.h>
22
23 #include "compat.h"
24 #include "appname.h"
25 #include "appwin_pub.h"
26 #include "luacmds.h"
27 #include "luafuncs.h"
28 #include "luafx.h"
29 #include "fxasq_lua.h"
30
31 #include "intl.h"
32 #include "macro.h"
33
34
35
MacroRunner()36 MacroRunner::MacroRunner():FXObject(){}
37
38
~MacroRunner()39 MacroRunner::~MacroRunner()
40 {
41 LuaFuncsCleanup();
42 ClearKeepers();
43 }
44
45
46
47 /* Catch and report script errors */
traceback(lua_State * L)48 static FXint traceback(lua_State *L)
49 {
50 lua_getglobal(L, "debug");
51 if (!lua_istable(L, -1)) {
52 lua_pop(L, 1);
53 return 1;
54 }
55 lua_getfield(L, -1, "traceback");
56 if (!lua_isfunction(L, -1)) {
57 lua_pop(L, 2);
58 return 1;
59 }
60 lua_pushvalue(L, 1);
61 lua_pushinteger(L, 2);
62 lua_call(L, 2, 1);
63 return 1;
64 }
65
66
67
68 /* Class to record current script name and file position */
69 class StateInfo: public FXObject {
70 public:
71 lua_State *L;
72 FXString source;
73 FXString script;
74 FXint line;
75 FXint counter;
76 bool optimized;
77 StateInfo(lua_State *aL);
78 };
79
80
81
StateInfo(lua_State * aL)82 StateInfo::StateInfo(lua_State *aL)
83 {
84 L=aL;
85 source="";
86 script="";
87 line=-1;
88 counter=0;
89 optimized=false;
90 }
91
92
93 /*
94 It's probaly rare that we'll have more than one interpreter
95 running at a time, but we let them each carry their own info
96 just in case.
97 */
98 static FXObjectListOf<StateInfo> states;
99
100
101
102 /*
103 Find the StateInfo associated with this Lua state
104 */
LookupState(lua_State * L)105 static StateInfo* LookupState(lua_State *L)
106 {
107 for (FXint i=0; i<states.no(); i++) {
108 if ( states[i]->L == L ) { return states[i]; }
109 }
110 return NULL;
111 }
112
113
114 /*
115 The default Lua error messages are intended to be printed to the console.
116 Here we doctor them up so they look better when displayed in a dialog box.
117 In particular, we need to convert tabs to spaces, and replace the long
118 path names with an ellipsis (.../)
119 The current hack used here is to parse the lines that contain a slash
120 and chop out everything before the last slash. Of course this will
121 cause problems if the error message contains slashes beyond the filename,
122 but it should be OK in most cases.
123 */
format_message(const char * rawmsg,FXString & outmsg)124 static void format_message(const char *rawmsg, FXString &outmsg)
125 {
126 FXString s=rawmsg;
127 outmsg="";
128 s.substitute("\t", " ", true);
129 s.substitute("\r\n", "\n");
130 s.append("\n");
131 FXint n=s.contains("\n");
132 for (FXint i=0; i<n; i++) {
133 FXString sect=s.section('\n',i);
134 if (sect.contains(PATHSEP)) {
135 outmsg.append(".../"+FXPath::name(sect)+"\n");
136 } else {
137 outmsg.append(sect+"\n");
138 }
139 }
140 outmsg.substitute(": ", ":\n\n ", false);
141 outmsg.substitute("stack traceback:", "\nstack traceback:", false);
142 }
143
144
145
146 /*
147 Pop up a message dialog if we encounter a script error.
148 If we know the exact location of the error, give the user
149 a chance to open the offending file at the point of error.
150 */
script_error(const FXString & filename,const char * msg,bool need_name,FXint line)151 static void script_error(const FXString &filename, const char *msg, bool need_name, FXint line)
152 {
153 if ((!TopWinPub::instance()) || TopWinPub::Destroying() || TopWinPub::Closing()) {
154 fprintf(stderr, "%s\n", msg);
155 return;
156 }
157 if (need_name||filename.empty()) {
158 FXMessageBox::error(TopWinPub::instance(),MBOX_OK,_("Macro Error"),"%s\n%s\n", filename.text(),msg);
159 } else {
160 FXString m;
161 format_message(msg,m);
162 if ( FXMessageBox::error(TopWinPub::instance(), MBOX_YES_NO, _("Macro Error"), "%s\n\n\n%s",
163 m.text(), _("Edit script?"))==MBOX_CLICKED_YES) {
164 char linenum[8]="\0\0\0\0\0\0\0";
165 snprintf(linenum,sizeof(linenum)-1, "%d", line);
166 TopWinPub::OpenFile(filename.text(), linenum, false, false);
167 }
168 }
169 }
170
171
172
173 /*
174 Retrieve an error message from the Lua state if possible,
175 else make up our own message.
176 */
show_error(lua_State * L,const FXString & filename)177 static void show_error(lua_State *L, const FXString &filename)
178 {
179 StateInfo*si=LookupState(L);
180 if (!lua_isnil(L, -1)) {
181 const char *msg;
182 msg = lua_tostring(L, -1);
183 if (msg == NULL) {
184 msg = _("(error object is not a string)");
185 }
186 if (strncmp(msg,LuaQuitMessage(), strlen(LuaQuitMessage()))!=0) {
187 script_error(si->source.empty()?filename:si->source, msg, false, si->line);
188 }
189 lua_pop(L, 1);
190 } else {
191 script_error(si->source.empty()?filename:si->source, "Unknown error inside script.", false, si->line);
192 }
193 }
194
195
196
197 /* Provide the user with a function to return the script's filename */
scriptname(lua_State * L)198 static int scriptname(lua_State *L)
199 {
200 lua_pushstring(L, LookupState(L)->script.text());
201 return 1;
202 }
203
204
205 /* Override Lua's standard os.exit() function, so we can have a clean shutdown. */
osexit(lua_State * L)206 static int osexit(lua_State *L)
207 {
208 TopWinPub::CloseWait();
209 lua_pushstring(L,LuaQuitMessage());
210 lua_error(L);
211 return 0;
212 }
213
214
215 /* Override Lua's standard print() function, so we can send the data to the output pane. */
print(lua_State * L)216 static int print(lua_State *L)
217 {
218 int argc=lua_gettop(L);
219 FXString data="";
220 for (int i=1; i<=argc; i++) {
221 const void* p=NULL;
222 char buf[128];
223 memset(buf,0,sizeof(buf));
224 switch (lua_type(L,i)) {
225 case LUA_TNONE: {
226 data.append(_("<out-of-bounds>"));
227 break;
228 }
229 case LUA_TNIL: {
230 data.append("nil");
231 break;
232 }
233 case LUA_TNUMBER: {
234 snprintf(buf, sizeof(buf)-1, "%g", lua_tonumber(L,i));
235 data.append(buf);
236 break;
237 }
238 case LUA_TBOOLEAN: {
239 data.append(lua_toboolean(L,i)?"true":"false");
240 break;
241 }
242 case LUA_TSTRING: {
243 data.append(lua_tostring(L,i));
244 break;
245 }
246 case LUA_TTABLE: {
247 p=lua_topointer(L,i);
248 data.append("table: ");
249 break;
250 }
251 case LUA_TFUNCTION: {
252 p=lua_topointer(L,i);
253 data.append("function: ");
254 break;
255 }
256 case LUA_TLIGHTUSERDATA:
257 case LUA_TUSERDATA: {
258 p=lua_topointer(L,i);
259 data.append("userdata: ");
260 break;
261 }
262 case LUA_TTHREAD: {
263 p=lua_topointer(L,i);
264 data.append("userdata: ");
265 break;
266 }
267 }
268 if (p) {
269 snprintf(buf, sizeof(buf)-1, "%p", p);
270 data.append(buf);
271 }
272 if (i!=argc) {
273 data.append(" ");
274 }
275 }
276 data.substitute('\t', ' ');
277 data.substitute("\r\n", "\n");
278 FXint sects=data.contains('\n');
279 for (FXint sect=0; sect<sects; sect++) {
280 TopWinPub::AddOutput(data.section('\n', sect));
281 }
282 if (!data.section('\n', sects).empty()) { TopWinPub::AddOutput(data.section('\n', sects)); }
283 return 0;
284 }
285
286
287 /* Scripts that call this function will bypass most of the debug hook's functionality. */
optimize(lua_State * L)288 static int optimize(lua_State *L)
289 {
290 LookupState(L)->optimized=true;
291 return 0;
292 }
293
294
295 /*
296 This hook gets called each time the interpreter executes a line of code.
297 It stores the current script file name and line number, checks if the
298 user is trying to cancel the script, and occasionally refreshes the GUI.
299 */
debug_hook(lua_State * L,lua_Debug * ar)300 static void debug_hook(lua_State *L, lua_Debug *ar)
301 {
302 StateInfo*si=LookupState(L);
303 if (!si->optimized) {
304 if (lua_getinfo(L,"Sl",ar)) {
305 if (ar->source && (ar->source[0]=='@') && (strcmp(si->source.text(), ar->source+1)!=0)) {
306 si->source=ar->source+1;
307 }
308 si->line=ar->currentline;
309 }
310 if ( (!TopWinPub::instance()) || TopWinPub::IsMacroCancelled()) {
311 lua_pushstring(L, _("Macro cancelled by user."));
312 lua_error(L);
313 return;
314 }
315 if (si->counter > 100000) {
316 TopWinPub::update();
317 FXApp::instance()->runWhileEvents();
318 si->counter=0;
319 } else si->counter++;
320 }
321 }
322
323
324 /* Override a builtin Lua function, or add a new one if it doesn't exist */
override(lua_State * L,const char * module,const char * funcname,lua_CFunction newfunc)325 static void override(lua_State *L, const char*module, const char* funcname, lua_CFunction newfunc)
326 {
327 lua_getglobal(L,module);
328 if (lua_istable(L,-1)) {
329 lua_pushstring(L,funcname);
330 if (newfunc) {
331 lua_pushcfunction(L,newfunc);
332 } else {
333 lua_pushnil(L);
334 }
335 lua_rawset(L,-3);
336 } else {
337 lua_pop(L, 1);
338 }
339 }
340
341
342 /* Don't let scripts try to read from stdin, as this would block indefinitely */
close_stdin(lua_State * L)343 static void close_stdin(lua_State *L)
344 {
345 lua_getglobal(L, "io");
346 if (!lua_istable(L, -1)) {
347 lua_pop(L, 1);
348 return;
349 }
350 lua_getfield(L, -1, "input");
351 if (!lua_isfunction(L, -1)) {
352 lua_pop(L, 2);
353 return;
354 }
355 lua_pushstring(L, NULL_FILE);
356 lua_call(L, 1, 1);
357 }
358
359
360 /* Push a name=value variable into the application module's namespace */
set_string_token(lua_State * L,const char * name,const char * value)361 static void set_string_token(lua_State *L, const char*name, const char*value)
362 {
363 lua_getglobal(L, LUA_MODULE_NAME);
364 if (lua_istable(L, -1)) {
365 lua_pushstring(L,name);
366 lua_pushstring(L,value);
367 lua_settable(L, -3);
368 } else {
369 fxwarning(_("*** %s: Failed to set value for %s\n"), LUA_MODULE_NAME, name);
370 }
371 }
372
373
374
375 typedef struct {
376 int t;
377 union {
378 char*s;
379 lua_Number n;
380 bool b;
381 };
382 } PersistRecord;
383
384
ClearKeepers()385 void MacroRunner::ClearKeepers()
386 {
387 if (UsedSlotsInDict(&keepers)>0) for (FXint i=0; i<TotalSlotsInDict(&keepers); ++i) {
388 PersistRecord*pr=(PersistRecord*)keepers.data(i);
389 if (pr) {
390 if (pr->t==LUA_TSTRING) {
391 free(pr->s);
392 delete pr;
393 }
394 }
395 }
396 keepers.clear();
397 }
398
399
400 #define PERSIST_TABLE_NAME "keep"
401
PushKeepers(lua_State * L)402 void MacroRunner::PushKeepers(lua_State *L)
403 {
404 lua_getglobal(L, LUA_MODULE_NAME);
405 if (lua_istable(L, -1)) {
406 lua_pushstring(L,PERSIST_TABLE_NAME);
407 lua_newtable(L);
408 lua_settable(L, -3);
409 if (UsedSlotsInDict(&keepers)>0) for (FXint i=0; i<TotalSlotsInDict(&keepers); ++i) {
410 PersistRecord*pr=(PersistRecord*)keepers.data(i);
411 if (!pr) { continue; }
412 lua_getglobal(L, LUA_MODULE_NAME);
413 lua_getfield(L,-1,PERSIST_TABLE_NAME);
414 lua_pushstring(L,DictKeyName(keepers,i));
415 switch (pr->t) {
416 case LUA_TNUMBER: { lua_pushnumber(L, pr->n); break;}
417 case LUA_TBOOLEAN:{ lua_pushboolean(L, pr->b); break;}
418 case LUA_TSTRING: { lua_pushstring(L,pr->s); break;}
419 }
420 lua_rawset(L,-3);
421 }
422 } else {
423 fxwarning(_("*** %s: Failed to set value for %s\n"), LUA_MODULE_NAME, "keep");
424 }
425 }
426
427
428
PopKeepers(lua_State * L)429 void MacroRunner::PopKeepers(lua_State *L)
430 {
431 lua_getglobal(L, LUA_MODULE_NAME);
432 if ( lua_istable(L, -1) ) {
433 lua_pushstring(L, PERSIST_TABLE_NAME);
434 lua_gettable(L, -2);
435 if (lua_istable(L, -1)) {
436 ClearKeepers();
437 lua_pushnil(L); // make room for first key
438 while (lua_next(L, -2) != 0) { // walk the table
439 if (lua_type(L, -2)==LUA_TSTRING) { // 'key' is at index -2
440 PersistRecord*pr=NULL;
441 switch (lua_type(L, -1)) { // 'value' is at index -1
442 case LUA_TBOOLEAN: {
443 pr=new PersistRecord;
444 pr->t=LUA_TBOOLEAN;
445 pr->b=lua_toboolean(L,-1);
446 }
447 case LUA_TNUMBER: {
448 pr=new PersistRecord;
449 pr->t=LUA_TNUMBER;
450 pr->n=lua_tonumber(L,-1);
451 }
452 case LUA_TSTRING: {
453 pr=new PersistRecord;
454 pr->t=LUA_TSTRING;
455 pr->s=strdup(lua_tostring(L,-1));
456 }
457 }
458 if (pr) {
459 ReplaceInDict(&keepers,lua_tostring(L,-2),pr);
460 }
461 }
462 lua_pop(L, 1);
463 }
464 } else {
465 if (lua_isnil(L, -1)) {
466 ClearKeepers();
467 }
468 }
469 }
470 }
471
472
473 /*
474 Create a new Lua state and execute the source as
475 a string of code or a script filename
476 */
RunMacro(const FXString & source,bool isfilename)477 bool MacroRunner::RunMacro(const FXString &source, bool isfilename)
478 {
479 FXint status;
480 lua_State *L=luaL_newstate();
481 luaL_openlibs(L);
482 luaopen_dialog(L);
483 #if LUA_VERSION_NUM>=502
484 lua_setglobal(L, "dialog");
485 #endif
486 override(L,"os","exit", osexit);
487 override(L,"io","stdin", NULL);
488 override(L,"_G","print", print);
489 close_stdin(L);
490 StateInfo*si=new StateInfo(L);
491 si->script=isfilename?source.text():NULL;
492 states.append(si);
493 lua_sethook(L,debug_hook,LUA_MASKLINE,1);
494 #if LUA_VERSION_NUM<502
495 luaL_Register(L, LUA_MODULE_NAME, LuaFuncs());
496 luaL_Register(L, LUA_MODULE_NAME, LuaFxUtils(TopWinPub::instance(), EXE_NAME));
497 luaL_Register(L, LUA_MODULE_NAME, LuaCommands(TopWinPub::instance()));
498 #else
499 int n=0;
500 const luaL_Reg*p;
501 const luaL_Reg*funcs = LuaFuncs();
502 const luaL_Reg*utils = LuaFxUtils(TopWinPub::instance(), EXE_NAME);
503 const luaL_Reg*cmds = LuaCommands(TopWinPub::instance());
504 for (p=funcs; p->name; p++) { n++; }
505 for (p=utils; p->name; p++) { n++; }
506 for (p=cmds; p->name; p++) { n++; }
507 lua_createtable(L, 0, n);
508 luaL_setfuncs(L, funcs, 0);
509 luaL_setfuncs(L, utils, 0);
510 luaL_setfuncs(L, cmds, 0);
511 lua_setglobal(L, LUA_MODULE_NAME);
512 #endif
513 override(L,LUA_MODULE_NAME,"script", scriptname);
514 override(L,LUA_MODULE_NAME,"optimize", optimize);
515 set_string_token(L, "_VERSION", VERSION);
516 PushKeepers(L);
517 if (isfilename) {
518 status = luaL_loadfile(L, source.text());
519 } else {
520 status = luaL_loadstring(L, source.text());
521 }
522 switch (status) {
523 case 0: {
524 FXint base = lua_gettop(L); /* function index */
525 lua_pushcfunction(L, traceback); /* push traceback function */
526 lua_insert(L, base); /* put it under chunk and args */
527 status = lua_pcall(L, 0, 0, base);
528 lua_remove(L, base); /* remove traceback function */
529 if (0 == status) {
530 PopKeepers(L);
531 } else {
532 lua_gc(L, LUA_GCCOLLECT, 0); /* force garbage collection if error */
533 show_error(L, si->script);
534 }
535 break;
536 }
537 case LUA_ERRSYNTAX:
538 show_error(L, si->script);
539 break;
540 case LUA_ERRMEM:
541 script_error(si->script,_("Out of memory."),true,-1);
542 break;
543 case LUA_ERRFILE:
544 script_error(si->script,_("Failed to open script file."),true,-1);
545 break;
546 default:
547 script_error(si->script,_("Unknown error while loading script file."),true,-1);
548 }
549 lua_close(L);
550 states.erase(states.find(si));
551 delete si;
552 return (status==0);
553 }
554
555
556
557 /*
558 Execute the specified script file, called by
559 the user-made commands from the "Tools" menu.
560 */
DoFile(const FXString & filename)561 bool MacroRunner::DoFile(const FXString &filename)
562 {
563 return RunMacro(filename, true);
564 }
565
566
567
568 /*
569 Execute the specified source code directly,
570 used by the macro playback function and the
571 -e command line option.
572 */
DoString(const FXString & sourcecode)573 bool MacroRunner::DoString(const FXString &sourcecode)
574 {
575 return RunMacro(sourcecode, false);
576 }
577
578