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