source: trunk/SL/MACROS/recmac.cxx

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