Ddeepin-ci-robotchore: init
781dfa83创建于 2023年9月8日历史提交
/*
 * AppConsole.vala
 *
 * Copyright 2012-2018 Tony George <teejeetech@gmail.com>
 *
 * 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.
 *
 *
 */

using GLib;
using Gtk;
using Gee;
//using Soup;
using Json;

using TeeJee.Logging;
using TeeJee.FileSystem;
using TeeJee.JsonHelper;
using TeeJee.ProcessHelper;
using TeeJee.GtkHelper;
using TeeJee.System;
using TeeJee.Misc;

public Main App;
public const string AppName = "Timeshift";
public const string AppShortName = "timeshift";
public const string AppVersion = Constants.VERSION;
public const string AppAuthor = "Tony George";
public const string AppAuthorEmail = "teejeetech@gmail.com";

const string GETTEXT_PACKAGE = "";
const string LOCALE_DIR = "/usr/share/locale";

extern void exit(int exit_code);

public class AppConsole : GLib.Object {

	public int snapshot_list_start_index = 0;

	public static int main (string[] args) {
		
		set_locale();

		LOG_TIMESTAMP = false;
		
		if (args.length > 1) {
			switch (args[1].down()) {
				case "--help":
				case "-h":
					stdout.printf (help_message ());
					return 0;

				case "--version":
					stdout.printf (version_message ());
					return 0;

			}
		}
		else if (args.length == 1){
			stdout.printf (help_message ());
			return 0;
		}

		LOG_ENABLE = false;
		init_tmp(AppShortName);
		LOG_ENABLE = true;
		
		check_if_admin();

		App = new Main(args, false);
		parse_arguments(args);
		App.initialize();
		
		var console =  new AppConsole();
		bool ok = console.start_application();
		App.exit_app((ok) ? 0 : 1);

		return (ok) ? 0 : 1;
	}

	private static void set_locale() {
		
		log_debug("setting locale...");
		Intl.setlocale(GLib.LocaleCategory.MESSAGES, "timeshift");
		Intl.textdomain(GETTEXT_PACKAGE);
		Intl.bind_textdomain_codeset(GETTEXT_PACKAGE, "utf-8");
		Intl.bindtextdomain(GETTEXT_PACKAGE, LOCALE_DIR);
	}

	public static void check_if_admin(){
		
		if (!user_is_admin()) {
			log_msg(_("Application needs admin access."));
			log_msg(_("Please run the application as admin (using 'sudo' or 'su')"));
			//App.exit_app(1);
			exit(1);
		}
	}

	// console members --------------

	private static void parse_arguments(string[] args){

		log_debug("AppConsole: parse_arguments()");
		
		for (int k = 1; k < args.length; k++) // Oth arg is app path
		{
			switch (args[k].down()){
				//case "--backup": // deprecated
				case "--check":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					App.app_mode = "backup";
					break;

				case "--delete":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					App.app_mode = "delete";
					break;

				case "--delete-all":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					App.app_mode = "delete-all";
					break;

				case "--restore":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					App.mirror_system = false;
					App.app_mode = "restore";
					break;

				case "--clone":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					App.mirror_system = true;
					App.app_mode = "restore";
					break;

				//case "--backup-now": // deprecated
				case "--create":
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					App.app_mode = "ondemand";
					break;

				case "--comment":
				case "--comments":
					App.cmd_comments = args[++k];
					break; 

				case "--skip-grub":
					App.cmd_skip_grub = true;
					break;

				case "--verbose":
					App.cmd_verbose = true;
					break;

				case "--quiet":
					App.cmd_verbose = false;
					break;

				case "--scripted":
					App.cmd_scripted = true;
					break;

				case "--yes":
					App.cmd_confirm = true;
					break;

				case "--grub":
				case "--grub-device":
					App.reinstall_grub2 = true;
					App.cmd_grub_device = args[++k];
					break;

				case "--target":
				case "--target-device":
					App.cmd_target_device = args[++k];
					break;

				case "--backup-device":
				case "--snapshot-device":
					App.cmd_backup_device = args[++k];
					break;

				case "--snapshot":
				case "--snapshot-name":
					App.cmd_snapshot = args[++k];
					break;

				case "--tags":
					App.cmd_tags = args[++k];
					App.validate_cmd_tags();
					break;

				case "--debug":
					LOG_COMMANDS = true;
					LOG_DEBUG = true;
					break;

				case "--list":
				case "--list-snapshots":
					App.app_mode = "list-snapshots";
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					break;

				case "--list-devices":
					App.app_mode = "list-devices";
					LOG_TIMESTAMP = false;
					LOG_DEBUG = false;
					break;

				case "--btrfs":
					App.btrfs_mode = true;
					App.cmd_btrfs_mode = true;
					break;

				case "--rsync":
					App.btrfs_mode = false;
					App.cmd_btrfs_mode = false;
					break;

				case "--backup":
					log_error("Option --backup has been replaced by option --check");
					log_error("Run 'timeshift --help' to list all available options");
					App.exit_app(1);
					break;

				case "--backup-now":
					log_error("Option --backup-now has been replaced by option --create");
					log_error("Run 'timeshift --help' to list all available options");
					App.exit_app(1);
					break;
					
				default:
					LOG_TIMESTAMP = false;
					log_error("%s: %s".printf(
						_("Invalid command line arguments"), args[k]), true);
					log_msg(help_message());
					App.exit_app(1);
					break;
			}
		}

		/* LOG_ENABLE = false; 		disables all console output
		 * LOG_TIMESTAMP = false;	disables the timestamp prepended to every line in terminal output
		 * LOG_DEBUG = false;		disables additional console messages
		 * LOG_COMMANDS = true;		enables printing of all commands on terminal
		 * */

		//if (app_mode == ""){
			//Initialize GTK
		//	LOG_TIMESTAMP = true;
		//}

		//Gtk.init(ref args);
		//X.init_threads();
	}

