ARB
recmac.cxx
Go to the documentation of this file.
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) {
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 
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 
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);
405 
406  free(fulldest);
407 }
408 
409 #endif // UNIT_TESTS
410 
411 // --------------------------------------------------------------------------------
#define ma_assert(bed)
Definition: macro_gui.cxx:28
void track_action(const char *action_id)
Definition: recmac.cxx:161
const char * id
Definition: AliAdmin.cxx:17
#define implicated(hypothesis, conclusion)
Definition: arb_assert.h:289
#define MACRO_WINDOW_ID
void track_awar_change(AW_awar *awar)
Definition: recmac.cxx:180
long GB_mode_of_file(const char *path)
Definition: arb_file.cxx:60
RecordingMacro(const char *filename, const char *application_id_, const char *stop_action_name_, bool expand_existing)
Definition: recmac.cxx:42
GB_ERROR GB_IO_error(const char *action, const char *filename)
Definition: arb_msg.cxx:285
const char * ARB_date_string()
Definition: arb_string.cxx:35
const char * GBS_global_string(const char *templat,...)
Definition: arb_msg.cxx:203
char * AW_get_selected_fullname(AW_root *awr, const char *awar_prefix)
Definition: AW_file.cxx:906
char * ARB_strpartdup(const char *start, const char *end)
Definition: arb_string.h:51
int GB_unlink(const char *path)
Definition: arb_file.cxx:188
char * parse_quoted_string(const char *&line)
Definition: recmac.cxx:228
#define ARRAY_ELEMS(array)
Definition: arb_defs.h:19
GB_ERROR stop()
Definition: recmac.cxx:199
GB_ERROR GB_await_error()
Definition: arb_msg.cxx:342
static AW_root * SINGLETON
Definition: aw_root.hxx:102
#define TEST_EXPECT(cond)
Definition: test_unit.h:1328
static void error(const char *msg)
Definition: mkptypes.cxx:96
GB_CSTR GB_path_in_ARBHOME(const char *relative_path)
Definition: adsocket.cxx:1149
bool opens_macro_dialog(const char *line)
Definition: recmac.cxx:278
char * str
Definition: defines.h:20
char * read_as_string() const
#define TEST_EXPECT_ZERO_OR_SHOW_ERRNO(iocond)
Definition: test_unit.h:1090
const char * GBT_relativeMacroname(const char *macro_name)
Definition: adtools.cxx:907
void write_awar_change(const char *app_id, const char *awar_name, const char *content)
Definition: recmac.cxx:144
GB_CSTR GB_path_in_ARBLIB(const char *relative_path)
Definition: adsocket.cxx:1156
GB_ERROR GB_set_mode_of_file(const char *path, long mode)
Definition: arb_file.cxx:231
char * awar_name
Definition: aw_awar.hxx:103
void write_planned_interruption(const char *displayed_text)
Definition: recmac.cxx:153
GB_CSTR GB_concat_path(GB_CSTR anypath_left, GB_CSTR anypath_right)
Definition: adsocket.cxx:1069
const char * closing_quote(const char *str, char qchar)
Definition: recmac.cxx:218
#define NEVER_RECORDED_ID
Definition: aw_window.hxx:117
#define AWAR_MACRO_BASE
void write_action(const char *app_id, const char *action_name)
Definition: recmac.cxx:104
#define TEST_RUN_TOOL_NEVER_VALGRIND(cmdline)
Definition: test_runtool.h:110
#define TEST_EXPECT_NO_ERROR(call)
Definition: test_unit.h:1118
bool is_end_of_macro(const char *line)
Definition: recmac.cxx:282
void aw_message(const char *msg)
Definition: AW_status.cxx:1142
static int line
Definition: arb_a2ps.c:296
#define NULp
Definition: cxxforward.h:116
bool GB_is_readablefile(const char *filename)
Definition: arb_file.cxx:172
bool ARB_strBeginsWith(const char *str, const char *with)
Definition: arb_str.h:42
#define TEST_EXPECT_TEXTFILE_DIFFLINES(fgot, fwant, diff)
Definition: test_unit.h:1416
bool is_comment(const char *line)
Definition: recmac.cxx:287
void warn_unrecordable(const char *what)
Definition: recmac.cxx:30
#define MACRO_PLAYBACK_MARKED_ID
GB_CSTR GB_getenvARBMACROHOME(void)
Definition: adsocket.cxx:628
static const char * action_name[]
Definition: AWTI_edit.cxx:37
#define MACRO_PLAYBACK_ID
char * modifies_awar(const char *line, char *&app_id)
Definition: recmac.cxx:246
char * GBS_global_string_copy(const char *templat,...)
Definition: arb_msg.cxx:194
char * GB_read_file(const char *path)
Definition: adsocket.cxx:287