Skip to content

Commit

Permalink
Merge pull request #10 from adam-p/accounts-rc
Browse files Browse the repository at this point in the history
Implement PsiCash accounts
  • Loading branch information
adam-p authored Aug 9, 2021
2 parents 22fef8d + 06c17ed commit e63f18f
Show file tree
Hide file tree
Showing 26 changed files with 28,293 additions and 11,152 deletions.
6 changes: 4 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

cmake_minimum_required(VERSION 3.4.1)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD 20)

add_definitions(-DTESTING)

Expand All @@ -15,6 +15,8 @@ list(FILTER TEST_SOURCES INCLUDE REGEX "(.*_test\\.cpp)|(test_.*\\.cpp)$")
#message("TEST_SOURCES: ${TEST_SOURCES}")

if(MSVC)
add_compile_options(/std:c++latest)

# From https://stackoverflow.com/questions/10113017/setting-the-msvc-runtime-in-cmake
# Default to statically-linked runtime.
if("${MSVC_RUNTIME}" STREQUAL "")
Expand Down Expand Up @@ -58,7 +60,6 @@ add_library( # Sets the name of the library.
# Provides a relative path to your source file(s).
${SOURCES} )

# TODO: Coverage stuff should not be done unconditionally
SET(GCC_COVERAGE_COMPILE_FLAGS "-Wall -fprofile-arcs -ftest-coverage -g -O0")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GCC_COVERAGE_COMPILE_FLAGS}")

Expand Down Expand Up @@ -105,5 +106,6 @@ add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src
# Now simply link against gtest or gtest_main or gmock_main as needed.
add_executable(runUnitTests ${TEST_SOURCES})
target_link_libraries(runUnitTests gmock_main)
target_link_libraries(runUnitTests ssl crypto)
target_link_libraries(runUnitTests psicash)
#add_test(NAME example_test COMMAND runUnitTests)
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This library relies on the native environment to provide an HTTP request callbac