	public bool start_application(){

		log_debug("AppConsole: start_application()");
		
		bool is_success = true;

		if (App.live_system()){
			switch(App.app_mode){
			case "backup":
			case "ondemand":
				log_error(_("Snapshots cannot be created in Live CD mode"));
				return false;
			}
		}

		switch(App.app_mode){
			case "backup":
				is_success = create_snapshot(false);
				return is_success;

			case "restore":
				is_success = restore_snapshot();
				return is_success;

			case "delete":
				is_success = delete_snapshot();
				return is_success;

			case "delete-all":
				is_success = delete_all_snapshots();
				return is_success;

			case "ondemand":
				is_success = create_snapshot(true);
				return is_success;

			case "list-snapshots":
				LOG_ENABLE = true;

				App.repo.print_status();

				if (App.repo.has_snapshots()){
					list_snapshots(false);
					log_msg("");
					return true;
				}
				else{
					log_msg(_("No snapshots found"));
					return false;
				}

			case "list-devices":
				LOG_ENABLE = true;
				log_msg("\n" + _("Devices with Linux file systems") + ":\n");
				list_all_devices();
				log_msg("");
				return true;

			default:
				return true;
		}
	}

	private static string version_message (){
		string msg = "%s %s\n".printf( AppName, AppVersion);
		return msg;
	}

