Refactor noninteractive mode. All yes/no questions are forced to define a default that will be used in noninteractive mode.

This commit is contained in:
Sebastian Messmer 2016-09-24 20:28:56 +02:00
parent d00151af37
commit 1e9fdc9aa7
16 changed files with 213 additions and 144 deletions

View File

@ -14,6 +14,9 @@ set(SOURCES
network/CurlHttpClient.cpp
network/FakeHttpClient.cpp
io/Console.cpp
io/DontEchoStdinToStdoutRAII.cpp
io/IOStreamConsole.cpp
io/NoninteractiveConsole.cpp
io/pipestream.cpp
thread/LoopThread.cpp
thread/ThreadSystem.cpp

View File

@ -1,99 +1,2 @@
#include "Console.h"
#include <boost/optional.hpp>
#include <boost/algorithm/string/trim.hpp>
using std::string;
using std::vector;
using std::ostream;
using std::istream;
using std::flush;
using std::getline;
using boost::optional;
using boost::none;
using std::function;
using namespace cpputils;
IOStreamConsole::IOStreamConsole(): IOStreamConsole(std::cout, std::cin) {
}
IOStreamConsole::IOStreamConsole(ostream &output, istream &input): _output(output), _input(input) {
}
optional<int> parseInt(const string &str) {
try {
string trimmed = str;
boost::algorithm::trim(trimmed);
int parsed = std::stoi(str);
if (std::to_string(parsed) != trimmed) {
return none;
}
return parsed;
} catch (const std::invalid_argument &e) {
return none;
} catch (const std::out_of_range &e) {
return none;
}
}
function<optional<unsigned int>(const std::string &input)> parseUIntWithMinMax(unsigned int min, unsigned int max) {
return [min, max] (const string &input) {
optional<int> parsed = parseInt(input);
if (parsed == none) {
return optional<unsigned int>(none);
}
unsigned int value = static_cast<unsigned int>(*parsed);
if (value < min || value > max) {
return optional<unsigned int>(none);
}
return optional<unsigned int>(value);
};
}
template<typename Return>
Return IOStreamConsole::_askForChoice(const string &question, function<optional<Return> (const string&)> parse) {
optional<Return> choice = none;
do {
_output << question << flush;
string choiceStr;
getline(_input, choiceStr);
choice = parse(choiceStr);
} while(choice == none);
return *choice;
}
unsigned int IOStreamConsole::ask(const string &question, const vector<string> &options) {
if(options.size() == 0) {
throw std::invalid_argument("options should have at least one entry");
}
_output << question << "\n";
for (unsigned int i = 0; i < options.size(); ++i) {
_output << " [" << (i+1) << "] " << options[i] << "\n";
}
int choice = _askForChoice("Your choice [1-" + std::to_string(options.size()) + "]: ", parseUIntWithMinMax(1, options.size()));
return choice-1;
}
function<optional<bool>(const string &input)> parseYesNo() {
return [] (const string &input) {
string trimmed = input;
boost::algorithm::trim(trimmed);
if(trimmed == "Y" || trimmed == "y" || trimmed == "Yes" || trimmed == "yes") {
return optional<bool>(true);
} else if (trimmed == "N" || trimmed == "n" || trimmed == "No" || trimmed == "no") {
return optional<bool>(false);
} else {
return optional<bool>(none);
}
};
}
bool IOStreamConsole::askYesNo(const string &question) {
_output << question << "\n";
return _askForChoice("Your choice [y/n]: ", parseYesNo());
}
void IOStreamConsole::print(const std::string &output) {
_output << output << std::flush;
}

View File

@ -7,6 +7,7 @@
#include <iostream>
#include <boost/optional.hpp>
#include "../macros.h"
#include "../pointer/unique_ref.h"
namespace cpputils {
@ -14,27 +15,10 @@ class Console {
public:
virtual ~Console() {}
virtual unsigned int ask(const std::string &question, const std::vector<std::string> &options) = 0;
virtual bool askYesNo(const std::string &question) = 0;
virtual bool askYesNo(const std::string &question, bool defaultValue) = 0; // NoninteractiveConsole will just return the default value without asking the user.
virtual void print(const std::string &output) = 0;
};
class IOStreamConsole final: public Console {
public:
IOStreamConsole();
IOStreamConsole(std::ostream &output, std::istream &input);
unsigned int ask(const std::string &question, const std::vector<std::string> &options) override;
bool askYesNo(const std::string &question) override;
void print(const std::string &output) override;
private:
template<typename Return>
Return _askForChoice(const std::string &question, std::function<boost::optional<Return> (const std::string&)> parse);
std::ostream &_output;
std::istream &_input;
DISALLOW_COPY_AND_ASSIGN(IOStreamConsole);
};
}

View File

@ -0,0 +1,98 @@
#include "IOStreamConsole.h"
#include <boost/algorithm/string/trim.hpp>
using std::ostream;
using std::istream;
using std::string;
using std::vector;
using std::flush;
using std::function;
using boost::optional;
using boost::none;
namespace cpputils {
IOStreamConsole::IOStreamConsole(): IOStreamConsole(std::cout, std::cin) {
}
IOStreamConsole::IOStreamConsole(ostream &output, istream &input): _output(output), _input(input) {
}
optional<int> IOStreamConsole::_parseInt(const string &str) {
try {
string trimmed = str;
boost::algorithm::trim(trimmed);
int parsed = std::stoi(str);
if (std::to_string(parsed) != trimmed) {
return none;
}
return parsed;
} catch (const std::invalid_argument &e) {
return none;
} catch (const std::out_of_range &e) {
return none;
}
}
function<optional<unsigned int>(const string &input)> IOStreamConsole::_parseUIntWithMinMax(unsigned int min, unsigned int max) {
return [min, max] (const string &input) {
optional<int> parsed = _parseInt(input);
if (parsed == none) {
return optional<unsigned int>(none);
}
unsigned int value = static_cast<unsigned int>(*parsed);
if (value < min || value > max) {
return optional<unsigned int>(none);
}
return optional<unsigned int>(value);
};
}
template<typename Return>
Return IOStreamConsole::_askForChoice(const string &question, function<optional<Return> (const string&)> parse) {
optional<Return> choice = none;
do {
_output << question << flush;
string choiceStr;
getline(_input, choiceStr);
choice = parse(choiceStr);
} while(choice == none);
return *choice;
}
unsigned int IOStreamConsole::ask(const string &question, const vector<string> &options) {
if(options.size() == 0) {
throw std::invalid_argument("options should have at least one entry");
}
_output << question << "\n";
for (unsigned int i = 0; i < options.size(); ++i) {
_output << " [" << (i+1) << "] " << options[i] << "\n";
}
int choice = _askForChoice("Your choice [1-" + std::to_string(options.size()) + "]: ", _parseUIntWithMinMax(1, options.size()));
return choice-1;
}
function<optional<bool>(const string &input)> IOStreamConsole::_parseYesNo() {
return [] (const string &input) {
string trimmed = input;
boost::algorithm::trim(trimmed);
if(trimmed == "Y" || trimmed == "y" || trimmed == "Yes" || trimmed == "yes") {
return optional<bool>(true);
} else if (trimmed == "N" || trimmed == "n" || trimmed == "No" || trimmed == "no") {
return optional<bool>(false);
} else {
return optional<bool>(none);
}
};
}
bool IOStreamConsole::askYesNo(const string &question, bool /*defaultValue*/) {
_output << question << "\n";
return _askForChoice("Your choice [y/n]: ", _parseYesNo());
}
void IOStreamConsole::print(const std::string &output) {
_output << output << std::flush;
}
}

View File

@ -0,0 +1,30 @@
#pragma once
#ifndef MESSMER_CPPUTILS_IO_IOSTREAMCONSOLE_H
#define MESSMER_CPPUTILS_IO_IOSTREAMCONSOLE_H
#include "Console.h"
namespace cpputils {
class IOStreamConsole final: public Console {
public:
IOStreamConsole();
IOStreamConsole(std::ostream &output, std::istream &input);
unsigned int ask(const std::string &question, const std::vector<std::string> &options) override;
bool askYesNo(const std::string &question, bool defaultValue) override;
void print(const std::string &output) override;
private:
template<typename Return>
Return _askForChoice(const std::string &question, std::function<boost::optional<Return> (const std::string&)> parse);
static std::function<boost::optional<bool>(const std::string &input)> _parseYesNo();
static std::function<boost::optional<unsigned int>(const std::string &input)> _parseUIntWithMinMax(unsigned int min, unsigned int max);
static boost::optional<int> _parseInt(const std::string &str);
std::ostream &_output;
std::istream &_input;
DISALLOW_COPY_AND_ASSIGN(IOStreamConsole);
};
}
#endif

View File

@ -0,0 +1,23 @@
#include "NoninteractiveConsole.h"
using std::string;
using std::vector;
namespace cpputils {
NoninteractiveConsole::NoninteractiveConsole(unique_ref<Console> baseConsole): _baseConsole(std::move(baseConsole)) {
}
bool NoninteractiveConsole::askYesNo(const string &/*question*/, bool defaultValue) {
return defaultValue;
}
void NoninteractiveConsole::print(const std::string &output) {
_baseConsole->print(output);
}
unsigned int NoninteractiveConsole::ask(const string &/*question*/, const vector<string> &/*options*/) {
throw std::logic_error("Tried to ask a multiple choice question in noninteractive mode");
}
}

View File

@ -0,0 +1,25 @@
#pragma once
#ifndef MESSMER_CPPUTILS_IO_NONINTERACTIVECONSOLE_H
#define MESSMER_CPPUTILS_IO_NONINTERACTIVECONSOLE_H
#include "Console.h"
namespace cpputils {
//TODO Add test cases for NoninteractiveConsole
class NoninteractiveConsole final: public Console {
public:
NoninteractiveConsole(unique_ref<Console> baseConsole);
unsigned int ask(const std::string &question, const std::vector<std::string> &options) override;
bool askYesNo(const std::string &question, bool defaultValue) override;
void print(const std::string &output) override;
private:
unique_ref<Console> _baseConsole;
DISALLOW_COPY_AND_ASSIGN(NoninteractiveConsole);
};
}
#endif

View File

@ -22,6 +22,7 @@
#include "VersionChecker.h"
#include <gitversion/VersionCompare.h>
#include <cpp-utils/io/NoninteractiveConsole.h>
#include "Environment.h"
//TODO Fails with gpg-homedir in filesystem: gpg --homedir gpg-homedir --gen-key
@ -38,7 +39,7 @@ using program_options::ProgramOptions;
using cpputils::make_unique_ref;
using cpputils::Random;
using cpputils::IOStreamConsole;
using cpputils::NoninteractiveConsole;
using cpputils::TempFile;
using cpputils::RandomGenerator;
using cpputils::unique_ref;
@ -74,9 +75,14 @@ using gitversion::VersionCompare;
namespace cryfs {
Cli::Cli(RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, shared_ptr<Console> console, shared_ptr<HttpClient> httpClient):
_keyGenerator(keyGenerator), _scryptSettings(scryptSettings), _console(console), _httpClient(httpClient), _noninteractive(false) {
Cli::Cli(RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, unique_ref<Console> console, shared_ptr<HttpClient> httpClient):
_keyGenerator(keyGenerator), _scryptSettings(scryptSettings), _console(), _httpClient(httpClient), _noninteractive(false) {
_noninteractive = Environment::isNoninteractive();
if (_noninteractive) {
_console = make_shared<NoninteractiveConsole>(std::move(console));
} else {
_console = cpputils::to_unique_ptr(std::move(console));
}
}
void Cli::_showVersion() {
@ -212,12 +218,12 @@ namespace cryfs {
return CryConfigLoader(_console, _keyGenerator, _scryptSettings,
&Cli::_askPasswordNoninteractive,
&Cli::_askPasswordNoninteractive,
cipher, blocksizeBytes, _noninteractive).loadOrCreate(configFilePath);
cipher, blocksizeBytes).loadOrCreate(configFilePath);
} else {
return CryConfigLoader(_console, _keyGenerator, _scryptSettings,
&Cli::_askPasswordForExistingFilesystem,
&Cli::_askPasswordForNewFilesystem,
cipher, blocksizeBytes, _noninteractive).loadOrCreate(configFilePath);
cipher, blocksizeBytes).loadOrCreate(configFilePath);
}
}
@ -294,11 +300,7 @@ namespace cryfs {
void Cli::_checkDirAccessible(const bf::path &dir, const std::string &name) {
if (!bf::exists(dir)) {
if (_noninteractive) {
//If we use the noninteractive frontend, don't ask whether to create the directory, but just fail.
throw std::runtime_error(name + " not found");
}
bool create = _console->askYesNo("Could not find " + name + ". Do you want to create it?");
bool create = _console->askYesNo("Could not find " + name + ". Do you want to create it?", false);
if (create) {
if (!bf::create_directory(dir)) {
throw std::runtime_error("Error creating "+name);

View File

@ -15,7 +15,7 @@
namespace cryfs {
class Cli final {
public:
Cli(cpputils::RandomGenerator &keyGenerator, const cpputils::SCryptSettings &scryptSettings, std::shared_ptr<cpputils::Console> console, std::shared_ptr<cpputils::HttpClient> httpClient);
Cli(cpputils::RandomGenerator &keyGenerator, const cpputils::SCryptSettings &scryptSettings, cpputils::unique_ref<cpputils::Console> console, std::shared_ptr<cpputils::HttpClient> httpClient);
int main(int argc, const char *argv[]);
private:

View File

@ -2,19 +2,21 @@
#include <cpp-utils/random/Random.h>
#include <cpp-utils/crypto/kdf/Scrypt.h>
#include <cpp-utils/network/CurlHttpClient.h>
#include <cpp-utils/io/IOStreamConsole.h>
using namespace cryfs;
using cpputils::Random;
using cpputils::SCrypt;
using cpputils::CurlHttpClient;
using cpputils::make_unique_ref;
using cpputils::IOStreamConsole;
using std::make_shared;
using std::cerr;
using cpputils::IOStreamConsole;
int main(int argc, const char *argv[]) {
try {
auto &keyGenerator = Random::OSRandom();
return Cli(keyGenerator, SCrypt::DefaultSettings, make_shared<IOStreamConsole>(),
return Cli(keyGenerator, SCrypt::DefaultSettings, make_unique_ref<IOStreamConsole>(),
make_shared<CurlHttpClient>()).main(argc, argv);
} catch (const std::exception &e) {
cerr << "Error: " << e.what();

View File

@ -13,8 +13,8 @@ namespace cryfs {
constexpr const char *CryConfigConsole::DEFAULT_CIPHER;
constexpr uint32_t CryConfigConsole::DEFAULT_BLOCKSIZE_BYTES;
CryConfigConsole::CryConfigConsole(shared_ptr<Console> console, bool noninteractive)
: _console(std::move(console)), _useDefaultSettings(noninteractive ? optional<bool>(true) : none) {
CryConfigConsole::CryConfigConsole(shared_ptr<Console> console)
: _console(std::move(console)), _useDefaultSettings(none) {
}
string CryConfigConsole::askCipher() {
@ -43,7 +43,7 @@ namespace cryfs {
if (warning == none) {
return true;
}
return _console->askYesNo(string() + (*warning) + " Do you want to take this cipher nevertheless?");
return _console->askYesNo(string() + (*warning) + " Do you want to take this cipher nevertheless?", true);
}
uint32_t CryConfigConsole::askBlocksizeBytes() {
@ -70,7 +70,7 @@ namespace cryfs {
bool CryConfigConsole::_checkUseDefaultSettings() {
if (_useDefaultSettings == none) {
_useDefaultSettings = _console->askYesNo("Use default settings?");
_useDefaultSettings = _console->askYesNo("Use default settings?", true);
}
return *_useDefaultSettings;
}

View File

@ -9,7 +9,7 @@
namespace cryfs {
class CryConfigConsole final {
public:
CryConfigConsole(std::shared_ptr<cpputils::Console> console, bool noninteractive);
CryConfigConsole(std::shared_ptr<cpputils::Console> console);
CryConfigConsole(CryConfigConsole &&rhs) = default;
std::string askCipher();

View File

@ -15,8 +15,8 @@ using boost::none;
namespace cryfs {
CryConfigCreator::CryConfigCreator(shared_ptr<Console> console, RandomGenerator &encryptionKeyGenerator, bool noninteractive)
:_console(console), _configConsole(console, noninteractive), _encryptionKeyGenerator(encryptionKeyGenerator) {
CryConfigCreator::CryConfigCreator(shared_ptr<Console> console, RandomGenerator &encryptionKeyGenerator)
:_console(console), _configConsole(console), _encryptionKeyGenerator(encryptionKeyGenerator) {
}
CryConfig CryConfigCreator::create(const optional<string> &cipherFromCommandLine, const optional<uint32_t> &blocksizeBytesFromCommandLine) {

View File

@ -11,7 +11,7 @@
namespace cryfs {
class CryConfigCreator final {
public:
CryConfigCreator(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &encryptionKeyGenerator, bool noninteractive);
CryConfigCreator(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &encryptionKeyGenerator);
CryConfigCreator(CryConfigCreator &&rhs) = default;
CryConfig create(const boost::optional<std::string> &cipherFromCommandLine, const boost::optional<uint32_t> &blocksizeBytesFromCommandLine);

View File

@ -11,7 +11,6 @@ namespace bf = boost::filesystem;
using cpputils::unique_ref;
using cpputils::make_unique_ref;
using cpputils::Console;
using cpputils::IOStreamConsole;
using cpputils::Random;
using cpputils::RandomGenerator;
using cpputils::SCryptSettings;
@ -27,8 +26,8 @@ using namespace cpputils::logging;
namespace cryfs {
CryConfigLoader::CryConfigLoader(shared_ptr<Console> console, RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, function<string()> askPasswordForExistingFilesystem, function<string()> askPasswordForNewFilesystem, const optional<string> &cipherFromCommandLine, const boost::optional<uint32_t> &blocksizeBytesFromCommandLine, bool noninteractive)
: _console(console), _creator(console, keyGenerator, noninteractive), _scryptSettings(scryptSettings),
CryConfigLoader::CryConfigLoader(shared_ptr<Console> console, RandomGenerator &keyGenerator, const SCryptSettings &scryptSettings, function<string()> askPasswordForExistingFilesystem, function<string()> askPasswordForNewFilesystem, const optional<string> &cipherFromCommandLine, const boost::optional<uint32_t> &blocksizeBytesFromCommandLine)
: _console(console), _creator(console, keyGenerator), _scryptSettings(scryptSettings),
_askPasswordForExistingFilesystem(askPasswordForExistingFilesystem), _askPasswordForNewFilesystem(askPasswordForNewFilesystem),
_cipherFromCommandLine(cipherFromCommandLine), _blocksizeBytesFromCommandLine(blocksizeBytesFromCommandLine) {
}
@ -58,13 +57,13 @@ optional<CryConfigFile> CryConfigLoader::_loadConfig(const bf::path &filename) {
void CryConfigLoader::_checkVersion(const CryConfig &config) {
if (gitversion::VersionCompare::isOlderThan(gitversion::VersionString(), config.Version())) {
if (!_console->askYesNo("This filesystem is for CryFS " + config.Version() + " and should not be opened with older versions. It is strongly recommended to update your CryFS version. However, if you have backed up your base directory and know what you're doing, you can continue trying to load it. Do you want to continue?")) {
throw std::runtime_error("Not trying to load file system.");
if (!_console->askYesNo("This filesystem is for CryFS " + config.Version() + " and should not be opened with older versions. It is strongly recommended to update your CryFS version. However, if you have backed up your base directory and know what you're doing, you can continue trying to load it. Do you want to continue?", false)) {
throw std::runtime_error("This filesystem is for CryFS " + config.Version() + ". Please update your CryFS version.");
}
}
if (gitversion::VersionCompare::isOlderThan(config.Version(), gitversion::VersionString())) {
if (!_console->askYesNo("This filesystem is for CryFS " + config.Version() + ". It can be migrated to CryFS " + gitversion::VersionString() + ", but afterwards couldn't be opened anymore with older versions. Do you want to migrate it?")) {
throw std::runtime_error(string() + "Not migrating file system.");
if (!_console->askYesNo("This filesystem is for CryFS " + config.Version() + ". It can be migrated to CryFS " + gitversion::VersionString() + ", but afterwards couldn't be opened anymore with older versions. Do you want to migrate it?", false)) {
throw std::runtime_error("This filesystem is for CryFS " + config.Version() + ". It has to be migrated.");
}
}
}

View File

@ -13,7 +13,7 @@ namespace cryfs {
class CryConfigLoader final {
public:
CryConfigLoader(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &keyGenerator, const cpputils::SCryptSettings &scryptSettings, std::function<std::string()> askPasswordForExistingFilesystem, std::function<std::string()> askPasswordForNewFilesystem, const boost::optional<std::string> &cipherFromCommandLine, const boost::optional<uint32_t> &blocksizeBytesFromCommandLine, bool noninteractive);
CryConfigLoader(std::shared_ptr<cpputils::Console> console, cpputils::RandomGenerator &keyGenerator, const cpputils::SCryptSettings &scryptSettings, std::function<std::string()> askPasswordForExistingFilesystem, std::function<std::string()> askPasswordForNewFilesystem, const boost::optional<std::string> &cipherFromCommandLine, const boost::optional<uint32_t> &blocksizeBytesFromCommandLine);
CryConfigLoader(CryConfigLoader &&rhs) = default;
boost::optional<CryConfigFile> loadOrCreate(const boost::filesystem::path &filename);