www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

ipc.js (14958B)


      1 /*
      2     ***** BEGIN LICENSE BLOCK *****
      3     
      4     Copyright © 2011 Center for History and New Media
      5                      George Mason University, Fairfax, Virginia, USA
      6                      http://zotero.org
      7     
      8     This file is part of Zotero.
      9     
     10     Zotero is free software: you can redistribute it and/or modify
     11     it under the terms of the GNU Affero General Public License as published by
     12     the Free Software Foundation, either version 3 of the License, or
     13     (at your option) any later version.
     14     
     15     Zotero is distributed in the hope that it will be useful,
     16     but WITHOUT ANY WARRANTY; without even the implied warranty of
     17     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18     GNU Affero General Public License for more details.
     19     
     20     You should have received a copy of the GNU Affero General Public License
     21     along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
     22     
     23     ***** END LICENSE BLOCK *****
     24 */
     25 
     26 Zotero.IPC = new function() {
     27 	var _libc, _libcPath, _instancePipe, _user32, open, write, close;
     28 	
     29 	/**
     30 	 * Initialize pipe for communication with connector
     31 	 */
     32 	this.init = function() {
     33 		if(!Zotero.isWin) {	// no pipe support on Fx 3.6
     34 			_instancePipe = _getPipeDirectory();
     35 			if(!_instancePipe.exists()) {
     36 				_instancePipe.create(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
     37 			}
     38 			_instancePipe.append(Zotero.instanceID);
     39 			
     40 			Zotero.IPC.Pipe.initPipeListener(_instancePipe, this.parsePipeInput);
     41 		}
     42 	}
     43 	
     44 	/**
     45 	 * Parses input received via instance pipe
     46 	 */
     47 	this.parsePipeInput = function(msgs) {
     48 		for (let msg of msgs.split("\n")) {
     49 			if(!msg) continue;
     50 			Zotero.debug('IPC: Received "'+msg+'"');
     51 			
     52 			/*
     53 			 * The below messages coordinate switching Zotero for Firefox from extension mode to
     54 			 * connector mode without restarting after Zotero Standalone has been launched. The
     55 			 * dance typically proceeds as follows:
     56 			 *
     57 			 * 1. SA sends a releaseLock message to Z4Fx that tells it to release its lock.
     58 			 * 2. Z4Fx releases its lock and sends a lockReleased message to SA.
     59 			 * 3. Z4Fx restarts in connector mode. Once it's ready for an IPC command, it sends
     60 			 *    a checkInitComplete message to SA.
     61 			 * 4. Once SA finishes initializing, or immediately after a checkInitComplete message
     62 			 *    has been received if it is already initialized, SA sends an initComplete message 
     63 			 *    to Z4Fx.
     64 			 */
     65 			if(msg.substr(0, 11) === "releaseLock") {
     66 				// Standalone sends this to the Firefox extension to tell the Firefox extension to
     67 				// release its lock on the Zotero database
     68 				if(!Zotero.isConnector && (msg.length === 11 ||
     69 						msg.substr(12) === Zotero.DataDirectory.getDatabase())) {
     70 					switchConnectorMode(true);
     71 				}
     72 			} else if(msg === "lockReleased") {
     73 				// The Firefox extension sends this to Standalone to let Standalone know that it has
     74 				// released its lock
     75 				Zotero.onDBLockReleased();
     76 			} else if(msg === "checkInitComplete") {
     77 				// The Firefox extension sends this to Standalone to tell Standalone to send an
     78 				// initComplete message when it is fully initialized
     79 				if(Zotero.initialized) {
     80 					Zotero.IPC.broadcast("initComplete");
     81 				} else {
     82 					var observerService = Components.classes["@mozilla.org/observer-service;1"]
     83 						.getService(Components.interfaces.nsIObserverService);
     84 					var _loadObserver = function() {
     85 						Zotero.IPC.broadcast("initComplete");
     86 						observerService.removeObserver(_loadObserver, "zotero-loaded");
     87 					};
     88 					observerService.addObserver(_loadObserver, "zotero-loaded", false);
     89 				}
     90 			} else if(msg === "initComplete") {
     91 				// Standalone sends this to the Firefox extension to let the Firefox extension
     92 				// know that Standalone has fully initialized and it should pull the list of
     93 				// translators
     94 				Zotero.initComplete();
     95 			}
     96 			else if (msg == "reinit") {
     97 				if (Zotero.isConnector) {
     98 					reinit(false, true);
     99 				}
    100 			}
    101 		}
    102 	}
    103 	
    104 	/**
    105 	 * Writes safely to a file, avoiding blocking.
    106 	 * @param {nsIFile} pipe The pipe as an nsIFile.
    107 	 * @param {String} string The string to write to the file.
    108 	 * @param {Boolean} [block] Whether we should block. Usually, we don't want this.
    109 	 * @return {Boolean} True if write succeeded; false otherwise
    110 	 */
    111 	this.safePipeWrite = function(pipe, string, block) {
    112 		if(!open) {
    113 			// safely write to instance pipes
    114 			var lib = Zotero.IPC.getLibc();
    115 			if(!lib) return false;
    116 			
    117 			// int open(const char *path, int oflag);
    118 			open = lib.declare("open", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.int);
    119 			// ssize_t write(int fildes, const void *buf, size_t nbyte);
    120 			write = lib.declare("write", ctypes.default_abi, ctypes.ssize_t, ctypes.int, ctypes.char.ptr, ctypes.size_t);
    121 			// int close(int filedes);
    122 			close = lib.declare("close", ctypes.default_abi, ctypes.int, ctypes.int);
    123 		}
    124 		
    125 		// On OS X and FreeBSD, O_NONBLOCK = 0x0004
    126 		// On Linux, O_NONBLOCK = 00004000
    127 		// On both, O_WRONLY = 0x0001
    128 		var mode = 0x0001;
    129 		if(!block) mode = mode | (Zotero.isLinux ? 0o0004000 : 0x0004);
    130 		
    131 		var fd = open(pipe.path, mode);
    132 		if(fd === -1) return false;			
    133 		write(fd, string, string.length);
    134 		close(fd);
    135 		return true;
    136 	}
    137 	
    138 	/**
    139 	 * Broadcast a message to all other Zotero instances
    140 	 */
    141 	this.broadcast = function(msg) {
    142 		if(Zotero.isWin) {		// communicate via WM_COPYDATA method
    143 			Components.utils.import("resource://gre/modules/ctypes.jsm");
    144 			
    145 			// communicate via message window
    146 			var user32 = ctypes.open("user32.dll");
    147 			
    148 			/* http://msdn.microsoft.com/en-us/library/ms633499%28v=vs.85%29.aspx
    149 			 * HWND WINAPI FindWindow(
    150 			 *   __in_opt  LPCTSTR lpClassName,
    151 			 *   __in_opt  LPCTSTR lpWindowName
    152 			 * );
    153 			 */
    154 			var FindWindow = user32.declare("FindWindowW", ctypes.winapi_abi, ctypes.int32_t,
    155 					ctypes.jschar.ptr, ctypes.jschar.ptr);
    156 			
    157 			/* http://msdn.microsoft.com/en-us/library/ms633539%28v=vs.85%29.aspx
    158 			 * BOOL WINAPI SetForegroundWindow(
    159 			 *   __in  HWND hWnd
    160 			 * );
    161 			 */
    162 			var SetForegroundWindow = user32.declare("SetForegroundWindow", ctypes.winapi_abi,
    163 					ctypes.bool, ctypes.int32_t);
    164 			
    165 			/*
    166 			 * LRESULT WINAPI SendMessage(
    167 			 *   __in  HWND hWnd,
    168 			 *   __in  UINT Msg,
    169 			 *   __in  WPARAM wParam,
    170 			 *   __in  LPARAM lParam
    171 			 * );
    172 			 */
    173 			var SendMessage = user32.declare("SendMessageW", ctypes.winapi_abi, ctypes.uintptr_t,
    174 					ctypes.int32_t, ctypes.unsigned_int, ctypes.voidptr_t, ctypes.voidptr_t);
    175 			
    176 			/* http://msdn.microsoft.com/en-us/library/ms649010%28v=vs.85%29.aspx
    177 			 * typedef struct tagCOPYDATASTRUCT {
    178 			 *   ULONG_PTR dwData;
    179 			 *   DWORD     cbData;
    180 			 *   PVOID     lpData;
    181 			 * } COPYDATASTRUCT, *PCOPYDATASTRUCT;
    182 			 */
    183 			var COPYDATASTRUCT = ctypes.StructType("COPYDATASTRUCT", [
    184 					{"dwData":ctypes.voidptr_t},
    185 					{"cbData":ctypes.uint32_t},
    186 					{"lpData":ctypes.voidptr_t}
    187 			]);
    188 			
    189 			// Aurora/Nightly are always named "Firefox" in
    190 			// application.ini
    191 			const appNames = ["Firefox", "Zotero"];
    192 			
    193 			// Different from Zotero.appName; this corresponds to the
    194 			// name in application.ini
    195 			const myAppName = Services.appinfo.name;
    196 
    197 			for (let appName of appNames) {
    198 				// don't send messages to ourself
    199 				if(appName === myAppName) continue;
    200 				
    201 				var thWnd = FindWindow(appName+"MessageWindow", null);
    202 				if(thWnd) {
    203 					Zotero.debug('IPC: Broadcasting "'+msg+'" to window "'+appName+'MessageWindow"');
    204 					
    205 					// allocate message
    206 					var data = ctypes.char.array()('firefox.exe -silent -ZoteroIPC "'+msg.replace('"', '""', "g")+'"\x00C:\\');
    207 					var dataSize = data.length*data.constructor.size;
    208 					
    209 					// create new COPYDATASTRUCT
    210 					var cds = new COPYDATASTRUCT();
    211 					cds.dwData = null;
    212 					cds.cbData = dataSize;
    213 					cds.lpData = data.address();
    214 					
    215 					// send COPYDATASTRUCT
    216 					var success = SendMessage(thWnd, 0x004A /** WM_COPYDATA **/, null, cds.address());
    217 					
    218 					user32.close();
    219 					return !!success;
    220 				}
    221 			}
    222 			
    223 			user32.close();
    224 			return false;
    225 		} else {			// communicate via pipes
    226 			// look for other Zotero instances
    227 			var pipes = [];
    228 			var pipeDir = _getPipeDirectory();
    229 			if(pipeDir.exists()) {
    230 				var dirEntries = pipeDir.directoryEntries;
    231 				while (dirEntries.hasMoreElements()) {
    232 					var pipe = dirEntries.getNext().QueryInterface(Ci.nsILocalFile);
    233 					if(pipe.leafName[0] !== "." && (!_instancePipe || !pipe.equals(_instancePipe))) {
    234 						pipes.push(pipe);
    235 					}
    236 				}
    237 			}
    238 			
    239 			if(!pipes.length) return false;
    240 			var success = false;
    241 			for (let pipe of pipes) {
    242 				Zotero.debug('IPC: Trying to broadcast "'+msg+'" to instance '+pipe.leafName);
    243 				
    244 				var defunct = false;
    245 				
    246 				if(pipe.isFile()) {
    247 					// not actually a pipe
    248 					if(pipe.isDirectory()) {
    249 						// not a file, so definitely defunct
    250 						defunct = true;
    251 					} else {
    252 						// check to see whether the size exceeds a certain threshold that we find
    253 						// reasonable for the queue, and if not, delete the pipe, because it's 
    254 						// probably just a file that wasn't deleted on shutdown and is now
    255 						// accumulating vast amounts of data
    256 						defunct = pipe.fileSize > 1024;
    257 					}
    258 				}
    259 				
    260 				if(!defunct) {
    261 					// Try to write to the pipe for 100 ms
    262 					var time = Date.now(), timeout = time+100, wroteToPipe;
    263 					do {
    264 						wroteToPipe = Zotero.IPC.safePipeWrite(pipe, msg+"\n");
    265 					} while(Date.now() < timeout && !wroteToPipe);
    266 					if (wroteToPipe) Zotero.debug('IPC: Pipe took '+(Date.now()-time)+' ms to become available');
    267 					success = success || wroteToPipe;
    268 					defunct = !wroteToPipe;
    269 				}
    270 				
    271 				if(defunct) {
    272 					Zotero.debug('IPC: Removing defunct pipe '+pipe.leafName);
    273 					try {
    274 						pipe.remove(true);
    275 					} catch(e) {};
    276 				}
    277 			}
    278 			
    279 			return success;
    280 		}
    281 	}
    282 	
    283 	/**
    284 	 * Get directory containing Zotero pipes
    285 	 */
    286 	function _getPipeDirectory() {
    287 		var dir = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
    288 		dir.append("pipes");
    289 		return dir;
    290 	}
    291 	
    292 	this.pipeExists = Zotero.Promise.coroutine(function* () {
    293 		var dir = _getPipeDirectory().path;
    294 		return (yield OS.File.exists(dir)) && !(yield Zotero.File.directoryIsEmpty(dir));
    295 	});
    296 	
    297 	/**
    298 	 * Gets the path to libc as a string
    299 	 */
    300 	this.getLibcPath = function() {
    301 		if(_libcPath) return _libcPath;
    302 		
    303 		Components.utils.import("resource://gre/modules/ctypes.jsm");
    304 		
    305 		// get possible names for libc
    306 		if(Zotero.isMac) {
    307 			var possibleLibcs = ["/usr/lib/libc.dylib"];
    308 		} else {
    309 			var possibleLibcs = [
    310 				"libc.so.6",
    311 				"libc.so.6.1",
    312 				"libc.so"
    313 			];
    314 		}
    315 		
    316 		// try all possibilities
    317 		while(possibleLibcs.length) {
    318 			var libPath = possibleLibcs.shift();
    319 			try {
    320 				var lib = ctypes.open(libPath);
    321 				break;
    322 			} catch(e) {}
    323 		}
    324 	
    325 		// throw appropriate error on failure
    326 		if(!lib) {
    327 			Components.utils.reportError("Zotero: libc could not be loaded. Word processor integration "+
    328 				"and other functionality will not be available. Please post on the Zotero Forums so we "+
    329 				"can add support for your operating system.");
    330 			return;
    331 		}
    332 		
    333 		_libc = lib;	
    334 		_libcPath = libPath;
    335 		return libPath;
    336 	}
    337 
    338 	/**
    339 	 * Gets standard C library via ctypes
    340 	 */
    341 	this.getLibc = function() {
    342 		if(!_libc) this.getLibcPath();
    343 		return _libc;
    344 	}
    345 }
    346 
    347 /**
    348  * Methods for reading from and writing to a pipe
    349  */
    350 Zotero.IPC.Pipe = new function() {
    351 	var _mkfifo, _pipeClass;
    352 	
    353 	/**
    354 	 * Creates and listens on a pipe
    355 	 *
    356 	 * @param {nsIFile} file The location where the pipe should be created
    357 	 * @param {Function} callback A function to be passed any data recevied on the pipe
    358 	 */
    359 	this.initPipeListener = function(file, callback) {
    360 		Zotero.debug("IPC: Initializing pipe at "+file.path);
    361 		
    362 		// determine type of pipe
    363 		if(!_pipeClass) {
    364 			var verComp = Components.classes["@mozilla.org/xpcom/version-comparator;1"]
    365 				.getService(Components.interfaces.nsIVersionComparator);
    366 			var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
    367 				getService(Components.interfaces.nsIXULAppInfo);
    368 			if(verComp.compare("2.2a1pre", appInfo.platformVersion) <= 0) {			// Gecko 5
    369 				_pipeClass = Zotero.IPC.Pipe.DeferredOpen;
    370 			}
    371 		}
    372 		
    373 		// make new pipe
    374 		new _pipeClass(file, callback);
    375 	}
    376 	
    377 	/**
    378 	 * Makes a fifo
    379 	 * @param {nsIFile}		file		Location to create the fifo
    380 	 */
    381 	this.mkfifo = function(file) {
    382 		// int mkfifo(const char *path, mode_t mode);
    383 		if(!_mkfifo) {
    384 			var libc = Zotero.IPC.getLibc();
    385 			if(!libc) return false;
    386 			_mkfifo = libc.declare("mkfifo", ctypes.default_abi, ctypes.int, ctypes.char.ptr, ctypes.unsigned_int);
    387 		}
    388 		
    389 		// make pipe
    390 		var ret = _mkfifo(file.path, 0o600);
    391 		return file.exists();
    392 	}
    393 	
    394 	/**
    395 	 * Adds a shutdown listener for a pipe that writes "Zotero shutdown\n" to the pipe and then
    396 	 * deletes it
    397 	 */
    398 	this.writeShutdownMessage = function(pipe, file) {
    399 		// Make sure pipe actually exists
    400 		if(!file.exists()) {
    401 			Zotero.debug("IPC: Not closing pipe "+file.path+": already deleted");
    402 			return;
    403 		}
    404 		
    405 		// Keep trying to write to pipe until we succeed, in case pipe is not yet open
    406 		Zotero.debug("IPC: Closing pipe "+file.path);
    407 		Zotero.IPC.safePipeWrite(file, "Zotero shutdown\n");
    408 		
    409 		// Delete pipe
    410 		file.remove(false);
    411 	}
    412 }
    413 
    414 /**
    415  * Listens asynchronously for data on the integration pipe and reads it when available
    416  * 
    417  * Used to read from pipe on Gecko 5+
    418  */
    419 Zotero.IPC.Pipe.DeferredOpen = function(file, callback) {
    420 	this._file = file;
    421 	this._callback = callback;
    422 	
    423 	if(!Zotero.IPC.Pipe.mkfifo(file)) return;
    424 	
    425 	this._initPump();
    426 	
    427 	// add shutdown listener
    428 	Zotero.addShutdownListener(Zotero.IPC.Pipe.writeShutdownMessage.bind(null, this, file));
    429 }
    430 
    431 Zotero.IPC.Pipe.DeferredOpen.prototype = {
    432 	"onStartRequest":function() {},
    433 	"onStopRequest":function() {},
    434 	"onDataAvailable":function(request, context, inputStream, offset, count) {
    435 		// read from pipe
    436 		var converterInputStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
    437 			.createInstance(Components.interfaces.nsIConverterInputStream);
    438 		converterInputStream.init(inputStream, "UTF-8", 4096,
    439 			Components.interfaces.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
    440 		var out = {};
    441 		converterInputStream.readString(count, out);
    442 		inputStream.close();
    443 		
    444 		if(out.value === "Zotero shutdown\n") return
    445 		
    446 		this._initPump();
    447 		this._callback(out.value);
    448 	},
    449 	
    450 	/**
    451 	 * Initializes the nsIInputStream and nsIInputStreamPump to read from _fifoFile
    452 	 *
    453 	 * Used after reading from file on Gecko 5+
    454 	 */
    455 	"_initPump":function() {
    456 		var fifoStream = Components.classes["@mozilla.org/network/file-input-stream;1"].
    457 			createInstance(Components.interfaces.nsIFileInputStream);
    458 		fifoStream.QueryInterface(Components.interfaces.nsIFileInputStream);
    459 		// 16 = open as deferred so that we don't block on open
    460 		fifoStream.init(this._file, -1, 0, 16);
    461 		
    462 		var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"].
    463 			createInstance(Components.interfaces.nsIInputStreamPump);
    464 		pump.init(fifoStream, -1, -1, 4096, 1, true);
    465 		pump.asyncRead(this, null);
    466 		
    467 		this._openTime = Date.now();
    468 	}
    469 };