diff options
author | Sergei Golubchik <sergii@pisem.net> | 2011-10-01 21:23:01 +0200 |
---|---|---|
committer | Sergei Golubchik <sergii@pisem.net> | 2011-10-01 21:23:01 +0200 |
commit | 4c34bcf8562fac61a918c6364e3bbbe385e8505d (patch) | |
tree | dd58f9392e4e8f4c9e0e8deba29894388c1fd6ff | |
download | mariadb-git-4c34bcf8562fac61a918c6364e3bbbe385e8505d.tar.gz |
initial checkin
-rw-r--r-- | CMakeLists.txt | 11 | ||||
-rw-r--r-- | Makefile.am | 19 | ||||
-rw-r--r-- | feedback.cc | 349 | ||||
-rw-r--r-- | feedback.h | 67 | ||||
-rw-r--r-- | plug.in | 19 | ||||
-rw-r--r-- | sender_thread.cc | 294 | ||||
-rw-r--r-- | url_base.cc | 51 | ||||
-rw-r--r-- | url_http.cc | 308 | ||||
-rw-r--r-- | utils.cc | 299 |
9 files changed, 1417 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000000..a94232a4698 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,11 @@ +INCLUDE("${PROJECT_SOURCE_DIR}/storage/mysql_storage_engine.cmake") + +INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/sql ${CMAKE_SOURCE_DIR}/regex + ${CMAKE_SOURCE_DIR}/extra/yassl/include) + +SET(FEEDBACK_SOURCES feedback.cc sender_thread.cc + url_base.cc url_http.cc utils.cc) + +SET(FEEDBACK_LIBS Ws2_32) + +MYSQL_PLUGIN(FEEDBACK) diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 00000000000..0aea977b464 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,19 @@ +pkgplugindir = $(pkglibdir)/plugin +INCLUDES = -I$(top_srcdir)/include -I$(top_builddir)/include \ + -I$(top_srcdir)/regex -I$(top_srcdir)/sql + +EXTRA_LTLIBRARIES = feedback.la +pkgplugin_LTLIBRARIES = @plugin_feedback_shared_target@ +feedback_la_LDFLAGS = -module -rpath $(pkgplugindir) +feedback_la_CXXFLAGS = -shared -DMYSQL_DYNAMIC_PLUGIN +feedback_la_SOURCES = feedback.cc utils.cc url_base.cc url_http.cc \ + sender_thread.cc + +EXTRA_LIBRARIES = libfeedback.a +noinst_LIBRARIES = @plugin_feedback_static_target@ +libfeedback_a_SOURCES= feedback.cc utils.cc url_base.cc url_http.cc \ + sender_thread.cc + +noinst_HEADERS = feedback.h +EXTRA_DIST = CMakeLists.txt plug.in + diff --git a/feedback.cc b/feedback.cc new file mode 100644 index 00000000000..89fe981cc25 --- /dev/null +++ b/feedback.cc @@ -0,0 +1,349 @@ +/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +#include "feedback.h" + +/* MySQL functions/variables not declared in mysql_priv.h */ +int fill_variables(THD *thd, TABLE_LIST *tables, COND *cond); +int fill_status(THD *thd, TABLE_LIST *tables, COND *cond); +extern ST_SCHEMA_TABLE schema_tables[]; + +namespace feedback { + +char server_uid_buf[SERVER_UID_SIZE+1]; ///< server uid will be written here + +/* backing store for system variables */ +static char *server_uid= server_uid_buf, *url; +char *user_info; +ulong send_timeout, send_retry_wait; + +/** + these three are used to communicate the shutdown signal to the + background thread +*/ +pthread_mutex_t sleep_mutex; +pthread_cond_t sleep_condition; +volatile bool shutdown_plugin; +static pthread_t sender_thread; + +Url **urls; ///< list of urls to send the report to +uint url_count; + +ST_SCHEMA_TABLE *i_s_feedback; ///< table descriptor for our I_S table + +/* + the column names *must* match column names in GLOBAL_VARIABLES and + GLOBAL_STATUS tables otherwise condition pushdown below will not work +*/ +static ST_FIELD_INFO feedback_fields[] = +{ + {"VARIABLE_NAME", 255, MYSQL_TYPE_STRING, 0, 0, 0, 0}, + {"VARIABLE_VALUE", 1024, MYSQL_TYPE_STRING, 0, 0, 0, 0}, + {0, 0, MYSQL_TYPE_NULL, 0, 0, 0, 0} +}; + +static COND * const OOM= (COND*)1; + +/** + Generate the COND tree for the condition pushdown + + This function takes a list of strings and generates an Item tree + corresponding to the following expression: + + field LIKE str1 OR field LIKE str2 OR field LIKE str3 OR ... + + where 'field' is the first field in the table - VARIABLE_NAME field - + and str1, str2... are strings from the list. + + This condition is used to filter the selected rows, emulating + + SELECT * FROM INFORMATION_SCHEMA.GLOBAL_VARIABLES WHERE ... +*/ +static COND* make_cond(THD *thd, TABLE_LIST *tables, LEX_STRING *filter) +{ + Item_cond_or *res= NULL; + Name_resolution_context nrc; + const char *db= tables->db, *table= tables->alias, + *field= tables->table->field[0]->field_name; + CHARSET_INFO *cs= &my_charset_latin1; + + if (!filter->str) + return 0; + + nrc.init(); + nrc.resolve_in_table_list_only(tables); + + res= new Item_cond_or(); + if (!res) + return OOM; + + for (; filter->str; filter++) + { + Item_field *fld= new Item_field(&nrc, db, table, field); + Item_string *pattern= new Item_string(filter->str, filter->length, cs); + Item_string *escape= new Item_string("\\", 1, cs); + + if (!fld || !pattern || !escape) + return OOM; + + Item_func_like *like= new Item_func_like(fld, pattern, escape, 0); + + if (!like) + return OOM; + + res->add(like); + } + + if (res->fix_fields(thd, (Item**)&res)) + return OOM; + + return res; +} + +/** + System variables that we want to see in the feedback report +*/ +static LEX_STRING vars_filter[]= { + {C_STRING_WITH_LEN("auto\\_increment%")}, + {C_STRING_WITH_LEN("binlog\\_format")}, + {C_STRING_WITH_LEN("character\\_set\\_%")}, + {C_STRING_WITH_LEN("collation%")}, + {C_STRING_WITH_LEN("engine\\_condition\\_pushdown")}, + {C_STRING_WITH_LEN("event\\_scheduler")}, + {C_STRING_WITH_LEN("feedback\\_%")}, + {C_STRING_WITH_LEN("ft\\_m%")}, + {C_STRING_WITH_LEN("have\\_%")}, + {C_STRING_WITH_LEN("%\\_size")}, + {C_STRING_WITH_LEN("%\\_length%")}, + {C_STRING_WITH_LEN("%\\_timeout")}, + {C_STRING_WITH_LEN("large\\_%")}, + {C_STRING_WITH_LEN("lc_time_names")}, + {C_STRING_WITH_LEN("log")}, + {C_STRING_WITH_LEN("log_bin")}, + {C_STRING_WITH_LEN("log_output")}, + {C_STRING_WITH_LEN("log_slow_queries")}, + {C_STRING_WITH_LEN("log_slow_time")}, + {C_STRING_WITH_LEN("lower_case%")}, + {C_STRING_WITH_LEN("max_allowed_packet")}, + {C_STRING_WITH_LEN("max_connections")}, + {C_STRING_WITH_LEN("max_prepared_stmt_count")}, + {C_STRING_WITH_LEN("max_sp_recursion_depth")}, + {C_STRING_WITH_LEN("max_user_connections")}, + {C_STRING_WITH_LEN("max_write_lock_count")}, + {C_STRING_WITH_LEN("myisam_recover_options")}, + {C_STRING_WITH_LEN("myisam_repair_threads")}, + {C_STRING_WITH_LEN("myisam_stats_method")}, + {C_STRING_WITH_LEN("myisam_use_mmap")}, + {C_STRING_WITH_LEN("net\\_%")}, + {C_STRING_WITH_LEN("new")}, + {C_STRING_WITH_LEN("old%")}, + {C_STRING_WITH_LEN("optimizer%")}, + {C_STRING_WITH_LEN("profiling")}, + {C_STRING_WITH_LEN("query_cache%")}, + {C_STRING_WITH_LEN("secure_auth")}, + {C_STRING_WITH_LEN("slow_launch_time")}, + {C_STRING_WITH_LEN("sql%")}, + {C_STRING_WITH_LEN("storage_engine")}, + {C_STRING_WITH_LEN("sync_binlog")}, + {C_STRING_WITH_LEN("table_definition_cache")}, + {C_STRING_WITH_LEN("table_open_cache")}, + {C_STRING_WITH_LEN("thread_handling")}, + {C_STRING_WITH_LEN("time_zone")}, + {C_STRING_WITH_LEN("timed_mutexes")}, + {C_STRING_WITH_LEN("version%")}, + {0, 0} +}; + +/** + Status variables that we want to see in the feedback report + + (empty list = no WHERE condition) +*/ +static LEX_STRING status_filter[]= {{0, 0}}; + +/** + Fill our I_S table with data + + This function works by invoking fill_variables() and + fill_status() of the corresponding I_S tables - to have + their data UNION-ed in the same target table. + After that it invokes our own fill_* functions + from the utils.cc - to get the data that aren't available in the + I_S.GLOBAL_VARIABLES and I_S.GLOBAL_STATUS. +*/ +int fill_feedback(THD *thd, TABLE_LIST *tables, COND *unused) +{ + int res; + COND *cond; + + tables->schema_table= schema_tables + SCH_GLOBAL_VARIABLES; + cond= make_cond(thd, tables, vars_filter); + res= (cond == OOM) ? 1 : fill_variables(thd, tables, cond); + + tables->schema_table= schema_tables + SCH_GLOBAL_STATUS; + if (!res) + { + cond= make_cond(thd, tables, status_filter); + res= (cond == OOM) ? 1 : fill_status(thd, tables, cond); + } + + tables->schema_table= i_s_feedback; + res= res || fill_plugin_version(thd, tables) + || fill_misc_data(thd, tables) + || fill_linux_info(thd, tables); + + return res; +} + +/** + plugin initialization function +*/ +static int init(void *p) +{ + i_s_feedback= (ST_SCHEMA_TABLE*) p; + /* initialize the I_S descriptor structure */ + i_s_feedback->fields_info= feedback_fields; ///< field descriptor + i_s_feedback->fill_table= fill_feedback; ///< how to fill the I_S table + i_s_feedback->idx_field1 = 0; ///< virtual index on the 1st col + + if (calculate_server_uid(server_uid_buf)) + return 1; + + prepare_linux_info(); + + url_count= 0; + if (*url) + { + // now we split url on spaces and store them in Url objects + int slot; + char *s, *e; + + for (s= url, url_count= 1; *s; s++) + if (*s == ' ') + url_count++; + + urls= (Url **)my_malloc(url_count*sizeof(Url*), MYF(MY_WME)); + if (!urls) + return 1; + + for (s= url, e = url+1, slot= 0; e[-1]; e++) + if (*e == 0 || *e == ' ') + { + if (e > s && (urls[slot]= Url::create(s, e - s))) + slot++; + else + { + if (e > s) + sql_print_error("feedback plugin: invalid url '%.*s'", (int)(e-s), s); + url_count--; + } + s= e + 1; + } + + // create a background thread to handle urls, if any + if (url_count) + { + pthread_mutex_init(&sleep_mutex, 0); + pthread_cond_init(&sleep_condition, 0); + shutdown_plugin= false; + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + if (pthread_create(&sender_thread, &attr, background_thread, 0) != 0) + { + sql_print_error("feedback plugin: failed to start a background thread"); + return 1; + } + } + else + my_free(urls, MYF(0)); + } + + return 0; +} + +/** + plugin deinitialization function +*/ +static int free(void *p) +{ + if (url_count) + { + pthread_mutex_lock(&sleep_mutex); + shutdown_plugin= true; + pthread_cond_signal(&sleep_condition); + pthread_mutex_unlock(&sleep_mutex); + pthread_join(sender_thread, NULL); + + pthread_mutex_destroy(&sleep_mutex); + pthread_cond_destroy(&sleep_condition); + + for (uint i= 0; i < url_count; i++) + delete urls[i]; + my_free(urls, MYF(0)); + } + return 0; +} + +static MYSQL_SYSVAR_STR(server_uid, server_uid, + PLUGIN_VAR_READONLY | PLUGIN_VAR_NOCMDOPT, + "Automatically calculated server unique id hash.", NULL, NULL, 0); +static MYSQL_SYSVAR_STR(user_info, user_info, + PLUGIN_VAR_READONLY | PLUGIN_VAR_RQCMDARG, + "User specified string that will be included in the feedback report.", + NULL, NULL, ""); +static MYSQL_SYSVAR_STR(url, url, PLUGIN_VAR_READONLY | PLUGIN_VAR_RQCMDARG, + "Space separated URLs to send the feedback report to.", NULL, NULL, + "https://mariadb.org/feedback_plugin/post"); +static MYSQL_SYSVAR_ULONG(send_timeout, send_timeout, PLUGIN_VAR_RQCMDARG, + "Timeout (in seconds) for the sending the report.", + NULL, NULL, 60, 1, 60*60*24, 1); +static MYSQL_SYSVAR_ULONG(send_retry_wait, send_retry_wait, PLUGIN_VAR_RQCMDARG, + "Wait this many seconds before retrying a failed send.", + NULL, NULL, 60, 1, 60*60*24, 1); + +static struct st_mysql_sys_var* settings[] = { + MYSQL_SYSVAR(server_uid), + MYSQL_SYSVAR(user_info), + MYSQL_SYSVAR(url), + MYSQL_SYSVAR(send_timeout), + MYSQL_SYSVAR(send_retry_wait), + NULL +}; + + +static struct st_mysql_information_schema feedback = +{ MYSQL_INFORMATION_SCHEMA_INTERFACE_VERSION }; + +} // namespace feedback + +mysql_declare_plugin(feedback) +{ + MYSQL_INFORMATION_SCHEMA_PLUGIN, + &feedback::feedback, + "FEEDBACK", + "Sergei Golubchik", + "MariaDB User Feedback Plugin", + PLUGIN_LICENSE_GPL, + feedback::init, + feedback::free, + 0x0100, + NULL, + feedback::settings, + NULL +} +mysql_declare_plugin_end; + diff --git a/feedback.h b/feedback.h new file mode 100644 index 00000000000..60ef72eed84 --- /dev/null +++ b/feedback.h @@ -0,0 +1,67 @@ +/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +#define MYSQL_SERVER +#include <mysql_priv.h> + +namespace feedback { + +int fill_feedback(THD *thd, TABLE_LIST *tables, COND *cond); +int fill_plugin_version(THD *thd, TABLE_LIST *tables); +int fill_misc_data(THD *thd, TABLE_LIST *tables); +int fill_linux_info(THD *thd, TABLE_LIST *tables); + +static const int SERVER_UID_SIZE= 29; +extern char server_uid_buf[SERVER_UID_SIZE+1], *user_info; +int calculate_server_uid(char *); +int prepare_linux_info(); + +extern ST_SCHEMA_TABLE *i_s_feedback; + +extern ulong send_timeout, send_retry_wait; + +pthread_handler_t background_thread(void *arg); + +/** + The class for storing urls to send report data to. + + Constructors are private, the object should be created with create() method. + send() method does the actual sending. +*/ +class Url { + protected: + Url(LEX_STRING &url_arg) : full_url(url_arg) {} + const LEX_STRING full_url; + + public: + virtual ~Url() { my_free(full_url.str, MYF(0)); } + + const char *url() { return full_url.str; } + size_t url_length() { return full_url.length; } + virtual int send(const char* data, size_t data_length) = 0; + + static Url* create(const char *url, size_t url_length); +}; + +extern Url **urls; +extern uint url_count; + +/* these are used to communicate with the background thread */ +extern pthread_mutex_t sleep_mutex; +extern pthread_cond_t sleep_condition; +extern volatile bool shutdown_plugin; + +} // namespace feedback + diff --git a/plug.in b/plug.in new file mode 100644 index 00000000000..0c93fef2327 --- /dev/null +++ b/plug.in @@ -0,0 +1,19 @@ +MYSQL_PLUGIN(feedback,[MariaDB User Feedback Plugin], + [MariaDB User Feedback Plugin]) + +dnl Although it's not exactly obvious, top-level CMakeLists.txt parses plug.in +dnl files, in particular looking for what the library name should be. It uses +dnl regexp that matches MYSQL_PLUGIN_DYNAMIC or MYSQL_PLUGIN_STATIC, followed +dnl by an open parenthesys, and the plugin name. Having engine name enclosed in +dnl square brackets below causes this regexp to fail and as a result feedback +dnl plugin will not be considered for dynamic builds on Windows. +dnl Unfortunately, feedback cannot be built dynamically on Windows, because it +dnl needs to access server internals that aren't designed for plugin use and +dnl aren't marked with MYSQL_PLUGIN_IMPORT. +MYSQL_PLUGIN_DYNAMIC([feedback], [feedback.la]) +MYSQL_PLUGIN_STATIC(feedback, [libfeedback.a]) + +MYSQL_PLUGIN_ACTIONS(feedback, [ + AC_CHECK_HEADERS([netdb.h sys/utsname.h]) +]) + diff --git a/sender_thread.cc b/sender_thread.cc new file mode 100644 index 00000000000..813690b8819 --- /dev/null +++ b/sender_thread.cc @@ -0,0 +1,294 @@ +/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +#include "feedback.h" +#include <time.h> + +namespace feedback { + +static THD *thd= 0; ///< background thread thd +static my_thread_id thd_thread_id; ///< its thread_id + +static size_t needed_size= 20480; + +static const time_t next_interval= 60*60*24*7; ///< in seconds (one week) +static const time_t first_interval= 60*60*24; ///< in seconds (one day) + +/** + reads the rows from a table and puts them, concatenated, in a String + + @note + 1. only supports two column tables - no less, no more. + 2. it emulates mysql -e "select * from..." and thus it separates + columns with \t and starts the output with column names. +*/ +static int table_to_string(TABLE *table, String *result) +{ + bool res; + char buff1[MAX_FIELD_WIDTH], buff2[MAX_FIELD_WIDTH]; + String str1(buff1, sizeof(buff1), system_charset_info); + String str2(buff2, sizeof(buff2), system_charset_info); + + res= table->file->ha_rnd_init(1); + + dbug_tmp_use_all_columns(table, table->read_set); + + while(!res && !table->file->rnd_next(table->record[0])) + { + table->field[0]->val_str(&str1); + table->field[1]->val_str(&str2); + if (result->reserve(str1.length() + str2.length() + 3)) + res= 1; + else + { + result->qs_append(str1.ptr(), str1.length()); + result->qs_append('\t'); + result->qs_append(str2.ptr(), str2.length()); + result->qs_append('\n'); + } + } + + res = res || result->append('\n'); + + /* + Note, "|=" and not "||" - because we want to call ha_rnd_end() + even if res is already 1. + */ + res |= table->file->ha_rnd_end(); + + return res; +} + +/** + Initialize the THD and TABLE_LIST + + The structures must be sufficiently initialized for create_tmp_table() + and fill_feedback() to work. +*/ +static int prepare_for_fill(TABLE_LIST *tables) +{ + /* + Add our thd to the list, for it to be visible in SHOW PROCESSLIST. + But don't generate thread_id every time - use the saved value + (every increment of global thread_id counts as a new connection + in SHOW STATUS and we want to avoid skewing the statistics) + */ + thd->thread_id= thd->variables.pseudo_thread_id= thd_thread_id; + pthread_mutex_lock(&LOCK_thread_count); + thread_count++; + threads.append(thd); + pthread_mutex_unlock(&LOCK_thread_count); + thd->thread_stack= (char*) &tables; + if (thd->store_globals()) + return 1; + + thd->mysys_var->current_cond= &sleep_condition; + thd->mysys_var->current_mutex= &sleep_mutex; + thd->proc_info="feedback"; + thd->command=COM_SLEEP; + thd->version=refresh_version; + thd->system_thread= SYSTEM_THREAD_EVENT_WORKER; // whatever + thd->set_time(); + thd->init_for_queries(); + thd->real_id= pthread_self(); + thd->db= NULL; + thd->db_length= 0; + thd->security_ctx->host_or_ip= ""; + thd->security_ctx->db_access= DB_ACLS; + thd->security_ctx->master_access= ~NO_ACCESS; + bzero((char*) &thd->net, sizeof(thd->net)); + lex_start(thd); + mysql_init_select(thd->lex); + + tables->init_one_table(INFORMATION_SCHEMA_NAME.str, + i_s_feedback->table_name, TL_READ); + tables->schema_table= i_s_feedback; + tables->table= i_s_feedback->create_table(thd, tables); + if (!tables->table) + return 1; + + tables->table->pos_in_table_list= tables; + + return 0; +} + +/** + Try to detect if this thread is going down + + which can happen for different reasons: + * plugin is being unloaded + * mysqld server is being shut down + * the thread is being killed + +*/ +static bool going_down() +{ + return shutdown_plugin || shutdown_in_progress || (thd && thd->killed); +} + +/** + just like sleep, but waits on a condition and checks "plugin shutdown" status +*/ +static int delay(time_t sec) +{ + struct timespec abstime; + int ret= 0; + + set_timespec(abstime, sec); + + pthread_mutex_lock(&sleep_mutex); + while (!going_down() && ret != ETIMEDOUT) + ret= pthread_cond_timedwait(&sleep_condition, &sleep_mutex, &abstime); + pthread_mutex_unlock(&sleep_mutex); + + return going_down(); +} + +/** + create a feedback report and send it to all specified urls + + If "when" argument is not null, only it and the server uid are sent. + Otherwise a full report is generated. +*/ +static void send_report(const char *when) +{ + TABLE_LIST tables; + String str; + int i, last_todo; + Url **todo= (Url**)alloca(url_count*sizeof(Url*)); + + str.alloc(needed_size); // preallocate it to avoid many small mallocs + + /* + on startup and shutdown the server may not be completely + initialized, and full report won't work. + We send a short status notice only. + */ + if (when) + { + str.length(0); + str.append(STRING_WITH_LEN("FEEDBACK_SERVER_UID")); + str.append('\t'); + str.append(server_uid_buf); + str.append('\n'); + str.append(STRING_WITH_LEN("FEEDBACK_WHEN")); + str.append('\t'); + str.append(when); + str.append('\n'); + str.append(STRING_WITH_LEN("FEEDBACK_USER_INFO")); + str.append('\t'); + str.append(user_info); + str.append('\n'); + str.append('\n'); + } + else + { + /* + otherwise, prepare the THD and TABLE_LIST, + create and fill the temporary table with data just like + SELECT * FROM IFROEMATION_SCHEMA.feedback is doing, + read and concatenate table data into a String. + */ + if (!(thd= new THD())) + return; + + if (prepare_for_fill(&tables)) + goto ret; + + if (fill_feedback(thd, &tables, NULL)) + goto ret; + + if (table_to_string(tables.table, &str)) + goto ret; + + needed_size= (size_t)(str.length() * 1.1); + + free_tmp_table(thd, tables.table); + tables.table= 0; + } + + /* + Try to send the report on every url from the list, remove url on success, + keep failed in the list. Repeat until the list is empty. + */ + memcpy(todo, urls, url_count*sizeof(Url*)); + last_todo= url_count - 1; + do + { + for (i= 0; i <= last_todo;) + { + Url *url= todo[i]; + + if (thd) // for nicer SHOW PROCESSLIST + thd->set_query(const_cast<char*>(url->url()), url->url_length()); + + if (url->send(str.ptr(), str.length())) + i++; + else + todo[i]= todo[last_todo--]; + } + if (last_todo < 0) + break; + } while (delay(send_retry_wait) == 0); // wait a little bit before retrying + +ret: + if (thd) + { + if (tables.table) + free_tmp_table(thd, tables.table); + /* + clean up, free the thd. + reset all thread local status variables to minimize + the effect of the background thread on SHOW STATUS. + */ + pthread_mutex_lock(&LOCK_thread_count); + bzero(&thd->status_var, sizeof(thd->status_var)); + thread_count--; + thd->killed= THD::KILL_CONNECTION; + pthread_cond_broadcast(&COND_thread_count); + pthread_mutex_unlock(&LOCK_thread_count); + delete thd; + thd= 0; + } +} + +/** + background sending thread +*/ +pthread_handler_t background_thread(void *arg __attribute__((unused))) +{ + time_t interval; + + if (my_thread_init()) + return 0; + + pthread_mutex_lock(&LOCK_thread_count); + thd_thread_id= thread_id++; + pthread_mutex_unlock(&LOCK_thread_count); + + send_report("startup"); + + for (interval= first_interval; delay(interval) == 0; interval= next_interval) + send_report(NULL); + + send_report("shutdown"); + + my_thread_end(); + pthread_exit(0); + return 0; +} + +} // namespace feedback + diff --git a/url_base.cc b/url_base.cc new file mode 100644 index 00000000000..38b2ca86e9f --- /dev/null +++ b/url_base.cc @@ -0,0 +1,51 @@ +/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +#include "feedback.h" + +namespace feedback { + +Url* http_create(const char *url, size_t url_length); + +/** + creates an Url object out of an url, if possible. + + This is done by invoking corresponding creator functions + of the derived classes, until the first not NULL result. +*/ +Url* Url::create(const char *url, size_t url_length) +{ + url= my_strndup(url, url_length, MYF(MY_WME)); + + if (!url) + return NULL; + + Url *self= http_create(url, url_length); + + /* + here we can add + + if (!self) self= smtp_create(url, url_length); + if (!self) self= tftp_create(url, url_length); + etc + */ + + if (!self) + my_free(const_cast<char*>(url), MYF(0)); + + return self; +} + +} // namespace feedback diff --git a/url_http.cc b/url_http.cc new file mode 100644 index 00000000000..efe987327d3 --- /dev/null +++ b/url_http.cc @@ -0,0 +1,308 @@ +/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +#include "feedback.h" + +#ifdef HAVE_NETDB_H +#include <netdb.h> +#endif + +#ifdef _WIN32 +#undef VOID +#define VOID void +#include <ws2tcpip.h> +#define addrinfo ADDRINFOA +#endif + +namespace feedback { + +static const char *http= "http://"; +static const size_t http_len= 7; +static const char *https= "https://"; +static const size_t https_len= 8; + +static const uint FOR_READING= 0; +static const uint FOR_WRITING= 1; + +#ifdef MARIADB_BASE_VERSION +#define ssl_connect(A,B,C,D) sslconnect(A,B,C,D) +#else +#define ssl_connect(A,B,C,D) sslconnect(A,B,C) +#endif + +/** + implementation of the Url class that sends the data via HTTP POST request. + + Both http:// and https:// protocols are supported. +*/ +class Url_http: public Url { + protected: + const LEX_STRING host, port, path; + bool ssl; + + Url_http(LEX_STRING &url_arg, LEX_STRING &host_arg, + LEX_STRING &port_arg, LEX_STRING &path_arg, bool ssl_arg) : + Url(url_arg), host(host_arg), port(port_arg), path(path_arg), ssl(ssl_arg) + {} + ~Url_http() + { + my_free(host.str, MYF(0)); + my_free(port.str, MYF(0)); + my_free(path.str, MYF(0)); + } + + public: + int send(const char* data, size_t data_length); + + friend Url* http_create(const char *url, size_t url_length); +}; + +/** + create a Url_http object out of the url, if possible. + + @note + Arbitrary limitations here. + + The url must be http[s]://hostname[:port]/path + No username:password@ or ?script=parameters are supported. + + But it's ok. This is not a generic purpose www browser - it only needs to be + good enough to POST the data to mariadb.org. +*/ +Url* http_create(const char *url, size_t url_length) +{ + const char *s; + LEX_STRING full_url= {const_cast<char*>(url), url_length}; + LEX_STRING host, port, path; + bool ssl= false; + + if (is_prefix(url, http)) + s= url + http_len; +#ifdef HAVE_OPENSSL + else if (is_prefix(url, https)) + { + ssl= true; + s= url + https_len; + } +#endif + else + return NULL; + + for (url= s; *s && *s != ':' && *s != '/'; s++) /* no-op */; + host.str= const_cast<char*>(url); + host.length= s-url; + + if (*s == ':') + { + for (url= ++s; *s && *s >= '0' && *s <= '9'; s++) /* no-op */; + port.str= const_cast<char*>(url); + port.length= s-url; + } + else + { + if (ssl) + { + port.str= const_cast<char*>("443"); + port.length=3; + } + else + { + port.str= const_cast<char*>("80"); + port.length=2; + } + } + + if (*s == 0) + { + path.str= const_cast<char*>("/"); + path.length= 1; + } + else + { + path.str= const_cast<char*>(s); + path.length= strlen(s); + } + if (!host.length || !port.length || path.str[0] != '/') + return NULL; + + host.str= my_strndup(host.str, host.length, MYF(MY_WME)); + port.str= my_strndup(port.str, port.length, MYF(MY_WME)); + path.str= my_strndup(path.str, path.length, MYF(MY_WME)); + + if (!host.str || !port.str || !path.str) + { + my_free(host.str, MYF(MY_ALLOW_ZERO_PTR)); + my_free(port.str, MYF(MY_ALLOW_ZERO_PTR)); + my_free(path.str, MYF(MY_ALLOW_ZERO_PTR)); + return NULL; + } + + return new Url_http(full_url, host, port, path, ssl); +} + +/* do the vio_write and check that all data were sent ok */ +#define write_check(VIO, DATA, LEN) \ + (vio_write((VIO), (uchar*)(DATA), (LEN)) != (LEN)) + +int Url_http::send(const char* data, size_t data_length) +{ + my_socket fd= INVALID_SOCKET; + char buf[1024]; + uint len; + + addrinfo *addrs, *addr, filter= {0, AF_UNSPEC, SOCK_STREAM, 6, 0, 0, 0, 0}; + int res= getaddrinfo(host.str, port.str, &filter, &addrs); + + if (res) + { + sql_print_error("feedback plugin: getaddrinfo() failed for url '%s': %s", + full_url.str, gai_strerror(res)); + return 1; + } + + for (addr= addrs; addr != NULL; addr= addr->ai_next) + { + fd= socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); + if (fd == INVALID_SOCKET) + continue; + + if (connect(fd, addr->ai_addr, addr->ai_addrlen) == 0) + break; + + closesocket(fd); + } + + freeaddrinfo(addrs); + + if (fd == INVALID_SOCKET) + { + sql_print_error("feedback plugin: could not connect for url '%s'", + full_url.str); + return 1; + } + + Vio *vio= vio_new(fd, VIO_TYPE_TCPIP, 0); + if (!vio) + { + sql_print_error("feedback plugin: vio_new failed for url '%s'", + full_url.str); + closesocket(fd); + return 1; + } + +#ifdef HAVE_OPENSSL + struct st_VioSSLFd *ssl_fd; + if (ssl) + { + buf[0]= 0; + if (!(ssl_fd= new_VioSSLConnectorFd(0, 0, 0, 0, 0)) || + ssl_connect(ssl_fd, vio, send_timeout, buf)) + { + sql_print_error("feedback plugin: ssl failed for url '%s' %s", + full_url.str, buf); + if (ssl_fd) + free_vio_ssl_acceptor_fd(ssl_fd); + closesocket(fd); + vio_delete(vio); + return 1; + } + } +#endif + + static const LEX_STRING boundary= + { C_STRING_WITH_LEN("----------------------------ba4f3696b39f") }; + static const LEX_STRING header= + { C_STRING_WITH_LEN("\r\n" + "Content-Disposition: form-data; name=\"data\"; filename=\"-\"\r\n" + "Content-Type: application/octet-stream\r\n\r\n") + }; + + len= my_snprintf(buf, sizeof(buf), + "POST %s HTTP/1.0\r\n" + "User-Agent: MariaDB User Feedback Plugin\r\n" + "Host: %s:%s\r\n" + "Accept: */*\r\n" + "Content-Length: %u\r\n" + "Content-Type: multipart/form-data; boundary=%s\r\n" + "\r\n", + path.str, host.str, port.str, + (uint)(2*boundary.length + header.length + data_length + 4), + boundary.str + 2); + + vio_timeout(vio, FOR_READING, send_timeout); + vio_timeout(vio, FOR_WRITING, send_timeout); + res = write_check(vio, buf, len) + || write_check(vio, boundary.str, boundary.length) + || write_check(vio, header.str, header.length) + || write_check(vio, data, data_length) + || write_check(vio, boundary.str, boundary.length) + || write_check(vio, "--\r\n", 4); + + if (res) + sql_print_error("feedback plugin: failed to send report to '%s'", + full_url.str); + else + { + sql_print_information("feedback plugin: report to '%s' was sent", + full_url.str); + + /* + if the data were send successfully, read the reply. + Extract the first string between <h1>...</h1> tags + and put it as a server reply into the error log. + */ + len= vio_read(vio, (uchar*)buf, sizeof(buf)-1); + if (len && len < sizeof(buf)) + { + char *from; + + buf[len+1]= 0; // safety + + if ((from= strstr(buf, "<h1>"))) + { + from+= 4; + char *to= strstr(from, "</h1>"); + if (to) + *to= 0; + else + from= NULL; + } + if (from) + sql_print_information("feedback plugin: server replied '%s'", from); + else + sql_print_warning("feedback plugin: failed to parse server reply"); + } + else + { + res= 1; + sql_print_error("feedback plugin: failed to read server reply"); + } + } + + vio_delete(vio); + +#ifdef HAVE_OPENSSL + if (ssl) + { + SSL_CTX_free(ssl_fd->ssl_context); + my_free(ssl_fd, MYF(0)); + } +#endif + + return res; +} + +} // namespace feedback + diff --git a/utils.cc b/utils.cc new file mode 100644 index 00000000000..f5afd427ebf --- /dev/null +++ b/utils.cc @@ -0,0 +1,299 @@ +/* Copyright (C) 2010 Sergei Golubchik and Monty Program Ab + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ + +#include "feedback.h" + +#ifdef HAVE_UNISTD_H +#include <unistd.h> +#endif + +#include <base64.h> +#include <sha1.h> + +#ifdef HAVE_SYS_UTSNAME_H +#include <sys/utsname.h> +static bool have_ubuf= false; +static struct utsname ubuf; +#endif + +#ifdef TARGET_OS_LINUX +#include <glob.h> +static bool have_distribution= false; +static char distribution[256]; + +static const char *masks[]= { + "/etc/*-version", "/etc/*-release", + "/etc/*_version", "/etc/*_release" +}; +#endif + +bool schema_table_store_record(THD *thd, TABLE *table); + +namespace feedback { + +/* + convenience macros for inserting rows into I_S table. +*/ +#define INSERT2(NAME,LEN,VALUE) \ + do { \ + table->field[0]->store(NAME, LEN, system_charset_info); \ + table->field[1]->store VALUE; \ + if (schema_table_store_record(thd, table)) \ + return 1; \ + } while (0) + +#define INSERT1(NAME,VALUE) \ + do { \ + table->field[0]->store(NAME, sizeof(NAME)-1, system_charset_info); \ + table->field[1]->store VALUE; \ + if (schema_table_store_record(thd, table)) \ + return 1; \ + } while (0) + +static const bool UNSIGNED= true; ///< used below when inserting integers + +/** + callback for fill_plugin_version() - insert a plugin name and its version +*/ +static my_bool show_plugins(THD *thd, plugin_ref plugin, void *arg) +{ + TABLE *table= (TABLE*) arg; + char version[20]; + size_t version_len; + + version_len= my_snprintf(version, sizeof(version), "%d.%d", + (plugin_decl(plugin)->version) >> 8, + (plugin_decl(plugin)->version) & 0xff); + + INSERT2(plugin_name(plugin)->str, plugin_name(plugin)->length, + (version, version_len, system_charset_info)); + + return 0; +} + +/** + inserts all plugins and their versions into I_S.FEEDBACK +*/ +int fill_plugin_version(THD *thd, TABLE_LIST *tables) +{ + return plugin_foreach_with_mask(thd, show_plugins, MYSQL_ANY_PLUGIN, + ~PLUGIN_IS_FREED, tables->table); +} + +#if defined(_SC_PAGE_SIZE) && !defined(_SC_PAGESIZE) +#define _SC_PAGESIZE _SC_PAGE_SIZE +#endif + +/** + return the amount of physical memory +*/ +static ulonglong my_getphysmem() +{ + ulonglong pages= 0; +#ifdef _SC_PHYS_PAGES + pages= sysconf(_SC_PHYS_PAGES); +#else + return 0; +#endif + +#ifdef _SC_PAGESIZE + return pages * sysconf(_SC_PAGESIZE); +#else + return pages * my_getpagesize(); +#endif +} + +/* get the number of (online) CPUs */ +int my_getncpus() +{ +#ifdef _SC_NPROCESSORS_ONLN + return sysconf(_SC_NPROCESSORS_ONLN); +#elif defined(__WIN__) + SYSTEM_INFO sysinfo; + GetSystemInfo(&sysinfo); + return sysinfo.dwNumberOfProcessors; +#else + return 0; +#endif +} + +/** + Find the version of the kernel and the linux distribution +*/ +int prepare_linux_info() +{ +#ifdef HAVE_SYS_UTSNAME_H + have_ubuf= (uname(&ubuf) != -1); +#endif + +#ifdef TARGET_OS_LINUX + /* + let's try to find what linux distribution it is + we read *[-_]{release,version} file in /etc. + + Either it will be /etc/lsb-release, such as + + ==> /etc/lsb-release <== + DISTRIB_ID=Ubuntu + DISTRIB_RELEASE=8.04 + DISTRIB_CODENAME=hardy + DISTRIB_DESCRIPTION="Ubuntu 8.04.4 LTS" + + Or a one-liner with the description (/etc/SuSE-release has more + than one line, but the description is the first, so it can be + treated as a one-liner). + + We'll read lsb-release first, and if it's not found will search + for other files (*-version *-release *_version *_release) +*/ + int fd; + have_distribution= false; + if ((fd= my_open("/etc/lsb-release", O_RDONLY, MYF(0))) != -1) + { + /* Cool, LSB-compliant distribution! */ + size_t len= my_read(fd, (uchar*)distribution, sizeof(distribution)-1, MYF(0)); + my_close(fd, MYF(0)); + if (len != (size_t)-1) + { + distribution[len]= 0; // safety + char *found= strstr(distribution, "DISTRIB_DESCRIPTION="); + if (found) + { + have_distribution= true; + char *end= strstr(found, "\n"); + if (end == NULL) + end= distribution + len; + found+= 20; + + if (*found == '"' && end[-1] == '"') + { + found++; + end--; + } + *end= 0; + + char *to= strmov(distribution, "lsb: "); + memmove(to, found, end - found + 1); + } + } + } + + /* if not an LSB-compliant distribution */ + for (uint i= 0; !have_distribution && i < array_elements(masks); i++) + { + glob_t found; + if (glob(masks[i], GLOB_NOSORT, NULL, &found) == 0) + { + int fd; + if ((fd= my_open(found.gl_pathv[0], O_RDONLY, MYF(0))) != -1) + { + /* + +5 and -8 below cut the file name part out of the + full pathname that corresponds to the mask as above. + */ + char *to= strmov(distribution, found.gl_pathv[0] + 5) - 8; + *to++= ':'; + *to++= ' '; + + size_t to_len= distribution + sizeof(distribution) - 1 - to; + size_t len= my_read(fd, (uchar*)to, to_len, MYF(0)); + my_close(fd, MYF(0)); + if (len != (size_t)-1) + { + to[len]= 0; // safety + char *end= strstr(to, "\n"); + if (end) + *end= 0; + have_distribution= true; + } + } + } + globfree(&found); + } +#endif + return 0; +} + +/** + Add the linux distribution and the kernel version +*/ +int fill_linux_info(THD *thd, TABLE_LIST *tables) +{ + TABLE *table= tables->table; + CHARSET_INFO *cs= system_charset_info; + +#ifdef HAVE_SYS_UTSNAME_H + if (have_ubuf) + { + INSERT1("Uname_sysname", (ubuf.sysname, strlen(ubuf.sysname), cs)); + INSERT1("Uname_release", (ubuf.release, strlen(ubuf.release), cs)); + INSERT1("Uname_version", (ubuf.version, strlen(ubuf.version), cs)); + INSERT1("Uname_machine", (ubuf.machine, strlen(ubuf.machine), cs)); + } +#endif + +#ifdef TARGET_OS_LINUX + if (have_distribution) + INSERT1("Uname_distribution", (distribution, strlen(distribution), cs)); +#endif + + return 0; +} + +/** + Adds varios bits of information to the I_S.FEEDBACK +*/ +int fill_misc_data(THD *thd, TABLE_LIST *tables) +{ + TABLE *table= tables->table; + +#ifdef MY_ATOMIC_OK + INSERT1("Cpu_count", (my_getncpus(), UNSIGNED)); +#endif + INSERT1("Mem_total", (my_getphysmem(), UNSIGNED)); + + return 0; +} + +/** + calculates the server unique identifier + + UID is a base64 encoded SHA1 hash of the MAC address of one of + the interfaces, and the tcp port that the server is listening on +*/ +int calculate_server_uid(char *dest) +{ + uchar rawbuf[2 + 6]; + uchar shabuf[SHA1_HASH_SIZE]; + SHA1_CONTEXT ctx; + + int2store(rawbuf, mysqld_port); + if (my_gethwaddr(rawbuf + 2)) + { + sql_print_error("feedback plugin: failed to retrieve the MAC address"); + return 1; + } + + mysql_sha1_reset(&ctx); + mysql_sha1_input(&ctx, rawbuf, sizeof(rawbuf)); + mysql_sha1_result(&ctx, shabuf); + + assert(base64_needed_encoded_length(sizeof(shabuf)) <= SERVER_UID_SIZE); + base64_encode(shabuf, sizeof(shabuf), dest); + + return 0; +} + +} // namespace feedback |