aboutsummaryrefslogtreecommitdiff
path: root/src/tracking/change_tracker.cc
blob: 0feae34ce33d3cb5112563737b74305588ac1c4d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include "change_tracker.h"

#include <boost/filesystem.hpp>
#include <boost/filesystem/fstream.hpp>

namespace {

// constants for increasing pair access readability
constexpr unsigned int EMPLACE_SUCCESS = 1;
constexpr unsigned int FILE_PATH       = 0;
constexpr unsigned int FILE_CONTENT    = 1;

boost::process::context getDefaultContext() {
	boost::process::context context;

	context.environment     = boost::process::self::get_environment();
	context.stdout_behavior = boost::process::capture_stream();
	context.stdin_behavior  = boost::process::capture_stream();

	return context;
}

// Build `diff` command to be used to display the tracked changes in a
// human readable fashion.
//
// As the original file contents are read by this library and forwarded
// to `diff` via its standard input the command has to be structured as
// follows:
//
//   diff -u --label $file_path - $file_path
//
std::string getDiffCommand(
	const std::string& diff_cmd, const std::string& file_path) {
	return diff_cmd +  " --label " + file_path + " - " + file_path;
}

}

namespace tracking {

ChangeTracker::ChangeTracker(utility::Logger* logger, const std::string& diff_cmd):
	diff_cmd_(diff_cmd),
	logger_(logger),
	children_() { }

ChangeTracker::ChangeTracker(utility::Logger* logger):
	ChangeTracker(logger, "diff -u") { }

ChangeTracker::~ChangeTracker() {
	for ( auto&& tracked : this->children_ ) {
		const auto& tracked_path = std::get<FILE_PATH>(tracked);

		if ( boost::filesystem::exists(tracked_path) ) {
			boost::process::child diffProcess{
				boost::process::launch_shell(
					getDiffCommand(this->diff_cmd_, tracked_path),
					getDefaultContext()
				)
			};

			diffProcess.get_stdin() << std::get<FILE_CONTENT>(tracked)->rdbuf();
			diffProcess.get_stdin().close();

			this->logger_->forward(diffProcess.get_stdout());

			diffProcess.wait();
		}
	}
}

// Both `is_tracked` and `create_child` may throw filesystem exceptions
// if the given path doesn't exist. We are able to disregard this as
// both functions are only and should only be called when existance is
// guaranteed. If this is not the case we want the library to fail visibly.

bool ChangeTracker::is_tracked(const std::string& file_path) const {
	return this->children_.find(
		boost::filesystem::canonical(file_path).string()
	) != this->children_.end();
}

auto ChangeTracker::create_child(const std::string& file_path) {
	std::lock_guard<std::mutex> guard(this->write_mutex_);

	return this->children_.emplace(
		boost::filesystem::canonical(file_path).string(),
		std::make_unique<std::stringstream>()
	);
}

// Begins tracking changes to a file reachable by a given path
//
// _Tracking_ consists of adding the full file path to the `children_`
// map and reading the full pre-change file contents into the mapped
// `std::stringstream` instance.
//
// This seems to be the most workable of various tested approaches
// to providing `diff` with proper input in all circumstances. Leaving
// the intermediate preservation of the original file content to
// `diff` sadly failed randomly as it is _too intelligent_ in actually
// reading the file from permanent storage. e.g. it opens all relevant
// file descriptors at launch and only reads the first block of the
// file contents until more is required. This leads to problems when
// the tracked file is modified after `diff` has been spawned.
//
bool ChangeTracker::track(const std::string& file_path) {
	auto result = this->create_child(file_path);

	if ( std::get<EMPLACE_SUCCESS>(result) ) {
		try {
			boost::filesystem::ifstream file(
				std::get<FILE_PATH>(*result.first)
			);

			if ( file.is_open() ) {
				*std::get<FILE_CONTENT>(*result.first) << file.rdbuf();
				std::get<FILE_CONTENT>(*result.first)->sync();
			}
		} catch ( boost::filesystem::filesystem_error& ) {
			// we catch this exception in case the parent process is
			// performing system calls that are allowed to fail - e.g.
			// in this instance writing to files that we are not allowed
			// to read. 
		}

		return true;
	} else {
		return false;
	}
}

}