	private static string help_message (){
		
		string msg = "\n%s v%s by Tony George (%s)\n".printf(
			AppName, AppVersion, AppAuthorEmail);
			
		msg += "\n";
		msg += "Syntax:\n";
		msg += "\n";
		msg += "  timeshift --check\n";
		msg += "  timeshift --create [OPTIONS]\n";
		msg += "  timeshift --restore [OPTIONS]\n";
		msg += "  timeshift --delete-[all] [OPTIONS]\n";
		msg += "  timeshift --list-{snapshots|devices} [OPTIONS]\n";
		msg += "\n";
		msg += _("Options") + ":\n";
		msg += "\n";
		msg += _("List") + ":\n";
		msg += "  --list[-snapshots]         " + _("List snapshots") + "\n";
		msg += "  --list-devices             " + _("List devices") + "\n";
		msg += "\n";
		msg += _("Backup") + ":\n";
		msg += "  --check                    " + _("Create snapshot if scheduled") + "\n";
		msg += "  --create                   " + _("Create snapshot (even if not scheduled)") + "\n";
		msg += "  --comments <string>        " + _("Set snapshot description") + "\n";
		msg += "  --tags {O,B,H,D,W,M}       " + _("Add tags to snapshot (default: O)") + "\n";;
		msg += "\n";
		msg += _("Restore") + ":\n";
		msg += "  --restore                  " + _("Restore snapshot") + "\n";
		//msg += "  --clone                    " + _("Clone current system") + "\n"; // broken feature, not supported
		msg += "  --snapshot <name>          " + _("Specify snapshot to restore") + "\n";
		msg += "  --target[-device] <device> " + _("Specify target device") + "\n";
		msg += "  --grub[-device] <device>   " + _("Specify device for installing GRUB2 bootloader") + "\n";
		msg += "  --skip-grub                " + _("Skip GRUB2 reinstall") + "\n";
		msg += "\n";
		msg += _("Delete") + ":\n";
		msg += "  --delete                   " + _("Delete snapshot") + "\n";
		msg += "  --delete-all               " + _("Delete all snapshots") + "\n";
		msg += "\n";
		msg += _("Global") + ":\n";
		msg += "  --snapshot-device <device> " + _("Specify backup device (default: config)") + "\n";
		msg += "  --yes                      " + _("Answer YES to all confirmation prompts") + "\n";
		msg += "  --btrfs                    " + _("Switch to BTRFS mode (default: config)") + "\n";
		msg += "  --rsync                    " + _("Switch to RSYNC mode (default: config)") + "\n";
		msg += "  --debug                    " + _("Show additional debug messages") + "\n";
		msg += "  --verbose                  " + _("Show rsync output (default)") + "\n";
		msg += "  --quiet                    " + _("Hide rsync output") + "\n";
		msg += "  --scripted                 " + _("Run in non-interactive mode") + "\n";
		msg += "  --help                     " + _("Show all options") + "\n";
		msg += "  --version                  " + _("Print version number") + "\n";
		msg += "\n";

		msg += _("Examples") + ":\n";
		msg += "\n";
		msg += "timeshift --list\n";
		msg += "timeshift --list --snapshot-device /dev/sda1\n";
		msg += "timeshift --create --comments \"after update\" --tags D\n";
		msg += "timeshift --restore \n";
		msg += "timeshift --restore --snapshot '2014-10-12_16-29-08' --target /dev/sda1\n";
		msg += "timeshift --delete  --snapshot '2014-10-12_16-29-08'\n";
		msg += "timeshift --delete-all \n";
		msg += "\n";

		msg += _("Notes") + ":\n";
		msg += "\n";
		msg += "  1) --create will always create a new snapshot\n";
		msg += "  2) --check will create a snapshot only if a scheduled snapshot is due\n";
		msg += "  3) Use --restore without other options to select options interactively\n";
		msg += "  4) UUID can be specified instead of device name\n";
		msg += "  5) Default values will be loaded from app config if options are not specified\n";
		msg += "\n";
		return msg;
	}

	//console functions

	private void list_snapshots(bool paginate, int page_size = 20){
		int count = 0;
		for(int index = 0; index < App.repo.snapshots.size; index++){
			if (!paginate || ((index >= snapshot_list_start_index) && (index < snapshot_list_start_index + page_size))){
				count++;
			}
		}

		string[,] grid = new string[count+1,5];
		bool[] right_align = { false, false, false, false, false};

		int row = 0;
		int col = -1;
		grid[row, ++col] = _("Num");
		grid[row, ++col] = "";
		grid[row, ++col] = _("Name");
		grid[row, ++col] = _("Tags");
		grid[row, ++col] = _("Description");
		row++;

		for(int index = 0; index < App.repo.snapshots.size; index++){
			Snapshot bak = App.repo.snapshots[index];
			if (!paginate || ((index >= snapshot_list_start_index) && (index < snapshot_list_start_index + page_size))){
				col = -1;
				grid[row, ++col] = "%d".printf(index);
				grid[row, ++col] = ">";
				grid[row, ++col] = "%s".printf(bak.name);
				grid[row, ++col] = "%s".printf(bak.taglist_short);
				grid[row, ++col] = "%s".printf(bak.description);
				row++;
			}
		}

		print_grid(grid, right_align);
	}

	private void list_devices(Gee.ArrayList<Device> device_list){
		
		string[,] grid = new string[device_list.size+1,6];
		bool[] right_align = { false, false, false, true, true, false};

		int row = 0;
		int col = -1;
		grid[row, ++col] = _("Num");
		grid[row, ++col] = "";
		grid[row, ++col] = _("Device");
		//grid[row, ++col] = _("UUID");
		grid[row, ++col] = _("Size");
		grid[row, ++col] = _("Type");
		grid[row, ++col] = _("Label");
		row++;

		foreach(var pi in device_list) {
			col = -1;
			grid[row, ++col] = "%d".printf(row - 1);
			grid[row, ++col] = ">";
			grid[row, ++col] = "%s".printf(pi.device_name_with_parent);
			//grid[row, ++col] = "%s".printf(pi.uuid);
			grid[row, ++col] = "%s".printf((pi.size_bytes > 0) ? "%s".printf(pi.size) : "?? GB");
			grid[row, ++col] = "%s".printf(pi.fstype);
			grid[row, ++col] = "%s".printf(pi.label);
			row++;
		}

		print_grid(grid, right_align);
	}

