// // Created by Tristan Juricek on 9/19/15. // #include <algorithm> #include <iostream> #include <map> #include <memory> #include <mutex> #include <p4/clientapi.h> #include <pwd.h> #include <set> #include <string> #include <sstream> #include <sys/types.h> #include <unistd.h> #include <uuid/uuid.h> #include <vector> #include "Timer.h" #include "Workspace.h" using std::back_inserter; using std::chrono::system_clock; using std::chrono::milliseconds; using std::cout; using std::cerr; using std::endl; using std::find; using std::map; using std::mutex; using std::set; using std::shared_ptr; using std::string; using std::stringstream; using std::transform; using std::vector; class ServerMessage { public: string text; int severity; int code; ServerMessage() = default; ServerMessage(const Error *err) { StrBuf strBuf; err->Fmt(&strBuf); text = strBuf.Text(); severity = err->GetSeverity(); code = err->GetGeneric(); } }; //---------------------------------------------------------------------------- // CommandResult //---------------------------------------------------------------------------- // Encapsulates both the return from a command, and the collected results. // class CommandResult { public: CommandResult(const vector<ServerMessage> & messages, const vector<map<string, string>> & results) : _messages(messages), _results(results) { } ~CommandResult() = default; bool hasError() const { for (ServerMessage m : _messages) { if (m.severity >= E_FAILED) return true; } return false; } const vector<ServerMessage> & messages() const { return _messages; } const vector<map<string, string>> & results() const { return _results; } int processReturn() const { if (hasError()) { for (ServerMessage message : _messages) { cerr << message.code << ": " << message.text << endl; } return Workspace::COMMAND_FAILURE; } return 0; } void log() const { if (!_messages.empty()) { cout << "messages:" << endl; for (ServerMessage message : _messages) { cout << '\t' << message.code << ": " << message.text << endl; } } if (!_results.empty()) { for (auto result : _results) { cout << "{" << endl; for (auto entry : result) { cout << " " << entry.first << " -> " << entry.second << endl; } cout << "}" << endl; } } } private: vector<ServerMessage> _messages; vector<map<string, string>> _results; }; //---------------------------------------------------------------------------- // CachingClientUser //---------------------------------------------------------------------------- // Handles converting much of the output from the ClientApi to something // less ... attached... to the p4api code. class CachingClientUser : public ClientUser { public: CachingClientUser() = default; ~CachingClientUser() = default; virtual void OutputStat(StrDict *varList) override; virtual void Message(Error *err) override; const vector<map<string, string>> & results() const { return _results; }; const vector<ServerMessage> & messages() const { return _messages; } void clear() { _results.clear(); _messages.clear(); } private: vector<map<string, string>> _results; vector<ServerMessage> _messages; }; void CachingClientUser::OutputStat(StrDict *varList) { StrRef var, val; map<string, string> result; for (int index = 0; varList->GetVar(index, var, val); ++index) { if (var == "specdef" || var == "func" || var == "specFormatted") continue; result[var.Text()] = val.Text(); } _results.push_back(result); } void CachingClientUser::Message(Error *err) { _messages.push_back(ServerMessage(err)); } //---------------------------------------------------------------------------- // ClientApiHandle //---------------------------------------------------------------------------- // Use RAII around connecting and disconnecting to the Perforce server. // We do not handle re-using connections just yet. // // Since the ClientApi is rather verbose, this contains some high-level methods // to collate data. class ClientApiHandle { public: static ClientApiHandle *create(string workspace); ClientApiHandle(); ~ClientApiHandle(); const ClientApi & api() const { return *_api; } ClientApi & api() { return *_api; } // High-level method to execute the command with optional args and return // results. template<typename... Strings> CommandResult run(const string & cmd, Strings...args); CommandResult run(const string & cmd, vector<const char *> argv); CommandResult run(const string & cmd, vector<string> argv); bool isConnected() const { return _connected; } void setConnected(bool c) { _connected = c; } private: CachingClientUser *_user; ClientApi *_api; bool _connected; }; // This will set the context of the ClientApi to the workspace path. // It's assumed that the workspace path has a .p4config file set up with // proper connection settings, etc, for you to commence work. ClientApiHandle * ClientApiHandle::create(string workspace) { ClientApiHandle *handle = new ClientApiHandle; handle->api().SetCwd(workspace.c_str()); // Setting the ticket file explicitly to the home directory, mostly because // it's our current default convention. // // TODO There has to be a better way to figure out the ticket file string ticketFile; struct passwd *pwdinfo = getpwuid(getuid()); if (pwdinfo) { ticketFile = pwdinfo->pw_dir; } ticketFile = ticketFile + "/.p4tickets"; cout << "Setting ticket file " << ticketFile << endl; handle->api().SetTicketFile(ticketFile.c_str()); return handle; } ClientApiHandle::ClientApiHandle() : _user(new CachingClientUser), _api(new ClientApi), _connected(false) { } ClientApiHandle::~ClientApiHandle() { if (_connected) { Error e; _api->Final(&e); if (e.IsError()) { StrBuf strBuf; e.Fmt(&strBuf); cerr << "ClientApi::Final failed: " << strBuf.Text() << endl; } } delete _api; delete _user; } void collect(vector<const char *> & v) { // Do nothing } void collect(vector<const char *> & v, const string & a) { v.push_back(a.c_str()); } template<typename... Strings> void collect(vector<const char *> & v, const string & a, Strings...rest) { v.push_back(a.c_str()); return collect(v, rest...); } template<typename... Strings> CommandResult ClientApiHandle::run(const string & cmd, Strings... args) { vector<const char *> argv; collect(argv, args...); return run(cmd, argv); } CommandResult ClientApiHandle::run(const string & cmd, vector<const char *> argv) { _user->clear(); if (!argv.empty()) { _api->SetArgv(argv.size(), const_cast<char **>(argv.data())); } _api->Run(cmd.c_str(), _user); return CommandResult(_user->messages(), _user->results()); } CommandResult ClientApiHandle::run(const string & cmd, vector<string> argv) { vector<const char *> chargv; transform(argv.begin(), argv.end(), back_inserter(chargv), [](string & s) -> const char * { return s.c_str(); }); return run(cmd, chargv); } // Will call "p4 opened" and then pass those depot paths to "p4 where" int loadLocalOpenedPaths(ClientApiHandle & handle, vector<string> & paths) { CommandResult openedResult = handle.run("opened"); if (openedResult.hasError()) return openedResult.processReturn(); // If there's no results, there's really nothing to do if (openedResult.results().empty()) return 0; vector<string> depotPaths; transform(openedResult.results().begin(), openedResult.results().end(), back_inserter(depotPaths), [](const map<string, string> & m) -> string { return m.at("depotFile"); }); CommandResult whereResult = handle.run("where", depotPaths); if (whereResult.hasError()) return whereResult.processReturn(); transform(whereResult.results().begin(), whereResult.results().end(), back_inserter(paths), [](const map<string, string> & m) -> string { return m.at("path"); }); return 0; } //---------------------------------------------------------------------------- // WorkspaceImpl //---------------------------------------------------------------------------- // We use a PIMPL technique on this class to ensure use of the P4API is // restricted to *only* this file. class WorkspaceImpl { public: WorkspaceImpl(Workspace *parent); ~WorkspaceImpl() = default; // Use this specific changelist number instead of the default changelist // for calls to p4 reconcile. void setChangelist(const std::string & c); // Use this specific changelist number instead of the default changelist // for calls to p4 reconcile. const std::string & changelist() const; // The time we wait from the last file operation to trigger p4 reconcile. int pauseAmount() const; // The time we wait from the last file operation to trigger p4 reconcile. void setPauseAmount(int t); // If true, the workspace is churning bool processing() const; // Debugging status for the last operations std::string status(); void setStatus(const std::string & s); void enqueue(const Notification & n); template<typename Lambda> int execute(Lambda lambda); private: // Methods that should never ever see the light of day void startCheckTimer(); void check(); void startProcessTimer(); void process(); private: Workspace *_parent; string _changelist; int _pauseAmount; Timer _timer; mutex _statusMutex; string _status; Timer::timer_id _checkTimer; Timer::timer_id _processTimer; // We maintain a simple vector of Notification instances, and lock access // when we attempt to add or clear the vector. mutex _notificationMutex; vector<Notification> _notifications; }; WorkspaceImpl::WorkspaceImpl(Workspace *parent) : _parent(parent), _changelist(), _pauseAmount(500), _timer(), _statusMutex(), _status("ok"), _checkTimer(-1), _processTimer(-1), _notificationMutex(), _notifications() { } void WorkspaceImpl::setChangelist(const std::string & c) { _changelist = c; } const std::string & WorkspaceImpl::changelist() const { return _changelist; } int WorkspaceImpl::pauseAmount() const { return _pauseAmount; } void WorkspaceImpl::setPauseAmount(int t) { _pauseAmount = t; } bool WorkspaceImpl::processing() const { return _checkTimer != -1 && _processTimer != -1; } std::string WorkspaceImpl::status() { _statusMutex.lock(); string s = _status; _statusMutex.unlock(); return s; } void WorkspaceImpl::setStatus(const std::string & s) { _statusMutex.lock(); _status = s; _statusMutex.unlock(); } void WorkspaceImpl::enqueue(const Notification & n) { _notificationMutex.lock(); _notifications.push_back(n); _notificationMutex.unlock(); if (!processing()) startCheckTimer(); } void WorkspaceImpl::startCheckTimer() { _checkTimer = _timer.create(pauseAmount(), 0, [this]() { this->check(); }); } void WorkspaceImpl::check() { _checkTimer = -1; _notificationMutex.lock(); if ((_notifications.back().time + milliseconds(_pauseAmount)) < system_clock::now()) { _notifications.clear(); startProcessTimer(); } else { // Come back later startCheckTimer(); } _notificationMutex.unlock(); } void WorkspaceImpl::startProcessTimer() { _processTimer = _timer.create(0, 0, [this]() { this->process(); }); } void WorkspaceImpl::process() { execute([this](ClientApiHandle & handle) { CommandResult result = handle.run("reconcile"); if (result.hasError()) { stringstream ss; for (auto message : result.messages()) { ss << message.text << endl; } setStatus(ss.str()); } else { setStatus("ok"); } return 0; }); _processTimer = -1; } // Basically handles connecting and disconnecting the ClientApiHandle, which // creates our initial approach to connection management. // // The Lambda should be a function that returns an int and takes the // ClientApiHandle as a reference. template<typename Lambda> int WorkspaceImpl::execute(Lambda lambda) { shared_ptr<ClientApiHandle> apiHandle( ClientApiHandle::create(_parent->workspace())); apiHandle->api().SetProtocol("tag", ""); Error e; apiHandle->api().Init(&e); if (e.Test()) { StrBuf strBuf; e.Fmt(&strBuf); cerr << strBuf.Text() << endl; return Workspace::CONNECTION_FAILURE; } apiHandle->setConnected(true); return lambda(*apiHandle); } //---------------------------------------------------------------------------- // Workspace //---------------------------------------------------------------------------- Workspace::~Workspace() { delete _impl; } Workspace::Workspace() : _workspace(), _impl(new WorkspaceImpl(this)) { } Workspace::Workspace(const std::string & workspace) : _workspace(workspace), _impl(new WorkspaceImpl(this)) { } std::string Workspace::path(const std::string & p) const { // This actually works. FUSE just sends in paths that always start with "/". return _workspace + p; } void Workspace::setChangelist(const std::string & c) { _impl->setChangelist(c); } const std::string & Workspace::changelist() const { return _impl->changelist(); } int Workspace::pauseAmount() const { return _impl->pauseAmount(); } void Workspace::setPauseAmount(int t) { _impl->setPauseAmount(t); } bool Workspace::processing() const { return _impl->processing(); } std::string Workspace::status() { return _impl->status(); } void Workspace::enqueue(const Notification & n) { _impl->enqueue(n); }
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#3 | 16236 | tjuricek |
Revise FUSE-client to call p4 reconcile intelligently. This uses the main FUSE callbacks like a loopback with a notification mechanism. After no real disk access after a short period of time (like 500ms) we'll trigger a call to p4 reconcile. The "interface" to this application is currently just a file handle: /.status - Lists "ok" if there's no errors, otherwise, outputs a list of messages |
||
#2 | 16208 | tjuricek |
Naive implementation that adds guesses at rename and unlink abilities. It's becoming *very* apparent that we need a different approach to handling file system events. We likely need to create a log of "here's what I've done" and then after a certain period of time trigger a system that allows you to possibly just reconcile the changes. Matching filesystem calls to p4 commands ends up with *a lot* of p4 commands. |
||
#1 | 16129 | tjuricek |
Rename/move files again... this time to the hyphenated-approach. |
||
//guest/tjuricek/file_system_client/main/Workspace.cpp | |||||
#1 | 16119 | tjuricek | Rename/move to meet workshop project conventions. | ||
//guest/tjuricek/fsclient/Workspace.cpp | |||||
#1 | 16118 | tjuricek |
FSClient initial version: handles add, edit This is a proof-of-concept app that mirrors an existing Perforce workspace to handle running commands like "p4 add" and "p4 edit" automatically when your apps add and write files. See the readme for more information. |