2 /**************************************************************************
3 ** (c) Copyright 2002, Andromeda Technology & Automation
4 ** This is free software; you can redistribute it and/or modify it under the
5 ** terms of the GNU General Public License, see the file COPYING.
6 ***************************************************************************
7 ** MODULE INFORMATION *
8 ***********************
9 ** FILE NAME : message.cpp
10 ** SYSTEM NAME : Gnucomo - Gnu Computer Monitoring
11 ** VERSION NUMBER : $Revision: 1.10 $
13 ** DESCRIPTION : Implementation of the message handling classes
18 ***************************************************************************
19 ** ADMINISTRATIVE INFORMATION *
20 ********************************
21 ** ORIGINAL AUTHOR : Arjen Baart - arjen@andromeda.nl
22 ** CREATION DATE : Sep 16, 2002
23 ** LAST UPDATE : Apr 29, 2003
25 **************************************************************************/
27 /*****************************
29 Revision 1.10 2003-04-29 09:16:44 arjen
31 Only cooked log entries for now.
33 Revision 1.9 2003/03/29 09:04:10 arjen
34 Extract the hostname out of the 'From:' or 'Message-Id:' line
37 Revision 1.8 2003/03/16 09:42:40 arjen
38 Read IRIX system logs.
40 Revision 1.7 2003/02/21 08:08:05 arjen
41 Gcm_input also detects packages that are removed from the system.
42 Determining the version number of a package in a RPM
43 list is improved. Only the last one or two parts of the string that
44 begin with a '-' and a number are considered the version.
46 Revision 1.6 2003/02/05 09:37:51 arjen
47 Create notifications when a new package is discovered
48 in a 'rpm -qa' list or when the version of a package is changed.
50 Revision 1.4 2002/12/06 22:26:28 arjen
51 Set the value of log.processed to FALSE when inserting a
52 new log entry into the database
53 When a syslog entry arrives from last year, gcm_input subtracts one from the
54 year of arrival to create the year of the log entry.
55 Read output from "rpm -qa" and enter packages in the parameter table.
57 Revision 1.3 2002/11/09 08:04:27 arjen
58 Added a reference to the GPL
60 Revision 1.2 2002/11/04 10:13:36 arjen
61 Use proper namespace for iostream classes
63 Revision 1.1 2002/10/05 10:25:49 arjen
64 Creation of gcm_input and a first approach to a web interface
66 *****************************/
68 static const char *RCSID = "$Id: message.cpp,v 1.10 2003-04-29 09:16:44 arjen Exp $";
71 #include <libxml/xpath.h>
72 #include <libxml/debugXML.h>
75 extern bool verbose; /* Defined in the main application */
78 /* Utility functions */
80 String SQL_Escape(String s);
82 /*=========================================================================
84 ** SYNOPSIS : bool operator >> (message_buffer &, String &)
86 ** RETURN VALUE : True if input was available.
88 ** DESCRIPTION : Input operator. Read the next line from the message.
94 ** LAST MODIFIED : Nov 04, 2002
95 **=========================================================================
98 bool operator >> (message_buffer &b, String &s)
100 bool input_ok = false;
102 if (b.next_line == b.buffer.end())
108 b.buffer.push_back(l);
110 // next_line keeps pointing to the end.
125 client_message::client_message(std::istream *in, gnucomo_database db)
132 gpg_encrypted = false;
133 classification = UNKNOWN;
138 static const String syslog_date_re("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
139 static const String mail_date_re("[[:alpha:]]{3}, [ 123]?[0-9] [[:alpha:]]{3} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [+-][0-9]{4}");
140 static const String unix_date_re("[[:alpha:]]{3} [[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4}");
141 static const String email_address_re("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
143 static const regex re_syslog(syslog_date_re + " [[:alnum:]]+ [[:alpha:]]+.*:.+");
144 static const regex re_syslog_irix(syslog_date_re + " [0-7][A-T]:[[:alnum:]]+ [[:alpha:]]+.*:.+");
145 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
146 static const regex re_dump("^ *DUMP: Date of this level");
147 static const regex re_accesslog("(GET|POST) .+ HTTP");
148 static const regex re_errorlog("^\\[" + unix_date_re + "\\] \\[(error|notice)\\] .+");
149 static const regex re_rpm("[[:alnum:]+-]+-[0-9][[:alnum:].-]");
151 static const regex re_syslog_date("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
152 static const regex re_uxmail_from("^From [^ \t]+[ ]+" + unix_date_re);
153 static const regex re_mail_From("^From:[[:blank:]]+");
154 static const regex re_mail_Date("^Date:[[:blank:]]+" + mail_date_re);
155 static const regex re_mail_MsId("^Message-Id:[[:blank:]]+");
156 static const regex re_email_address("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
157 static const regex re_email_user("[[:alnum:]_.-]+@");
158 static const regex re_xml_header("\?xml .*\?>$");
160 /*=========================================================================
161 ** NAME : readXMLinput
162 ** SYNOPSIS : int readXMLinput(String first_line)
164 ** RETURN VALUE : Parse the XML input and extract the header information
172 ** LAST MODIFIED : Apr 28, 2003
173 **=========================================================================
176 int client_message::readXMLinput(String first_line)
178 xmlParserCtxtPtr ctxt;
180 xmlNodePtr root, item;
182 xmlXPathObjectPtr res;
183 xmlXPathContextPtr pathcontext;
186 ctxt = xmlCreatePushParserCtxt(NULL, NULL, first_line, ~first_line, NULL);
187 while (input >> line)
189 xmlParseChunk(ctxt, line, ~line, 0);
191 xmlParseChunk(ctxt, "", 0, 1);
192 xmlDom = ctxt->myDoc;
193 xmlFreeParserCtxt(ctxt);
195 root = xmlDocGetRootElement(xmlDom);
196 //TODO Ought to check root->name and root->ns->href
198 pathcontext = xmlXPathNewContext(xmlDom);
199 pathcontext->node = xmlDocGetRootElement(xmlDom);
201 res = xmlXPathEval((const xmlChar *)"header/messagetype/text()", pathcontext);
202 if (res->nodesetval != NULL)
204 item = *res->nodesetval->nodeTab;
206 res = xmlXPathEval((const xmlChar *)"header/hostname/text()", pathcontext);
207 if (res->nodesetval != NULL)
209 item = *res->nodesetval->nodeTab;
210 hostname = (const char *)item->content;
212 res = xmlXPathEval((const xmlChar *)"header/service/text()", pathcontext);
213 if (res->nodesetval != NULL)
215 item = *res->nodesetval->nodeTab;
216 service = (const char *)item->content;
218 res = xmlXPathEval((const xmlChar *)"header/time/text()", pathcontext);
219 if (res->nodesetval != NULL)
221 item = *res->nodesetval->nodeTab;
222 arrival = String((char *)item->content);
225 //xmlDebugDumpNodeList(stdout, *res->nodesetval->nodeTab, 0);
229 /*=========================================================================
231 ** SYNOPSIS : double classify(String host, date arriv_d, hour arriv_t, String serv)
233 ** RETURN VALUE : The certainty with which the message is classified.
241 ** LAST MODIFIED : Apr 28, 2003
242 **=========================================================================
245 double client_message::classify(String host, UTC arriv, String serv)
253 /* First, check if the message has a mail header. */
255 if (input >> line && line == re_uxmail_from)
261 /* Scan ahead for the hostname and date of arrival. */
263 while (input >> line && line != "")
265 if (line == re_mail_From)
267 from_address = line(re_email_address);
268 from_address(re_email_user) = ""; // Remove the user part;
269 if (from_address != "")
271 hostname = from_address;
274 if (line == re_mail_MsId)
276 from_address = line(re_email_address);
277 from_address(re_email_user) = ""; // Remove the user part;
278 if (from_address != "")
280 hostname = from_address;
283 if (line == re_mail_Date)
285 arrival = UTC(line(regex(mail_date_re)));
291 // Push the first line back, we need to read it again.
297 * Now that we have the mail header out of the way, try to figure
298 * out what the content of the message is.
302 while (input >> line && certainty < 0.9)
306 std::cout << " testing: " << line << "\n";
309 if (line == re_xml_header)
312 classification = XML;
315 std::cout << "XML input detected.\n";
319 else if (line == re_syslog)
322 classification = SYSLOG;
325 std::cout << "Syslog detected.\n";
328 else if (line == re_syslog_irix)
331 classification = SYSLOG_IRIX;
334 std::cout << "IRIX Syslog detected.\n";
337 else if (line == re_PGP)
340 gpg_encrypted = true;
341 std::cerr << "The message is PGP/GnuPG encrypted.\n";
343 else if (line == re_dump)
348 std::cout << "DUMP output detected.\n";
351 else if (line == re_accesslog)
354 classification = ACCESSLOG;
358 std::cout << "HTTP access log detected.\n";
361 else if (line == re_errorlog)
364 classification = ERRORLOG;
368 std::cout << "HTTP error log detected.\n";
371 else if (line == re_rpm)
374 classification = RPMLIST;
378 std::cout << "RPM package list detected.\n";
386 std::cerr << "Can not determine the hostname where the message came from.\n";
389 else if (!arrival.proper())
391 std::cerr << "Arrival time is not knwon.\n";
402 /*=========================================================================
404 ** SYNOPSIS : int enterXML()
406 ** RETURN VALUE : None
408 ** DESCRIPTION : Analyze the DOM tree from the XML input.
409 ** The DOM tree was previously parsed by readXMLinput().
415 ** LAST MODIFIED : Apr 29, 2003
416 **=========================================================================
419 void client_message::enterXML()
421 xmlXPathObjectPtr res;
422 xmlXPathContextPtr pathcontext;
424 /* Try to find the host in the database */
428 objectid = database.find_host(hostname);
431 std::cerr << "Please define the host " << hostname << " in the database.\n";
436 std::cout << "Object id for " << hostname << " is " << objectid << "\n";
439 pathcontext = xmlXPathNewContext(xmlDom);
440 pathcontext->node = xmlDocGetRootElement(xmlDom);
441 res = xmlXPathEval((const xmlChar *)"data/node()", pathcontext);
443 if (res->nodesetval != NULL)
445 // Find the first child element of the <data> element.
447 xmlNodePtr node = *res->nodesetval->nodeTab;
448 while (node->type != XML_ELEMENT_NODE)
452 if (strcmp((char *)node->name, "log") == 0)
454 // Each child contains a log entry, raw or cooked.
456 node = node->children;
459 if (node->type == XML_ELEMENT_NODE)
461 if (strcmp((char *)node->name, "raw") == 0)
463 std::cerr << "Can not cook <raw> log elements yet.\n";
465 else if (strcmp((char *)node->name, "cooked") == 0)
467 // Find the parts of the log entry
477 std::cout << "Analyzing cooked element.\n";
479 pathcontext->node = node;
481 res = xmlXPathEval((const xmlChar *)"hostname/text()", pathcontext);
482 if (res->nodesetval != NULL)
484 item = *res->nodesetval->nodeTab;
485 log_hostname = (const char *)item->content;
486 if (log_hostname != hostname(0, ~log_hostname))
488 std::cerr << "Hostname " << log_hostname << " does not match.\n";
494 log_hostname = hostname;
497 res = xmlXPathEval((const xmlChar *)"service/text()", pathcontext);
498 if (res->nodesetval != NULL)
500 item = *res->nodesetval->nodeTab;
501 log_service = (const char *)item->content;
505 log_service = service;
508 res = xmlXPathEval((const xmlChar *)"timestamp/text()", pathcontext);
509 if (res->nodesetval != NULL)
511 item = *res->nodesetval->nodeTab;
512 log_date = String((const char *)item->content);
516 std::cerr << "<timestamp> missing from cooked log element.\n";
519 res = xmlXPathEval((const xmlChar *)"raw/text()", pathcontext);
520 if (res->nodesetval != NULL)
522 item = *res->nodesetval->nodeTab;
523 raw = String((const char *)item->content);
527 std::cerr << "<raw> missing from cooked log element.\n";
530 if (raw != "" && log_hostname != "" && log_date.proper())
532 String insertion("insert into log (objectid, servicecode,"
533 " object_timestamp, timestamp, rawdata, processed) values (");
535 /* Insert a new record into the log table */
537 insertion += "'" + objectid + "',";
538 insertion += "'" + log_service + "',";
539 insertion += "'" + log_date.format("%Y-%m-%d %T") + "',";
540 insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
541 insertion += "'" + SQL_Escape(raw) + "',FALSE";
546 std::cout << insertion << "\n";
550 database.Query(insertion);
565 std::cerr << "Data element " << node->name << " is not supported.\n";
570 std::cerr << "Data node not found.\n";
574 /*=========================================================================
576 ** SYNOPSIS : int enter()
578 ** RETURN VALUE : The number of lines successfully parsed from the input
586 ** LAST MODIFIED : Mar 28, 2003
587 **=========================================================================
590 int client_message::enter()
592 if (classification == XML)
602 String change_notification("");
603 String create_notification("");
604 bool initial_entry = false;
606 std::list<String> packages;
609 /* Double-check the classification of the message */
611 if (classification == UNKNOWN || certainty < 0.9 || gpg_encrypted)
618 // Skip the mail header.
620 while (input >> line && line != "");
623 /* Try to find the host in the database */
627 objectid = database.find_host(hostname);
630 std::cerr << "Please define the host " << hostname << " in the database.\n";
635 std::cout << "Object id for " << hostname << " is " << objectid << "\n";
638 if (classification == RPMLIST)
643 /* Read all packages, so we will know which ones are */
644 /* missing at the end. */
646 qry = "select name from parameter where objectid='";
647 qry += objectid + "' and class='package'";
648 n_packages = database.Query(qry);
649 initial_entry = n_packages == 0;
651 std::cout << n_packages << " packages in database.\n";
652 for (int t = 0; t < n_packages; t++)
654 packages.push_back(database.Field(t, "name"));
656 std::cout << "Package list built: " << packages.size() << ".\n";
659 /* Scan the input line by line, entring records into the database */
661 String rest; // Rest of the line to be parsed
663 while (input >> line)
667 std::cout << line << "\n";
671 /* Check each line if it contains valid information */
675 switch (classification)
681 check = &re_syslog_irix;
684 check = &re_accesslog;
687 check = &re_errorlog;
700 String insertion("insert into log (objectid, servicecode,"
701 " object_timestamp, timestamp, rawdata, processed) values (");
704 switch (classification)
709 if (log_date.Year() < 0 || log_date.Year() > 2500)
711 // The year is not in the log file. Assume the year of arrival,
712 // unless this puts the log entry at a later date than the arrival date.
713 // This happens e.g. when a log entry from December arrives in Januari.
715 log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year());
716 if (log_date > date(arrival))
718 log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year() - 1);
724 std::cout << " Log timestamp = " << log_date << " " << log_time << "\n";
728 if (rest(0,i) == hostname(0,i))
733 std::cout << " Hostname matches.\n";
734 std::cout << " rest = " << rest << "\n";
736 for (i = 0; isalpha(rest[i]) && i < ~rest; i++);
739 std::cout << " Service name = " << rest(0,i) << "\n";
742 /* Insert a new record into the log table */
744 insertion += "'" + objectid + "',";
745 insertion += "'" + rest(0,i) + "',";
746 insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
747 insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
748 insertion += "'" + SQL_Escape(line) + "',FALSE";
753 std::cout << insertion << "\n";
757 database.Query(insertion);
769 std::cerr << " Hostname " << rest(0,i) << " does not match.\n";
776 if (log_date.Year() < 0 || log_date.Year() > 2500)
778 // The year is not in the log file. Assume the year of arrival,
779 // unless this puts the log entry at a later date than the arrival date.
780 // This happens e.g. when a log entry from December arrives in Januari.
782 log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year());
783 if (log_date > date(arrival))
785 log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year() - 1);
791 std::cout << " Log timestamp = " << log_date << " " << log_time << "\n";
795 if (rest(0,i) == hostname(0,i))
800 std::cout << " Hostname matches.\n";
801 std::cout << " rest = " << rest << "\n";
803 for (i = 0; isalpha(rest[i]) && i < ~rest; i++);
806 std::cout << " Service name = " << rest(0,i) << "\n";
809 /* Insert a new record into the log table */
811 insertion += "'" + objectid + "',";
812 insertion += "'" + rest(0,i) + "',";
813 insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
814 insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
815 insertion += "'" + SQL_Escape(line) + "',FALSE";
820 std::cout << insertion << "\n";
824 database.Query(insertion);
836 std::cerr << " Hostname " << rest(0,i) << " does not match.\n";
841 datestring = line(regex("\\[.+\\]"));
844 datestring[datestring.index(':')] = ' ';
845 log_date = datestring;
846 log_time = datestring;
849 std::cout << " Log timestamp = " << log_date << " " << log_time << "\n";
851 insertion += "'" + objectid + "',";
852 insertion += "'" + service + "',";
853 insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
854 insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
855 insertion += "'" + SQL_Escape(line) + "',FALSE";
860 std::cout << insertion << "\n";
864 database.Query(insertion);
876 datestring = line(regex("\\[.+\\]"));
879 log_date = datestring;
880 log_time = datestring;
883 std::cout << " Log timestamp = " << log_date << " " << log_time << "\n";
885 insertion += "'" + objectid + "',";
886 insertion += "'" + service + "',";
887 insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
888 insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
889 insertion += "'" + SQL_Escape(line) + "',FALSE";
894 std::cout << insertion << "\n";
898 database.Query(insertion);
910 // Scan a list of packages and versions from "rpm -a".
911 // A similar listing can be created on IRIX 6.5 by using the
912 // command "showprods -3 -n|awk '{printf "%s-%s\n",$2,$3}'|grep -v '^[-=]' \
913 // |grep -v Version-Description".
915 // We have to separate the package name and the version.
916 // The separation is marked by a '-', followed by a digit.
917 // However, there may be other sequences of '-'digit in the package name,
918 // do we have to scan ahead until there is at most one such sequence
919 // left in the version string. The '-'digit seqeunce inside the
920 // version usually separates the version and the release number.
922 int version_start, next_version_start;
926 next_version_start = i;
928 while (i < ~line - 1)
930 while (i < ~line - 1 && !(line[i] == '-' && isdigit(line[i + 1])))
936 version_start = next_version_start;
937 next_version_start = i;
942 if (!isdigit(line[version_start + 1]))
944 version_start = next_version_start;
946 String package(line(0,version_start));
947 String version(line(version_start + 1, ~line));
954 std::cout << "Package is " << package;
955 std::cout << ", version is " << version << "\n";
958 // Construct a qry to check the package's existance
960 qry = "select paramid from parameter where objectid='";
961 qry += objectid + "' and class='package' and name='";
962 qry += package + "'";
964 if (database.Query(qry) == 1)
966 std::list<String>::iterator lp;
968 lp = find(packages.begin(), packages.end(), package);
969 if (lp != packages.end())
975 std::cerr << "Could NOT find " << package << " in list.\n";
978 paramid = database.Field(0, "paramid");
979 qry = "select value from property where paramid='";
980 qry += paramid + "' and name='version'";
981 if (database.Query(qry) == 0)
983 std::cerr << "Database corruption: Package " << package;
984 std::cerr << " does not have a 'version' property.\n";
986 else if (database.Field(0, "value") != version)
990 std::cout << " Parameter " << package << " has different version\n";
992 insertion = "update property set value='";
993 insertion += version + "' where paramid='";
994 insertion += paramid + "' and name='version'";
996 insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
997 insert_h += " values ('";
998 insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'MODIFIED', 'version', '";
999 insert_h += version + "')";
1001 database.Query(insertion);
1002 database.Query(insert_h);
1004 if (change_notification == "")
1006 remark = "Gnucomo detected a different version for package parameter(s) ";
1007 change_notification = database.new_notification(objectid, "property modified", remark);
1010 if (change_notification != "")
1012 insertion = "insert into parameter_notification (notificationid, paramid) values ('";
1013 insertion += change_notification + "', '";
1014 insertion += paramid + "')";
1016 database.Query(insertion);
1020 std::cerr << "gcm_input ERROR: Cannot create 'property modified' notification.\n";
1027 std::cout << " Parameter " << package << " has not changed.\n";
1036 std::cout << " Parameter " << package << " does not exist.\n";
1038 // Create a new package parameter, including version property and history record
1040 insertion = "insert into parameter (objectid, name, class, description) values ('";
1041 insertion += objectid + "', '" + package + "', 'package', 'RPM package " + package + "')";
1045 std::cout << insertion << "\n";
1049 database.Query(insertion);
1050 qry = "select paramid from parameter where objectid='";
1051 qry += objectid + "' and class='package' and name='";
1052 qry += package + "'";
1053 database.Query(qry);
1054 paramid = database.Field(0, "paramid");
1057 insertion = "insert into property (paramid, name, value, type) values ('";
1058 insertion += paramid + "', 'version', '";
1059 insertion += version + "', 'STATIC')";
1060 insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
1061 insert_h += " values ('";
1062 insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'CREATED', 'version', '";
1063 insert_h += version + "')";
1067 std::cout << insertion << "\n" << insert_h << "\n";
1071 database.Query(insertion);
1072 database.Query(insert_h);
1075 if (create_notification == "")
1077 remark = "Gnucomo detected new parameter(s) of class package";
1078 create_notification = database.new_notification(objectid, "parameter created", remark);
1080 if (create_notification != "")
1082 insertion = "insert into parameter_notification (notificationid, paramid) values ('";
1083 insertion += create_notification + "', '";
1084 insertion += paramid + "')";
1086 database.Query(insertion);
1090 std::cerr << "gcm_input ERROR: Cannot create 'parameter created' notification.\n";
1108 std::cerr << "gcm_input WARNING: Not a valid line: " << line << "\n";
1112 if (classification == RPMLIST)
1114 std::list<String>::iterator lp;
1115 String remove_notification("");
1118 * If there are any packages left in the list, they seem to have
1119 * disappeared from the system.
1122 for (lp = packages.begin(); lp != packages.end(); lp++)
1128 // Construct a qry to check the package's existance
1130 qry = "select paramid from parameter where objectid='";
1131 qry += objectid + "' and class='package' and name='";
1134 if (database.Query(qry) == 1)
1136 paramid = database.Field(0, "paramid");
1137 qry ="select change_nature from history where paramid='";
1138 qry += paramid + "' order by modified desc";
1139 if (database.Query(qry) <= 0)
1141 std::cerr << "Database ERROR: no history record for parameter " << *lp << ".\n";
1143 else if (database.Field(0, "change_nature") != "REMOVED")
1147 std::cout << "Removing parameter " << *lp << ".\n";
1150 insert = "insert into history (paramid, modified, change_nature)";
1151 insert += " values ('";
1152 insert += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'REMOVED')";
1154 database.Query(insert);
1156 if (remove_notification == "")
1158 remark = "Gnucomo detected that package(s) have disappeared ";
1159 remove_notification = database.new_notification(objectid, "parameter removed", remark);
1162 if (remove_notification != "")
1164 insert = "insert into parameter_notification (notificationid, paramid) values ('";
1165 insert += remove_notification + "', '";
1166 insert += paramid + "')";
1168 database.Query(insert);
1172 std::cerr << "gcm_input ERROR: Cannot create 'parameter removed' notification.\n";
1181 std::cout << nr_lines << " lines parsed from the log file.\n";
1186 /*=========================================================================
1187 ** NAME : SQL_Escape
1188 ** SYNOPSIS : String SQL_Escape(String)
1192 ** DESCRIPTION : Insert backslashes before single quotes.
1199 **=========================================================================
1202 String SQL_Escape(String s)
1206 for (i = 0; i < ~s; i++)