	private Gee.ArrayList<Device> list_all_devices(){

		//add devices
		var device_list = new Gee.ArrayList<Device>();
		foreach(var dev in Device.get_block_devices_using_lsblk()) {
			if (dev.has_linux_filesystem()){
				device_list.add(dev);
			}
		}

		string[,] grid = new string[device_list.size+1,6];
		bool[] right_align = { false, false, false, true, true, false};

		int row = 0;
		int col = -1;
		grid[row, ++col] = _("Num");
		grid[row, ++col] = "";
		grid[row, ++col] = _("Device");
		//grid[row, ++col] = _("UUID");
		grid[row, ++col] = _("Size");
		grid[row, ++col] = _("Type");
		grid[row, ++col] = _("Label");
		row++;

		foreach(var pi in device_list) {
			col = -1;
			grid[row, ++col] = "%d".printf(row - 1);
			grid[row, ++col] = ">";
			grid[row, ++col] = "%s".printf(pi.device_name_with_parent);
			//grid[row, ++col] = "%s".printf(pi.uuid);
			grid[row, ++col] = "%s".printf((pi.size_bytes > 0) ? format_file_size(pi.size_bytes): "?? GB");
			grid[row, ++col] = "%s".printf(pi.fstype);
			grid[row, ++col] = "%s".printf(pi.label);
			row++;
		}

		print_grid(grid, right_align);

		return device_list;
	}

	private Gee.ArrayList<Device> list_grub_devices(bool print_to_console = true){
		//add devices
		var grub_device_list = new Gee.ArrayList<Device>();
		foreach(var dev in Device.get_block_devices_using_lsblk()) {
			if (dev.type == "disk"){
				grub_device_list.add(dev);
			}
			else if (dev.type == "part"){ 
				if (dev.has_linux_filesystem()){
					grub_device_list.add(dev);
				}
			}
			// skip crypt/loop
		}

		/*Note: Lists are already sorted. No need to sort again */

		string[,] grid = new string[grub_device_list.size+1,4];
		bool[] right_align = { false, false, false, false };

		int row = 0;
		int col = -1;
		grid[row, ++col] = _("Num");
		grid[row, ++col] = "";
		grid[row, ++col] = _("Device");
		grid[row, ++col] = _("Description");
		row++;

		string desc = "";
		foreach(Device pi in grub_device_list) {
			col = -1;
			grid[row, ++col] = "%d".printf(row - 1);
			grid[row, ++col] = ">";
			grid[row, ++col] = "%s".printf(pi.short_name_with_alias);

			if (pi.type == "disk"){
				desc = "%s".printf(((pi.vendor.length > 0)||(pi.model.length > 0)) ? (pi.vendor + " " + pi.model  + " [MBR]") : "");
			}
			else{
				desc = "%5s, ".printf(pi.fstype);
				desc += "%10s".printf((pi.size_bytes > 0) ? "%s GB".printf(pi.size) : "?? GB");
				desc += "%s".printf((pi.label.length > 0) ? ", " + pi.label : "");
			}
			grid[row, ++col] = "%s".printf(desc);
			row++;
		}

		print_grid(grid, right_align);

		return grub_device_list;
	}

	private void print_grid(string[,] grid, bool[] right_align, bool has_header = true){
		int[] col_width = new int[grid.length[1]];

		for(int col=0; col<grid.length[1]; col++){
			for(int row=0; row<grid.length[0]; row++){
				if (grid[row,col].length > col_width[col]){
					col_width[col] = grid[row,col].length;
				}
			}
		}

		for(int row=0; row<grid.length[0]; row++){
			for(int col=0; col<grid.length[1]; col++){
				string fmt = "%" + (right_align[col] ? "+" : "-") + col_width[col].to_string() + "s  ";
				stdout.printf(fmt.printf(grid[row,col]));
			}
			stdout.printf("\n");

			if (has_header && (row == 0)){
				stdout.printf(string.nfill(78, '-'));
				stdout.printf("\n");
			}
		}
	}

	// create

	private bool create_snapshot(bool ondemand){
		select_snapshot_device(false);
		return App.create_snapshot(ondemand, null);
	}
	
	// restore
	
