source: branches/alilink/SL/MACROS/recmac.cxx

Last change on this file was 16768, checked in by westram, 7 years ago
File size: 13.2 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(ARB_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(NULp),
45      out(NULp),
46      error(NULp)
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 = NULp;
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 (action_id[0] == '$') { // actions starting with '$' are interpreted as "unrecordable"
159        warn_unrecordable(GBS_global_string("unrecordable action '%s'", action_id));
160    }
161    else if (strcmp(action_id, stop_action_name) != 0) { // silently ignore stop-recording button press
162        write_action(application_id, action_id);
163    }
164}
165
166void RecordingMacro::track_awar_change(AW_awar *awar) {
167    // see also trackers.cxx@AWAR_CHANGE_TRACKING
168
169    ma_assert(out && !error);
170
171    char *svalue = awar->read_as_string();
172    if (!svalue) {
173        warn_unrecordable(GBS_global_string("change of '%s'", awar->awar_name));
174    }
175    else {
176        write_awar_change(application_id, awar->awar_name, svalue);
177        free(svalue);
178    }
179}
180
181GB_ERROR RecordingMacro::stop() {
182    if (out) {
183        write_dated_comment("recording stopped");
184        write("ARB::close($gb_main);\n");
185        fclose(out);
186
187        post_process();
188
189        long mode = GB_mode_of_file(path);
190        error     = GB_set_mode_of_file(path, mode | ((mode >> 2)& 0111));
191
192        out = NULp;
193    }
194    return error;
195}
196
197// -------------------------
198//      post processing
199
200inline const char *closing_quote(const char *str, char qchar) {
201    const char *found = strchr(str, qchar);
202    if (found>str) {
203        if (found[-1] == '\\') { // escaped -> search behind
204            return closing_quote(found+1, qchar);
205        }
206    }
207    return found;
208}
209
210inline char *parse_quoted_string(const char *& line) {
211    // read '"string"' from start of line.
212    // return 'string'.
213    // skips spaces.
214
215    while (isspace(line[0])) ++line;
216    if (line[0] == '\"' || line[0] == '\'') {
217        const char *other_quote = closing_quote(line+1, line[0]);
218        if (other_quote) {
219            char *str = ARB_strpartdup(line+1, other_quote-1);
220            line      = other_quote+1;
221            while (isspace(line[0])) ++line;
222            return str;
223        }
224    }
225    return NULp;
226}
227
228inline char *modifies_awar(const char *line, char *& app_id) {
229    // return awar_name, if line modifies an awar.
230    // return NULp otherwise
231    //
232    // if 'app_id' is NULp, it'll be set to found application id.
233    // otherwise it'll be checked against found id. function returns NULp on mimatch.
234
235    while (isspace(line[0])) ++line;
236
237    const char cmd[]  = "BIO::remote_awar($gb_main,";
238    const int cmd_len = ARRAY_ELEMS(cmd)-1;
239
240    if (strncmp(line, cmd, cmd_len) == 0) {
241        line     += cmd_len;
242        char *id  = parse_quoted_string(line);
243        if (app_id) {
244            bool app_id_differs = strcmp(app_id, id) != 0;
245            free(id);
246            if (app_id_differs) return NULp;
247        }
248        else {
249            app_id = id;
250        }
251        if (line[0] == ',') {
252            ++line;
253            char *awar = parse_quoted_string(line);
254            return awar;
255        }
256    }
257    return NULp;
258}
259
260inline bool opens_macro_dialog(const char *line) {
261    // return true, if the macro-command in 'line' opens the macro dialog
262    return strcmp(line, "BIO::remote_action($gb_main,\'ARB_NT\',\'macros\');") == 0;
263}
264inline bool is_end_of_macro(const char *line) {
265    // return true, if the macro-command in 'line' belongs to code at end (of any macro)
266    return strcmp(line, "ARB::close($gb_main);") == 0;
267}
268
269inline bool is_comment(const char *line) {
270    int i = 0;
271    while (isspace(line[i])) ++i;
272    return line[i] == '#';
273}
274
275void RecordingMacro::post_process() {
276    ma_assert(!error);
277
278    FileContent macro(path);
279    error = macro.has_error();
280    if (!error) {
281        StrArray& line = macro.lines();
282
283        // remove duplicate awar-changes
284        for (size_t i = 0; i<line.size(); ++i) {
285            char *app_id   = NULp;
286            char *mod_awar = modifies_awar(line[i], app_id);
287            if (mod_awar) {
288                for (size_t n = i+1; n<line.size(); ++n) {
289                    if (!is_comment(line[n])) {
290                        char *mod_next_awar = modifies_awar(line[n], app_id);
291                        if (mod_next_awar) {
292                            if (strcmp(mod_awar, mod_next_awar) == 0) {
293                                // seen two lines (i and n) which modify the same awar
294                                // -> remove the 1st line
295                                line.remove(i);
296
297                                // make sure that it also works for 3 or more consecutive modifications
298                                ma_assert(i>0);
299                                i--;
300                            }
301                            free(mod_next_awar);
302                        }
303                        break;
304                    }
305                }
306                free(mod_awar);
307            }
308            else if (opens_macro_dialog(line[i])) {
309                bool isLastCommand = true;
310                for (size_t n = i+1; n<line.size() && isLastCommand; ++n) {
311                    if (!is_comment(line[n]) && !is_end_of_macro(line[n])) {
312                        isLastCommand = false;
313                    }
314                }
315                if (isLastCommand) {
316                    free(line.replace(i, GBS_global_string_copy("# %s", line[i])));
317                }
318            }
319            free(app_id);
320        }
321        error = macro.save();
322    }
323}
324
325// --------------------------------------------------------------------------------
326
327#ifdef UNIT_TESTS
328#ifndef TEST_UNIT_H
329#include <test_unit.h>
330#endif
331#include <test_runtool.h>
332
333#define TEST_PARSE_QUOTED_STRING(in,res_exp,out_exp) do {       \
334        const char *line = (in);                                \
335        char *res =parse_quoted_string(line);                   \
336        TEST_EXPECT_EQUAL(res, res_exp);                        \
337        TEST_EXPECT_EQUAL(line, out_exp);                       \
338        free(res);                                              \
339    } while(0)
340
341#define TEST_MODIFIES_AWAR(cmd,app_exp,awar_exp,app_in) do {    \
342        char *app  = app_in;                                    \
343        char *awar = modifies_awar(cmd, app);                   \
344        TEST_EXPECTATION(all().of(that(awar).is_equal_to(awar_exp),  \
345                             that(app).is_equal_to(app_exp)));  \
346        free(awar);                                             \
347        free(app);                                              \
348    } while(0)
349
350void TEST_parse() {
351    const char *null = NULp;
352    TEST_PARSE_QUOTED_STRING("", null, "");
353    TEST_PARSE_QUOTED_STRING("\"str\"", "str", "");
354    TEST_PARSE_QUOTED_STRING("\"part\", rest", "part", ", rest");
355    TEST_PARSE_QUOTED_STRING("\"\"", "", "");
356    TEST_PARSE_QUOTED_STRING("\"\"rest", "", "rest");
357    TEST_PARSE_QUOTED_STRING("\"unmatched", null, "\"unmatched");
358
359    TEST_MODIFIES_AWAR("# BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", null, null, NULp);
360    TEST_MODIFIES_AWAR("BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "app", "awar_name", NULp);
361    TEST_MODIFIES_AWAR("BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "app", "awar_name", strdup("app"));
362    TEST_MODIFIES_AWAR("BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "diff", null, strdup("diff"));
363
364    TEST_MODIFIES_AWAR("   \t BIO::remote_awar($gb_main,\"app\", \"awar_name\", \"value\");", "app", "awar_name", NULp);
365}
366
367void TEST_post_process() {
368    // ../../UNIT_TESTER/run/general
369    const char *source   = "general/pp.amc";
370    const char *dest     = "general/pp_out.amc";
371    const char *expected = "general/pp_exp.amc";
372
373    TEST_RUN_TOOL_NEVER_VALGRIND(GBS_global_string("cp %s %s", source, dest));
374
375    char *fulldest = strdup(GB_path_in_ARBHOME(GB_concat_path("UNIT_TESTER/run", dest)));
376    TEST_EXPECT(GB_is_readablefile(fulldest));
377
378    {
379        RecordingMacro recording(fulldest, "whatever", "whatever", true);
380
381        TEST_EXPECT_NO_ERROR(recording.has_error());
382        TEST_EXPECT_NO_ERROR(recording.stop()); // triggers post_process
383    }
384
385    TEST_EXPECT_TEXTFILE_DIFFLINES_IGNORE_DATES(dest, expected, 0);
386    TEST_EXPECT_ZERO_OR_SHOW_ERRNO(GB_unlink(dest));
387
388    free(fulldest);
389}
390
391#endif // UNIT_TESTS
392
393// --------------------------------------------------------------------------------
Note: See TracBrowser for help on using the repository browser.