gnucash/libgnucash/engine/qofsession.cpp
John Ralls 43af50bd8a Bug 796759 - --add-price-quotes <sql file> leaves a lock on the file.
First, save isn't necessary if the book is dirty, so don't... but that
means that the book has to be marked dirty after a session swap. No more
laziness.

Second, regardless of the outcome of inner_main_add_price_quotes the
session must be destroyed to remove the lock.

A couple of cleanups in QofSessionImpl::save as well: Rewrote the
descriptive comment to reflect how it really works when the backend has
gotten disconnected and removed the superfluous qof_book_set_backend
with the backend that we'd *just gotten from the book*.
2018-07-14 17:09:22 -07:00

745 lines
20 KiB
C++

/********************************************************************\
* qofsesssion.cpp -- session access (connection to backend) *
* *
* 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, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
/**
* @file qofsession.c
* @brief Encapsulate a connection to a storage backend.
*
* HISTORY:
* Created by Linas Vepstas December 1998
@author Copyright (c) 1998-2004 Linas Vepstas <linas@linas.org>
@author Copyright (c) 2000 Dave Peticolas
@author Copyright (c) 2005 Neil Williams <linux@codehelp.co.uk>
@author Copyright (c) 2016 Aaron Laws
*/
extern "C"
{
#include <config.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#else
# ifdef __GNUC__
# warning "<unistd.h> required."
# endif
#endif
#include <glib.h>
#include "qof.h"
#include "qofobject-p.h"
static QofLogModule log_module = QOF_MOD_SESSION;
} //extern 'C'
#include "qofbook-p.h"
#include "qof-backend.hpp"
#include "qofsession.hpp"
#include "gnc-backend-prov.hpp"
#include <vector>
#include <boost/algorithm/string.hpp>
#include <vector>
#include <algorithm>
#include <string>
#include <sstream>
using ProviderVec = std::vector<QofBackendProvider_ptr>;
static ProviderVec s_providers;
/*
* These getters are used in tests to reach static vars from outside
* They should be removed when no longer needed
*/
ProviderVec& get_providers (void );
bool get_providers_initialized (void );
ProviderVec&
get_providers (void)
{
return s_providers;
}
bool
get_providers_initialized (void)
{
return !s_providers.empty();
}
void
qof_backend_register_provider (QofBackendProvider_ptr&& prov)
{
s_providers.emplace_back(std::move(prov));
}
void
qof_backend_unregister_all_providers ()
{
s_providers.clear ();
}
/* Called from C so we have to keep the GList for now. */
GList*
qof_backend_get_registered_access_method_list(void)
{
GList* list = NULL;
std::for_each(s_providers.begin(), s_providers.end(),
[&list](QofBackendProvider_ptr& provider) {
gpointer method = reinterpret_cast<gpointer>(const_cast<char*>(provider->access_method));
list = g_list_prepend(list, method);
});
return list;
}
/* QofSessionImpl */
/* ====================================================================== */
/* Constructor/Destructor ----------------------------------*/
QofSessionImpl::QofSessionImpl () noexcept
: m_book {qof_book_new ()},
m_book_id {},
m_saving {false},
m_last_err {},
m_error_message {}
{
clear_error ();
}
QofSessionImpl::~QofSessionImpl () noexcept
{
ENTER ("sess=%p book_id=%s", this, m_book_id.c_str ());
end ();
destroy_backend ();
qof_book_set_backend (m_book, nullptr);
qof_book_destroy (m_book);
m_book = nullptr;
LEAVE ("sess=%p", this);
}
void
qof_session_destroy (QofSession * session)
{
delete session;
}
QofSession *
qof_session_new (void)
{
return new QofSessionImpl;
}
void
QofSessionImpl::destroy_backend () noexcept
{
auto backend = qof_book_get_backend (m_book);
if (backend)
{
clear_error ();
delete backend;
qof_book_set_backend (m_book, nullptr);
}
}
/* ====================================================================== */
void
QofSessionImpl::load_backend (std::string access_method) noexcept
{
std::ostringstream s;
s << " list=" << s_providers.size();
ENTER ("%s", s.str().c_str());
for (auto const & prov : s_providers)
{
if (!boost::iequals (access_method, prov->access_method))
{
PINFO ("The provider providers access_method, %s, but we're loading for access_method, %s. Skipping.",
prov->access_method, access_method.c_str ());
continue;
}
PINFO (" Selected provider %s", prov->provider_name);
// Only do a type check when trying to open an existing file
// When saving over an existing file the contents of the original file don't matter
if (!m_creating && !prov->type_check (m_book_id.c_str ()))
{
PINFO("Provider, %s, reported not being usable for book, %s.",
prov->provider_name, m_book_id.c_str ());
continue;
}
auto backend = prov->create_backend();
qof_book_set_backend (m_book, backend);
LEAVE (" ");
return;
}
std::string msg {"failed to get_backend using access method \"" + access_method + "\""};
push_error (ERR_BACKEND_NO_HANDLER, msg);
LEAVE (" ");
}
void
QofSessionImpl::load (QofPercentageFunc percentage_func) noexcept
{
if (!m_book_id.size ()) return;
ENTER ("sess=%p book_id=%s", this, m_book_id.c_str ());
/* At this point, we should are supposed to have a valid book
* id and a lock on the file. */
QofBook * oldbook {m_book};
QofBook * newbook {qof_book_new ()};
m_book = newbook;
PINFO ("new book=%p", newbook);
clear_error ();
/* This code should be sufficient to initialize *any* backend,
* whether http, postgres, or anything else that might come along.
* Basically, the idea is that by now, a backend has already been
* created & set up. At this point, we only need to get the
* top-level account group out of the backend, and that is a
* generic, backend-independent operation.
*/
auto be (qof_book_get_backend (oldbook));
qof_book_set_backend (newbook, be);
/* Starting the session should result in a bunch of accounts
* and currencies being downloaded, but probably no transactions;
* The GUI will need to do a query for that.
*/
if (be)
{
be->set_percentage(percentage_func);
be->load (newbook, LOAD_TYPE_INITIAL_LOAD);
push_error (be->get_error(), {});
}
/* XXX if the load fails, then we try to restore the old set of books;
* however, we don't undo the session id (the URL). Thus if the
* user attempts to save after a failed load, they weill be trying to
* save to some bogus URL. This is wrong. XXX FIXME.
*/
auto err = get_error ();
if ((err != ERR_BACKEND_NO_ERR) &&
(err != ERR_FILEIO_FILE_TOO_OLD) &&
(err != ERR_FILEIO_NO_ENCODING) &&
(err != ERR_FILEIO_FILE_UPGRADE) &&
(err != ERR_SQL_DB_TOO_OLD) &&
(err != ERR_SQL_DB_TOO_NEW))
{
/* Something broke, put back the old stuff */
qof_book_set_backend (newbook, NULL);
qof_book_destroy (newbook);
m_book = oldbook;
LEAVE ("error from backend %d", get_error ());
return;
}
qof_book_set_backend (oldbook, NULL);
qof_book_destroy (oldbook);
LEAVE ("sess = %p, book_id=%s", this, m_book_id.c_str ());
}
void
QofSessionImpl::begin (std::string new_book_id, bool ignore_lock,
bool create, bool force) noexcept
{
ENTER (" sess=%p ignore_lock=%d, book-id=%s",
this, ignore_lock, new_book_id.c_str ());
clear_error ();
/* Check to see if this session is already open */
if (m_book_id.size ())
{
if (ERR_BACKEND_NO_ERR != get_error ())
push_error (ERR_BACKEND_LOCKED, {});
LEAVE("push error book is already open ");
return;
}
/* seriously invalid */
if (!new_book_id.size ())
{
if (ERR_BACKEND_NO_ERR != get_error ())
push_error (ERR_BACKEND_BAD_URL, {});
LEAVE("push error missing new_book_id");
return;
}
char * scheme {g_uri_parse_scheme (new_book_id.c_str ())};
char * filename {nullptr};
if (g_strcmp0 (scheme, "file") == 0)
filename = g_filename_from_uri (new_book_id.c_str (), nullptr, nullptr);
else if (!scheme)
filename = g_strdup (new_book_id.c_str ());
if (filename && g_file_test (filename, G_FILE_TEST_IS_DIR))
{
if (ERR_BACKEND_NO_ERR == get_error ())
push_error (ERR_BACKEND_BAD_URL, {});
g_free (filename);
g_free (scheme);
LEAVE("Can't open a directory");
return;
}
/* destroy the old backend */
destroy_backend ();
/* Store the session URL */
m_book_id = new_book_id;
m_creating = create;
if (filename)
load_backend ("file");
else /* access method found, load appropriate backend */
load_backend (scheme);
g_free (filename);
g_free (scheme);
/* No backend was found. That's bad. */
auto backend = qof_book_get_backend (m_book);
if (backend == nullptr)
{
m_book_id = {};
if (ERR_BACKEND_NO_ERR == get_error ())
push_error (ERR_BACKEND_BAD_URL, {});
LEAVE (" BAD: no backend: sess=%p book-id=%s",
this, new_book_id.c_str ());
return;
}
/* If there's a begin method, call that. */
backend->session_begin(this, m_book_id.c_str(), ignore_lock, create, force);
PINFO ("Done running session_begin on backend");
QofBackendError const err {backend->get_error()};
auto msg (backend->get_message());
if (err != ERR_BACKEND_NO_ERR)
{
m_book_id = {};
push_error (err, msg);
LEAVE (" backend error %d %s", err, msg.empty() ? "(null)" : msg.c_str());
return;
}
if (!msg.empty())
{
PWARN("%s", msg.c_str());
}
LEAVE (" sess=%p book-id=%s", this, new_book_id.c_str ());
}
void
QofSessionImpl::end () noexcept
{
ENTER ("sess=%p book_id=%s", this, m_book_id.c_str ());
auto backend = qof_book_get_backend (m_book);
if (backend != nullptr)
backend->session_end();
clear_error ();
m_book_id.clear();
LEAVE ("sess=%p book_id=%s", this, m_book_id.c_str ());
}
/* error handling functions --------------------------------*/
void
QofSessionImpl::clear_error () noexcept
{
m_last_err = ERR_BACKEND_NO_ERR;
m_error_message = {};
/* pop the stack on the backend as well. */
if (auto backend = qof_book_get_backend (m_book))
{
QofBackendError err = ERR_BACKEND_NO_ERR;
do
err = backend->get_error();
while (err != ERR_BACKEND_NO_ERR);
}
}
void
QofSessionImpl::push_error (QofBackendError const err, std::string message) noexcept
{
m_last_err = err;
m_error_message = message;
}
QofBackendError
QofSessionImpl::get_error () noexcept
{
/* if we have a local error, return that. */
if (m_last_err != ERR_BACKEND_NO_ERR)
return m_last_err;
auto qof_be = qof_book_get_backend (m_book);
if (qof_be == nullptr) return ERR_BACKEND_NO_ERR;
m_last_err = qof_be->get_error();
return m_last_err;
}
std::string
QofSessionImpl::get_error_message () const noexcept
{
return m_error_message;
}
QofBackendError
QofSessionImpl::pop_error () noexcept
{
QofBackendError err {get_error ()};
clear_error ();
return err;
}
/* Accessors (getters/setters) -----------------------------*/
QofBook *
QofSessionImpl::get_book () const noexcept
{
if (!m_book) return nullptr;
if ('y' == m_book->book_open)
return m_book;
return nullptr;
}
QofBackend *
QofSession::get_backend () const noexcept
{
return qof_book_get_backend (m_book);
}
std::string
QofSessionImpl::get_file_path () const noexcept
{
auto backend = qof_book_get_backend (m_book);
if (!backend) return nullptr;
return backend->get_uri();
}
std::string const &
QofSessionImpl::get_book_id () const noexcept
{
return m_book_id;
}
bool
QofSessionImpl::is_saving () const noexcept
{
return m_saving;
}
/* Manipulators (save, load, etc.) -------------------------*/
void
QofSessionImpl::save (QofPercentageFunc percentage_func) noexcept
{
if (!qof_book_session_not_saved (m_book)) //Clean book, nothing to do.
return;
m_saving = true;
ENTER ("sess=%p book_id=%s", this, m_book_id.c_str ());
/* If there is a backend, the book is dirty, and the backend is reachable
* (i.e. we can communicate with it), then synchronize with the backend. If
* we cannot contact the backend (e.g. because we've gone offline, the
* network has crashed, etc.) then raise an error so that the controlling
* dialog can offer the user a chance to save in a different way.
*/
auto backend = qof_book_get_backend (m_book);
if (backend)
{
backend->set_percentage(percentage_func);
backend->sync(m_book);
auto err = backend->get_error();
if (err != ERR_BACKEND_NO_ERR)
{
push_error (err, {});
m_saving = false;
return;
}
/* If we got to here, then the backend saved everything
* just fine, and we are done. So return. */
clear_error ();
LEAVE("Success");
}
else
{
push_error (ERR_BACKEND_NO_HANDLER, "failed to load backend");
LEAVE("error -- No backend!");
}
m_saving = false;
}
void
QofSessionImpl::safe_save (QofPercentageFunc percentage_func) noexcept
{
auto backend = qof_book_get_backend (m_book);
if (!backend) return;
backend->set_percentage(percentage_func);
backend->safe_sync(get_book ());
auto err = backend->get_error();
auto msg = backend->get_message();
if (err != ERR_BACKEND_NO_ERR)
{
m_book_id = nullptr;
push_error (err, msg);
}
}
void
QofSessionImpl::ensure_all_data_loaded () noexcept
{
auto backend = qof_book_get_backend (m_book);
if (!backend) return;
backend->load(m_book, LOAD_TYPE_LOAD_ALL);
push_error (backend->get_error(), {});
}
void
QofSessionImpl::swap_books (QofSessionImpl & other) noexcept
{
ENTER ("sess1=%p sess2=%p", this, &other);
// don't swap (that is, double-swap) read_only flags
std::swap (m_book->read_only, other.m_book->read_only);
std::swap (m_book, other.m_book);
auto mybackend = qof_book_get_backend (m_book);
qof_book_set_backend (m_book, qof_book_get_backend (other.m_book));
qof_book_set_backend (other.m_book, mybackend);
LEAVE (" ");
}
bool
QofSessionImpl::events_pending () const noexcept
{
return false;
}
bool
QofSessionImpl::process_events () const noexcept
{
return false;
}
/* XXX This exports the list of accounts to a file. It does not
* export any transactions. It's a place-holder until full
* book-closing is implemented.
*/
bool
QofSessionImpl::export_session (QofSessionImpl & real_session,
QofPercentageFunc percentage_func) noexcept
{
auto real_book = real_session.get_book ();
ENTER ("tmp_session=%p real_session=%p book=%p book_id=%s",
this, &real_session, real_book, m_book_id.c_str ());
/* There must be a backend or else. (It should always be the file
* backend too.)
*/
auto backend2 = qof_book_get_backend(m_book);
if (!backend2) return false;
backend2->set_percentage(percentage_func);
backend2->export_coa(real_book);
auto err = backend2->get_error();
if (err != ERR_BACKEND_NO_ERR)
return false;
return true;
}
/* C Wrapper Functions */
/* ====================================================================== */
const char *
qof_session_get_error_message (const QofSession * session)
{
if (!session) return "";
return session->get_error_message ().c_str ();
}
QofBackendError
qof_session_pop_error (QofSession * session)
{
if (!session) return ERR_BACKEND_NO_BACKEND;
return session->pop_error ();
}
QofBook *
qof_session_get_book (const QofSession *session)
{
if (!session) return NULL;
return session->get_book ();
}
const char *
qof_session_get_file_path (const QofSession *session)
{
if (!session) return NULL;
return session->get_file_path ().c_str ();
}
void
qof_session_ensure_all_data_loaded (QofSession *session)
{
if (session == nullptr) return;
return session->ensure_all_data_loaded ();
}
const char *
qof_session_get_url (const QofSession *session)
{
if (!session) return NULL;
return session->get_book_id ().c_str ();
}
QofBackend *
qof_session_get_backend (const QofSession *session)
{
if (!session) return NULL;
return session->get_backend ();
}
void
qof_session_begin (QofSession *session, const char * book_id,
gboolean ignore_lock, gboolean create, gboolean force)
{
if (!session) return;
session->begin((book_id ? book_id : ""), ignore_lock, create, force);
}
void
qof_session_load (QofSession *session,
QofPercentageFunc percentage_func)
{
if (!session) return;
session->load (percentage_func);
}
void
qof_session_save (QofSession *session,
QofPercentageFunc percentage_func)
{
if (!session) return;
session->save (percentage_func);
}
void
qof_session_safe_save(QofSession *session, QofPercentageFunc percentage_func)
{
if (!session) return;
session->safe_save (percentage_func);
}
gboolean
qof_session_save_in_progress(const QofSession *session)
{
if (!session) return false;
return session->is_saving ();
}
void
qof_session_end (QofSession *session)
{
if (!session) return;
session->end ();
}
void
qof_session_swap_data (QofSession *session_1, QofSession *session_2)
{
if (session_1 == session_2) return;
if (!session_1 || !session_2) return;
session_1->swap_books (*session_2);
}
gboolean
qof_session_events_pending (const QofSession *session)
{
if (!session) return false;
return session->events_pending ();
}
gboolean
qof_session_process_events (QofSession *session)
{
if (!session) return FALSE;
return session->process_events ();
}
gboolean
qof_session_export (QofSession *tmp_session,
QofSession *real_session,
QofPercentageFunc percentage_func)
{
if ((!tmp_session) || (!real_session)) return FALSE;
return tmp_session->export_session (*real_session, percentage_func);
}
/* ================= Static function access for testing ================= */
void init_static_qofsession_pointers (void);
void qof_session_load_backend (QofSession * session, const char * access_method)
{
session->load_backend (access_method);
}
static void
qof_session_clear_error (QofSession * session)
{
session->clear_error ();
}
static void
qof_session_destroy_backend (QofSession * session)
{
session->destroy_backend ();
}
void qof_session_set_book_id (QofSession * session, char const * book_id)
{
if (!book_id)
session->m_book_id = "";
else
session->m_book_id = book_id;
}
void (*p_qof_session_load_backend) (QofSession *, const char * access_method);
void (*p_qof_session_clear_error) (QofSession *);
void (*p_qof_session_destroy_backend) (QofSession *);
void (*p_qof_session_set_book_id) (QofSession *, char const * book_id);
void
init_static_qofsession_pointers (void)
{
p_qof_session_load_backend = &qof_session_load_backend;
p_qof_session_clear_error = &qof_session_clear_error;
p_qof_session_destroy_backend = &qof_session_destroy_backend;
p_qof_session_set_book_id = &qof_session_set_book_id;
}
QofBackendError
qof_session_get_error (QofSession * session)
{
if (!session) return ERR_BACKEND_NO_BACKEND;
return session->get_error();
}