	private bool restore_snapshot(){

		select_snapshot_device(true);

		select_snapshot_for_restore();
		
		stdout.printf("\n\n");
		log_msg(string.nfill(78, '*'));
		stdout.printf(_("To restore with default options, press the ENTER key for all prompts!") + "\n");
		log_msg(string.nfill(78, '*'));
		stdout.printf(_("\nPress ENTER to continue..."));
		stdout.flush();
		stdin.read_line();

		init_mounts();
		
		if (!App.btrfs_mode){

			map_devices();

			select_grub_device();
		}

		confirm_restore();

		bool ok = App.mount_target_devices();
		if (!ok){
			return false;
		}

		return App.restore_snapshot(null);
	}

	private void select_snapshot_device(bool prompt_if_empty){

		if (App.mirror_system){
			return;
		}

		var list = new Gee.ArrayList<Device>();
		foreach(var pi in App.partitions){
			if (pi.has_linux_filesystem()){
				list.add(pi);
			}
		}
					
		if ((App.repo.device == null) || (prompt_if_empty && (App.repo.snapshots.size == 0))){
			//prompt user for backup device
			log_msg("");

			if (App.cmd_scripted){
				
				if (App.repo.device == null){
					if (App.backup_uuid.length == 0){
						log_debug("device is null");
						string status_message = _("Snapshot device not selected");
						log_msg(status_message);
					}
					else{
						string status_message = _("Snapshot device not available");
						string status_details = _("Device not found") + ": UUID='%s'".printf(App.backup_uuid);
						log_msg(status_message);
						log_msg(status_details);
					}
				}

				App.exit_app(1);
			}

			log_msg(_("Select backup device") + ":\n");
			list_devices(list);
			log_msg("");

			Device dev = null;
			int attempts = 0;
			while (dev == null){
				attempts++;
				if (attempts > 3) { break; }
				stdout.printf("" +
					_("Enter device name or number (a=Abort)") + ": ");
				stdout.flush();

				dev = read_stdin_device(list, "");

				if (App.btrfs_mode && !App.check_device_for_backup(dev, true)){
					log_error(_("Selected snapshot device is not a system disk"));
					log_error(_("Select BTRFS system disk with root subvolume (@)"));
					dev = null;
				}
			}

			log_msg("");
			
			if (dev == null){
				log_error(_("Failed to get input from user in 3 attempts"));
				log_msg(_("Aborted."));
				App.exit_app(1);
			}

			App.repo = new SnapshotRepo.from_device(dev, null, App.btrfs_mode);
			if (!App.repo.available()){
				App.exit_app(1);
			}
		}
	}

	private Snapshot? select_snapshot(){

		Snapshot selected_snapshot = null;
		
		log_debug("AppConsole: select_snapshot()");
		
		if (App.mirror_system){
			return null;
		}
		
		if (App.cmd_snapshot.length > 0){

			//check command line arguments
			bool found = false;
			foreach(var bak in App.repo.snapshots) {
				if (bak.name == App.cmd_snapshot){
					return bak;
				}
			}

			//check if found
			if (!found){
				log_error(_("Could not find snapshot") + ": '%s'".printf(App.cmd_snapshot));
				return null;
			}
		}

		//prompt user for snapshot
		if (selected_snapshot == null){

			if (!App.repo.has_snapshots()){
				log_error(_("No snapshots found on device") + ": '%s'".printf(App.repo.device.device));
				App.exit_app(0);
				return null;
			}

			log_msg("");
			log_msg(_("Select snapshot") + ":\n");
			list_snapshots(true);
			log_msg("");

			int attempts = 0;
			while (selected_snapshot == null){
				attempts++;
				if (attempts > 3) { break; }
				stdout.printf(_("Enter snapshot number (a=Abort, p=Previous, n=Next)") + ": ");
				stdout.flush();
				selected_snapshot = read_stdin_snapshot();
			}
			log_msg("");
			
			if (selected_snapshot == null){
				log_error(_("Failed to get input from user in 3 attempts"));
				log_msg(_("Aborted."));
				App.exit_app(0);
			}
		}

		return selected_snapshot;
	}

	private void select_snapshot_for_restore(){
		App.snapshot_to_restore = select_snapshot();
		if (App.snapshot_to_restore == null){
			log_error("Snapshot not selected");
			App.exit_app(1);
		}
	}

	private void select_snapshot_for_deletion(){
		App.snapshot_to_delete = select_snapshot();
		if (App.snapshot_to_delete == null){
			log_error("Snapshot not selected");
			App.exit_app(1);
		}
	}
	
