simple-scan-team team mailing list archive
-
simple-scan-team team
-
Mailing list archive
-
Message #00500
[Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
Timo Kluck has proposed merging lp:~tkluck/simple-scan/autosaves into lp:simple-scan.
Requested reviews:
Simple Scan Development Team (simple-scan-team)
Related bugs:
Bug #897469 in Simple Scan: "Simple Scan crashes result in preventable data loss"
https://bugs.launchpad.net/simple-scan/+bug/897469
For more details, see:
https://code.launchpad.net/~tkluck/simple-scan/autosaves/+merge/101563
--
https://code.launchpad.net/~tkluck/simple-scan/autosaves/+merge/101563
Your team Simple Scan Development Team is requested to review the proposed merge of lp:~tkluck/simple-scan/autosaves into lp:simple-scan.
=== modified file '.bzrignore'
--- .bzrignore 2011-06-12 10:13:06 +0000
+++ .bzrignore 2012-04-11 14:22:28 +0000
@@ -32,3 +32,4 @@
src/scanner.c
src/simple-scan.c
src/ui.c
+src/autosave-manager.c
=== modified file 'configure.ac'
--- configure.ac 2012-03-26 22:36:31 +0000
+++ configure.ac 2012-04-11 14:22:28 +0000
@@ -30,6 +30,7 @@
cairo
gdk-pixbuf-2.0
gudev-1.0
+ sqlite3
])
PKG_CHECK_MODULES(COLORD, [
=== modified file 'src/Makefile.am'
--- src/Makefile.am 2011-07-10 06:59:21 +0000
+++ src/Makefile.am 2012-04-11 14:22:28 +0000
@@ -11,13 +11,15 @@
sane.vapi \
simple-scan.vala \
scanner.vala \
- ui.vala
+ ui.vala \
+ autosave-manager.vala
simple_scan_VALAFLAGS = \
--pkg=zlib \
--pkg=gudev-1.0 \
--pkg=gio-2.0 \
- --pkg=gtk+-3.0
+ --pkg=gtk+-3.0 \
+ --pkg=sqlite3
if HAVE_COLORD
simple_scan_VALAFLAGS += -D HAVE_COLORD
=== added file 'src/autosave-manager.vala'
--- src/autosave-manager.vala 1970-01-01 00:00:00 +0000
+++ src/autosave-manager.vala 2012-04-11 14:22:28 +0000
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2011 Timo Kluck
+ * Author: Timo Kluck <tkluck@xxxxxxxx>
+ *
+ * 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 3 of the License, or (at your option) any later
+ * version. See http://www.gnu.org/copyleft/gpl.html the full text of the
+ * license.
+ */
+
+/*
+ * We store autosaves in a database named
+ * ~/.cache/simple-scan/autosaves/autosaves.db
+ * It contains a single table of pages, each containing the process id (pid) of
+ * the simple-scan instance that saved it, and a hash of the Book and Page
+ * objects corresponding to it. The pixels are saved as a BLOB.
+ * Additionally, the autosaves directory contains a number of tiff files that
+ * the user can use for manual recovery.
+ *
+ * At startup, we check whether autosaves.db contains any records
+ * with a pid that does not match a current pid for simple-scan. If so, we take
+ * ownership by an UPDATE statement changing to our own pid. Then, we
+ * recover the book. We're trying our best to avoid the possible race
+ * condition if several instances of simple-scan are started simultaneously.
+ *
+ * At application exit, we delete the records corresponding to our own pid.
+ *
+ * Important notes:
+ * - We enforce that there is only one AutosaveManager instance in a given
+ * process by using a create function.
+ * - It should be possible to change the book object at runtime, although this
+ * is not used in the current implementation so it has not been tested.
+ */
+
+public class AutosaveManager
+{
+ private static string AUTOSAVE_DIR = Path.build_filename (Environment.get_user_cache_dir (), "simple-scan", "autosaves");
+ private static string AUTOSAVE_NAME = "autosaves";
+ private static string AUTOSAVE_EXT = ".db";
+ private static string AUTOSAVE_FILENAME = Path.build_filename (AUTOSAVE_DIR, AUTOSAVE_NAME + AUTOSAVE_EXT);
+
+ private static string PID = ((int)(Posix.getpid ())).to_string ();
+ private static int number_of_instances = 0;
+
+ private Sqlite.Database database_connection;
+ private Book _book = null;
+
+ public Book book
+ {
+ get
+ {
+ return _book;
+ }
+ set
+ {
+ if (_book != null)
+ {
+ for (var i = 0; i < _book.get_n_pages (); i++)
+ {
+ var page = _book.get_page (i);
+ on_page_removed (page);
+ }
+ _book.page_added.disconnect (on_page_added);
+ _book.page_removed.disconnect (on_page_removed);
+ _book.reordered.disconnect (on_reordered);
+ _book.cleared.disconnect (on_cleared);
+ }
+ _book = value;
+ _book.page_added.connect (on_page_added);
+ _book.page_removed.connect (on_page_removed);
+ _book.reordered.connect (on_reordered);
+ _book.cleared.connect (on_cleared);
+ }
+ }
+
+ public static AutosaveManager? create (ref Book book)
+ {
+ /* compare autosave directories with pids of current instances of simple-scan
+ * take ownership of one of the ones that are unowned by renaming to the
+ * own pid. Then open the database and fill the book with the pages it
+ * contains.
+ */
+ if (number_of_instances > 0)
+ {
+ assert_not_reached ();
+ }
+
+ var man = new AutosaveManager ();
+ number_of_instances++;
+
+ try
+ {
+ man.database_connection = open_database_connection ();
+ }
+ catch
+ {
+ warning ("Could not connect to the autosave database; no autosaves will be kept.");
+ return null;
+ }
+
+ try
+ {
+ // FIXME: this only works on linux
+ string current_pids;
+ Process.spawn_command_line_sync ("pidof simple-scan | sed \"s/ /,/g\"", out current_pids);
+ current_pids = current_pids.strip ();
+ Sqlite.Statement stmt;
+ if(Sqlite.OK == man.database_connection.prepare_v2 (@"
+ SELECT process_id, book_hash, book_revision FROM pages
+ WHERE NOT process_id IN ($current_pids)
+ LIMIT 1
+ ", -1, out stmt))
+ {
+ while (Sqlite.ROW == stmt.step ())
+ {
+ var unowned_pid = stmt.column_int (0);
+ var book_hash = stmt.column_int (1);
+ var book_revision = stmt.column_int (2);
+ /* there's a possible race condition here when several instances
+ * try to take ownership of the same rows. What would happen is
+ * that this operations would affect no rows if another process
+ * has taken ownership in the mean time. In that case, recover_book
+ * does nothing, so there should be no problem.
+ */
+ if (Sqlite.OK == man.database_connection.exec (@"
+ UPDATE pages
+ SET process_id = $PID
+ WHERE process_id = $unowned_pid
+ AND book_hash = $book_hash
+ AND book_revision = $book_revision"))
+ {
+ man.recover_book (ref book);
+ }
+ else
+ {
+ warn_if_reached ();
+ }
+ }
+ }
+ else
+ {
+ warn_if_reached ();
+ }
+ }
+ catch (SpawnError e)
+ {
+ warning ("Could not obtain current process ids; not restoring any autosaves");
+ }
+
+ man.book = book;
+ /* FIXME: we would like to connect to a scan_fished signal on a page,
+ * but it does not exist. Updating the database every time a scanline
+ * has changed is much to slow. We choose to update the database every
+ * now and then, instead.
+ * FIXME: this takes a reference on man, so it will never be destroyed.
+ */
+ GLib.Timeout.add_seconds (3, man.on_update_all_pages);
+
+ return man;
+ }
+
+ private AutosaveManager ()
+ {
+ }
+
+ public void cleanup () {
+ // delete autosave records
+ warn_if_fail (Sqlite.OK == database_connection.exec (@"
+ DELETE FROM pages
+ WHERE process_id = $PID
+ "));
+ }
+
+
+ static Sqlite.Database open_database_connection () throws Error
+ {
+ var autosaves_dir = File.new_for_path (AUTOSAVE_DIR);
+ try
+ {
+ autosaves_dir.make_directory_with_parents ();
+ }
+ catch
+ { // the directory already exists
+ // pass
+ }
+ Sqlite.Database connection;
+ if (Sqlite.OK != Sqlite.Database.open (AUTOSAVE_FILENAME, out connection))
+ {
+ throw new IOError.FAILED ("Could not connect to autosave database");
+ }
+ warn_if_fail (Sqlite.OK == connection.exec (@"
+ CREATE TABLE IF NOT EXISTS pages (
+ id integer PRIMARY KEY,
+ process_id integer,
+ page_hash integer,
+ book_hash integer,
+ book_revision integer,
+ page_number integer,
+ dpi integer,
+ width integer,
+ height integer,
+ depth integer,
+ n_channels integer,
+ rowstride integer,
+ color_profile string,
+ crop_x integer,
+ crop_y integer,
+ crop_width integer,
+ crop_height integer,
+ scan_direction integer,
+ pixels binary
+ )"));
+ return connection;
+ }
+
+ void on_page_added (Page page)
+ {
+ insert_page (page);
+ // TODO: save a tiff file
+ page.size_changed.connect (on_page_changed);
+ page.scan_line_changed.connect (on_page_changed);
+ page.scan_direction_changed.connect (on_page_changed);
+ page.crop_changed.connect (on_page_changed);
+ }
+
+ public void on_page_removed (Page page)
+ {
+ page.pixels_changed.disconnect (on_page_changed);
+ page.size_changed.disconnect (on_page_changed);
+ page.scan_line_changed.disconnect (on_page_changed);
+ page.scan_direction_changed.disconnect (on_page_changed);
+ page.crop_changed.disconnect (on_page_changed);
+
+ warn_if_fail (Sqlite.OK == database_connection.exec (@"
+ DELETE FROM pages
+ WHERE process_id = $PID
+ AND page_hash = $(direct_hash (page))
+ AND book_hash = $(direct_hash (book))
+ AND book_revision = $cur_book_revision
+ "));
+ }
+
+ public void on_reordered ()
+ {
+ for (var i=0; i < book.get_n_pages (); i++)
+ {
+ var page = book.get_page (i);
+ warn_if_fail (Sqlite.OK == database_connection.exec (@"
+ UPDATE pages SET page_number = $i
+ WHERE process_id = $PID
+ AND page_hash = $(direct_hash (page))
+ AND book_hash = $(direct_hash (book))
+ AND book_revision = $cur_book_revision
+ "));
+ }
+ }
+
+ public void on_page_changed (Page page)
+ {
+ /* we don't update the database as it is to slow to do so each time
+ * a scan line is received.
+ */
+ //update_page (page);
+ }
+
+ public void on_needs_saving_changed (Book book)
+ {
+ for (int n = 0; n < book.get_n_pages (); n++)
+ {
+ var page = book.get_page (n);
+ update_page (page);
+ }
+ }
+
+ private int cur_book_revision = 0;
+
+ public void on_cleared ()
+ {
+ cur_book_revision++;
+ }
+
+ private void insert_page (Page page)
+ {
+ warn_if_fail (Sqlite.OK == database_connection.exec (@"
+ INSERT INTO pages
+ (process_id,
+ page_hash,
+ book_hash,
+ book_revision)
+ VALUES
+ ($PID,
+ $(direct_hash (page)),
+ $(direct_hash (book)),
+ $cur_book_revision)
+ "));
+ update_page (page);
+ }
+
+ private bool on_update_all_pages ()
+ {
+ for (int n = 0; n < book.get_n_pages (); n++)
+ {
+ var page = book.get_page (n);
+ update_page (page);
+ }
+ return true;
+ }
+
+ private void update_page (Page page)
+ {
+ int crop_x;
+ int crop_y;
+ int crop_width;
+ int crop_height;
+ page.get_crop (out crop_x, out crop_y, out crop_width, out crop_height);
+ Sqlite.Statement stmt;
+ return_if_fail (Sqlite.OK == database_connection.prepare_v2 (@"
+ UPDATE pages
+ SET
+ page_number=$(book.get_page_index (page)),
+ dpi=$(page.get_dpi ()),
+ width=$(page.get_width ()),
+ height=$(page.get_height ()),
+ depth=$(page.get_depth ()),
+ n_channels=$(page.get_n_channels ()),
+ rowstride=$(page.get_rowstride ()),
+ crop_x=$crop_x,
+ crop_y=$crop_y,
+ crop_width=$crop_width,
+ crop_height=$crop_height,
+ scan_direction=$((int)page.get_scan_direction ()),
+ color_profile=?1,
+ pixels=?2
+ WHERE process_id = $PID
+ AND page_hash = $(direct_hash (page))
+ AND book_hash = $(direct_hash (book))
+ AND book_revision = $cur_book_revision
+ ", -1, out stmt));
+ warn_if_fail (Sqlite.OK == stmt.bind_text (1, page.get_color_profile () ?? ""));
+ if (page.get_pixels () != null)
+ {
+ // (-1) is the special value SQLITE_TRANSIENT
+ warn_if_fail (Sqlite.OK == stmt.bind_blob (2, page.get_pixels (), page.get_pixels ().length, (DestroyNotify)(-1)));
+ }
+ else
+ {
+ warn_if_fail (Sqlite.OK == stmt.bind_null (2));
+ }
+
+ warn_if_fail (Sqlite.DONE == stmt.step ());
+ }
+
+ private void recover_book (ref Book book)
+ {
+ Sqlite.Statement stmt;
+ return_if_fail (Sqlite.OK == database_connection.prepare_v2 (@"
+ SELECT process_id,
+ page_hash,
+ book_hash,
+ book_revision,
+ page_number,
+ dpi,
+ width,
+ height,
+ depth,
+ n_channels,
+ rowstride,
+ color_profile,
+ crop_x,
+ crop_y,
+ crop_width,
+ crop_height,
+ scan_direction,
+ pixels,
+ id
+ FROM pages
+ WHERE process_id = $PID
+ AND book_revision = (
+ SELECT MAX(book_revision) WHERE process_id = $PID
+ )
+ ORDER BY page_number
+ ", -1, out stmt));
+ bool first = true;
+ while (Sqlite.ROW == stmt.step ())
+ {
+ if (first)
+ {
+ book.clear ();
+ first = false;
+ }
+ var dpi = stmt.column_int (5);
+ var width = stmt.column_int (6);
+ var height = stmt.column_int (7);
+ var depth = stmt.column_int (8);
+ var n_channels = stmt.column_int (9);
+ var scan_direction = (ScanDirection)stmt.column_int (16);
+
+ if (width <= 0 || height <= 0)
+ {
+ continue;
+ }
+
+ var new_page = book.append_page (width, height, dpi, scan_direction);
+
+ if (depth > 0 && n_channels > 0)
+ {
+ var info = new ScanPageInfo ();
+ info.width = width;
+ info.height = height;
+ info.depth = depth;
+ info.n_channels = n_channels;
+ info.dpi = dpi;
+ info.device = "";
+ new_page.set_page_info (info);
+ }
+
+ new_page.set_color_profile (stmt.column_text (11));
+ var crop_x = stmt.column_int (12);
+ var crop_y = stmt.column_int (13);
+ var crop_width = stmt.column_int (14);
+ var crop_height = stmt.column_int (15);
+ if (crop_width > 0 && crop_height > 0)
+ {
+ new_page.set_custom_crop (crop_width, crop_height);
+ new_page.move_crop (crop_x, crop_y);
+ }
+
+ uchar[] new_pixels = new uchar[stmt.column_bytes (17)];
+ Memory.copy (new_pixels, stmt.column_blob (17), stmt.column_bytes (17));
+ new_page.set_pixels (new_pixels);
+
+ var id = stmt.column_int (18);
+ warn_if_fail (Sqlite.OK == database_connection.exec (@"
+ UPDATE pages
+ SET page_hash=$(direct_hash (new_page)),
+ book_hash=$(direct_hash (book)),
+ book_revision=$cur_book_revision
+ WHERE id = $id
+ "));
+ }
+ }
+}
=== modified file 'src/page.vala'
--- src/page.vala 2011-06-18 04:36:59 +0000
+++ src/page.vala 2012-04-11 14:22:28 +0000
@@ -541,6 +541,13 @@
return pixels;
}
+ public void set_pixels (uchar[] new_pixels)
+ {
+ pixels = new_pixels;
+ has_data_ = new_pixels != null;
+ pixels_changed ();
+ }
+
// FIXME: Copied from page-view, should be shared code
private uchar get_sample (uchar[] pixels, int offset, int x, int depth, int n_channels, int channel)
{
=== modified file 'src/ui.vala'
--- src/ui.vala 2012-04-01 18:13:06 +0000
+++ src/ui.vala 2012-04-11 14:22:28 +0000
@@ -69,6 +69,8 @@
private Book book;
private string? book_uri = null;
+ private AutosaveManager? autosave_manager;
+
private BookView book_view;
private bool updating_page_menu;
private int default_page_width;
@@ -99,6 +101,8 @@
settings = new Settings ("org.gnome.SimpleScan");
load ();
+
+ autosave_manager = AutosaveManager.create (ref book);
}
private bool find_scan_device (string device, out Gtk.TreeIter iter)
@@ -1120,6 +1124,11 @@
quit ();
+ if (autosave_manager != null)
+ {
+ autosave_manager.cleanup ();
+ }
+
return true;
}
Follow ups
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2013-01-19
-
[Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: noreply, 2013-01-19
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Timo Kluck, 2012-12-18
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-12-11
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-12-11
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Timo Kluck, 2012-12-06
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-11-27
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Timo Kluck, 2012-11-27
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-11-27
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-11-27
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Timo Kluck, 2012-08-09
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-07-16
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-06-06
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Timo Kluck, 2012-05-30
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-05-30
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Timo Kluck, 2012-05-30
-
Re: [Merge] lp:~tkluck/simple-scan/autosaves into lp:simple-scan
From: Robert Ancell, 2012-05-28