There is an _example_ implementation in the Android wrapper project. But note that it _does not support proxied requests_, which may be necessary depending on the environment. (E.g., it probably doesn't matter on iOS, since our app only supported full-device VPN. But on Windows the app mostly uses a local proxy, so the HTTP Requester must support proxying.)

Note that the requester _must_ do HTTPS certificate validation.

### Thread Safety

All datastore reads and writes are mutexed. So, the only consistency guarantee is for individual data accesses. This means that multiple data accesses might get data from different states; for example, between getting the balance and getting the purchases list, there might have been a purchase, which would alter the balance. We will state this in positive terms as: "you will always get the very latest value". This is fine for our use cases at this time, but we might want to add "get a consistent set of data" in the future.
Expand All @@ -34,3 +36,38 @@ If Android Studio is saying that a new `.cpp` file is not part of the project, g
If you get a `SIGABRT` error in JNI code: You have probably triggered a JNI exception (that hasn't been cleared). It's possible that it's expected or acceptable and you just need to clear it, but it's more likely that it's a bug.

If you get a `SIGSEGV` error when hitting a breakpoint in JNI code: Yeah, beats me. I get it on MacOS but not Windows.

## State diagram

```
+-----------+
| |
| no tokens +<----------------------------+
| | |
+-----+-----+ |
| |
+ |
RefreshState+NewTracker |
+ |
| |
v |
+--------+--------+ +
| | ResetUser()
| Tracker tokens | +
| | |
+--------+--------+ |
| |
+ |
Login |
+ |
v +-----+------------------------+
+--------+--------+ token expiry | |
| +------------------->+ IsAccount() && !HasTokens() |
| Account tokens | | |
| +------------------->+ "logged out" |
+--------------+--+ AccountLogout() | |
^ +--+---------------------------+
| |
+--------------------------+
AccountLogin()
```
11 changes: 7 additions & 4 deletions build-windows.bat
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ chdir build


REM Make the MSVC project
cmake -G "Visual Studio 14 2015" ..
cmake -G "Visual Studio 16 2019" -A Win32 ..
if "%ERRORLEVEL%" == "1" exit /B 1

REM Build for Debug and MinSizeRel
call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin\vcvars32.bat"
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat"
if "%ERRORLEVEL%" == "1" exit /B 1
msbuild.exe -p:Configuration=Debug -p:PlatformToolset=v140_xp -p:PreferredToolArchitecture=x86 psicash.vcxproj
msbuild.exe -p:Configuration=Debug -p:PlatformToolset=v140 -p:PreferredToolArchitecture=x86 -p:Platform=x86 -p:PlatformTarget=x86 psicash.vcxproj
if "%ERRORLEVEL%" == "1" exit /B 1
msbuild.exe -p:Configuration=MinSizeRel -p:PlatformToolset=v140_xp -p:PreferredToolArchitecture=x86 psicash.vcxproj
msbuild.exe -p:Configuration=MinSizeRel -p:PlatformToolset=v140 -p:PreferredToolArchitecture=x86 -p:Platform=x86 -p:PlatformTarget=x86 psicash.vcxproj
if "%ERRORLEVEL%" == "1" exit /B 1
REM Resulting libs (and pdb) are in build/Debug and build/MinSizeRel

Expand All @@ -32,3 +32,6 @@ REM TODO: put exported include files into an "include" directory and modify buil
robocopy /V . dist-windows datetime.hpp error.hpp url.hpp psicash.hpp
robocopy /V vendor\ dist-windows\vendor /S
git describe --always --long --dirty --tags > dist-windows/git.txt

del /F /S /Q build 1>nul
rmdir /S /Q build
56 changes: 42 additions & 14 deletions datastore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@

using json = nlohmann::json;

namespace psicash {

using namespace std;
using namespace psicash;
using namespace error;

static string FilePath(const string& file_root, const string& suffix);
Expand All @@ -52,46 +53,63 @@ Error Datastore::Init(const string& file_root, const string& suffix) {

#define MUST_BE_INITIALIZED if (!initialized_) { return MakeCriticalError("must only be called on an initialized datastore"); }

Error Datastore::Clear(const string& file_path) {
Error Datastore::Reset(const string& file_path, json new_value) {
SYNCHRONIZE(mutex_);
paused_ = false;
auto empty_json = json::object();
if (auto err = FileStore(paused_, file_path, empty_json)) {
if (auto err = FileStore(paused_, file_path, new_value)) {
return PassError(err);
}
json_ = empty_json;
json_ = new_value;
return error::nullerr;
}

Error Datastore::Clear(const string& file_root, const string& suffix) {
return PassError(Clear(FilePath(file_root, suffix)));
Error Datastore::Reset(const string& file_root, const string& suffix, json new_value) {
return PassError(Reset(FilePath(file_root, suffix), new_value));
}

Error Datastore::Clear() {
Error Datastore::Reset(json new_value) {
SYNCHRONIZE(mutex_);
MUST_BE_INITIALIZED;
return PassError(Clear(file_path_));
return PassError(Reset(file_path_, new_value));
}

void Datastore::PauseWrites() {
bool Datastore::PauseWrites() {
SYNCHRONIZE(mutex_);
auto was_paused = paused_;
paused_ = true;
return !was_paused;
}

Error Datastore::UnpauseWrites() {
Error Datastore::UnpauseWrites(bool commit) {
SYNCHRONIZE(mutex_);
MUST_BE_INITIALIZED;
if (!paused_) {
return nullerr;
}
paused_ = false;
return PassError(FileStore(paused_, file_path_, json_));
if (commit) {
return PassError(FileStore(paused_, file_path_, json_));
}

// Revert to what's on disk
auto res = FileLoad(file_path_);
if (!res) {
return PassError(res.error());
}
json_ = *res;
return nullerr;
}

error::Result<nlohmann::json> Datastore::Get() const {
SYNCHRONIZE(mutex_);
MUST_BE_INITIALIZED;
return json_;
}

Error Datastore::Set(const json& in) {
Error Datastore::Set(const json::json_pointer& p, json v) {
SYNCHRONIZE(mutex_);
MUST_BE_INITIALIZED;
json_.update(in);
json_[p] = v;
return PassError(FileStore(paused_, file_path_, json_));
}

Expand Down Expand Up @@ -143,6 +161,14 @@ static Result<json> FileLoad(const string& file_path) {
// We'll continue on with the rest of the logic, which will read the new empty file.
}

uint64_t file_size = 0;
if (auto err = utils::FileSize(file_path, file_size)) {
return WrapError(err, utils::Stringer("unable to get file size; errno=", errno));
}
if (file_size == 0) {
return MakeCriticalError("file size is zero");
}

ifstream f;
f.open(file_path, ios::in | ios::binary);
if (!f) {
Expand Down Expand Up @@ -214,3 +240,5 @@ static Error FileStore(bool paused, const string& file_path, const json& json) {

return nullerr;
}

} // namespace psicash
42 changes: 24 additions & 18 deletions datastore.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,60 +50,66 @@ class Datastore {
/// Returns false if there's an unrecoverable error (such as an inability to use the filesystem).
error::Error Init(const std::string& file_root, const std::string& suffix);

/// Clears the in-memory structure and the persistent file.
/// Resets the in-memory structure and the persistent file, setting it to `new_value`
/// (which may be an empty object).
/// Calling this does not change the initialized state. If the datastore was already
/// initialized with a different file_root+suffix, then the result is undefined.
error::Error Clear(const std::string& file_root, const std::string& suffix);
error::Error Reset(const std::string& file_root, const std::string& suffix, json new_value);

/// Clears the in-memory structure and the persistent file.
/// Reset the in-memory structure and the persistent file, setting it to `new_value`
/// (which may be an empty object).
/// Calling this does not change the initialized state.
/// Init() must have already been called, successfully.
error::Error Clear();
error::Error Reset(json new_value);

/// Stops writing of updates to disk until UnpauseWrites is called.
void PauseWrites();
/// Unpauses writing and causes an immediate write.
error::Error UnpauseWrites();
/// Returns false if writing was already paused (so this call did nothing).
bool PauseWrites();
/// Unpauses writing. If commit is true, it writes the changes immediately; if false
/// it discards the changes.
error::Error UnpauseWrites(bool commit);

/// Returns the value, or an error indicating the failure reason.
template<typename T>
nonstd::expected<T, DatastoreGetError> Get(const char* key) const {
nonstd::expected<T, DatastoreGetError> Get(const json::json_pointer& p) const {
try {
// Not returning inside the synchronize block to avoid compiler warning about
// "control reached end of non-void function without returning a value".
T val;
SYNCHRONIZE_BLOCK(mutex_) {
// Not using MUST_BE_INITIALIZED so we don't need it in the header.
if (!initialized_) {
return nonstd::make_unexpected(kDatastoreUninitialized);
}
if (json_.find(key) == json_.end()) {

if (p.empty() || !json_.contains(p)) {
return nonstd::make_unexpected(kNotFound);
}

val = json_[key].get<T>();
val = json_.at(p).get<T>();
}
return val;
}
catch (json::type_error& e) {
catch (json::type_error&) {
return nonstd::make_unexpected(kTypeMismatch);
}
catch (json::out_of_range& e) {
catch (json::out_of_range&) {
// This should be avoided by the explicit check above. But we'll be safe.
return nonstd::make_unexpected(kNotFound);
}
}

/// To set a single key-value: `set({{"k1", "v1"}})`.
/// To set multiple key-values: `set({{"k1", "v1"}, {"k2", "v2"}})`.
/// NOTE: If you use too few curly braces, you'll accidentally create arrays instead of objects.
error::Result<nlohmann::json> Get() const;

// Sets the value v in the datastore at path p.
/// NOTE: Set is not atomic. If the file operation fails, the intermediate object will still be
/// updated. We may want this to be otherwise in the future, but for now I think that it's preferable.
/// Returns false if the file operation failed.
error::Error Set(const json& in);
error::Error Set(const json::json_pointer& p, json v);

protected:
/// Helper for the public Clear methods
error::Error Clear(const std::string& file_path);
/// Helper for the public Reset methods
error::Error Reset(const std::string& file_path, json new_value);

private:
mutable std::recursive_mutex mutex_;
Expand Down
Loading

0 comments on commit e63f18f

Please sign in to comment.