	private void init_mounts(){

		log_debug("AppConsole: init_mounts()");
		
		App.init_mount_list();

		// remove mount points which will remain on root fs
		for(int i = App.mount_list.size-1; i >= 0; i--){
			
			var entry = App.mount_list[i];
			
			if (entry.device == null){
				App.mount_list.remove(entry);
			}
		}
	}

	private void map_devices(){

		log_debug("AppConsole: map_devices()");
		
		if (App.cmd_target_device.length > 0){

			//check command line arguments
			bool found = false;
			foreach(Device pi in App.partitions) {
				
				if (!pi.has_linux_filesystem()) { continue; }
				
				if ((pi.device == App.cmd_target_device)||((pi.uuid == App.cmd_target_device))){
					App.dst_root = pi;
					found = true;
					break;
				}
				else {
					foreach(string symlink in pi.symlinks){
						if (symlink == App.cmd_target_device){
							App.dst_root = pi;
							found = true;
							break;
						}
					}
					if (found){ break; }
				}
			}

			//check if found
			if (!found){
				log_error(_("Could not find device") + ": '%s'".printf(App.cmd_target_device));
				App.exit_app(1);
				return;
			}
		}

		for(int i = 0; i < App.mount_list.size; i++){
			
			MountEntry mnt = App.mount_list[i];
			Device dev = null;
			string default_device = "";

			log_debug("selecting: %s".printf(mnt.mount_point));

			// no need to ask user to map remaining devices if restoring same system
			if ((App.dst_root != null) && (App.sys_root != null)
				&& (App.dst_root.uuid == App.sys_root.uuid)){
					
				break;
			}

			if (App.mirror_system){
				default_device = (App.dst_root != null) ? App.dst_root.device : "";
			}
			else{
				if (mnt.device != null){
					default_device = mnt.device.device;
				}
				else{
					default_device = (App.dst_root != null) ? App.dst_root.device : "";
				}
			}

			//prompt user for device
			if (dev == null){
				log_msg("");
				log_msg(_("Select '%s' device (default = %s)").printf(
					mnt.mount_point, default_device) + ":\n");
				var device_list = list_all_devices();
				log_msg("");

				int attempts = 0;
				while (dev == null){
					attempts++;
					if (attempts > 3) { break; }
					
					stdout.printf("" +
						_("[ENTER = Default (%s), r = Root device, a = Abort]").printf(default_device) + "\n\n");
						
					stdout.printf(
						_("Enter device name or number")
							+ ": ");
							
					stdout.flush();
					dev = read_stdin_device_mounts(device_list, mnt);
				}
				log_msg("");

				if (dev == null){
					log_error(_("Failed to get input from user in 3 attempts"));
					log_msg(_("Aborted."));
					App.exit_app(0);
				}
			}

			if (dev != null){

				log_debug("selected: %s".printf(dev.uuid));
				
				mnt.device = dev;

				log_msg(string.nfill(78, '*'));
				
				if ((mnt.mount_point != "/")
					&& (App.dst_root != null)
					&& (dev.device == App.dst_root.device)){
						
					log_msg(_("'%s' will be on root device").printf(mnt.mount_point), true);
				}
				else{
					log_msg(_("'%s' will be on '%s'").printf(
						mnt.mount_point, mnt.device.short_name_with_alias), true);
						
					//log_debug("UUID=%s".printf(dst_root.uuid));
				}
				log_msg(string.nfill(78, '*'));
			}
		
		}
	}

