| 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 | |
|---|
| 30 | void warn_unrecordable(const char *what) { |
|---|
| 31 | aw_message(GBS_global_string("could not record %s", what)); |
|---|
| 32 | } |
|---|
| 33 | |
|---|
| 34 | void 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 | |
|---|
| 42 | RecordingMacro::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 | |
|---|
| 88 | void 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 | |
|---|
| 104 | void 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 | } |
|---|
| 144 | void 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 | |
|---|
| 153 | void 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 | |
|---|
| 161 | void 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 | |
|---|
| 180 | void 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 | |
|---|
| 199 | GB_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 | |
|---|
| 218 | inline 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 | |
|---|
| 228 | inline 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 | |
|---|
| 246 | inline 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 | |
|---|
| 278 | inline 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 | } |
|---|
| 282 | inline 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 | |
|---|
| 287 | inline bool is_comment(const char *line) { |
|---|
| 288 | int i = 0; |
|---|
| 289 | while (isspace(line[i])) ++i; |
|---|
| 290 | return line[i] == '#'; |
|---|
| 291 | } |
|---|
| 292 | |
|---|
| 293 | void 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 | |
|---|
| 368 | void 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 | |
|---|
| 385 | void 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 | // -------------------------------------------------------------------------------- |
|---|