source: tags/arb-6.0/SL/MACROS/recmac.cxx

Last change on this file was 12267, checked in by westram, 10 years ago
File size: 13.1 KB
Line 
1// ============================================================= //
2//                                                               //
3//   File      : recmac.cxx                                      //
4//   Purpose   :                                                 //
5//                                                               //
6//   Coded by Ralf Westram (coder@reallysoft.de) in April 2012   //
7//   Institute of Microbiology (Technical University Munich)     //
8//   http://www.arb-home.de/                                     //
9//                                                               //
10// ============================================================= //
11
12#include "recmac.hxx"
13#include "macros_local.hxx"
14
15#include <arbdbt.h>
16
17#include <arb_file.h>
18#include <arb_defs.h>
19#include <arb_diff.h>
20#include <aw_msg.hxx>
21#include <aw_root.hxx>
22
23#include <FileContent.h>
24
25#include <cctype>
26#include <arb_str.h>
27#include <aw_file.hxx>
28
29void warn_unrecordable(const char *what) {
30    aw_message(GBS_global_string("could not record %s", what));
31}
32
33void RecordingMacro::write_dated_comment(const char *what) const {
34    write("# ");
35    write(what);
36    write(" @ ");
37    write(GB_date_string());
38    write('\n');
39}
40
41RecordingMacro::RecordingMacro(const char *filename, const char *application_id_, const char *stop_action_name_, bool expand_existing)
42    : stop_action_name(strdup(stop_action_name_)),
43      application_id(strdup(application_id_)),
44      path(NULL),
45      out(NULL),
46      error(NULL)
47{
48    path = (filename[0] == '/')
49        ? strdup(filename)
50        : GBS_global_string_copy("%s/%s", GB_getenvARBMACROHOME(), filename);
51
52    if (expand_existing && !GB_is_readablefile(path)) {
53        error = GBS_global_string("Can only expand existing macros (no such file: %s)", path);
54    }
55
56    if (!error) {
57        char *content = NULL;
58        {
59            const char *from    = expand_existing ? path : GB_path_in_ARBLIB("macro.head");
60            content             = GB_read_file(from);
61            if (!content) error = GB_await_error();
62            else {
63                if (expand_existing) {
64                    // cut off end of macro
65                    char *close = strstr(content, "ARB::close");
66                    if (close) close[0] = 0;
67                }
68            }
69        }
70
71        if (!error) {
72            out = fopen(path, "w");
73
74            if (out) {
75                write(content);
76                write_dated_comment(expand_existing ? "recording resumed" : "recording started");
77                flush();
78            }
79            else error = GB_IO_error("recording to", filename);
80        }
81
82        free(content);
83        ma_assert(implicated(error, !out));
84    }
85}
86
87void RecordingMacro::write_as_perl_string(const char *value) const {
88    const char SQUOTE = '\'';
89    write(SQUOTE);
90    for (int i = 0; value[i]; ++i) {
91        char c = value[i];
92        if (c == SQUOTE) {
93            write('\\');
94            write(SQUOTE);
95        }
96        else {
97            write(c);
98        }
99    }
100    write(SQUOTE);
101}
102
103void RecordingMacro::write_action(const char *app_id, const char *action_name) {
104    bool handled = false;
105
106    // Recording "macro-execution" as GUI-clicks caused multiple macros running asynchronously (see #455)
107    // Instead of recording GUI-clicks, macros are called directly:
108    static const char *MACRO_ACTION_START = MACRO_WINDOW_ID "/";
109    if (ARB_strBeginsWith(action_name, MACRO_ACTION_START)) {
110        static int  MACRO_START_LEN = strlen(MACRO_ACTION_START);
111        const char *sub_action      = action_name+MACRO_START_LEN;
112
113        int playbackType = 0;
114        if      (strcmp(sub_action, MACRO_PLAYBACK_ID)        == 0) playbackType = 1;
115        else if (strcmp(sub_action, MACRO_PLAYBACK_MARKED_ID) == 0) playbackType = 2;
116
117        if (playbackType) {
118            char       *macroFullname = AW_get_selected_fullname(AW_root::SINGLETON, AWAR_MACRO_BASE);
119            const char *macroName     = GBT_relativeMacroname(macroFullname); // points into macroFullname
120
121            write("BIO::macro_execute(");
122            write_as_perl_string(macroName); // use relative macro name (allows to share macros between users)
123            write(", ");
124            write('0'+(playbackType-1));
125            write(", 0);\n"); // never run asynchronously (otherwise (rest of) current and called macro will interfere)
126            flush();
127
128            free(macroFullname);
129
130            handled = true;
131        }
132    }
133
134    // otherwise "normal" operation (=trigger GUI element)
135    if (!handled) {
136        write("BIO::remote_action($gb_main");
137        write(','); write_as_perl_string(app_id);
138        write(','); write_as_perl_string(action_name);
139        write(");\n");
140    }
141    flush();
142}
143void RecordingMacro::write_awar_change(const char *app_id, const char *awar_name, const char *content) {
144    write("BIO::remote_awar($gb_main");
145    write(','); write_as_perl_string(app_id);
146    write(','); write_as_perl_string(awar_name);
147    write(','); write_as_perl_string(content);
148    write(");\n");
149    flush();
150}
151
152
153void RecordingMacro::track_action(const char *action_id) {
154    ma_assert(out && !error);
155    if (!action_id) {
156        warn_unrecordable("anonymous GUI element");
157    }
158    else if (strcmp(action_id, stop_action_name) != 0) { // silently ignore stop-recording button press
159        write_action(application_id, action_id);
160    }
161}
162
163void RecordingMacro::track_awar_change(AW_awar *awar) {
164    // see also trackers.cxx@AWAR_CHANGE_TRACKING
165
166    ma_assert(out && !error);
167
168    char *svalue = awar->read_as_string();
169    if (!svalue) {
170        warn_unrecordable(GBS_global_string("change of '%s'", awar->awar_name));
171    }
172    else {
173        write_awar_change(application_id, awar->awar_name, svalue);
174        free(svalue);
175    }
176}
177
178GB_ERROR RecordingMacro::stop() {
179    if (out) {
180        write_dated_comment("recording stopped");
181        write("ARB::close($gb_main);\n");
182        fclose(out);
183
184        post_process();
185
186        long mode = GB_mode_of_file(path);
187        error     = GB_set_mode_of_file(path, mode | ((mode >> 2)& 0111));
188
189        out = 0;
190    }
191    return error;
192}
193
194// -------------------------
195//      post processing
196
197inline const char *closing_quote(const char *str, char qchar) {
198    const char *found = strchr(str, qchar);
199    if (found>str) {
200        if (found[-1] == '\\') { // escaped -> search behind
201            return closing_quote(found+1, qchar);
202        }
203    }
204    return found;
205}
206
207inline char *parse_quoted_string(const char *& line) {
208    // read '"string"' from start of line.
209    // return 'string'.
210    // skips spaces.
211
212    while (isspace(line[0])) ++line;
213    if (line[0] == '\"' || line[0] == '\'') {
214        const char *other_quote = closing_quote(line+1, line[0]);
215        if (other_quote) {
216            char *str = GB_strpartdup(line+1, other_quote-1);
217            line      = other_quote+1;
218            while (isspace(line[0])) ++line;
219            return str;
220        }
221    }
222    return NULL;
223}
224
225inline char *modifies_awar(const char *line, char *& app_id) {
226    // return awar_name, if line modifies an awar.
227    // return NULL otherwise
228    //
229    // if 'app_id' is NULL, it'll be set to found application id.
230    // otherwise it'll be checked against found id. function returns NULL on mimatch.
231
232    while (isspace(line[0])) ++line;
233
234    const char cmd[]  = "BIO::remote_awar($gb_main,";
235    const int cmd_len = ARRAY_ELEMS(cmd)-1;
236
237    if (strncmp(line, cmd, cmd_len) == 0) {
238        line     += cmd_len;
239        char *id  = parse_quoted_string(line);
240        if (app_id) {
241            bool app_id_differs = strcmp(app_id, id) != 0;
242            free(id);
243            if (app_id_differs) return NULL;
244        }
245        else {
246            app_id = id;
247        }
248        if (line[0] == ',') {
249            ++line;
250            char *awar = parse_quoted_string(line);
251            return awar;
252        }
253    }
254    return NULL;
255}
256
257inline bool opens_macro_dialog(const char *line) {
258    // return true, if the macro-command in 'line' opens the macro dialog
259    return strcmp(line, "BIO::remote_action($gb_main,\'ARB_NT\',\'macros\');") == 0;
260}
261inline bool is_end_of_macro(const char *line) {
262    // return true, if the macro-command in 'line' belongs to code at end (of any macro)
263    return strcmp(line, "ARB::close($gb_main);") == 0;
264}
265
266inline bool is_comment(const char *line) {
267    int i = 0;
268    while (isspace(line[i])) ++i;
269    return line[i] == '#';
270}
271
272void RecordingMacro::post_process() {
273    ma_assert(!error);
274
275    FileContent macro(path);
276    error = macro.has_error();
277    if (!error) {
278        StrArray& line = macro.lines();
279
280        // remove duplicate awar-changes
281        for (size_t i = 0; i<line.size(); ++i) {
282            char *app_id   = NULL;
283            char *mod_awar = modifies_awar(line[i], app_id);
284            if (mod_awar) {
285                for (size_t n = i+1; n<line.size(); ++n) {
286                    if (!is_comment(line[n])) {
287                        char *mod_next_awar = modifies_awar(line[n], app_id);
288                        if (mod_next_awar) {
289                            if (strcmp(mod_awar, mod_next_awar) == 0) {
290                                // seen two lines (i and n) which modify the same awar
291                                // -> remove the 1st line
292                                line.remove(i);
293
294                                // make sure that it also works for 3 or more consecutive modifications
295                                ma_assert(i>0);
296                                i--;
297                            }
298                            free(mod_next_awar);
299                        }
300                        break;
301                    }
302                }
303                free(mod_awar);
304            }
305            else if (opens_macro_dialog(line[i])) {
306                bool isLastCommand = true;
307                for (size_t n = i+1; n<line.size() && isLastCommand; ++n) {
308                    if (!is_comment(line[n]) && !is_end_of_macro(line[n])) {
309                        isLastCommand = false;
310                    }
311                }
312                if (isLastCommand) {
313                    free(line.replace(i, GBS_global_string_copy("# %s", line[i])));
314                }
315            }
316            free(app_id);
317        }
318        error = macro.save();
319    }
320}
321
322// --------------------------------------------------------------------------------
323
324#ifdef UNIT_TESTS
325#ifndef TEST_UNIT_H
326#include <test_unit.h>
327#endif
328
329#define TEST_PARSE_QUOTED_STRING(in,res_exp,out_exp) do {       \
330        const char *line = (in);                                \
331        char *res =parse_quoted_string(line);                   \
332        TEST_EXPECT_EQUAL(res, res_exp);                        \
333        TEST_EXPECT_EQUAL(line, out_exp);                       \
334        free(res);                                              \
335    } while(0)
336
337#define TEST_MODIFIES_AWAR(cmd,app_exp,awar_exp,app_in) do {    \
338        char *app  = app_in;                                    \
339        char *awar = modifies_awar(cmd, app);                   \
340        TEST_EXPECTATION(all().of(that(awar).is_equal_to(awar_exp),  \
341                             that(app).is_equal_to(app_exp)));  \
342        free(awar);                                             \
343        free(app);                                              \
344    } while(0)
345
346void TEST_parse() {
347    const char *null = NULL;
348    TEST_PARSE_QUOTED_STRING("", null, "");
349    TEST_PARSE_QUOTED_STRING("\"str\"", "str", "");
350    TEST_PARSE_QUOTED_STRING("\"part\", rest", "part", ", rest");
351    TEST_PARSE_QUOTED_STRING("\"\"", "", "");
352    TEST_PARSE_QUOTED_STRING("\"\"rest", "", "rest");
353    TEST_PARSE_QUOTED_STRING("\"unmatched", null, "\"unmatched");
354
355    TEST_MODIFIES_AWAR("# BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", null, null, NULL);
356    TEST_MODIFIES_AWAR("BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "app", "awar_name", NULL);
357    TEST_MODIFIES_AWAR("BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "app", "awar_name", strdup("app"));
358    TEST_MODIFIES_AWAR("BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "diff", null, strdup("diff"));
359
360    TEST_MODIFIES_AWAR("   \t BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "app", "awar_name", NULL);
361}
362
363#define RUN_TOOL_NEVER_VALGRIND(cmdline)      GBK_system(cmdline)
364#define TEST_RUN_TOOL_NEVER_VALGRIND(cmdline) TEST_EXPECT_NO_ERROR(RUN_TOOL_NEVER_VALGRIND(cmdline))
365
366void TEST_post_process() {
367    // ../../UNIT_TESTER/run/general
368    const char *source   = "general/pp.amc";
369    const char *dest     = "general/pp_out.amc";
370    const char *expected = "general/pp_exp.amc";
371
372    TEST_RUN_TOOL_NEVER_VALGRIND(GBS_global_string("cp %s %s", source, dest));
373
374    char *fulldest = strdup(GB_path_in_ARBHOME(GB_concat_path("UNIT_TESTER/run", dest)));
375    TEST_EXPECT(GB_is_readablefile(fulldest));
376
377    {
378        RecordingMacro recording(fulldest, "whatever", "whatever", true);
379
380        TEST_EXPECT_NO_ERROR(recording.has_error());
381        TEST_EXPECT_NO_ERROR(recording.stop()); // triggers post_process
382    }
383
384    TEST_EXPECT_TEXTFILE_DIFFLINES_IGNORE_DATES(dest, expected, 0);
385    TEST_EXPECT_ZERO_OR_SHOW_ERRNO(GB_unlink(dest));
386
387    free(fulldest);
388}
389
390#endif // UNIT_TESTS
391
392// --------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.