	private void select_grub_device(){

		string grub_device_default = App.grub_device;
		bool grub_reinstall_default = App.reinstall_grub2;
		App.reinstall_grub2 = false;
		App.grub_device = "";
		
		if (App.cmd_grub_device.length > 0){

			log_debug("Grub device is specified as command argument");
			
			//check command line arguments
			bool found = false;
			var device_list = list_grub_devices(false);
			
			foreach(Device dev in device_list) {
				
				if ((dev.device == App.cmd_grub_device)
					||((dev.uuid.length > 0) && (dev.uuid == App.cmd_grub_device))){

					App.grub_device = dev.device;
					found = true;
					break;
				}
				else {
					if (dev.type == "part"){
						foreach(string symlink in dev.symlinks){
							if (symlink == App.cmd_grub_device){
								App.grub_device = dev.device;
								found = true;
								break;
							}
						}
						if (found){ break; }
					}
				}
			}

			//check if found
			if (!found){
				log_error(_("Could not find device") + ": '%s'".printf(App.cmd_grub_device));
				App.exit_app(1);
				return;
			}
		}
		
		if (App.mirror_system){
			App.reinstall_grub2 = true;
		}
		else {
			if ((App.cmd_skip_grub == false) && (App.reinstall_grub2 == false)){
				log_msg("");

				int attempts = 0;
				while ((App.cmd_skip_grub == false) && (App.reinstall_grub2 == false)){
					attempts++;
					if (attempts > 3) { break; }
					stdout.printf(_("Re-install GRUB2 bootloader?") + (grub_reinstall_default ? " (recommended)" : "") + " (y/n): ");
					stdout.flush();
					read_stdin_grub_install(grub_reinstall_default);
				}

				if ((App.cmd_skip_grub == false) && (App.reinstall_grub2 == false)){
					log_error(_("Failed to get input from user in 3 attempts"));
					log_msg(_("Aborted."));
					App.exit_app(0);
				}
			}
		}

		if ((App.reinstall_grub2) && (App.grub_device.length == 0)){
			
			log_msg("");
			log_msg(_("Select GRUB device") + ":\n");
			var device_list = list_grub_devices();
			log_msg("");

			int attempts = 0;
			while (App.grub_device.length == 0){
				
				attempts++;
				if (attempts > 3) { break; }

				if (grub_device_default.length > 0){
					stdout.printf("" +
						_("[ENTER = Default (%s), a = Abort]").printf(grub_device_default) + "\n\n");
				}

				stdout.printf(_("Enter device name or number (a=Abort)") + ": ");
				stdout.flush();

				// TODO: provide option for default boot device

				var list = new Gee.ArrayList<Device>();
				foreach(var pi in App.partitions){
					if (pi.has_linux_filesystem()){
						list.add(pi);
					}
				}
				
				Device dev = read_stdin_device(device_list, grub_device_default);
				if (dev != null) { App.grub_device = dev.device; }
			}
			
			log_msg("");

			if (App.grub_device.length == 0){
				
				log_error(_("Failed to get input from user in 3 attempts"));
				log_msg(_("Aborted."));
				App.exit_app(0);
			}
		}

		if ((App.reinstall_grub2) && (App.grub_device.length > 0)){
			
			log_msg(string.nfill(78, '*'));
			log_msg(_("GRUB Device") + ": %s".printf(App.grub_device));
			log_msg(string.nfill(78, '*'));
		}
		else{
			log_msg(string.nfill(78, '*'));
			log_msg(_("GRUB will NOT be reinstalled"));
			log_msg(string.nfill(78, '*'));
		}
	}

	private void confirm_restore(){
		
		if (App.cmd_confirm == false){

			string msg_devices = "";
			string msg_reboot = "";
			string msg_disclaimer = "";

			App.get_restore_messages(
				false, out msg_devices, out msg_reboot,
				out msg_disclaimer);

			int attempts = 0;
			while (App.cmd_confirm == false){
				attempts++;
				if (attempts > 3) { break; }
				stdout.printf(_("Continue with restore? (y/n): "));
				stdout.flush();
				read_stdin_restore_confirm();
			}

			if (App.cmd_confirm == false){
				log_error(_("Failed to get input from user in 3 attempts"));
				log_msg(_("Aborted."));
				App.exit_app(0);
			}
		}
	}
	
	private Device? read_stdin_device(Gee.ArrayList<Device> device_list, string device_default){
		
		var counter = new TimeoutCounter();
		counter.exit_on_timeout();
		string? line = stdin.read_line();
		counter.stop();

		line = (line != null) ? line.strip() : "";

		Device selected_device = null;

		if (line.down() == "a"){
			log_msg(_("Aborted."));
			App.exit_app(0);
		}
		else if ((line == null)||(line.length == 0)||(line.down() == "c")||(line.down() == "d")){
			if (device_default.length > 0){
				selected_device = Device.get_device_by_name(device_default);
			}
			else{
				log_error("Invalid input");
			}
		}
		else if (line.contains("/")){
			selected_device = Device.get_device_by_name(line);
			if (selected_device == null){
				log_error("Invalid input");
			}
		}
		else{
			selected_device = get_device_from_index(device_list, line);
			if (selected_device == null){
				log_error("Invalid input");
			}
		}

		return selected_device;
	}

