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 };