[glom/mysql] Initial structure of MySQL support
- From: Murray Cumming <murrayc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [glom/mysql] Initial structure of MySQL support
- Date: Tue, 1 Jan 2013 15:49:04 +0000 (UTC)
commit 4a8699e639ca69a801c5afc54f874eb9daf10241
Author: Murray Cumming <murrayc murrayc com>
Date: Tue Jan 1 15:22:16 2013 +0100
Initial structure of MySQL support
configure.ac | 37 +-
glom/libglom/connectionpool_backends/mysql.cc | 962 ++++++++++++++++++++
glom/libglom/connectionpool_backends/mysql.h | 121 +++
glom/libglom/connectionpool_backends/mysql_self.cc | 724 +++++++++++++++
glom/libglom/connectionpool_backends/mysql_self.h | 95 ++
glom/libglom/filelist.am | 6 +-
glom/main.cc | 63 ++-
7 files changed, 2002 insertions(+), 6 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index e75b566..ca1ed24 100644
--- a/configure.ac
+++ b/configure.ac
@@ -110,6 +110,8 @@ AC_ARG_ENABLE([ui-tests],
[glom_enable_ui_tests=yes])
AM_CONDITIONAL([GLOM_ENABLE_UI_TESTS], [test "x$glom_enable_ui_tests" = xyes])
+
+# SQLite support:
AC_ARG_ENABLE([sqlite],
[AS_HELP_STRING([--enable-sqlite],
[Allow creation of SQLite databases and opening
@@ -125,6 +127,22 @@ AS_IF([test "x$glom_enable_sqlite" = xyes],
[AC_DEFINE([GLOM_ENABLE_SQLITE], [1],
[Whether to enable support for SQLite databases.])])
+
+# MySQL support:
+AC_ARG_ENABLE([mysql],
+ [AS_HELP_STRING([--disable-mysql],
+ [do not build with support for MySQL databases])],
+ [glom_enable_mysql=$enableval],
+ [glom_enable_mysql=no])
+
+AM_CONDITIONAL([GLOM_ENABLE_MYSQL], [test "x$glom_enable_mysql" = xyes])
+
+AS_IF([test "x$glom_enable_mysql" = xyes],
+ [AC_DEFINE([GLOM_ENABLE_MYSQL], [1],
+ [Whether to enable support for MySQL databases.])])
+
+
+# PostgreSQL support:
AC_ARG_ENABLE([postgresql],
[AS_HELP_STRING([--disable-postgresql],
[do not build with support for PostgreSQL databases])],
@@ -137,8 +155,9 @@ AS_IF([test "x$glom_enable_postgresql" = xyes],
[AC_DEFINE([GLOM_ENABLE_POSTGRESQL], [1],
[Whether to enable support for PostgreSQL databases.])])
+
# Libraries used by libglom:
-REQUIRED_LIBGLOM_LIBS='giomm-2.4 >= 2.32.0 libxml++-2.6 >= 2.23.1 libxslt >= 1.1.10 pygobject-3.0 >= 2.29.0 libgdamm-5.0 >= 4.99.6 libgda-5.0 >= 5.0.3 libgda-postgres-5.0'
+REQUIRED_LIBGLOM_LIBS='giomm-2.4 >= 2.32.0 libxml++-2.6 >= 2.23.1 libxslt >= 1.1.10 pygobject-3.0 >= 2.29.0 libgdamm-5.0 >= 4.99.6 libgda-5.0 >= 5.0.3 libgda-postgres-5.0 libgda-postgres-5.0 libgda-mysql-5.0'
AS_IF([test "x$glom_host_win32" != xyes],
[REQUIRED_LIBGLOM_LIBS="$REQUIRED_LIBGLOM_LIBS libepc-1.0 >= 0.4.0"])
@@ -224,8 +243,9 @@ AC_ARG_ENABLE([update-mime-database],
AM_CONDITIONAL([UPDATE_MIME_DATABASE], [test "x$glom_update_mime_database" != xno])
-# Locate the directory containing the postgresql utilities, such as the
-# postmaster executable, so we can self-host postgresql databases.
+
+# Locate the directory containing the PostgreSQL utilities, such as the
+# postmaster executable, so we can self-host PostgreSQL databases.
AC_ARG_WITH([postgres-utils],
[AS_HELP_STRING([--with-postgres-utils=DIR],
[path to PostgreSQL utilities (overriding pg_config)])],
@@ -248,6 +268,17 @@ AC_DEFINE_UNQUOTED([POSTGRES_UTILS_PATH], ["$POSTGRES_UTILS_PATH"],
AC_DEFINE_UNQUOTED([EXEEXT], ["$EXEEXT"],
[Define to the file extension of executables on the target.])
+
+# Locate the directory containing the MySQL utilities, such as the
+# postmaster executable, so we can self-host MySQL databases.
+AC_ARG_WITH([mysql-utils],
+ [AS_HELP_STRING([--with-mysql-utils=DIR],
+ [path to MySQL utilities.])],
+ [MYSQL_UTILS_PATH=$withval])
+AC_DEFINE_UNQUOTED([MYSQL_UTILS_PATH], ["$MYSQL_UTILS_PATH"],
+ [Define to the location of the PostgreSQL utilities.])
+
+
GNOME_DOC_INIT([0.9.0],,
[AC_MSG_WARN([[gnome-doc-utils not found: documentation will not be built.]])])
diff --git a/glom/libglom/connectionpool_backends/mysql.cc b/glom/libglom/connectionpool_backends/mysql.cc
new file mode 100644
index 0000000..f738711
--- /dev/null
+++ b/glom/libglom/connectionpool_backends/mysql.cc
@@ -0,0 +1,962 @@
+/* Glom
+ *
+ * Copyright (C) 2001-2013 Murray Cumming
+ *
+ * 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; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * 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., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h" // For MYSQL_UTILS_PATH
+#include <libglom/libglom_config.h>
+
+#include <libglom/connectionpool_backends/mysql.h>
+#include <libglom/spawn_with_feedback.h>
+#include <libglom/utils.h>
+#include <libglom/db_utils.h>
+#include <libgdamm/config.h>
+#include <giomm/file.h>
+#include <glibmm/convert.h>
+#include <glibmm/fileutils.h> //For Glib::file_test().
+#include <glibmm/miscutils.h>
+#include <glibmm/shell.h>
+#include <glib/gstdio.h> /* For g_rename(). TODO: Wrap this in glibmm? */
+#include <glibmm/i18n.h>
+
+#include <iostream>
+
+// Uncomment to see debug messages
+//#define GLOM_CONNECTION_DEBUG
+
+namespace
+{
+
+static Glib::ustring create_auth_string(const Glib::ustring& username, const Glib::ustring& password)
+{
+ if(username.empty() and password.empty())
+ return Glib::ustring();
+ else
+ return "USERNAME=" + Glom::DbUtils::gda_cnc_string_encode(username) +
+ ";PASSWORD=" + Glom::DbUtils::gda_cnc_string_encode(password);
+}
+
+} //anonymous namespace
+
+namespace Glom
+{
+
+namespace ConnectionPoolBackends
+{
+
+MySQL::MySQL()
+: m_port(0),
+ m_mysql_server_version(0.0f)
+{
+}
+
+Glib::RefPtr<Gnome::Gda::Connection> MySQL::attempt_connect(const Glib::ustring& port, const Glib::ustring& database, const Glib::ustring& username, const Glib::ustring& password, bool fake_connection)
+{
+ //We must specify _some_ database even when we just want to create a database.
+ //This _might_ be different on some systems. I hope not. murrayc
+ const Glib::ustring default_database = "template1";
+ //const Glib::ustring& actual_database = (!database.empty()) ? database : default_database;;
+ const Glib::ustring cnc_string_main = "HOST=" + DbUtils::gda_cnc_string_encode(m_host)
+ + ";PORT=" + DbUtils::gda_cnc_string_encode(port);
+ const Glib::ustring cnc_string = cnc_string_main + ";DB_NAME=" + DbUtils::gda_cnc_string_encode(database);
+
+ Glib::RefPtr<Gnome::Gda::Connection> connection;
+ Glib::RefPtr<Gnome::Gda::DataModel> data_model;
+
+ const Glib::ustring auth_string = create_auth_string(username, password);
+
+#ifdef GLOM_CONNECTION_DEBUG
+ std::cout << std::endl << "DEBUG: Glom: trying to connect on port=" << port << std::endl;
+ std::cout << "debug: " << G_STRFUNC << ": cnc_string=" << cnc_string << std::endl;
+ std::cout << " DEBUG: auth_string=" << auth_string << std::endl;
+#endif
+
+ try
+ {
+ if(fake_connection)
+ {
+ connection = Gnome::Gda::Connection::create_from_string("MySQL",
+ cnc_string, auth_string,
+ Gnome::Gda::CONNECTION_OPTIONS_SQL_IDENTIFIERS_CASE_SENSITIVE);
+ }
+ else
+ {
+ connection = Gnome::Gda::Connection::open_from_string("MySQL",
+ cnc_string, auth_string,
+ Gnome::Gda::CONNECTION_OPTIONS_SQL_IDENTIFIERS_CASE_SENSITIVE);
+
+ connection->statement_execute_non_select("SET DATESTYLE = 'ISO'");
+ data_model = connection->statement_execute_select("SELECT version()");
+ }
+ }
+ catch(const Glib::Error& ex)
+ {
+#ifdef GLOM_CONNECTION_DEBUG
+ std::cout << "debug: " << G_STRFUNC << ": Attempt to connect to database failed on port=" << port << ", database=" << database << ": " << "error code=" << ex.code() << ", error message: " << ex.what() << std::endl;
+ std::cout << "debug: " << G_STRFUNC << ": Attempting to connect without specifying the database." << std::endl;
+#endif
+
+ const Glib::ustring cnc_string = cnc_string_main + ";DB_NAME=" + DbUtils::gda_cnc_string_encode(default_database);
+ Glib::RefPtr<Gnome::Gda::Connection> temp_conn;
+ Glib::ustring auth_string = create_auth_string(username, password);
+ try
+ {
+ temp_conn = Gnome::Gda::Connection::open_from_string("MySQL",
+ cnc_string, auth_string,
+ Gnome::Gda::CONNECTION_OPTIONS_SQL_IDENTIFIERS_CASE_SENSITIVE);
+ }
+ catch(const Glib::Error& /* ex */)
+ {
+ //Show this on stderr because it can contain useful clues such as a hostname that cannot be resolved.
+ //std::cerr << G_STRFUNC << ": Attempt to connect to default database failed on port=" << port << " : " << "error code=" << ex.code() << ", error message: " << ex.what() << std::endl;
+ }
+
+#ifdef GLOM_CONNECTION_DEBUG
+ if(temp_conn)
+ std::cout << " (Connection succeeds, but not to the specific database, database=" << database << std::endl;
+ else
+ std::cerr << " (Could not connect even to the default database, database=" << database << std::endl;
+#endif
+
+ throw ExceptionConnection(temp_conn ? ExceptionConnection::FAILURE_NO_DATABASE : ExceptionConnection::FAILURE_NO_SERVER);
+ return Glib::RefPtr<Gnome::Gda::Connection>();
+ }
+
+ if(data_model && data_model->get_n_rows() && data_model->get_n_columns())
+ {
+ const Gnome::Gda::Value value = data_model->get_value_at(0, 0);
+ if(value.get_value_type() == G_TYPE_STRING)
+ {
+ const Glib::ustring version_text = value.get_string();
+ //This seems to have the format "MySQL 7.4.11 on i486-pc-linux"
+ const Glib::ustring namePart = "MySQL ";
+ const Glib::ustring::size_type posName = version_text.find(namePart);
+ if(posName != Glib::ustring::npos)
+ {
+ const Glib::ustring versionPart = version_text.substr(namePart.size());
+ m_mysql_server_version = strtof(versionPart.c_str(), 0);
+
+#ifdef GLOM_CONNECTION_DEBUG
+ std::cout << " MySQL Server version: " << m_mysql_server_version << std::endl;
+#endif
+ }
+ }
+ }
+
+ return connection;
+}
+
+bool MySQL::change_columns(const Glib::RefPtr<Gnome::Gda::Connection>& connection, const Glib::ustring& table_name, const type_vec_const_fields& old_fields, const type_vec_const_fields& new_fields) throw()
+{
+ static const char TRANSACTION_NAME[] = "glom_change_columns_transaction";
+ static const char TEMP_COLUMN_NAME[] = "glom_temp_column"; // TODO: Find a unique name.
+
+ try
+ {
+ connection->begin_transaction(TRANSACTION_NAME, Gnome::Gda::TRANSACTION_ISOLATION_UNKNOWN); // TODO: What does the transaction isolation do?
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": begin_transaction failed: " << ex.what() << std::endl;
+ }
+
+ //Do this all in one big try/catch, block,
+ //reverting the transaction if anything fails:
+ try
+ {
+ for(unsigned int i = 0; i < old_fields.size(); ++ i)
+ {
+ // If the type did change, then we need to recreate the column. See
+ // http://www.mysqlql.org/docs/faqs.FAQ.html#item4.3
+ if(old_fields[i]->get_field_info()->get_g_type() != new_fields[i]->get_field_info()->get_g_type())
+ {
+ // Create a temporary column
+ sharedptr<Field> temp_field = glom_sharedptr_clone(new_fields[i]);
+ temp_field->set_name(TEMP_COLUMN_NAME);
+ // The temporary column must not be primary key as long as the original
+ // (primary key) column is still present, because there cannot be two
+ // primary key columns.
+ temp_field->set_primary_key(false);
+
+ const bool added = add_column(connection, table_name, temp_field);
+ if(!added)
+ {
+ std::cerr << G_STRFUNC << ": add_column() failed." << std::endl;
+ //TODO: Stop the transaction and return?
+ }
+
+ Glib::ustring conversion_command;
+ const Glib::ustring field_name_old_quoted = DbUtils::escape_sql_id(old_fields[i]->get_name());
+ const Field::glom_field_type old_field_type = old_fields[i]->get_glom_type();
+
+ if(Field::get_conversion_possible(old_fields[i]->get_glom_type(), new_fields[i]->get_glom_type()))
+ {
+ //TODO: mysql seems to give an error if the data cannot be converted (for instance if the text is not a numeric digit when converting to numeric) instead of using 0.
+ /*
+ Maybe, for instance:
+ http://groups.google.de/groups?hl=en&lr=&ie=UTF-8&frame=right&th=a7a62337ad5a8f13&seekm=23739.1073660245%40sss.pgh.pa.us#link5
+ UPDATE _table
+ SET _bbb = to_number(substring(_aaa from 1 for 5), '99999')
+ WHERE _aaa <> ' ';
+ */
+
+ switch(new_fields[i]->get_glom_type())
+ {
+ case Field::TYPE_BOOLEAN:
+ {
+ if(old_field_type == Field::TYPE_NUMERIC)
+ {
+ conversion_command = "(CASE WHEN " + field_name_old_quoted + " > 0 THEN true "
+ "WHEN " + field_name_old_quoted + " = 0 THEN false "
+ "WHEN " + field_name_old_quoted + " IS NULL THEN false END)";
+ }
+ else if(old_field_type == Field::TYPE_TEXT)
+ conversion_command = '(' + field_name_old_quoted + " !~~* \'false\')"; // !~~* means ! ILIKE
+ else // Dates and Times:
+ conversion_command = '(' + field_name_old_quoted + " IS NOT NULL)";
+ break;
+ }
+
+ case Field::TYPE_NUMERIC: // CAST does not work if the destination type is numeric
+ {
+ if(old_field_type == Field::TYPE_BOOLEAN)
+ {
+ conversion_command = "(CASE WHEN " + field_name_old_quoted + " = true THEN 1 "
+ "WHEN " + field_name_old_quoted + " = false THEN 0 "
+ "WHEN " + field_name_old_quoted + " IS NULL THEN 0 END)";
+ }
+ else
+ {
+ //We use to_number, with textcat() so that to_number always has usable data.
+ //Otherwise, it says
+ //invalid input syntax for type numeric: " "
+ //
+ //We must use single quotes with the 0, otherwise it says "column 0 does not exist.".
+ conversion_command = "to_number( textcat(\'0\', " + field_name_old_quoted + "), '999999999.99999999' )";
+ }
+
+ break;
+ }
+
+ case Field::TYPE_DATE: // CAST does not work if the destination type is date.
+ {
+ conversion_command = "to_date( " + field_name_old_quoted + ", 'YYYYMMDD' )"; // TODO: Standardise date storage format.
+ break;
+ }
+ case Field::TYPE_TIME: // CAST does not work if the destination type is timestamp.
+ {
+ conversion_command = "to_timestamp( " + field_name_old_quoted + ", 'HHMMSS' )"; // TODO: Standardise time storage format.
+ break;
+ }
+
+ default:
+ {
+ // To Text:
+
+ // bool to text:
+ if(old_field_type == Field::TYPE_BOOLEAN)
+ {
+ conversion_command = "(CASE WHEN " + field_name_old_quoted + " = true THEN \'true\' "
+ "WHEN " + field_name_old_quoted + " = false THEN \'false\' "
+ "WHEN " + field_name_old_quoted + " IS NULL THEN \'false\' END)";
+ }
+ else
+ {
+ // This works for most to-text conversions:
+ conversion_command = "CAST(" + field_name_old_quoted + " AS " + new_fields[i]->get_sql_type() + ")";
+ }
+
+ break;
+ }
+ }
+
+ //TODO: Use SqlBuilder here?
+ connection->statement_execute_non_select("UPDATE " + DbUtils::escape_sql_id(table_name) + " SET " + DbUtils::escape_sql_id(TEMP_COLUMN_NAME) + " = " + conversion_command);
+ }
+ else
+ {
+ // The conversion is not possible, so drop data in that column
+ }
+
+ drop_column(connection, table_name, old_fields[i]->get_name());
+
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " RENAME COLUMN " + DbUtils::escape_sql_id(TEMP_COLUMN_NAME) + " TO " + DbUtils::escape_sql_id(new_fields[i]->get_name()));
+
+ // Read primary key constraint
+ if(new_fields[i]->get_primary_key())
+ {
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " ADD PRIMARY KEY (" + DbUtils::escape_sql_id(new_fields[i]->get_name()) + ")");
+ }
+ }
+ else
+ {
+ // The type did not change. What could have changed: The field being a
+ // unique key, primary key, its name or its default value.
+
+ // Primary key
+ // TODO: Test whether this is able to remove unique key constraints
+ // added via libgda's DDL API in add_column(). Maybe override
+ // add_column() if we can't.
+ bool primary_key_was_set = false;
+ bool primary_key_was_unset = false;
+ if(old_fields[i]->get_primary_key() != new_fields[i]->get_primary_key())
+ {
+ if(new_fields[i]->get_primary_key())
+ {
+ primary_key_was_set = true;
+
+ // Primary key was added
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " ADD PRIMARY KEY (" + DbUtils::escape_sql_id(old_fields[i]->get_name()) + ")");
+
+ // Remove unique key constraint, because this is already implied in
+ // the field being primary key.
+ if(old_fields[i]->get_unique_key())
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " DROP CONSTRAINT " + DbUtils::escape_sql_id(old_fields[i]->get_name() + "_key"));
+ }
+ else
+ {
+ primary_key_was_unset = true;
+
+ // Primary key was removed
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " DROP CONSTRAINT " + DbUtils::escape_sql_id(table_name + "_pkey"));
+ }
+ }
+
+ // Uniqueness
+ if(old_fields[i]->get_unique_key() != new_fields[i]->get_unique_key())
+ {
+ // MySQL automatically makes primary keys unique, so we do not need
+ // to do that separately if we already made it a primary key
+ if(!primary_key_was_set && new_fields[i]->get_unique_key())
+ {
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " ADD CONSTRAINT " + DbUtils::escape_sql_id(old_fields[i]->get_name() + "_key") + " UNIQUE (" + DbUtils::escape_sql_id(old_fields[i]->get_name()) + ")");
+ }
+ else if(!primary_key_was_unset && !new_fields[i]->get_unique_key() && !new_fields[i]->get_primary_key())
+ {
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " DROP CONSTRAINT " + DbUtils::escape_sql_id(old_fields[i]->get_name() + "_key"));
+ }
+ }
+
+ if(!new_fields[i]->get_auto_increment()) // Auto-increment fields have special code as their default values.
+ {
+ if(old_fields[i]->get_default_value() != new_fields[i]->get_default_value())
+ {
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " ALTER COLUMN " + DbUtils::escape_sql_id(old_fields[i]->get_name()) + " SET DEFAULT " + new_fields[i]->sql(new_fields[i]->get_default_value(), connection));
+ }
+ }
+
+ if(old_fields[i]->get_name() != new_fields[i]->get_name())
+ {
+ connection->statement_execute_non_select("ALTER TABLE " + DbUtils::escape_sql_id(table_name) + " RENAME COLUMN " + DbUtils::escape_sql_id(old_fields[i]->get_name()) + " TO " + DbUtils::escape_sql_id(new_fields[i]->get_name()));
+ }
+ }
+ }
+
+ connection->commit_transaction(TRANSACTION_NAME);
+ return true;
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << "Exception: " << ex.what() << std::endl;
+ std::cerr << "Reverting the transaction." << std::endl;
+
+ try
+ {
+ connection->rollback_transaction(TRANSACTION_NAME);
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << "Could not rollback the transaction: Exception: " << ex.what() << std::endl;
+ }
+ }
+
+ return false;
+}
+
+bool MySQL::attempt_create_database(const SlotProgress& slot_progress, const Glib::ustring& database_name, const Glib::ustring& host, const Glib::ustring& port, const Glib::ustring& username, const Glib::ustring& password)
+{
+ slot_progress();
+
+ Glib::RefPtr<Gnome::Gda::ServerOperation> op =
+ Gnome::Gda::ServerOperation::prepare_create_database("MySQL", database_name);
+
+ slot_progress();
+
+ g_assert(op);
+ try
+ {
+ op->set_value_at("/SERVER_CNX_P/HOST", host);
+ op->set_value_at("/SERVER_CNX_P/PORT", port);
+ op->set_value_at("/SERVER_CNX_P/ADM_LOGIN", username);
+ op->set_value_at("/SERVER_CNX_P/ADM_PASSWORD", password);
+ op->perform_create_database("MySQL");
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception: " << ex.what() << std::endl;
+ return false;
+ }
+
+ slot_progress();
+
+ return true;
+}
+
+bool MySQL::check_mysql_gda_client_is_available()
+{
+ //This API is horrible.
+ //See libgda bug http://bugzilla.gnome.org/show_bug.cgi?id=575754
+ Glib::RefPtr<Gnome::Gda::DataModel> model = Gnome::Gda::Config::list_providers();
+ if(model && model->get_n_columns() && model->get_n_rows())
+ {
+ Glib::RefPtr<Gnome::Gda::DataModelIter> iter = model->create_iter();
+
+ do
+ {
+ //See http://library.gnome.org/devel/libgda/unstable/libgda-40-Configuration.html#gda-config-list-providers
+ //about the columns of this DataModel:
+ Gnome::Gda::Value name;
+ try
+ {
+ name = iter->get_value_at(0);
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception: " << ex.what() << std::endl;
+ }
+
+ if(name.get_value_type() != G_TYPE_STRING)
+ continue;
+
+ const Glib::ustring name_as_string = name.get_string();
+ //std::cout << "DEBUG: Provider name:" << name_as_string << std::endl;
+ if(name_as_string == "MySQL")
+ return true;
+ }
+ while(iter->move_next());
+ }
+
+ return false;
+}
+
+std::string MySQL::get_path_to_mysql_executable(const std::string& program, bool quoted)
+{
+#ifdef G_OS_WIN32
+ // Add the .exe extension on Windows:
+ std::string real_program = program + EXEEXT;
+
+ // Have a look at the bin directory of the application executable first.
+ // The installer installs mysql there. mysql needs to be installed
+ // in a directory called bin for its relocation stuff to work, so that
+ // it finds the share data in share. Unfortunately it does not look into
+ // share/mysqlql which would be nice to separate the mysql stuff
+ // from the other shared data. We can perhaps still change this later by
+ // building mysql with another prefix than /local/pgsql.
+ gchar* installation_directory = g_win32_get_package_installation_directory_of_module(0);
+ std::string test;
+
+ try
+ {
+ test = Glib::build_filename(installation_directory, Glib::build_filename("bin", real_program));
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception from Glib::build_filename(): " << ex.what() << std::endl;
+ return std::string();
+ }
+
+ g_free(installation_directory);
+
+ if(Glib::file_test(test, Glib::FILE_TEST_IS_EXECUTABLE))
+ {
+ if(quoted)
+ test = Glib::shell_quote(test);
+ return test;
+ }
+
+ // Look in PATH otherwise
+ std::string path = Glib::find_program_in_path(real_program);
+ if(quoted)
+ path = Glib::shell_quote(path);
+ return path;
+#else // G_OS_WIN32
+ // MYSQL_UTILS_PATH is defined in config.h, based on the configure.
+ try
+ {
+ std::string path = Glib::build_filename(MYSQL_UTILS_PATH, program + EXEEXT);
+ if(quoted)
+ path = Glib::shell_quote(path);
+ return path;
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception from Glib::build_filename(): " << ex.what() << std::endl;
+ return std::string();
+ }
+#endif // !G_OS_WIN32
+}
+
+
+Glib::ustring MySQL::port_as_string(unsigned int port_num)
+{
+ Glib::ustring result;
+ char* cresult = g_strdup_printf("%u", port_num);
+ if(cresult)
+ result = cresult;
+ g_free(cresult);
+
+ return result;
+}
+
+//Because ~/.pgpass is not an absolute path.
+static std::string get_absolute_pgpass_filepath()
+{
+ return Glib::build_filename(
+ Glib::get_home_dir(), ".pgpass");
+}
+
+bool MySQL::save_password_to_pgpass(const Glib::ustring username, const Glib::ustring& password, std::string& filepath_previous, std::string& filepath_original)
+{
+ //Initialize output variables:
+ filepath_previous.clear();
+ filepath_original.clear();
+
+ const std::string filepath_pgpass = get_absolute_pgpass_filepath();
+ filepath_original = filepath_pgpass;
+
+ //Move any existing file out of the way:
+ if(file_exists_filepath(filepath_pgpass))
+ {
+ //std::cout << "DEBUG: File exists: " << filepath_pgpass << std::endl;
+ filepath_previous = filepath_pgpass + ".glombackup";
+ if(g_rename(filepath_pgpass.c_str(), filepath_previous.c_str()) != 0)
+ {
+ std::cerr << G_STRFUNC << "Could not rename file: from=" << filepath_pgpass << ", to=" << filepath_previous << std::endl;
+ return false;
+ }
+ }
+
+ //See http://www.mysqlql.org/docs/8.4/static/libpq-pgpass.html
+ //TODO: Escape \ and : characters.
+ const Glib::ustring contents =
+ m_host + ":" + port_as_string(m_port) + ":*:" + username + ":" + password;
+
+ std::string uri;
+ try
+ {
+ uri = Glib::filename_to_uri(filepath_pgpass);
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception from Glib::filename_from_uri(): " << ex.what() << std::endl;
+ g_rename(filepath_previous.c_str(), filepath_pgpass.c_str());
+ return false;
+ }
+
+ const bool result = create_text_file(uri, contents, true /* current user only */);
+ if(!result)
+ {
+ std::cerr << G_STRFUNC << ": create_text_file() failed." << std::endl;
+ g_rename(filepath_previous.c_str(), filepath_pgpass.c_str());
+ return false;
+ }
+
+ return result;
+}
+
+bool MySQL::save_backup(const SlotProgress& slot_progress, const Glib::ustring& username, const Glib::ustring& password, const Glib::ustring& database_name)
+{
+/* TODO:
+ if(m_network_shared && !running)
+ {
+ std::cerr << G_STRFUNC << ": The self-hosted database is not running." << std::endl;
+ return;
+ }
+*/
+
+ if(m_host.empty())
+ {
+ std::cerr << G_STRFUNC << ": m_host is empty." << std::endl;
+ return false;
+ }
+
+ if(m_port == 0)
+ {
+ std::cerr << G_STRFUNC << ": m_port is empty." << std::endl;
+ return false;
+ }
+
+ //TODO: Remember the existing username and password?
+ if(username.empty())
+ {
+ std::cerr << G_STRFUNC << ": username is empty." << std::endl;
+ return false;
+ }
+
+ if(password.empty())
+ {
+ std::cerr << G_STRFUNC << ": password is empty." << std::endl;
+ return false;
+ }
+
+ // Save the password to ~/.pgpass, because this is the only way to use
+ // pg_dump without it asking for the password:
+ std::string pgpass_backup, pgpass_original;
+ const bool pgpass_created = save_password_to_pgpass(username, password, pgpass_backup, pgpass_original);
+ if(!pgpass_created)
+ {
+ std::cerr << G_STRFUNC << ": save_password_to_pgpass() failed." << std::endl;
+ return false;
+ }
+
+ const std::string path_backup = get_self_hosting_backup_path(std::string(), true /* create parent directory if necessary */);
+ if(path_backup.empty())
+ return false;
+
+ // Make sure to use double quotes for the executable path, because the
+ // CreateProcess() API used on Windows does not support single quotes.
+ const std::string command_dump = get_path_to_mysql_executable("pg_dump") +
+ " --format=c " + // The default (plain) format cannot be used with pg_restore.
+ " --create --file=" + Glib::shell_quote(path_backup) +
+ " --host=" + Glib::shell_quote(m_host) +
+ " --port=" + port_as_string(m_port) +
+ " --username=" + Glib::shell_quote(username) +
+ " " + database_name; //TODO: Quote database_name?
+
+
+ //std::cout << "DEBUG: command_dump=" << command_dump << std::endl;
+
+ const bool result = Glom::Spawn::execute_command_line_and_wait(command_dump, slot_progress);
+
+ //Move the previously-existing .pgpass file back:
+ //TODO: Really, we should just edit the file instead of completely replacing it,
+ // because another application might try to edit it in the meantime.
+ if(!pgpass_backup.empty())
+ {
+ g_rename(pgpass_backup.c_str(), pgpass_original.c_str());
+ }
+
+ if(!result)
+ {
+ std::cerr << "Error while attempting to call pg_dump." << std::endl;
+ }
+
+ return result;
+}
+
+bool MySQL::convert_backup(const SlotProgress& slot_progress, const std::string& base_directory, const Glib::ustring& username, const Glib::ustring& password, const Glib::ustring& database_name)
+{
+/* TODO:
+ if(m_network_shared && !running)
+ {
+ std::cerr << G_STRFUNC << ": The self-hosted database is not running." << std::endl;
+ return;
+ }
+*/
+
+ if(m_host.empty())
+ {
+ std::cerr << G_STRFUNC << ": m_host is empty." << std::endl;
+ return false;
+ }
+
+ if(m_port == 0)
+ {
+ std::cerr << G_STRFUNC << ": m_port is empty." << std::endl;
+ return false;
+ }
+
+ //TODO: Remember the existing username and password?
+ if(username.empty())
+ {
+ std::cerr << G_STRFUNC << ": username is empty." << std::endl;
+ return false;
+ }
+
+ if(password.empty())
+ {
+ std::cerr << G_STRFUNC << ": password is empty." << std::endl;
+ return false;
+ }
+
+ //Make sure the path exists:
+ const std::string path_backup = get_self_hosting_backup_path(base_directory);
+ if(path_backup.empty() || !file_exists_filepath(path_backup))
+ {
+ std::cerr << G_STRFUNC << ": Backup file not found: " << path_backup << std::endl;
+ return false;
+ }
+
+ // Save the password to ~/.pgpass, because this is the only way to use
+ // pg_dump without it asking for the password:
+ std::string pgpass_backup, pgpass_original;
+ const bool pgpass_created = save_password_to_pgpass(username, password, pgpass_backup, pgpass_original);
+ if(!pgpass_created)
+ {
+ std::cerr << G_STRFUNC << ": save_password_to_pgpass() failed." << std::endl;
+ return false;
+ }
+
+ // Make sure to use double quotes for the executable path, because the
+ // CreateProcess() API used on Windows does not support single quotes.
+ const std::string command_restore = get_path_to_mysql_executable("pg_restore") +
+ " -d " + database_name + //TODO: Quote database name?
+ " --host=" + Glib::shell_quote(m_host) +
+ " --port=" + port_as_string(m_port) +
+ " --username=" + Glib::shell_quote(username) +
+ " " + path_backup;
+
+ std::cout << "DEBUG: command_restore=" << command_restore << std::endl;
+
+ //TODO: Put the password in .pgpass
+
+ const bool result = Glom::Spawn::execute_command_line_and_wait(command_restore, slot_progress);
+
+ //Move the previously-existing .pgpass file back:
+ //TODO: Really, we should just edit the file instead of completely replacing it,
+ // because another application might try to edit it in the meantime.
+ if(!pgpass_backup.empty())
+ {
+ g_rename(pgpass_backup.c_str(), pgpass_original.c_str());
+ }
+
+ if(!result)
+ {
+ std::cerr << "Error while attempting to call pg_restore." << std::endl;
+ }
+
+ return result;
+}
+
+std::string MySQL::get_self_hosting_path(bool create, const std::string& child_directory)
+{
+ //Get the filepath of the directory that we should create:
+ const std::string dbdir_uri = m_database_directory_uri;
+ //std::cout << "debug: dbdir_uri=" << dbdir_uri << std::endl;
+
+ std::string dbdir;
+ try
+ {
+ dbdir = Glib::build_filename(
+ Glib::filename_from_uri(dbdir_uri), child_directory);
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception from Glib::build_filename(): " << ex.what() << std::endl;
+ }
+
+ if(file_exists_filepath(dbdir))
+ return dbdir;
+ else if(!create)
+ return std::string();
+
+ //Create the directory:
+
+ //std::cout << "debug: dbdir=" << dbdir << std::endl;
+ g_assert(!dbdir.empty());
+
+ if(create_directory_filepath(dbdir))
+ return dbdir;
+ else
+ return std::string();
+}
+
+std::string MySQL::get_self_hosting_config_path(bool create)
+{
+ return get_self_hosting_path(create, "config");
+}
+
+std::string MySQL::get_self_hosting_data_path(bool create)
+{
+ return get_self_hosting_path(create, "data");
+}
+
+std::string MySQL::get_self_hosting_backup_path(const std::string& base_directory, bool create_parent_dir)
+{
+ //This is a file, not a directory, so we don't use get_self_hosting_path("backup");
+ std::string dbdir;
+ if(base_directory.empty())
+ dbdir = get_self_hosting_path(create_parent_dir);
+ else
+ {
+ dbdir = base_directory;
+ }
+
+ if(dbdir.empty())
+ return std::string();
+
+ try
+ {
+ return Glib::build_filename(dbdir, "backup");
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception from Glib::build_filename(): " << ex.what() << std::endl;
+ return std::string();
+ }
+}
+
+bool MySQL::create_directory_filepath(const std::string& filepath)
+{
+ if(filepath.empty())
+ return false;
+
+ const int mkdir_succeeded = g_mkdir_with_parents(filepath.c_str(), 0770);
+ if(mkdir_succeeded == -1)
+ {
+ std::cerr << G_STRFUNC << ": Error from g_mkdir_with_parents() while trying to create directory: " << filepath << std::endl;
+ perror(" perror(): Error from g_mkdir_with_parents()");
+
+ return false;
+ }
+
+ return true;
+}
+
+bool MySQL::file_exists_filepath(const std::string& filepath)
+{
+ if(filepath.empty())
+ return false;
+
+ const Glib::RefPtr<Gio::File> file = Gio::File::create_for_path(filepath);
+ return file && file->query_exists();
+}
+
+bool MySQL::file_exists_uri(const std::string& uri) const
+{
+ if(uri.empty())
+ return false;
+
+ const Glib::RefPtr<Gio::File> file = Gio::File::create_for_uri(uri);
+ return file && file->query_exists();
+}
+
+
+
+bool MySQL::create_text_file(const std::string& file_uri, const std::string& contents, bool current_user_only)
+{
+ if(file_uri.empty())
+ return false;
+
+ Glib::RefPtr<Gio::File> file = Gio::File::create_for_uri(file_uri);
+ Glib::RefPtr<Gio::FileOutputStream> stream;
+
+ //Create the file if it does not already exist:
+#ifdef GLIBMM_EXCEPTIONS_ENABLED
+ try
+ {
+ if(file->query_exists())
+ {
+ if(current_user_only)
+ {
+ stream = file->replace(std::string() /* etag */, false /* make_backup */, Gio::FILE_CREATE_PRIVATE); //Instead of append_to().
+ }
+ else
+ {
+ stream = file->replace(); //Instead of append_to().
+ }
+ }
+ else
+ {
+ //By default files created are generally readable by everyone, but if we pass FILE_CREATE_PRIVATE in flags the file will be made readable only to the current user, to the level that is supported on the target filesystem.
+ if(current_user_only)
+ {
+ //TODO: Do we want to specify 0660 exactly? (means "this user and his group can read and write this non-executable file".)
+ stream = file->create_file(Gio::FILE_CREATE_PRIVATE);
+ }
+ else
+ {
+ stream = file->create_file();
+ }
+ }
+ }
+ catch(const Gio::Error& ex)
+ {
+#else
+ std::auto_ptr<Gio::Error> error;
+ stream.create(error);
+ if(error.get())
+ {
+ const Gio::Error& ex = *error.get();
+#endif
+ // If the operation was not successful, print the error and abort
+ std::cerr << "ConnectionPool::create_text_file(): exception while creating file." << std::endl
+ << " file uri:" << file_uri << std::endl
+ << " error:" << ex.what() << std::endl;
+ return false; // print_error(ex, output_uri_string);
+ }
+
+
+ if(!stream)
+ return false;
+
+
+ gssize bytes_written = 0;
+ const std::string::size_type contents_size = contents.size();
+#ifdef GLIBMM_EXCEPTIONS_ENABLED
+ try
+ {
+ //Write the data to the output uri
+ bytes_written = stream->write(contents.data(), contents_size);
+ }
+ catch(const Gio::Error& ex)
+ {
+#else
+ bytes_written = stream->write(contents.data(), contents_size, error);
+ if(error.get())
+ {
+ Gio::Error& ex = *error.get();
+#endif
+ // If the operation was not successful, print the error and abort
+ std::cerr << "ConnectionPool::create_text_file(): exception while writing to file." << std::endl
+ << " file uri:" << file_uri << std::endl
+ << " error:" << ex.what() << std::endl;
+ return false; //print_error(ex, output_uri_string);
+ }
+
+ if(bytes_written != (gssize)contents_size)
+ {
+ std::cerr << "ConnectionPool::create_text_file(): not all bytes written when writing to file." << std::endl
+ << " file uri:" << file_uri << std::endl;
+ return false;
+ }
+
+ return true; //Success.
+}
+
+bool MySQL::supports_remote_access() const
+{
+ return true;
+}
+
+Gnome::Gda::SqlOperatorType MySQL::get_string_find_operator() const
+{
+ //TODO_MySQL:
+ // ILIKE is a MySQL extension for locale-dependent case-insensitive matches.
+ //See http://developer.mysqlql.org/pgdocs/mysql/functions-matching.html
+ return Gnome::Gda::SQL_OPERATOR_TYPE_ILIKE;
+}
+
+const char* MySQL::get_public_schema_name() const
+{
+ return "public";
+}
+
+} //namespace ConnectionPoolBackends
+
+} //namespace Glom
diff --git a/glom/libglom/connectionpool_backends/mysql.h b/glom/libglom/connectionpool_backends/mysql.h
new file mode 100644
index 0000000..d711a38
--- /dev/null
+++ b/glom/libglom/connectionpool_backends/mysql.h
@@ -0,0 +1,121 @@
+/* Glom
+ *
+ * Copyright (C) 2001-2013 Murray Cumming
+ *
+ * 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; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * 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., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GLOM_BACKEND_MYSQL_H
+#define GLOM_BACKEND_MYSQL_H
+
+#include <libgdamm/connection.h>
+#include <libglom/connectionpool_backends/backend.h>
+
+#include <libglom/libglom_config.h>
+
+namespace Glom
+{
+
+namespace ConnectionPoolBackends
+{
+
+class MySQL : public Backend
+{
+public:
+ MySQL();
+
+ /** Check whether the libgda mysql provider is really available,
+ * so we can connect to mysql servers,
+ * in case the distro package has incorrect dependencies.
+ *
+ * @results True if everything is OK.
+ */
+ static bool check_mysql_gda_client_is_available();
+
+ /** Save a backup file, using the same directory layout as used by self-hosting.
+ */
+ virtual bool save_backup(const SlotProgress& slot_progress, const Glib::ustring& username, const Glib::ustring& password, const Glib::ustring& database_name);
+
+ virtual bool convert_backup(const SlotProgress& slot_progress, const std::string& base_directory, const Glib::ustring& username, const Glib::ustring& password, const Glib::ustring& database_name);
+
+ /** Return the quoted path to the specified PostgreSQL utility.
+ */
+ static std::string get_path_to_mysql_executable(const std::string& program, bool quoted = true);
+
+
+
+private:
+ virtual bool supports_remote_access() const;
+ virtual Gnome::Gda::SqlOperatorType get_string_find_operator() const;
+ virtual const char* get_public_schema_name() const;
+
+ virtual bool change_columns(const Glib::RefPtr<Gnome::Gda::Connection>& connection, const Glib::ustring& table_name, const type_vec_const_fields& old_fields, const type_vec_const_fields& new_fields) throw();
+
+protected:
+ bool attempt_create_database(const SlotProgress& slot_progress, const Glib::ustring& database_name, const Glib::ustring& host, const Glib::ustring& port, const Glib::ustring& username, const Glib::ustring& password);
+
+ /** Attempt to connect to the database with the specified criteria.
+ * @throws An ExceptionConnection if the correction failed.
+ */
+ Glib::RefPtr<Gnome::Gda::Connection> attempt_connect(const Glib::ustring& port, const Glib::ustring& database, const Glib::ustring& username, const Glib::ustring& password, bool fake_connection);
+
+ std::string get_self_hosting_path(bool create = false, const std::string& child_directory = std::string());
+
+ /** Get the path to the config sub-directory, optionally creating it.
+ */
+ std::string get_self_hosting_config_path(bool create = false);
+
+ /** Get the path to the data sub-directory, optionally creating it.
+ */
+ std::string get_self_hosting_data_path(bool create = false);
+
+ /** Get the path to the backup file, regardless of whether it exists.
+ * @param base_directory Where to find the backup file, under a normal Glom directory structure.
+ * If @a base_directory is empty then it uses get_database_directory_uri().
+ */
+ std::string get_self_hosting_backup_path(const std::string& base_directory = std::string(), bool create_parent_dir = false);
+
+ bool create_directory_filepath(const std::string& filepath);
+ bool file_exists_filepath(const std::string& filepath);
+ bool file_exists_uri(const std::string& uri) const;
+
+ /**
+ * @param current_user_only If true then only the current user will be able to read or write the file.
+ */
+ static bool create_text_file(const std::string& file_uri, const std::string& contents, bool current_user_only = false);
+
+ /**
+ * @param filepath_previous The path to which the previous .pgpass, if any was moved.
+ * @param filepath_original The path to which filepath_previous should be moved back after the caller has finished.
+ * @param result whether it succeeded.
+ */
+ bool save_password_to_pgpass(const Glib::ustring username, const Glib::ustring& password, std::string& filepath_previous, std::string& filepath_original);
+
+protected:
+ static Glib::ustring port_as_string(unsigned int port_num);
+
+ Glib::ustring m_host;
+ unsigned int m_port;
+
+private:
+ float m_mysql_server_version;
+};
+
+} //namespace ConnectionPoolBackends
+
+} //namespace Glom
+
+#endif //GLOM_BACKEND_MYSQL_H
diff --git a/glom/libglom/connectionpool_backends/mysql_self.cc b/glom/libglom/connectionpool_backends/mysql_self.cc
new file mode 100644
index 0000000..aa64130
--- /dev/null
+++ b/glom/libglom/connectionpool_backends/mysql_self.cc
@@ -0,0 +1,724 @@
+/* Glom
+ *
+ * Copyright (C) 2001-2013 Murray Cumming
+ *
+ * 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; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * 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., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ */
+
+#include <libglom/connectionpool_backends/mysql_self.h>
+#include <libglom/connectionpool.h>
+#include <libglom/utils.h>
+#include <libglom/db_utils.h>
+#include <libglom/spawn_with_feedback.h>
+#include <giomm/file.h>
+#include <glib/gstdio.h> // For g_remove
+
+#include <glibmm/convert.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/stringutils.h>
+#include <glibmm/regex.h>
+#include <glibmm/main.h>
+#include <glibmm/shell.h>
+#include <glibmm/i18n.h>
+
+#include <libglom/gst-package.h>
+#include <sstream> //For stringstream
+#include <iostream>
+
+#ifdef G_OS_WIN32
+# include <windows.h>
+# include <winsock2.h>
+#else
+# include <sys/types.h>
+# include <sys/socket.h>
+# include <errno.h>
+# include <netinet/in.h> //For sockaddr_in
+#endif
+
+#include <signal.h> //To catch segfaults
+
+// Uncomment to see debug messages
+//#define GLOM_CONNECTION_DEBUG
+
+namespace Glom
+{
+
+namespace ConnectionPoolBackends
+{
+
+
+#define DEFAULT_CONFIG_PG_HBA_LOCAL \
+"# TYPE DATABASE USER CIDR-ADDRESS METHOD\n" \
+"\n" \
+"# local is for Unix domain socket connections only\n" \
+"# trust allows connection from the current PC without a password:\n" \
+"local all all trust\n" \
+"local all all md5\n" \
+"\n" \
+"# TCP connections from the same computer, with a password:\n" \
+"host all all 127.0.0.1 255.255.255.255 md5\n" \
+"# IPv6 local connections:\n" \
+"host all all ::1/128 md5\n"
+
+#define DEFAULT_CONFIG_PG_HBA_REMOTE_EXTRA \
+"\n" \
+"# IPv4 local connections:\n" \
+"host all all 0.0.0.0/0 md5\n" \
+"# IPv6 local connections:\n" \
+"host all all ::1/128 md5\n"
+
+#define DEFAULT_CONFIG_PG_HBA_REMOTE \
+DEFAULT_CONFIG_PG_HBA_LOCAL \
+DEFAULT_CONFIG_PG_HBA_REMOTE_EXTRA
+
+static const int PORT_MYSQL_SELF_HOSTED_START = 5433;
+static const int PORT_MYSQL_SELF_HOSTED_END = 5500;
+
+static const char FILENAME_DATA[] = "data";
+static const char FILENAME_BACKUP[] = "backup";
+
+MySQLSelfHosted::MySQLSelfHosted()
+: m_network_shared(false)
+{
+ m_host = "localhost";
+}
+
+bool MySQLSelfHosted::get_self_hosting_active() const
+{
+ return m_port != 0;
+}
+
+unsigned int MySQLSelfHosted::get_port() const
+{
+ return m_port;
+}
+
+/** Try to install mysql on the distro, though this will require a
+ * distro-specific patch to the implementation.
+ */
+bool MySQLSelfHosted::install_mysql(const SlotProgress& /* slot_progress */)
+{
+#if 0
+ // This is example code for Ubuntu, and possibly Debian,
+ // using code from the gnome-system-tools Debian/Ubuntu patches.
+ // (But please, just fix the dependencies instead. MySQL is not optional.)
+ //
+ // You will also need to remove the "ifdef 0"s around the code in gst-package.[h|c],
+ // and define DISTRO_SPECIFIC_MYSQL_INSTALL_IMPLEMENTED above.
+
+ //Careful. Maybe you want a different version.
+ //Also, Glom will start its own instance of MySQL, on its own port, when it needs to,
+ //so there is no need to start a Glom service after installation at system startup,
+ //though it will not hurt Glom if you do that.
+ const gchar *packages[] = { "mysqlql-8.1", 0 };
+ const bool result = gst_packages_install(parent_window->gobj() /* parent window */, packages);
+ if(result)
+ {
+ std::cout << "Glom: gst_packages_install() reports success." << std::endl;
+ //Double-check, because gst_packages_install() incorrectly returns TRUE if it fails because
+ //a) synaptic is already running, or
+ //b) synaptic did not know about the package (no warning is shown in this case.)
+ //Maybe gst_packages_install() never returns FALSE.
+ return check_mysql_is_available_with_warning(); //This is recursive, but clicking Cancel will stop everything.
+ }
+ else
+ {
+ std::cout << "Glom: gst_packages_install() reports failure." << std::endl;
+ return false; //Failed to install mysql.
+ }
+#else
+ return false; //Failed to install mysql because no installation technique was implemented.
+#endif // #if 0
+}
+
+Backend::InitErrors MySQLSelfHosted::initialize(const SlotProgress& slot_progress, const Glib::ustring& initial_username, const Glib::ustring& password, bool network_shared)
+{
+ m_network_shared = network_shared;
+
+ if(m_database_directory_uri.empty())
+ {
+ std::cerr << G_STRFUNC << ": initialize: m_self_hosting_data_uri is empty." << std::endl;
+ return INITERROR_OTHER;
+ }
+
+ if(initial_username.empty())
+ {
+ std::cerr << "MySQLSelfHosted::initialize(). Username was empty while attempting to create self-hosting database" << std::endl;
+ return INITERROR_OTHER;
+ }
+
+ //Get the filepath of the directory that we should create:
+ const std::string dbdir_uri = m_database_directory_uri;
+ //std::cout << "debug: dbdir_uri=" << dbdir_uri << std::endl;
+
+ if(file_exists_uri(dbdir_uri))
+ return INITERROR_DIRECTORY_ALREADY_EXISTS;
+
+ const std::string dbdir = Glib::filename_from_uri(dbdir_uri);
+ //std::cout << "debug: dbdir=" << dbdir << std::endl;
+ g_assert(!dbdir.empty());
+
+ const bool dbdir_created = create_directory_filepath(dbdir);
+ if(!dbdir_created)
+ {
+ std::cerr << "Couldn't create directory: " << dbdir << std::endl;
+
+ return INITERROR_COULD_NOT_CREATE_DIRECTORY;
+ }
+
+ //Create the config directory:
+ const std::string dbdir_config = get_self_hosting_config_path(true /* create */);
+ if(dbdir_config.empty())
+ {
+ std::cerr << "Couldn't create the config directory: " << dbdir << std::endl;
+
+ return INITERROR_COULD_NOT_CREATE_DIRECTORY;
+ }
+
+ //Create these files: environment, pg_hba.conf, start.conf
+ set_network_shared(slot_progress, m_network_shared); //Creates pg_hba.conf
+
+ //Check that there is not an existing data directory:
+ const std::string dbdir_data = get_self_hosting_data_path(true /* create */);
+ if(dbdir_data.empty())
+ {
+ std::cerr << "Couldn't create the data directory: " << dbdir << std::endl;
+
+ return INITERROR_COULD_NOT_CREATE_DIRECTORY;
+ }
+
+ // initdb creates a new mysql database cluster:
+
+ //Get file:// URI for the tmp/ directory:
+ const std::string temp_pwfile = Utils::get_temp_file_path("glom_initdb_pwfile");
+ const Glib::ustring temp_pwfile_uri = Glib::filename_to_uri(temp_pwfile);
+ const bool pwfile_creation_succeeded = create_text_file(temp_pwfile_uri, password);
+ g_assert(pwfile_creation_succeeded);
+
+ // Make sure to use double quotes for the executable path, because the
+ // CreateProcess() API used on Windows does not support single quotes.
+ const std::string command_initdb = get_path_to_mysql_executable("initdb") + " -D " + Glib::shell_quote(dbdir_data) +
+ " -U " + initial_username + " --pwfile=" + Glib::shell_quote(temp_pwfile);
+
+ //Note that --pwfile takes the password from the first line of a file. It's an alternative to supplying it when prompted on stdin.
+ const bool result = Glom::Spawn::execute_command_line_and_wait(command_initdb, slot_progress);
+ if(!result)
+ {
+ std::cerr << "Error while attempting to create self-hosting database." << std::endl;
+ }
+
+ const int temp_pwfile_removed = g_remove(temp_pwfile.c_str()); //Of course, we don't want this to stay around. It would be a security risk.
+ g_assert(temp_pwfile_removed == 0);
+
+ return result ? INITERROR_NONE : INITERROR_COULD_NOT_START_SERVER;
+}
+
+Glib::ustring MySQLSelfHosted::get_mysqlql_utils_version(const SlotProgress& slot_progress)
+{
+ Glib::ustring result;
+
+ const std::string command = get_path_to_mysql_executable("pg_ctl") + " --version";
+
+ //The first command does not return, but the second command can check whether it succeeded:
+ std::string output;
+ const bool spawn_result = Glom::Spawn::execute_command_line_and_wait(command, slot_progress, output);
+ if(!spawn_result)
+ {
+ std::cerr << "Error while attempting to discover the pg_ctl version." << std::endl;
+ return result;
+ }
+
+ //Use a regex to get the version number:
+ Glib::RefPtr<Glib::Regex> regex;
+
+ //We want the characters at the end:
+ const gchar VERSION_REGEX[] = "pg_ctl \\(MySQL\\) (.*)";
+
+ try
+ {
+ regex = Glib::Regex::create(VERSION_REGEX);
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << "Glom: Glib::Regex::create() failed: " << ex.what() << std::endl;
+ return result;
+ }
+
+ if(!regex)
+ return result;
+
+ typedef std::vector<Glib::ustring> type_vec_strings;
+ const type_vec_strings vec = regex->split(output, Glib::REGEX_MATCH_NOTEMPTY);
+ //std::cout << "DEBUG: output == " << output << std::endl;
+ //std::cout << "DEBUG: vec.size() == " << vec.size() << std::endl;
+
+ // We get, for instance, "\n" and 8.4.1" and "\n".
+ for(type_vec_strings::const_iterator iter = vec.begin();
+ iter != vec.end();
+ ++iter)
+ {
+ const Glib::ustring str = *iter;
+ if(!str.empty())
+ return str; //Found.
+ }
+
+ return result;
+}
+
+float MySQLSelfHosted::get_mysqlql_utils_version_as_number(const SlotProgress& slot_progress)
+{
+ float result = 0;
+
+ const Glib::ustring version_str = get_mysqlql_utils_version(slot_progress);
+
+ Glib::RefPtr<Glib::Regex> regex;
+
+ //We want the characters at the end:
+ const gchar VERSION_REGEX[] = "^(\\d*)\\.(\\d*)";
+
+ try
+ {
+ regex = Glib::Regex::create(VERSION_REGEX);
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << "Glom: Glib::Regex::create() failed: " << ex.what() << std::endl;
+ return result;
+ }
+
+ if(!regex)
+ return result;
+
+ typedef std::vector<Glib::ustring> type_vec_strings;
+ const type_vec_strings vec = regex->split(version_str, Glib::REGEX_MATCH_NOTEMPTY);
+ //std::cout << "DEBUG: str == " << version_str << std::endl;
+ //std::cout << "DEBUG: vec.size() == " << vec.size() << std::endl;
+
+ //We need to loop over the numbers because we get some "" items that we want to ignore:
+ guint count = 0; //We want 2 numbers.
+ for(type_vec_strings::const_iterator iter = vec.begin();
+ iter != vec.end();
+ ++iter)
+ {
+ //std::cout << "regex item: START" << *iter << "END" << std::endl;
+
+ const Glib::ustring str = *iter;
+ if(str.empty())
+ continue;
+
+ const float num = atoi(str.c_str());
+ if(count == 0)
+ result = num;
+ else if(count == 1)
+ {
+ result += (0.1 * num);
+ break;
+ }
+
+ ++count;
+ }
+
+ return result;
+}
+
+
+Backend::StartupErrors MySQLSelfHosted::startup(const SlotProgress& slot_progress, bool network_shared)
+{
+ m_network_shared = network_shared;
+
+ // Don't risk random crashes, although this really shouldn't be called
+ // twice of course.
+ //g_assert(!get_self_hosting_active());
+
+ if(get_self_hosting_active())
+ {
+ std::cerr << G_STRFUNC << ": Already started." << std::endl;
+ return STARTUPERROR_NONE; //Just do it once.
+ }
+
+ const std::string dbdir_uri = m_database_directory_uri;
+
+ if(!(file_exists_uri(dbdir_uri)))
+ {
+ //TODO: Use a return enum or exception so we can tell the user about this:
+ std::cerr << G_STRFUNC << ": The data directory could not be found: " << dbdir_uri << std::endl;
+ return STARTUPERROR_FAILED_UNKNOWN_REASON;
+ }
+
+ const std::string dbdir = Glib::filename_from_uri(dbdir_uri);
+ g_assert(!dbdir.empty());
+
+ const std::string dbdir_data = Glib::build_filename(dbdir, FILENAME_DATA);
+ const Glib::ustring dbdir_data_uri = Glib::filename_to_uri(dbdir_data);
+ if(!(file_exists_uri(dbdir_data_uri)))
+ {
+ const std::string dbdir_backup = Glib::build_filename(dbdir, FILENAME_BACKUP);
+ const Glib::ustring dbdir_backup_uri = Glib::filename_to_uri(dbdir_backup);
+ if(file_exists_uri(dbdir_backup_uri))
+ {
+ std::cerr << G_STRFUNC << ": There is no data, but there is backup data." << std::endl;
+ //Let the caller convert the backup to real data and then try again:
+ return STARTUPERROR_FAILED_NO_DATA_HAS_BACKUP_DATA;
+ }
+ else
+ {
+ std::cerr << "ConnectionPool::create_self_hosting(): The data sub-directory could not be found." << dbdir_data_uri << std::endl;
+ return STARTUPERROR_FAILED_NO_DATA;
+ }
+ }
+
+ //Attempt to ensure that the config files are correct:
+ set_network_shared(slot_progress, m_network_shared); //Creates pg_hba.conf
+
+ const unsigned int available_port = discover_first_free_port(PORT_MYSQL_SELF_HOSTED_START, PORT_MYSQL_SELF_HOSTED_END);
+ //std::cout << "debug: " << G_STRFUNC << ":() : debug: Available port for self-hosting: " << available_port << std::endl;
+ if(available_port == 0)
+ {
+ //TODO: Use a return enum or exception so we can tell the user about this:
+ std::cerr << G_STRFUNC << ": No port was available between " << PORT_MYSQL_SELF_HOSTED_START << " and " << PORT_MYSQL_SELF_HOSTED_END << std::endl;
+ return STARTUPERROR_FAILED_UNKNOWN_REASON;
+ }
+
+ //TODO: Performance:
+ const std::string port_as_text = Glib::Ascii::dtostr(available_port);
+
+ // -D specifies the data directory.
+ // -c config_file= specifies the configuration file
+ // -k specifies a directory to use for the socket. This must be writable by us.
+ // Make sure to use double quotes for the executable path, because the
+ // CreateProcess() API used on Windows does not support single quotes.
+ const std::string dbdir_config = Glib::build_filename(dbdir, "config");
+ const std::string dbdir_hba = Glib::build_filename(dbdir_config, "pg_hba.conf");
+ const std::string dbdir_pid = Glib::build_filename(dbdir, "pid");
+ const std::string listen_address = (m_network_shared ? "*" : "localhost");
+ const std::string command_mysql_start = get_path_to_mysql_executable("mysql") + " -D " + Glib::shell_quote(dbdir_data)
+ + " -p " + port_as_text
+ + " -h " + listen_address
+ + " -c hba_file=" + Glib::shell_quote(dbdir_hba)
+ + " -k " + Glib::shell_quote(dbdir)
+ + " --external_pid_file=" + Glib::shell_quote(dbdir_pid);
+ //std::cout << G_STRFUNC << ": debug: " << command_mysql_start << std::endl;
+
+ // Make sure to use double quotes for the executable path, because the
+ // CreateProcess() API used on Windows does not support single quotes.
+ const std::string command_check_mysql_has_started = get_path_to_mysql_executable("pg_ctl") + " status -D " + Glib::shell_quote(dbdir_data);
+
+ //For mysql 8.1, this is "postmaster is running".
+ //For mysql 8.2, this is "server is running".
+ //This is a big hack that we should avoid. murrayc.
+ //
+ //pg_ctl actually seems to return a 0 result code for "is running" and a 1 for not running, at least with MySQL 8.2,
+ //so maybe we can avoid this in future.
+ //Please do test it with your mysql version, using "echo $?" to see the result code of the last command.
+ const std::string second_command_success_text = "is running"; //TODO: This is not a stable API. Also, watch out for localisation.
+
+ //The first command does not return, but the second command can check whether it succeeded:
+ const bool result = Glom::Spawn::execute_command_line_and_wait_until_second_command_returns_success(command_mysql_start, command_check_mysql_has_started, slot_progress, second_command_success_text);
+ if(!result)
+ {
+ std::cerr << "Error while attempting to self-host a database." << std::endl;
+ return STARTUPERROR_FAILED_UNKNOWN_REASON;
+ }
+
+ m_port = available_port; //Remember it for later.
+
+ return STARTUPERROR_NONE;
+}
+
+void MySQLSelfHosted::show_active_connections()
+{
+ Glib::RefPtr<Gnome::Gda::SqlBuilder> builder =
+ Gnome::Gda::SqlBuilder::create(Gnome::Gda::SQL_STATEMENT_SELECT);
+ builder->select_add_field("*", "pg_stat_activity");
+ builder->select_add_target("pg_stat_activity");
+
+ Glib::RefPtr<Gnome::Gda::Connection> gda_connection = connect(m_saved_database_name, m_saved_username, m_saved_password);
+ if(!gda_connection)
+ std::cerr << G_STRFUNC << ": connection failed." << std::endl;
+
+ Glib::RefPtr<Gnome::Gda::DataModel> datamodel = DbUtils::query_execute_select(builder);
+ if(!datamodel)
+ std::cerr << G_STRFUNC << ": pg_stat_activity SQL query failed." << std::endl;
+
+ const int rows_count = datamodel->get_n_rows();
+ if(datamodel->get_n_rows() < 1)
+ std::cerr << G_STRFUNC << ": pg_stat_activity SQL query returned no rows." << std::endl;
+
+ std::cout << "Active connections according to a pg_stat_activity SQL query:" << std::endl;
+ const int cols_count = datamodel->get_n_columns();
+ for(int row = 0; row < rows_count; ++row)
+ {
+ for(int col = 0; col < cols_count; ++col)
+ {
+ if(col != 0)
+ std::cout << ", ";
+
+ std::cout << datamodel->get_value_at(col, row).to_string();
+ }
+
+ std::cout << std::endl;
+ }
+
+ //Make sure that this connection does not stop a further attempt to stop the server.
+ gda_connection->close();
+}
+
+bool MySQLSelfHosted::cleanup(const SlotProgress& slot_progress)
+{
+ // This seems to be called twice sometimes, so we don't assert here until
+ // this is fixed.
+ //g_assert(get_self_hosting_active());
+
+ if(!get_self_hosting_active())
+ return true; //Don't try to stop it if we have not started it.
+
+ const std::string dbdir_uri = m_database_directory_uri;
+ const std::string dbdir = Glib::filename_from_uri(dbdir_uri);
+ g_assert(!dbdir.empty());
+
+ const std::string dbdir_data = Glib::build_filename(dbdir, FILENAME_DATA);
+
+
+ // TODO: Detect other instances on the same computer, and use a different port number,
+ // or refuse to continue, showing an error dialog.
+
+ // -D specifies the data directory.
+ // -c config_file= specifies the configuration file
+ // -k specifies a directory to use for the socket. This must be writable by us.
+ // We use "-m fast" instead of the default "-m smart" because that waits for clients to disconnect (and sometimes never succeeds).
+ // TODO: Warn about connected clients on other computers? Warn those other users?
+ // Make sure to use double quotes for the executable path, because the
+ // CreateProcess() API used on Windows does not support single quotes.
+ const std::string command_mysql_stop = get_path_to_mysql_executable("pg_ctl") + " -D " + Glib::shell_quote(dbdir_data) + " stop -m fast";
+ const bool result = Glom::Spawn::execute_command_line_and_wait(command_mysql_stop, slot_progress);
+ if(!result)
+ {
+ std::cerr << "Error while attempting to stop self-hosting of the database. Trying again." << std::endl;
+
+ //Show open connections for debugging:
+ try
+ {
+ show_active_connections();
+ }
+ catch(const Glib::Error& ex)
+ {
+ std::cerr << G_STRFUNC << ": exception while trying to show active connections: " << ex.what() << std::endl;
+ }
+
+ //I've seen it fail when running under valgrind, and there are reports of failures in bug #420962.
+ //Maybe it will help to try again:
+ const bool result = Glom::Spawn::execute_command_line_and_wait(command_mysql_stop, slot_progress);
+ if(!result)
+ {
+ std::cerr << "Error while attempting (for a second time) to stop self-hosting of the database." << std::endl;
+ return false;
+ }
+ }
+
+ m_port = 0;
+
+ return true;
+}
+
+
+
+bool MySQLSelfHosted::set_network_shared(const SlotProgress& /* slot_progress */, bool network_shared)
+{
+ //TODO: Use slot_progress, while doing async IO for create_text_file().
+
+ m_network_shared = network_shared;
+
+ const std::string dbdir_uri = m_database_directory_uri;
+ const std::string dbdir = Glib::filename_from_uri(dbdir_uri);
+
+ const std::string dbdir_uri_config = dbdir_uri + "/config";
+ const char* default_conf_contents = 0;
+
+ // Choose the configuration contents based on
+ // whether we want to be network-shared:
+ //const float mysqlql_version = get_mysqlql_utils_version_as_number(slot_progress);
+ //std::cout << "DEBUG: mysqlql_version=" << mysqlql_version << std::endl;
+
+ default_conf_contents = m_network_shared ? DEFAULT_CONFIG_PG_HBA_REMOTE : DEFAULT_CONFIG_PG_HBA_LOCAL;
+
+ //std::cout << "DEBUG: default_conf_contents=" << default_conf_contents << std::endl;
+
+ const bool hba_conf_creation_succeeded = create_text_file(dbdir_uri_config + "/pg_hba.conf", default_conf_contents);
+ g_assert(hba_conf_creation_succeeded);
+ if(!hba_conf_creation_succeeded)
+ return false;
+
+ return hba_conf_creation_succeeded;
+}
+
+static bool on_timeout_delay(const Glib::RefPtr<Glib::MainLoop>& mainloop)
+{
+ //Allow our mainloop.run() to return:
+ if(mainloop)
+ mainloop->quit();
+
+ return false;
+}
+
+
+Glib::RefPtr<Gnome::Gda::Connection> MySQLSelfHosted::connect(const Glib::ustring& database, const Glib::ustring& username, const Glib::ustring& password, bool fake_connection)
+{
+ if(!get_self_hosting_active())
+ {
+ throw ExceptionConnection(ExceptionConnection::FAILURE_NO_BACKEND); //TODO: But there is a backend. It's just not ready.
+ return Glib::RefPtr<Gnome::Gda::Connection>();
+ }
+
+ Glib::RefPtr<Gnome::Gda::Connection> result;
+ bool keep_trying = true;
+ guint count_retries = 0;
+ const guint MAX_RETRIES_KNOWN_PASSWORD = 30; /* seconds */
+ const guint MAX_RETRIES_EVER = 60; /* seconds */
+ while(keep_trying)
+ {
+ try
+ {
+ result = attempt_connect(port_as_string(m_port), database, username, password, fake_connection);
+ }
+ catch(const ExceptionConnection& ex)
+ {
+ if(ex.get_failure_type() == ExceptionConnection::FAILURE_NO_SERVER)
+ {
+ //It must be using a default password, so any failure would not be due to a wrong password.
+ //However, pg_ctl sometimes reports success before it is really ready to let us connect,
+ //so in this case we can just keep trying until it works, with a very long timeout.
+ count_retries++;
+ const guint max_retries = m_network_shared ? MAX_RETRIES_EVER : MAX_RETRIES_KNOWN_PASSWORD;
+ if(count_retries > max_retries)
+ {
+ keep_trying = false;
+ continue;
+ }
+
+ std::cout << "debug: " << G_STRFUNC << ": Waiting and retrying the connection due to suspected too-early success of pg_ctl. retries=" << count_retries << ", max_retries=" << m_network_shared << std::endl;
+
+ //Wait:
+ Glib::RefPtr<Glib::MainLoop> mainloop = Glib::MainLoop::create(false);
+ sigc::connection connection_timeout = Glib::signal_timeout().connect(
+ sigc::bind(sigc::ptr_fun(&on_timeout_delay), sigc::ref(mainloop)),
+ 1000 /* 1 second */);
+ mainloop->run();
+ connection_timeout.disconnect();
+
+ keep_trying = true;
+ continue;
+ }
+ else
+ {
+ throw ex;
+ }
+ }
+
+ keep_trying = false;
+ }
+
+ //Save the connection details _only_ for later debug use:
+
+ m_saved_database_name = database;
+ m_saved_username = username;
+ m_saved_password = password;
+ return result;
+}
+
+bool MySQLSelfHosted::create_database(const SlotProgress& slot_progress, const Glib::ustring& database_name, const Glib::ustring& username, const Glib::ustring& password)
+{
+ return attempt_create_database(slot_progress, database_name, "localhost", port_as_string(m_port), username, password);
+}
+
+unsigned int MySQLSelfHosted::discover_first_free_port(unsigned int start_port, unsigned int end_port)
+{
+ //Open a socket so we can try to bind it to a port:
+ const int fd = socket(AF_INET, SOCK_STREAM, 0);
+ if(fd == -1)
+ {
+#ifdef G_OS_WIN32
+ std::cerr << "Create socket: " << WSAGetLastError() << std::endl;
+#else
+ perror("Create socket");
+#endif //G_OS_WIN32
+ return 0;
+ }
+
+ //This code was originally suggested by Lennart Poettering.
+
+ struct ::sockaddr_in sa;
+ memset(&sa, 0, sizeof(sa));
+ sa.sin_family = AF_INET;
+
+ guint16 port_to_try = start_port;
+ while (port_to_try <= end_port)
+ {
+ sa.sin_port = htons(port_to_try);
+
+ const int result = bind(fd, (sockaddr*)&sa, sizeof(sa));
+ bool available = false;
+ if(result == 0)
+ available = true;
+ else if(result < 0)
+ {
+ #ifdef G_OS_WIN32
+ available = (WSAGetLastError() != WSAEADDRINUSE);
+ #endif // G_OS_WIN32
+
+ //Some BSDs don't have this.
+ //But watch out - if you don't include errno.h then this won't be
+ //defined on Linux either, but you really do need to check for it.
+ #ifdef EADDRINUSE
+ available = (errno != EADDRINUSE);
+ #endif
+
+ #ifdef EPORTINUSE //Linux doesn't have this.
+ available = (errno != EPORTINUSE);
+ #endif
+ }
+ else
+ {
+ //std::cout << "debug: " << G_STRFUNC << ": port in use: " << port_to_try << std::endl;
+ }
+
+ if(available)
+ {
+ #ifdef G_OS_WIN32
+ closesocket(fd);
+ #else
+ close(fd);
+ #endif //G_OS_WIN32
+
+ //std::cout << "debug: " << G_STRFUNC << ": Found: returning " << port_to_try << std::endl;
+ return port_to_try;
+ }
+
+ ++port_to_try;
+ }
+
+#ifdef G_OS_WIN32
+ closesocket(fd);
+#else
+ close(fd);
+#endif //G_OS_WIN32
+
+ std::cerr << G_STRFUNC << ": No port was available." << std::endl;
+ return 0;
+}
+
+} // namespace ConnectionPoolBackends
+
+} // namespcae Glom
diff --git a/glom/libglom/connectionpool_backends/mysql_self.h b/glom/libglom/connectionpool_backends/mysql_self.h
new file mode 100644
index 0000000..538a9b2
--- /dev/null
+++ b/glom/libglom/connectionpool_backends/mysql_self.h
@@ -0,0 +1,95 @@
+/* Glom
+ *
+ * Copyright (C) 2001-2013 Murray Cumming
+ *
+ * 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; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * 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., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GLOM_BACKEND_MYSQL_SELF_H
+#define GLOM_BACKEND_MYSQL_SELF_H
+
+#include <libglom/libglom_config.h>
+
+#include <libglom/connectionpool_backends/mysql.h>
+
+namespace Glom
+{
+
+namespace ConnectionPoolBackends
+{
+
+class MySQLSelfHosted : public MySQL
+{
+public:
+ MySQLSelfHosted();
+
+ /** Return whether the self-hosted server is currently running.
+ *
+ * @result True if it is running, and false otherwise.
+ */
+ bool get_self_hosting_active() const;
+
+ /** Returns the port number the local mysql server is running on.
+ *
+ * @result The port number of the self-hosted server, or 0 if it is not
+ * running.
+ */
+ unsigned int get_port() const;
+
+ /** Try to install mysql on the distro, though this will require a
+ * distro-specific patch to the implementation.
+ */
+ static bool install_mysql(const SlotProgress& slot_progress);
+
+private:
+ virtual InitErrors initialize(const SlotProgress& slot_progress, const Glib::ustring& initial_username, const Glib::ustring& password, bool network_shared = false);
+
+ virtual StartupErrors startup(const SlotProgress& slot_progress, bool network_shared = false);
+ virtual bool cleanup(const SlotProgress& slot_progress);
+ virtual bool set_network_shared(const SlotProgress& slot_progress, bool network_shared = true);
+
+ virtual Glib::RefPtr<Gnome::Gda::Connection> connect(const Glib::ustring& database, const Glib::ustring& username, const Glib::ustring& password, bool fake_connection = false);
+
+ virtual bool create_database(const SlotProgress& slot_progress, const Glib::ustring& database_name, const Glib::ustring& username, const Glib::ustring& password);
+
+private:
+ /** Examine ports one by one, starting at @a starting_port, in increasing
+ * order, and return the first one that is available.
+ */
+ static unsigned int discover_first_free_port(unsigned int start_port, unsigned int end_port);
+
+ /** Run the command-line with the --version option to discover what version
+ * of MySQL is installed, so we can use the appropriate configuration
+ * options when self-hosting.
+ */
+ Glib::ustring get_mysqlql_utils_version(const SlotProgress& slot_progress);
+
+ float get_mysqlql_utils_version_as_number(const SlotProgress& slot_progress);
+
+ void show_active_connections();
+
+ bool m_network_shared;
+
+ //These are only remembered in order to use them to provide debug
+ //information when the MySQL shutdown fails:
+ Glib::ustring m_saved_database_name, m_saved_username, m_saved_password;
+};
+
+} // namespace ConnectionPoolBackends
+
+} //namespace Glom
+
+#endif //GLOM_BACKEND_MYSQL_SELF_H
diff --git a/glom/libglom/filelist.am b/glom/libglom/filelist.am
index 9ca0169..7488026 100644
--- a/glom/libglom/filelist.am
+++ b/glom/libglom/filelist.am
@@ -180,6 +180,8 @@ libglom_sources = \
glom/libglom/python_embed/py_glom_ui_callbacks.h \
glom/libglom/python_embed/pygdavalue_conversions.cc \
glom/libglom/python_embed/pygdavalue_conversions.h \
+ glom/libglom/connectionpool_backends/mysql.cc \
+ glom/libglom/connectionpool_backends/mysql.h \
glom/libglom/connectionpool_backends/sqlite.cc \
glom/libglom/connectionpool_backends/sqlite.h \
glom/libglom/connectionpool_backends/postgres.cc \
@@ -190,5 +192,7 @@ libglom_sources = \
if !GLOM_ENABLE_CLIENT_ONLY
libglom_sources += \
glom/libglom/connectionpool_backends/postgres_self.cc \
- glom/libglom/connectionpool_backends/postgres_self.h
+ glom/libglom/connectionpool_backends/postgres_self.h \
+ glom/libglom/connectionpool_backends/mysql_self.cc \
+ glom/libglom/connectionpool_backends/mysql_self.h
endif
diff --git a/glom/main.cc b/glom/main.cc
index 18c40b2..6013c8c 100644
--- a/glom/main.cc
+++ b/glom/main.cc
@@ -36,7 +36,7 @@
#include <glibmm/convert.h>
#include <glibmm/miscutils.h>
-// For postgres availability checks:
+// For PostgreSQL availability checks:
#ifdef GLOM_ENABLE_POSTGRESQL
#include <libglom/connectionpool_backends/postgres.h>
#ifndef GLOM_ENABLE_CLIENT_ONLY
@@ -44,6 +44,14 @@
#endif //GLOM_ENABLE_CLIENT_ONLY
#endif //GLOM_ENABLE_POSTGRESQL
+// For MySQL availability checks:
+#ifdef GLOM_ENABLE_MYSQL
+#include <libglom/connectionpool_backends/mysql.h>
+#ifndef GLOM_ENABLE_CLIENT_ONLY
+#include <libglom/connectionpool_backends/mysql_self.h>
+#endif //GLOM_ENABLE_CLIENT_ONLY
+#endif //GLOM_ENABLE_MYSQL
+
// For sanity checks:
#include <glom/python_embed/glom_python.h>
@@ -220,7 +228,7 @@ bool check_user_is_not_root_with_warning()
// Message to packagers:
// If your Glom package does not depend on PostgreSQL, for some reason,
// then your distro-specific patch should uncomment this #define.
-// and implement ConnectionPool::install_posgres().
+// and implement ConnectionPool::install_postgres().
// But please, just make your Glom package depend on PostgreSQL instead,
// because this is silly.
//
@@ -273,6 +281,57 @@ bool check_postgres_is_available_with_warning()
#endif //GLOM_ENABLE_POSTGRESQL
+#ifdef GLOM_ENABLE_MYSQL
+#ifndef GLOM_ENABLE_CLIENT_ONLY
+
+/** Check whether MySQL is really available for self-hosting,
+ * in case the distro package has incorrect dependencies.
+ *
+ * @results True if everything is OK.
+ */
+bool check_mysql_is_available_with_warning()
+{
+ const std::string binpath = Glom::ConnectionPoolBackends::MySQLSelfHosted::get_path_to_mysql_executable("mysql", false /* not quoted */);
+
+ // TODO: At least on Windows we should probably also check for initdb and
+ // pg_ctl. Perhaps it would also be a good idea to access these files as
+ // long as glom runs so they cannot be (re)moved.
+ if(!binpath.empty())
+ {
+ const Glib::ustring uri_binpath = Glib::filename_to_uri(binpath);
+ if(Utils::file_exists(uri_binpath))
+ return true;
+ }
+
+ #ifdef DISTRO_SPECIFIC_MYSQL_INSTALL_IMPLEMENTED
+
+ //Show message to the user about the broken installation:
+ //This is a packaging bug, but it would probably annoy packagers to mention that in the dialog:
+ //Unlike for PostgreSQL, this warning is only shown if MySQL was specified in the build.
+ Gtk::MessageDialog dialog(Utils::bold_message(_("Incomplete Glom Installation")), true /* use_markup */, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_NONE, true /* modal */);
+ dialog.set_secondary_text(_("Your installation of Glom is not complete, because MySQL is not available on your system. MySQL is needed for self-hosting of some Glom databases.\n\nYou may now install MySQL to complete the Glom installation."));
+ dialog.add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
+ dialog.add_button(_("Install MySQL"), Gtk::RESPONSE_OK);
+ const int response = dialog.run();
+ if(response != Gtk::RESPONSE_OK)
+ return false; //Failure. Glom should now quit.
+ else
+ return install_mysql(&dialog);
+
+ #else //DISTRO_SPECIFIC_MYSQL_INSTALL_IMPLEMENTED
+
+ //Show message to the user about the broken installation:
+ Gtk::MessageDialog dialog(Utils::bold_message(_("Incomplete Glom Installation")), true /* use_markup */, Gtk::MESSAGE_ERROR, Gtk::BUTTONS_OK, true /* modal */);
+ dialog.set_secondary_text(_("Your installation of Glom is not complete, because MySQL is not available on your system. MySQL is needed for self-hosting of some Glom databases.\n\nPlease report this bug to your vendor, or your system administrator so it can be corrected."));
+ dialog.run();
+ return false;
+
+ #endif //DISTRO_SPECIFIC_MYSQL_INSTALL_IMPLEMENTED
+}
+#endif //GLOM_ENABLE_CLIENT_ONLY
+
+#endif //GLOM_ENABLE_MYSQL
+
bool check_pyglom_is_available_with_warning()
{
if(glom_python_module_is_available())
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]