![Simon Kornblith](/assets/img/avatar_default.png)
In practice, this appears to be both unnecessary and harmful. Although the first write happens effectively instantaneously, we can take several hundred milliseconds to read the second write, and Standalone can decide we are dead. This apparently fixes #783 for @dstillman, although I was never able to reproduce locally.
459 lines
No EOL
14 KiB
JavaScript
Executable file
459 lines
No EOL
14 KiB
JavaScript
Executable file
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2011 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
Zotero.IPC = new function() {
|
|
var _libc, _libcPath, _instancePipe, _user32, open, write, close;
|
|
|
|
/**
|
|
* Initialize pipe for communication with connector
|
|
*/
|
|
this.init = function() {
|
|
if(!Zotero.isWin) { // no pipe support on Fx 3.6
|
|
_instancePipe = _getPipeDirectory();
|
|
if(!_instancePipe.exists()) {
|
|
_instancePipe.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
|
|
}
|
|
_instancePipe.append(Zotero.instanceID);
|
|
|
|
Zotero.IPC.Pipe.initPipeListener(_instancePipe, this.parsePipeInput);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses input received via instance pipe
|
|
*/
|
|
this.parsePipeInput = function(msgs) {
|
|
for each(var msg in msgs.split("\n")) {
|
|
if(!msg) continue;
|
|
Zotero.debug('IPC: Received "'+msg+'"');
|
|
|
|
/*
|
|
* The below messages coordinate switching Zotero for Firefox from extension mode to
|
|
* connector mode without restarting after Zotero Standalone has been launched. The
|
|
* dance typically proceeds as follows:
|
|
*
|
|
* 1. SA sends a releaseLock message to Z4Fx that tells it to release its lock.
|
|
* 2. Z4Fx releases its lock and sends a lockReleased message to SA.
|
|
* 3. Z4Fx restarts in connector mode. Once it's ready for an IPC command, it sends
|
|
* a checkInitComplete message to SA.
|
|
* 4. Once SA finishes initializing, or immediately after a checkInitComplete message
|
|
* has been received if it is already initialized, SA sends an initComplete message
|
|
* to Z4Fx.
|
|
*/
|
|
if(msg.substr(0, 11) === "releaseLock") {
|
|
// Standalone sends this to the Firefox extension to tell the Firefox extension to
|
|
// release its lock on the Zotero database
|
|
if(!Zotero.isConnector && (msg.length === 11 ||
|
|
msg.substr(12) === Zotero.getZoteroDatabase().persistentDescriptor)) {
|
|
switchConnectorMode(true);
|
|
}
|
|
} else if(msg === "lockReleased") {
|
|
// The Firefox extension sends this to Standalone to let Standalone know that it has
|
|
// released its lock
|
|
Zotero.onDBLockReleased();
|
|
} else if(msg === "checkInitComplete") {
|
|
// The Firefox extension sends this to Standalone to tell Standalone to send an
|
|
// initComplete message when it is fully initialized
|
|
if(Zotero.initialized) {
|
|
Zotero.IPC.broadcast("initComplete");
|
|
} else {
|
|
var observerService = Components.classes["@mozilla.org/observer-service;1"]
|
|
.getService(Components.interfaces.nsIObserverService);
|
|
var _loadObserver = function() {
|
|
Zotero.IPC.broadcast("initComplete");
|
|
observerService.removeObserver(_loadObserver, "zotero-loaded");
|
|
};
|
|
observerService.addObserver(_loadObserver, "zotero-loaded", false);
|
|
}
|
|
} else if(msg === "initComplete") {
|
|
// Standalone sends this to the Firefox extension to let the Firefox extension
|
|
// know that Standalone has fully initialized and it should pull the list of
|
|
// translators
|
|
Zotero.initComplete();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes safely to a file, avoiding blocking.
|
|
* @param {nsIFile} pipe The pipe as an nsIFile.
|
|
* @param {String} string The string to write to the file.
|
|
* @param {Boolean} [block] Whether we should block. Usually, we don't want this.
|
|
* @return {Boolean} True if write succeeded; false otherwise
|
|
*/
|
|
this.safePipeWrite = function(pipe, string, block) {
|
|
if(!open) {
|
|
// safely write to instance pipes
|
|
var lib = Zotero.IPC.getLibc();
|
|
if(!lib) return false;
|
|
|
|
// int open(const char *path, int oflag);
|
|
open = lib.declare("open", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int);
|
|
// ssize_t write(int fildes, const void *buf, size_t nbyte);
|
|
write = lib.declare("write", ctypes.default_abi, ctypes.ssize_t, ctypes.int, ctypes.char.ptr, ctypes.size_t);
|
|
// int close(int filedes);
|
|
close = lib.declare("close", ctypes.default_abi, ctypes.int, ctypes.int);
|
|
}
|
|
|
|
// On OS X and FreeBSD, O_NONBLOCK = 0x0004
|
|
// On Linux, O_NONBLOCK = 00004000
|
|
// On both, O_WRONLY = 0x0001
|
|
var mode = 0x0001;
|
|
if(!block) mode = mode | (Zotero.isLinux ? 00004000 : 0x0004);
|
|
|
|
var fd = open(pipe.path, mode);
|
|
if(fd === -1) return false;
|
|
write(fd, string, string.length);
|
|
close(fd);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Broadcast a message to all other Zotero instances
|
|
*/
|
|
this.broadcast = function(msg) {
|
|
if(Zotero.isWin) { // communicate via WM_COPYDATA method
|
|
Components.utils.import("resource://gre/modules/ctypes.jsm");
|
|
|
|
// communicate via message window
|
|
var user32 = ctypes.open("user32.dll");
|
|
|
|
/* http://msdn.microsoft.com/en-us/library/ms633499%28v=vs.85%29.aspx
|
|
* HWND WINAPI FindWindow(
|
|
* __in_opt LPCTSTR lpClassName,
|
|
* __in_opt LPCTSTR lpWindowName
|
|
* );
|
|
*/
|
|
var FindWindow = user32.declare("FindWindowW", ctypes.winapi_abi, ctypes.int32_t,
|
|
ctypes.jschar.ptr, ctypes.jschar.ptr);
|
|
|
|
/* http://msdn.microsoft.com/en-us/library/ms633539%28v=vs.85%29.aspx
|
|
* BOOL WINAPI SetForegroundWindow(
|
|
* __in HWND hWnd
|
|
* );
|
|
*/
|
|
var SetForegroundWindow = user32.declare("SetForegroundWindow", ctypes.winapi_abi,
|
|
ctypes.bool, ctypes.int32_t);
|
|
|
|
/*
|
|
* LRESULT WINAPI SendMessage(
|
|
* __in HWND hWnd,
|
|
* __in UINT Msg,
|
|
* __in WPARAM wParam,
|
|
* __in LPARAM lParam
|
|
* );
|
|
*/
|
|
var SendMessage = user32.declare("SendMessageW", ctypes.winapi_abi, ctypes.uintptr_t,
|
|
ctypes.int32_t, ctypes.unsigned_int, ctypes.voidptr_t, ctypes.voidptr_t);
|
|
|
|
/* http://msdn.microsoft.com/en-us/library/ms649010%28v=vs.85%29.aspx
|
|
* typedef struct tagCOPYDATASTRUCT {
|
|
* ULONG_PTR dwData;
|
|
* DWORD cbData;
|
|
* PVOID lpData;
|
|
* } COPYDATASTRUCT, *PCOPYDATASTRUCT;
|
|
*/
|
|
var COPYDATASTRUCT = ctypes.StructType("COPYDATASTRUCT", [
|
|
{"dwData":ctypes.voidptr_t},
|
|
{"cbData":ctypes.uint32_t},
|
|
{"lpData":ctypes.voidptr_t}
|
|
]);
|
|
|
|
// Aurora/Nightly are always named "Firefox" in
|
|
// application.ini
|
|
const appNames = ["Firefox", "Zotero"];
|
|
|
|
// Different from Zotero.appName; this corresponds to the
|
|
// name in application.ini
|
|
const myAppName = Services.appinfo.name;
|
|
|
|
for each(var appName in appNames) {
|
|
// don't send messages to ourself
|
|
if(appName === myAppName) continue;
|
|
|
|
var thWnd = FindWindow(appName+"MessageWindow", null);
|
|
if(thWnd) {
|
|
Zotero.debug('IPC: Broadcasting "'+msg+'" to window "'+appName+'MessageWindow"');
|
|
|
|
// allocate message
|
|
var data = ctypes.char.array()('firefox.exe -silent -ZoteroIPC "'+msg.replace('"', '""', "g")+'"\x00C:\\');
|
|
var dataSize = data.length*data.constructor.size;
|
|
|
|
// create new COPYDATASTRUCT
|
|
var cds = new COPYDATASTRUCT();
|
|
cds.dwData = null;
|
|
cds.cbData = dataSize;
|
|
cds.lpData = data.address();
|
|
|
|
// send COPYDATASTRUCT
|
|
var success = SendMessage(thWnd, 0x004A /** WM_COPYDATA **/, null, cds.address());
|
|
|
|
user32.close();
|
|
return !!success;
|
|
}
|
|
}
|
|
|
|
user32.close();
|
|
return false;
|
|
} else { // communicate via pipes
|
|
// look for other Zotero instances
|
|
var pipes = [];
|
|
var pipeDir = _getPipeDirectory();
|
|
if(pipeDir.exists()) {
|
|
var dirEntries = pipeDir.directoryEntries;
|
|
while (dirEntries.hasMoreElements()) {
|
|
var pipe = dirEntries.getNext().QueryInterface(Ci.nsILocalFile);
|
|
if(pipe.leafName[0] !== "." && (!_instancePipe || !pipe.equals(_instancePipe))) {
|
|
pipes.push(pipe);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!pipes.length) return false;
|
|
var success = false;
|
|
for each(var pipe in pipes) {
|
|
Zotero.debug('IPC: Trying to broadcast "'+msg+'" to instance '+pipe.leafName);
|
|
|
|
var defunct = false;
|
|
|
|
if(pipe.isFile()) {
|
|
// not actually a pipe
|
|
if(pipe.isDirectory()) {
|
|
// not a file, so definitely defunct
|
|
defunct = true;
|
|
} else {
|
|
// check to see whether the size exceeds a certain threshold that we find
|
|
// reasonable for the queue, and if not, delete the pipe, because it's
|
|
// probably just a file that wasn't deleted on shutdown and is now
|
|
// accumulating vast amounts of data
|
|
defunct = pipe.fileSize > 1024;
|
|
}
|
|
}
|
|
|
|
if(!defunct) {
|
|
// Try to write to the pipe for 100 ms
|
|
var time = Date.now(), timeout = time+100, wroteToPipe;
|
|
do {
|
|
wroteToPipe = Zotero.IPC.safePipeWrite(pipe, msg+"\n");
|
|
} while(Date.now() < timeout && !wroteToPipe);
|
|
if (wroteToPipe) Zotero.debug('IPC: Pipe took '+(Date.now()-time)+' ms to become available');
|
|
success = success || wroteToPipe;
|
|
defunct = !wroteToPipe;
|
|
}
|
|
|
|
if(defunct) {
|
|
Zotero.debug('IPC: Removing defunct pipe '+pipe.leafName);
|
|
try {
|
|
pipe.remove(true);
|
|
} catch(e) {};
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get directory containing Zotero pipes
|
|
*/
|
|
function _getPipeDirectory() {
|
|
var dir = Zotero.getZoteroDirectory();
|
|
dir.append("pipes");
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Gets the path to libc as a string
|
|
*/
|
|
this.getLibcPath = function() {
|
|
if(_libcPath) return _libcPath;
|
|
|
|
Components.utils.import("resource://gre/modules/ctypes.jsm");
|
|
|
|
// get possible names for libc
|
|
if(Zotero.isMac) {
|
|
var possibleLibcs = ["/usr/lib/libc.dylib"];
|
|
} else {
|
|
var possibleLibcs = [
|
|
"libc.so.6",
|
|
"libc.so.6.1",
|
|
"libc.so"
|
|
];
|
|
}
|
|
|
|
// try all possibilities
|
|
while(possibleLibcs.length) {
|
|
var libPath = possibleLibcs.shift();
|
|
try {
|
|
var lib = ctypes.open(libPath);
|
|
break;
|
|
} catch(e) {}
|
|
}
|
|
|
|
// throw appropriate error on failure
|
|
if(!lib) {
|
|
Components.utils.reportError("Zotero: libc could not be loaded. Word processor integration "+
|
|
"and other functionality will not be available. Please post on the Zotero Forums so we "+
|
|
"can add support for your operating system.");
|
|
return;
|
|
}
|
|
|
|
_libc = lib;
|
|
_libcPath = libPath;
|
|
return libPath;
|
|
}
|
|
|
|
/**
|
|
* Gets standard C library via ctypes
|
|
*/
|
|
this.getLibc = function() {
|
|
if(!_libc) this.getLibcPath();
|
|
return _libc;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Methods for reading from and writing to a pipe
|
|
*/
|
|
Zotero.IPC.Pipe = new function() {
|
|
var _mkfifo, _pipeClass;
|
|
|
|
/**
|
|
* Creates and listens on a pipe
|
|
*
|
|
* @param {nsIFile} file The location where the pipe should be created
|
|
* @param {Function} callback A function to be passed any data recevied on the pipe
|
|
*/
|
|
this.initPipeListener = function(file, callback) {
|
|
Zotero.debug("IPC: Initializing pipe at "+file.path);
|
|
|
|
// determine type of pipe
|
|
if(!_pipeClass) {
|
|
var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"]
|
|
.getService(Components.interfaces.nsIVersionComparator);
|
|
var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
|
|
getService(Components.interfaces.nsIXULAppInfo);
|
|
if(verComp.compare("2.2a1pre", appInfo.platformVersion) <= 0) { // Gecko 5
|
|
_pipeClass = Zotero.IPC.Pipe.DeferredOpen;
|
|
}
|
|
}
|
|
|
|
// make new pipe
|
|
new _pipeClass(file, callback);
|
|
}
|
|
|
|
/**
|
|
* Makes a fifo
|
|
* @param {nsIFile} file Location to create the fifo
|
|
*/
|
|
this.mkfifo = function(file) {
|
|
// int mkfifo(const char *path, mode_t mode);
|
|
if(!_mkfifo) {
|
|
var libc = Zotero.IPC.getLibc();
|
|
if(!libc) return false;
|
|
_mkfifo = libc.declare("mkfifo", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.unsigned_int);
|
|
}
|
|
|
|
// make pipe
|
|
var ret = _mkfifo(file.path, 0600);
|
|
return file.exists();
|
|
}
|
|
|
|
/**
|
|
* Adds a shutdown listener for a pipe that writes "Zotero shutdown\n" to the pipe and then
|
|
* deletes it
|
|
*/
|
|
this.writeShutdownMessage = function(pipe, file) {
|
|
// Make sure pipe actually exists
|
|
if(!file.exists()) {
|
|
Zotero.debug("IPC: Not closing pipe "+file.path+": already deleted");
|
|
return;
|
|
}
|
|
|
|
// Keep trying to write to pipe until we succeed, in case pipe is not yet open
|
|
Zotero.debug("IPC: Closing pipe "+file.path);
|
|
Zotero.IPC.safePipeWrite(file, "Zotero shutdown\n");
|
|
|
|
// Delete pipe
|
|
file.remove(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listens asynchronously for data on the integration pipe and reads it when available
|
|
*
|
|
* Used to read from pipe on Gecko 5+
|
|
*/
|
|
Zotero.IPC.Pipe.DeferredOpen = function(file, callback) {
|
|
this._file = file;
|
|
this._callback = callback;
|
|
|
|
if(!Zotero.IPC.Pipe.mkfifo(file)) return;
|
|
|
|
this._initPump();
|
|
|
|
// add shutdown listener
|
|
Zotero.addShutdownListener(Zotero.IPC.Pipe.writeShutdownMessage.bind(null, this, file));
|
|
}
|
|
|
|
Zotero.IPC.Pipe.DeferredOpen.prototype = {
|
|
"onStartRequest":function() {},
|
|
"onStopRequest":function() {},
|
|
"onDataAvailable":function(request, context, inputStream, offset, count) {
|
|
// read from pipe
|
|
var converterInputStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
|
|
.createInstance(Components.interfaces.nsIConverterInputStream);
|
|
converterInputStream.init(inputStream, "UTF-8", 4096,
|
|
Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
|
|
var out = {};
|
|
converterInputStream.readString(count, out);
|
|
inputStream.close();
|
|
|
|
if(out.value === "Zotero shutdown\n") return
|
|
|
|
this._initPump();
|
|
this._callback(out.value);
|
|
},
|
|
|
|
/**
|
|
* Initializes the nsIInputStream and nsIInputStreamPump to read from _fifoFile
|
|
*
|
|
* Used after reading from file on Gecko 5+
|
|
*/
|
|
"_initPump":function() {
|
|
var fifoStream = Components.classes["@mozilla.org/network/file-input-stream;1"].
|
|
createInstance(Components.interfaces.nsIFileInputStream);
|
|
fifoStream.QueryInterface(Components.interfaces.nsIFileInputStream);
|
|
// 16 = open as deferred so that we don't block on open
|
|
fifoStream.init(this._file, -1, 0, 16);
|
|
|
|
var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"].
|
|
createInstance(Components.interfaces.nsIInputStreamPump);
|
|
pump.init(fifoStream, -1, -1, 4096, 1, true);
|
|
pump.asyncRead(this, null);
|
|
|
|
this._openTime = Date.now();
|
|
}
|
|
}; |