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 
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(ARB_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(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 
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) {
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 (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 
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 
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 
200 inline 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 
210 inline 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 
228 inline 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 
260 inline 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 }
264 inline 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 
269 inline bool is_comment(const char *line) {
270  int i = 0;
271  while (isspace(line[i])) ++i;
272  return line[i] == '#';
273 }
274 
275 void 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 
350 void 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 
367 void 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(dest, expected, 0);
387 
388  free(fulldest);
389 }
390 
391 #endif // UNIT_TESTS
392 
393 // --------------------------------------------------------------------------------
#define ma_assert(bed)
Definition: macro_gui.cxx:27
void track_action(const char *action_id)
Definition: recmac.cxx:153
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:166
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:41
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:210
#define ARRAY_ELEMS(array)
Definition: arb_defs.h:19
GB_ERROR stop()
Definition: recmac.cxx:181
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:260
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:143
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
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:200
#define AWAR_MACRO_BASE
void write_action(const char *app_id, const char *action_name)
Definition: recmac.cxx:103
#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:264
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:269
void warn_unrecordable(const char *what)
Definition: recmac.cxx:29
#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:228
char * GBS_global_string_copy(const char *templat,...)
Definition: arb_msg.cxx:194
char * GB_read_file(const char *path)
Definition: adsocket.cxx:287