| 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 | |
|---|
| 29 | void warn_unrecordable(const char *what) { |
|---|
| 30 | aw_message(GBS_global_string("could not record %s", what)); |
|---|
| 31 | } |
|---|
| 32 | |
|---|
| 33 | void 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 | |
|---|
| 41 | RecordingMacro::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 | |
|---|
| 87 | void 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 | |
|---|
| 103 | void 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 | } |
|---|
| 143 | void 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 | |
|---|
| 153 | void 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 | |
|---|
| 163 | void 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 | |
|---|
| 178 | GB_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 | |
|---|
| 197 | inline 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 | |
|---|
| 207 | inline 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 | |
|---|
| 225 | inline 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 | |
|---|
| 257 | inline 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 | } |
|---|
| 261 | inline 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 | |
|---|
| 266 | inline bool is_comment(const char *line) { |
|---|
| 267 | int i = 0; |
|---|
| 268 | while (isspace(line[i])) ++i; |
|---|
| 269 | return line[i] == '#'; |
|---|
| 270 | } |
|---|
| 271 | |
|---|
| 272 | void 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 | |
|---|
| 346 | void 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 | |
|---|
| 366 | void 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 | // -------------------------------------------------------------------------------- |
|---|