aboutsummaryrefslogtreecommitdiff
path: root/src/tracking/change_tracker.cc
blob: c966c043325e67d6921b5a48fa47f842dd4e9d79 (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
133
134
135
#include "change_tracker.h"

#include <boost/process.hpp>
#include <boost/filesystem.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 get_default_context() {
	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 get_diff_command(
	const std::string& diff_cmd, const std::string& file_path) {
	return diff_cmd +  " --label " + file_path + " - " + file_path;
}

void read_file_to_stream(
	const boost::filesystem::path& file_path,
	std::stringstream* const       stream
) {
	try {
		boost::filesystem::ifstream file(file_path);

		if ( file.is_open() ) {
			*stream << file.rdbuf();
			stream->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.
	}
}

}

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(
					get_diff_command(this->diff_cmd_, tracked_path),
					get_default_context()
				)
			};

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

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

			diffProcess.wait();
		}
	}
}

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

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

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

// _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.
//
void ChangeTracker::track(const std::string& file_path) {
	// May throw filesystem exceptions if the given path doesn't exist. We are
	// able to disregard this as this function should only be called when
	// existance is guaranteed. If this is not the case we want the library to
	// fail visibly.
	//
	const auto full_path = boost::filesystem::canonical(file_path);

	if ( !this->is_tracked(full_path) ) {
		auto result = this->create_child(full_path);

		if ( std::get<EMPLACE_SUCCESS>(result) ) {
			read_file_to_stream(
				full_path,
				std::get<FILE_CONTENT>(*result.first).get()
			);
		}
	}
}

}