Files
textmate/Applications/TextMate/src/RMateServer.cc
Allan Odgaard b139ac5097 Change push_back → emplace_back (C++11)
This is mainly motivated by readability, so I only did a few select replacements.
2013-02-08 11:20:35 +01:00

607 lines
16 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <oak/oak.h>
#include <text/parse.h>
#include <text/hexdump.h>
#include <document/collection.h>
#include <oak/debug.h>
#include <authorization/authorization.h>
#include <io/io.h>
#include <OakAppKit/IOAlertPanel.h>
OAK_DEBUG_VAR(RMateServer);
/*
open
path: [«path»|-]
real-path: «path»
token: «string»
display-name: «string»
selection: «line»[:«column»][-«line»[:«column»]]
file-type: «scope»
project-uuid: «uuid»
add-to-recents: «boolean»
re-activate: «boolean»
authorization: «blob»
wait: «boolean»
data-on-save: «boolean»
data-on-close: «boolean»
data: «integer»
data: «integer»
*/
// ============================
// = Socket run loop callback =
// ============================
struct socket_callback_t
{
WATCH_LEAKS(socket_callback_t);
template <typename F>
socket_callback_t (F f, socket_t const& fd)
{
D(DBF_RMateServer, bug("%p, %d\n", this, (int)fd););
helper.reset(new helper_t<F>(f, fd, this));
CFSocketContext const context = { 0, helper.get(), NULL, NULL, NULL };
socket = CFSocketCreateWithNative(kCFAllocatorDefault, fd, kCFSocketReadCallBack, callback, &context);
CFSocketSetSocketFlags(socket, CFSocketGetSocketFlags(socket) & ~kCFSocketCloseOnInvalidate);
run_loop_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), run_loop_source, kCFRunLoopDefaultMode);
}
~socket_callback_t ()
{
D(DBF_RMateServer, bug("%p\n", this););
ASSERT(CFRunLoopContainsSource(CFRunLoopGetCurrent(), run_loop_source, kCFRunLoopDefaultMode));
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source, kCFRunLoopDefaultMode);
CFSocketInvalidate(socket);
CFRelease(run_loop_source);
CFRelease(socket);
}
static void callback (CFSocketRef s, CFSocketCallBackType callbackType, CFDataRef address, void const* data, void* info)
{
(*(helper_base_t*)info)();
}
private:
struct helper_base_t
{
WATCH_LEAKS(helper_base_t);
virtual ~helper_base_t () { }
virtual void operator() () = 0;
};
template <typename F>
struct helper_t : helper_base_t
{
helper_t (F f, socket_t const& socket, socket_callback_t* parent) : f(f), socket(socket), parent(parent) { }
void operator() () { if(!f(socket)) delete parent; }
private:
F f;
socket_t socket;
socket_callback_t* parent;
};
typedef std::shared_ptr<helper_base_t> helper_ptr;
helper_ptr helper;
CFSocketRef socket;
CFRunLoopSourceRef run_loop_source;
};
typedef std::shared_ptr<socket_callback_t> socket_callback_ptr;
// ======================
// = Return system info =
// ======================
static std::string sys_info (int field)
{
char buf[1024];
size_t bufSize = sizeof(buf);
int request[] = { CTL_KERN, field };
if(sysctl(request, sizeofA(request), buf, &bufSize, NULL, 0) != -1)
return std::string(buf, buf + bufSize - 1);
return "?";
}
static bool rmate_connection_handler_t (socket_t const& socket);
namespace
{
static char const* socket_path ()
{
static std::string const str = text::format("/tmp/textmate-%d.sock", getuid());
return str.c_str();
}
struct mate_server_t
{
mate_server_t ()
{
_socket_path = socket_path();
D(DBF_RMateServer, bug("%s\n", _socket_path););
if(unlink(_socket_path) == -1 && errno != ENOENT)
{
OakRunIOAlertPanel("Unable to delete socket left from old instance:\n%s", _socket_path);
return;
}
socket_t fd(socket(AF_UNIX, SOCK_STREAM, 0));
fcntl(fd, F_SETFD, FD_CLOEXEC);
struct sockaddr_un addr = { 0, AF_UNIX };
strcpy(addr.sun_path, _socket_path);
addr.sun_len = SUN_LEN(&addr);
if(bind(fd, (sockaddr*)&addr, sizeof(addr)) == -1)
OakRunIOAlertPanel("Could not bind to socket:\n%s", _socket_path);
else if(listen(fd, 5) == -1)
OakRunIOAlertPanel("Could not listen to socket");
_callback.reset(new socket_callback_t(&rmate_connection_handler_t, fd));
}
~mate_server_t ()
{
D(DBF_RMateServer, bug("%s\n", _socket_path););
unlink(_socket_path);
}
private:
char const* _socket_path;
socket_callback_ptr _callback;
};
struct rmate_server_t
{
rmate_server_t (uint32_t ip, uint16_t port) : _ip(ip), _port(port)
{
D(DBF_RMateServer, bug("%08ux %ud\n", _ip, _port););
static int const on = 1;
socket_t fd(socket(AF_INET, SOCK_STREAM, 0));
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
fcntl(fd, F_SETFD, FD_CLOEXEC);
struct sockaddr_in iaddr = { sizeof(sockaddr_in), AF_INET, htons(_port), { htonl(_ip) } };
if(-1 == bind(fd, (sockaddr*)&iaddr, sizeof(iaddr)))
fprintf(stderr, "bind(): %s\n", strerror(errno));
if(-1 == listen(fd, 5))
fprintf(stderr, "listen(): %s\n", strerror(errno));
_callback.reset(new socket_callback_t(&rmate_connection_handler_t, fd));
}
~rmate_server_t ()
{
D(DBF_RMateServer, bug("%08ux %ud\n", _ip, _port););
}
uint32_t ip () const { return _ip; }
uint16_t port () const { return _port; }
private:
uint32_t _ip;
uint16_t _port;
socket_callback_ptr _callback;
};
}
void setup_rmate_server (bool enabled, uint32_t ip, uint16_t port)
{
static mate_server_t mate_server;
static std::shared_ptr<rmate_server_t> rmate_server;
if(!enabled || !rmate_server || ip != rmate_server->ip() || port != rmate_server->port())
{
rmate_server.reset();
if(enabled)
rmate_server.reset(new rmate_server_t(ip, port));
}
}
// ==================================
struct temp_file_t
{
temp_file_t ()
{
path = path::temp("rmate_buffer");
D(DBF_RMateServer, bug("create temp file: %s\n", path.c_str()););
}
operator char const* () const { return path.c_str(); }
~temp_file_t () { unlink(path.c_str()); }
private:
std::string path;
};
typedef std::shared_ptr<temp_file_t> temp_file_ptr;
struct record_t
{
WATCH_LEAKS(record_t);
record_t (std::string const& command) : command(command) { }
~record_t () { }
std::string command;
std::map<std::string, std::string> arguments;
temp_file_ptr file;
void accept_data (char const* first, char const* last)
{
if(!file)
{
file.reset(new temp_file_t);
arguments["data"] = std::string(*file);
}
if(FILE* fp = fopen(*file, "a"))
{
fwrite(first, 1, last - first, fp);
fclose(fp);
}
}
};
namespace // wrap in anonymous namespace to avoid clashing with other callbacks named the same
{
struct base_t : document::document_t::callback_t
{
WATCH_LEAKS(base_t);
virtual void save_document (document::document_ptr document) { }
virtual void close_document (document::document_ptr document) { }
void handle_document_event (document::document_ptr document, event_t event)
{
if(event == did_change_open_status && !document->is_open())
{
close_document(document);
D(DBF_RMateServer, bug("%p\n", this););
document->remove_callback(this);
delete this;
}
else if(event == did_save)
{
save_document(document);
}
}
};
struct retain_temp_file_callback_t : base_t
{
retain_temp_file_callback_t (temp_file_ptr file) : file(file) { }
temp_file_ptr file;
};
struct save_close_callback_t : base_t
{
WATCH_LEAKS(save_close_callback_t);
save_close_callback_t (std::string const& path, socket_t const& socket, bool data_on_save, bool data_on_close, std::string const& token) : path(path), socket(socket), data_on_save(data_on_save), data_on_close(data_on_close), token(token)
{
D(DBF_RMateServer, bug("%p\n", this););
}
void save_document (document::document_ptr document)
{
D(DBF_RMateServer, bug("%s\n", document->path().c_str()););
bool res = true;
res = res && write(socket, "save\r\n", 6) == 6;
res = res && write_token();
if(data_on_save)
res = res && write_data();
res = res && write(socket, "\r\n", 2) == 2;
if(!res)
fprintf(stderr, "*** rmate: callback failed to save %s\n", document->display_name().c_str());
}
void close_document (document::document_ptr document)
{
D(DBF_RMateServer, bug("%s\n", document->path().c_str()););
bool res = true;
res = res && write(socket, "close\r\n", 7) == 7;
res = res && write_token();
if(data_on_close)
res = res && write_data();
res = res && write(socket, "\r\n", 2) == 2;
if(!res)
fprintf(stderr, "*** rmate: callback failed while closing %s\n", document->display_name().c_str());
}
private:
bool write_token () const
{
if(token != NULL_STR)
{
std::string str = "token: " + token + "\r\n";
return write(socket, str.data(), str.size()) == str.size();
}
return true;
}
bool write_data () const
{
bool res = false;
if(FILE* fp = fopen(path.c_str(), "r"))
{
res = true;
char buf[1024];
while(size_t len = fread(buf, 1, sizeof(buf), fp))
{
std::string str = text::format("data: %zu\r\n", len);
res = res && write(socket, str.data(), str.size()) == str.size();
res = res && write(socket, buf, len) == len;
}
fclose(fp);
}
return res;
}
std::string path;
socket_t socket;
bool data_on_save;
bool data_on_close;
std::string token;
};
struct reactivate_callback_t : base_t
{
WATCH_LEAKS(reactivate_callback_t);
struct helper_t { helper_t () : open_documents(0) { } size_t open_documents; WATCH_LEAKS(reactivate_callback_t); };
typedef std::shared_ptr<helper_t> helper_ptr;
reactivate_callback_t () : helper(helper_ptr(new helper_t))
{
D(DBF_RMateServer, bug("%p\n", this););
GetFrontProcess(&psn);
}
void watch_document (document::document_ptr document)
{
++helper->open_documents;
document->add_callback(new reactivate_callback_t(*this));
}
void close_document (document::document_ptr document)
{
D(DBF_RMateServer, bug("%zu → %zu\n", helper->open_documents, helper->open_documents - 1););
if(--helper->open_documents == 0)
SetFrontProcess(&psn);
}
private:
helper_ptr helper;
struct ProcessSerialNumber psn;
};
}
// ==================
// = Handle Request =
// ==================
struct socket_observer_t
{
WATCH_LEAKS(socket_observer_t);
socket_observer_t () : state(command), bytesLeft(0) { }
std::vector<record_t> records;
enum { command, arguments, data, done } state;
std::string line;
ssize_t bytesLeft;
bool operator() (socket_t const& socket)
{
char buf[1024];
ssize_t len = read(socket, buf, sizeof(buf));
D(DBF_RMateServer, bug("%p, %d — %zd bytes\n", this, (int)socket, len););
if(len == 0)
return false;
if(len != -1)
{
receive_data(buf, len);
parse();
if(state == done)
{
D(DBF_RMateServer, bug("done\n"););
if(records.empty() || records.begin()->command == "open") // we treat no command as open to bring our application to front
open_documents(socket);
else
fprintf(stderr, "*** error unsupported command: %s\n", records.begin()->command.c_str());
return false;
}
}
return true;
}
void receive_data (char const* buf, ssize_t len)
{
if(state == data)
{
ssize_t dataLen = std::min(len, bytesLeft);
D(DBF_RMateServer, bug("Got data, %zd bytes\n", dataLen););
records.back().accept_data(buf, buf + dataLen);
bytesLeft -= dataLen;
state = bytesLeft == 0 ? arguments : data;
line.insert(line.end(), buf + dataLen, buf + len);
}
else
{
line.insert(line.end(), buf, buf + len);
}
}
void parse ()
{
while(line.find('\n') != std::string::npos)
{
std::string::size_type eol = line.find('\n');
std::string str = line.substr(0, eol);
if(!str.empty() && str[str.size()-1] == '\r')
str.resize(str.size()-1);
line.erase(line.begin(), line.begin() + eol + 1);
if(str.empty())
{
D(DBF_RMateServer, bug("Got end of record\n"););
state = command;
}
else if(state == command)
{
if(str == ".")
{
state = done;
}
else
{
records.emplace_back(str);
state = arguments;
}
D(DBF_RMateServer, bug("Got command %s\n", str.c_str()););
}
else if(state == arguments)
{
std::string::size_type n = str.find(':');
if(n != std::string::npos)
{
std::string const key = str.substr(0, n);
std::string const value = str.substr(n+2);
if(key == "data")
{
bytesLeft = strtol(value.c_str(), NULL, 10);
size_t dataLen = std::min((ssize_t)line.size(), bytesLeft);
D(DBF_RMateServer, bug("Got data of size %zd (%zu in this packet)\n", bytesLeft, dataLen););
records.back().accept_data(line.data(), line.data() + dataLen);
line.erase(line.begin(), line.begin() + dataLen);
bytesLeft -= dataLen;
state = bytesLeft == 0 ? arguments : data;
}
else
{
D(DBF_RMateServer, bug("Got argument: %s = %s\n", key.c_str(), value.c_str()););
if(!value.empty())
records.back().arguments.insert(std::make_pair(key, value));
}
}
}
}
}
void open_documents (socket_t const& socket)
{
reactivate_callback_t reactivate_callback;
std::vector<document::document_ptr> documents;
iterate(record, records)
{
std::map<std::string, std::string>& args = record->arguments;
bool wait = args["wait"] == "yes";
bool writeBackOnSave = args["data-on-save"] == "yes";
bool writeBackOnClose = args["data-on-close"] == "yes";
std::string token = args.find("token") != args.end() ? args["token"] : NULL_STR;
bool reActivate = args["re-activate"] == "yes";
std::string fileType = args.find("file-type") == args.end() ? NULL_STR : args["file-type"];
document::document_ptr doc;
if(args.find("path") != args.end())
{
if(path::is_directory(args["path"]))
{
document::show_browser(args["path"]);
continue;
}
doc = document::create(args["path"]);
}
else if(args.find("data") != args.end())
{
if(writeBackOnSave || writeBackOnClose)
{
doc = document::create(args["data"]);
doc->set_recent_tracking(false);
}
else
{
doc = document::from_content(path::content(args["data"]), fileType);
}
}
else
{
doc = document::create();
}
if(fileType != NULL_STR)
doc->set_file_type(fileType);
if(args.find("real-path") != args.end())
{
D(DBF_RMateServer, bug("set documents virtual path: %s\n", args["real-path"].c_str()););
doc->set_virtual_path(args["real-path"]);
}
if(!args["display-name"].empty())
doc->set_custom_name(args["display-name"]);
if(!args["selection"].empty())
doc->set_selection(args["selection"]);
if(args["add-to-recents"] != "yes")
doc->set_recent_tracking(false);
if(wait || writeBackOnSave || writeBackOnClose)
doc->add_callback(new save_close_callback_t(doc->path(), socket, writeBackOnSave, writeBackOnClose, token));
if(args.find("data") != args.end() && (writeBackOnSave || writeBackOnClose))
doc->add_callback(new retain_temp_file_callback_t(record->file));
if(reActivate)
reactivate_callback.watch_document(doc);
// std::string folder; // when there is no path we still may provide a default folder
// enum fallback_t { must_share_path, should_share_path, frontmost, create_new } project_fallback;
// bool bring_to_front;
if(args.find("authorization") != args.end())
doc->set_authorization(args["authorization"]);
if(oak::uuid_t::is_valid(args["project-uuid"]))
document::show(doc, args["project-uuid"]);
else documents.push_back(doc);
}
if(documents.empty())
SetFrontProcess(&(ProcessSerialNumber){ 0, kCurrentProcess });
else document::show(documents);
}
};
static bool rmate_connection_handler_t (socket_t const& socket)
{
socklen_t dummyLen = std::max(sizeof(sockaddr_un), sizeof(sockaddr_in));
char dummy[dummyLen];
int newFd = accept(socket, (sockaddr*)&dummy[0], &dummyLen);
std::string welcome = "220 " + sys_info(KERN_HOSTNAME) + " RMATE TextMate (" + sys_info(KERN_OSTYPE) + " " + sys_info(KERN_OSRELEASE) + ")\n";
ssize_t len = write(newFd, welcome.data(), welcome.size());
if(len == -1)
fprintf(stderr, "error writing: %s\n", strerror(errno));
new socket_callback_t(socket_observer_t(), newFd);
return true;
}