blob: c33ccc932e56e71f94fcc67ffed02b834b7ef0d3 [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "unix/ibus/selection_monitor.h"
#include <xcb/xcb.h>
#include <xcb/xfixes.h>
#include <cstdlib>
#include <memory>
#include <string>
#include "base/logging.h"
#include "base/mutex.h"
#include "base/thread.h"
#include "base/port.h"
#include "base/util.h"
namespace mozc {
namespace ibus {
namespace {
using std::unique_ptr;
class ScopedXcbGenericError {
public:
ScopedXcbGenericError()
: error_(NULL) {
}
~ScopedXcbGenericError() {
free(error_);
error_ = NULL;
}
const xcb_generic_error_t *get() const {
return error_;
}
xcb_generic_error_t **mutable_get() {
return &error_;
}
private:
xcb_generic_error_t *error_;
};
template <typename T>
struct FreeDeleter {
void operator()(T *ptr) const {
free(ptr);
}
};
// TODO(yukawa): Use template aliases when GCC 4.6 is retired.
typedef unique_ptr<xcb_get_property_reply_t,
FreeDeleter<xcb_get_property_reply_t>>
ScopedXcbGetPropertyReply;
typedef unique_ptr<xcb_get_atom_name_reply_t,
FreeDeleter<xcb_get_atom_name_reply_t>>
ScopedXcbGetAtomNameReply;
typedef unique_ptr<xcb_intern_atom_reply_t,
FreeDeleter<xcb_intern_atom_reply_t>>
ScopedXcbInternAtomReply;
typedef unique_ptr<xcb_xfixes_query_version_reply_t,
FreeDeleter<xcb_xfixes_query_version_reply_t>>
ScopedXcbXFixesQueqyVersionReply;
struct XcbAtoms {
xcb_atom_t mozc_selection_monitor;
xcb_atom_t net_wm_name;
xcb_atom_t net_wm_pid;
xcb_atom_t utf8_string;
xcb_atom_t wm_client_machine;
XcbAtoms()
: mozc_selection_monitor(XCB_NONE),
net_wm_name(XCB_NONE),
net_wm_pid(XCB_NONE),
utf8_string(XCB_NONE),
wm_client_machine(XCB_NONE) {
}
};
class SelectionMonitorServer {
public:
SelectionMonitorServer()
: connection_(NULL),
requestor_window_(0),
root_window_(0),
xfixes_first_event_(0),
xcb_maximum_request_len_(0) {
}
~SelectionMonitorServer() {
Release();
}
bool Init() {
connection_ = ::xcb_connect(NULL, NULL);
if (connection_ == NULL) {
return false;
}
if (!InitXFixes()) {
Release();
return false;
}
if (!InitAtoms()) {
Release();
return false;
}
const xcb_screen_t *screen =
::xcb_setup_roots_iterator(::xcb_get_setup(connection_)).data;
requestor_window_ = ::xcb_generate_id(connection_);
const uint32 mask = XCB_CW_EVENT_MASK;
const uint32 values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
root_window_ = screen->root;
::xcb_create_window(connection_, screen->root_depth,
requestor_window_, root_window_,
0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT,
screen->root_visual, mask, values);
const uint32 xfixes_mask =
XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER |
XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY |
XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE;
::xcb_xfixes_select_selection_input_checked(
connection_, requestor_window_, XCB_ATOM_PRIMARY, xfixes_mask);
::xcb_flush(connection_);
return true;
}
bool checkConnection() {
if (!connection_) {
return false;
}
if (::xcb_connection_has_error(connection_)) {
LOG(ERROR) << "XCB connection has error.";
connection_ = NULL;
return false;
}
return true;
}
bool WaitForNextSelectionEvent(size_t max_bytes, SelectionInfo *next_info) {
DCHECK(next_info);
if (!connection_) {
return false;
}
::xcb_flush(connection_);
unique_ptr<xcb_generic_event_t, void (*)(void*)> event(
::xcb_wait_for_event(connection_), &std::free);
if (event.get() == NULL) {
LOG(ERROR) << "NULL event returned.";
return false;
}
const uint32_t response_type = (event->response_type & ~0x80);
if (response_type ==
(xfixes_first_event_ + XCB_XFIXES_SELECTION_NOTIFY)) {
return OnXFixesSelectionNotify(event.get(), max_bytes, next_info);
}
if (response_type != XCB_SELECTION_NOTIFY) {
VLOG(2) << "Ignored a message. response_type: " << response_type;
return false;
}
return OnSelectionNotify(event.get(), max_bytes, next_info);
}
// Sends a harmless message to the |requestor_window_|. You can call this
// method to awake a message pump thread which is waiting for the next
// X11 message for |requestor_window_|.
void SendNoopEventMessage() {
if (!connection_ || !requestor_window_) {
return;
}
// Send a dummy event so that the event pump can wake up.
xcb_client_message_event_t event = {};
event.response_type = XCB_CLIENT_MESSAGE;
event.window = requestor_window_;
event.format = 32;
event.type = XCB_NONE;
::xcb_send_event(connection_, false, requestor_window_,
XCB_EVENT_MASK_NO_EVENT,
reinterpret_cast<const char *>(&event));
::xcb_flush(connection_);
}
private:
void Release() {
if (connection_) {
::xcb_disconnect(connection_);
connection_ = NULL;
}
}
bool CreateAtom(const string name, xcb_atom_t *atom) const {
DCHECK(atom);
*atom = XCB_NONE;
xcb_intern_atom_cookie_t cookie =
::xcb_intern_atom(connection_, false, name.size(), name.c_str());
ScopedXcbInternAtomReply reply(
::xcb_intern_atom_reply(connection_, cookie, 0));
if (reply.get() == NULL) {
LOG(ERROR) << "xcb_intern_atom_reply returned NULL reply.";
return false;
}
if (reply->atom == XCB_NONE) {
return false;
}
*atom = reply->atom;
return true;
}
bool InitAtoms() {
return
CreateAtom("MOZC_SEL_MON", &atoms_.mozc_selection_monitor) &&
CreateAtom("UTF8_STRING", &atoms_.utf8_string) &&
CreateAtom("_NET_WM_NAME", &atoms_.net_wm_name) &&
CreateAtom("_NET_WM_PID", &atoms_.net_wm_pid) &&
CreateAtom("WM_CLIENT_MACHINE", &atoms_.wm_client_machine);
}
bool InitXFixes() {
const xcb_query_extension_reply_t
*ext_reply = ::xcb_get_extension_data(connection_, &xcb_xfixes_id);
if (ext_reply == NULL) {
LOG(ERROR) << "xcb_get_extension_data returns NULL.";
return false;
}
const xcb_xfixes_query_version_cookie_t xfixes_query_cookie =
xcb_xfixes_query_version(
connection_,
XCB_XFIXES_MAJOR_VERSION,
XCB_XFIXES_MINOR_VERSION);
ScopedXcbGenericError xcb_error;
ScopedXcbXFixesQueqyVersionReply xfixes_query(
::xcb_xfixes_query_version_reply(
connection_, xfixes_query_cookie, xcb_error.mutable_get()));
if (xcb_error.get() != NULL) {
LOG(ERROR) << "xcb_xfixes_query_version_reply failed. error_code: "
<< static_cast<uint32>(xcb_error.get()->error_code);
return false;
}
if (xfixes_query.get() == NULL) {
return false;
}
xfixes_first_event_ = ext_reply->first_event;
LOG(INFO) << "XFixes ver: " << xfixes_query->major_version
<< "." << xfixes_query->major_version
<< ", first_event: " << xfixes_first_event_;
xcb_maximum_request_len_ = ::xcb_get_maximum_request_length(connection_);
if (xcb_maximum_request_len_ <= 0) {
LOG(ERROR) << "Unexpected xcb maximum request length: "
<< xcb_maximum_request_len_;
return false;
}
return true;
}
string GetAtomName(xcb_atom_t atom) const {
const xcb_get_atom_name_cookie_t cookie = ::xcb_get_atom_name(
connection_, atom);
ScopedXcbGenericError xcb_error;
ScopedXcbGetAtomNameReply reply(
::xcb_get_atom_name_reply(
connection_, cookie, xcb_error.mutable_get()));
if (xcb_error.get() != NULL) {
LOG(ERROR) << "xcb_get_atom_name_reply failed. error_code: "
<< static_cast<uint32>(xcb_error.get()->error_code);
return "";
}
if (reply.get() == NULL) {
VLOG(2) << "reply is NULL";
return "";
}
const char *ptr = ::xcb_get_atom_name_name(reply.get());
const size_t len = ::xcb_get_atom_name_name_length(reply.get());
return string(ptr, len);
}
bool GetByteArrayProperty(xcb_window_t window, xcb_atom_t property_atom,
xcb_atom_t property_type_atom,
size_t max_bytes,
string *retval) const {
DCHECK(retval);
retval->clear();
size_t bytes_after = 0;
int element_bit_size = 0;
{
const xcb_get_property_cookie_t cookie =
::xcb_get_property(connection_, false,
window,
property_atom,
property_type_atom,
0, 0);
ScopedXcbGetPropertyReply reply(
::xcb_get_property_reply(connection_, cookie, 0));
if (reply.get() == NULL) {
VLOG(2) << "reply is NULL";
return false;
}
if (reply->type == XCB_NONE) {
LOG(ERROR) << "reply type is XCB_NONE";
return false;
}
if (reply->type != property_type_atom) {
LOG(ERROR) << "unexpected atom type: " << GetAtomName(reply->type);
return false;
}
bytes_after = reply->bytes_after;
element_bit_size = reply->format;
}
if (max_bytes < bytes_after) {
LOG(WARNING) << "Exceeds size limit. Returns an empty string."
<< " max_bytes: " << max_bytes
<< ", bytes_after: " << bytes_after;
*retval = "";
return true;
}
if (element_bit_size == 0) {
VLOG(1) << "element_bit_size is 0. Assuming byte-size data.";
element_bit_size = 8;
}
if (element_bit_size != 8) {
LOG(ERROR) << "Unsupported bit size: " << element_bit_size;
return false;
}
int byte_offset = 0;
while (bytes_after > 0) {
const xcb_get_property_cookie_t cookie =
::xcb_get_property(connection_,
false,
window,
property_atom,
property_type_atom,
byte_offset,
max_bytes);
ScopedXcbGetPropertyReply reply(
::xcb_get_property_reply(connection_, cookie, 0));
if (reply.get() == NULL) {
VLOG(2) << "reply is NULL";
return false;
}
if (reply->format != element_bit_size) {
LOG(ERROR) << "bit size changed: " << reply->format;
return false;
}
bytes_after = reply->bytes_after;
const char *data = reinterpret_cast<const char *>(
::xcb_get_property_value(reply.get()));
const int length = ::xcb_get_property_value_length(reply.get());
*retval += string(data, length);
byte_offset += length;
}
return true;
}
template <typename T>
bool GetCardinalProperty(xcb_window_t window, xcb_atom_t property_atom,
T *retval) const {
*retval = 0;
const xcb_get_property_cookie_t cookie =
::xcb_get_property(connection_, false,
window,
property_atom,
XCB_ATOM_CARDINAL,
0, sizeof(T) * 8);
ScopedXcbGetPropertyReply reply(
::xcb_get_property_reply(connection_, cookie, 0));
if (reply.get() == NULL) {
VLOG(2) << "reply is NULL";
return false;
}
if (reply->type != XCB_ATOM_CARDINAL) {
LOG(ERROR) << "unexpected type: " << GetAtomName(reply->type);
return false;
}
// All data should be read.
if (reply->bytes_after != 0) {
LOG(ERROR) << "unexpectedly " << reply->bytes_after
<< " bytes data remain.";
return false;
}
if (reply->format != 0 && (reply->format != sizeof(T) * 8)) {
LOG(ERROR) << "unexpected bit size: " << reply->format;
return false;
}
*retval = *reinterpret_cast<const T *>(
::xcb_get_property_value(reply.get()));
return true;
}
bool OnXFixesSelectionNotify(const xcb_generic_event_t *event,
size_t max_bytes, SelectionInfo *next_info) {
const xcb_xfixes_selection_notify_event_t *event_notify =
reinterpret_cast<const xcb_xfixes_selection_notify_event_t *>(
event);
if (event_notify->selection != XCB_ATOM_PRIMARY) {
VLOG(2) << "Ignored :" << GetAtomName(event_notify->selection);
return false;
}
// Send request message for selection info.
::xcb_convert_selection(connection_, requestor_window_,
XCB_ATOM_PRIMARY, atoms_.utf8_string,
atoms_.mozc_selection_monitor,
XCB_CURRENT_TIME);
last_request_info_.timestamp = event_notify->selection_timestamp;
uint32_t net_wm_pid = 0;
if (GetCardinalProperty(event_notify->owner,
atoms_.net_wm_pid,
&net_wm_pid)) {
last_request_info_.process_id = net_wm_pid;
}
string net_wm_name;
if (GetByteArrayProperty(event_notify->owner,
atoms_.net_wm_name,
atoms_.utf8_string,
max_bytes,
&net_wm_name)) {
last_request_info_.window_title = net_wm_name;
}
string wm_client_machine;
if (GetByteArrayProperty(event_notify->owner,
atoms_.wm_client_machine,
XCB_ATOM_STRING,
max_bytes,
&wm_client_machine)) {
last_request_info_.machine_name = wm_client_machine;
}
*next_info = last_request_info_;
return true;
}
bool OnSelectionNotify(const xcb_generic_event_t *event, size_t max_bytes,
SelectionInfo *next_info) {
const xcb_selection_notify_event_t *event_notify =
reinterpret_cast<const xcb_selection_notify_event_t *>(event);
if (event_notify->selection != XCB_ATOM_PRIMARY) {
VLOG(2) << "Ignored a message. selection type:"
<< event_notify->selection;
return false;
}
if (event_notify->property == XCB_NONE) {
VLOG(2) << "Ignored a message whose property type is XCB_NONE";
return false;
}
string selected_text;
if (!GetByteArrayProperty(event_notify->requestor,
event_notify->property,
atoms_.utf8_string,
max_bytes,
&selected_text)) {
LOG(ERROR) << "Failed to retrieve selection text.";
return false;
}
// Update the result.
*next_info = last_request_info_;
next_info->selected_text = selected_text;
return true;
}
xcb_connection_t *connection_;
xcb_window_t requestor_window_;
xcb_window_t root_window_;
uint32 xfixes_first_event_;
uint32 xcb_maximum_request_len_;
SelectionInfo last_request_info_;
XcbAtoms atoms_;
DISALLOW_COPY_AND_ASSIGN(SelectionMonitorServer);
};
class SelectionMonitorImpl : public SelectionMonitorInterface,
public Thread {
public:
SelectionMonitorImpl(SelectionMonitorServer *server, size_t max_text_bytes)
: server_(server),
max_text_bytes_(max_text_bytes),
quit_(false) {
}
virtual ~SelectionMonitorImpl() {
// Currently mozc::Thread cannot safely detach the attached thread since
// the detached thread continues running on a heap allocated to this
// object.
// TODO(yukawa): Implement safer thread termination.
if (Thread::IsRunning()) {
QueryQuit();
// TODO(yukawa): Add Wait method to mozc::Thread.
Util::Sleep(100);
}
}
// Implements SelectionMonitorInterface::StartMonitoring.
virtual void StartMonitoring() {
Thread::Start();
}
// Implements SelectionMonitorInterface::QueryQuit.
virtual void QueryQuit() {
if (Thread::IsRunning()) {
quit_ = true;
// Awake the message pump thread so that it can see the updated
// |quit_| immediately.
server_->SendNoopEventMessage();
}
}
// Implements SelectionMonitorInterface::GetSelectionInfo.
virtual SelectionInfo GetSelectionInfo() {
SelectionInfo info;
{
scoped_lock l(&mutex_);
info = last_selection_info_;
}
return info;
}
// Implements Thread::Run.
virtual void Run() {
while (!quit_) {
if (!server_->checkConnection()) {
scoped_lock l(&mutex_);
last_selection_info_ = SelectionInfo();
quit_ = true;
break;
}
SelectionInfo next_info;
// Note that this is blocking call and will not return until the next
// X11 message is received. In order to interrupt, you can call
// SendNoopEventMessage() method from other threads.
if (server_->WaitForNextSelectionEvent(max_text_bytes_, &next_info)) {
scoped_lock l(&mutex_);
last_selection_info_ = next_info;
}
}
}
private:
unique_ptr<SelectionMonitorServer> server_;
const size_t max_text_bytes_;
volatile bool quit_;
Mutex mutex_;
SelectionInfo last_selection_info_;
DISALLOW_COPY_AND_ASSIGN(SelectionMonitorImpl);
};
} // namespace
SelectionMonitorInterface::~SelectionMonitorInterface() {}
SelectionInfo::SelectionInfo()
: timestamp(0), process_id(0) {
}
SelectionMonitorInterface *SelectionMonitorFactory::Create(
size_t max_text_bytes) {
unique_ptr<SelectionMonitorServer> server(new SelectionMonitorServer());
if (!server->Init()) {
return NULL;
}
return new SelectionMonitorImpl(server.release(), max_text_bytes);
}
} // namespace ibus
} // namespace mozc