	private Device? read_stdin_device_mounts(Gee.ArrayList<Device> device_list, MountEntry mnt){
		var counter = new TimeoutCounter();
		counter.exit_on_timeout();
		string? line = stdin.read_line();
		counter.stop();

		line = (line != null) ? line.strip() : "";

		Device selected_device = null;

		if ((line == null)||(line.length == 0)||(line.down() == "c")||(line.down() == "d")){
			//set default
			if (App.mirror_system){
				return App.dst_root; //root device
			}
			else{
				return mnt.device; //keep current
			}
		}
		else if (line.down() == "a"){
			log_msg("Aborted.");
			App.exit_app(0);
		}
		else if ((line.down() == "n")||(line.down() == "r")){
			return App.dst_root; //root device
		}
		else if (line.contains("/")){
			selected_device = Device.get_device_by_name(line);
			if (selected_device == null){
				log_error("Invalid input");
			}
		}
		else{
			selected_device = get_device_from_index(device_list, line);
			if (selected_device == null){
				log_error("Invalid input");
			}
		}

		return selected_device;
	}

	private Device? get_device_from_index(Gee.ArrayList<Device> device_list, string index_string){
		int64 index;
		if (int64.try_parse(index_string, out index)){
			int i = -1;
			foreach(Device pi in device_list) {
				if (++i == index){
					return pi;
				}
			}
		}

		return null;
	}

	private Snapshot read_stdin_snapshot(){
		var counter = new TimeoutCounter();
		counter.exit_on_timeout();
		string? line = stdin.read_line();
		counter.stop();

		line = (line != null) ? line.strip() : "";

		Snapshot selected_snapshot = null;

		if (line.down() == "a"){
			log_msg("Aborted.");
			App.exit_app(0);
		}
		else if (line.down() == "p"){
			snapshot_list_start_index -= 10;
			if (snapshot_list_start_index < 0){
				snapshot_list_start_index = 0;
			}
			log_msg("");
			list_snapshots(true);
			log_msg("");
		}
		else if (line.down() == "n"){
			if ((snapshot_list_start_index + 10) < App.repo.snapshots.size){
				snapshot_list_start_index += 10;
			}
			log_msg("");
			list_snapshots(true);
			log_msg("");
		}
		else if (line.contains("_")||line.contains("-")){
			//TODO: read name
			log_error("Invalid input");
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
		}
		else{
			int64 index;
			if (int64.try_parse(line, out index)){
				if (index < App.repo.snapshots.size){
					selected_snapshot = App.repo.snapshots[(int) index];
				}
				else{
					log_error("Invalid input");
				}
			}
			else{
				log_error("Invalid input");
			}
		}

		return selected_snapshot;
	}

	private bool read_stdin_grub_install(bool reinstall_default){
		var counter = new TimeoutCounter();
		counter.exit_on_timeout();
		string? line = stdin.read_line();
		counter.stop();

		line = (line != null) ? line.strip() : line;

		if ((line == null)||(line.length == 0)){
			App.reinstall_grub2 = reinstall_default;
			App.cmd_skip_grub = !reinstall_default;
			return true;
		}
		else if (line.down() == "a"){
			log_msg("Aborted.");
			App.exit_app(0);
			return true;
		}
		else if (line.down() == "y"){
			App.cmd_skip_grub = false;
			App.reinstall_grub2 = true;
			return true;
		}
		else if (line.down() == "n"){
			App.cmd_skip_grub = true;
			App.reinstall_grub2 = false;
			return true;
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
			return false;
		}
		else{
			log_error("Invalid input");
			return false;
		}
	}

	private bool read_stdin_restore_confirm(){
		var counter = new TimeoutCounter();
		counter.exit_on_timeout();
		
		string? line = stdin.read_line();
		counter.stop();

		line = (line != null) ? line.strip() : "";

		if ((line.down() == "a")||(line.down() == "n")){
			log_msg("Aborted.");
			App.exit_app(0);
			return true;
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
			return false;
		}
		else if (line.down() == "y"){
			App.cmd_confirm = true;
			return true;
		}
		else if ((line == null)||(line.length == 0)){
			log_error("Invalid input");
			return false;
		}
		else{
			log_error("Invalid input");
			return false;
		}
	}

	// delete

	public bool delete_snapshot(){

		select_snapshot_device(true);

		select_snapshot_for_deletion();

		if (App.snapshot_to_delete != null){
			App.snapshot_to_delete.remove(true);
		}

		return true;
	}

	public bool delete_all_snapshots(){
		
		select_snapshot_device(true);
		
		//return App.repo.remove_all();
		
		foreach(var snap in App.repo.snapshots){
			snap.remove(true);
		}

		return true;
	}

}