|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 /** |
|
6 * Asynchronous front-end for OS.File. |
|
7 * |
|
8 * This front-end is meant to be imported from the main thread. In turn, |
|
9 * it spawns one worker (perhaps more in the future) and delegates all |
|
10 * disk I/O to this worker. |
|
11 * |
|
12 * Documentation note: most of the functions and methods in this module |
|
13 * return promises. For clarity, we denote as follows a promise that may resolve |
|
14 * with type |A| and some value |value| or reject with type |B| and some |
|
15 * reason |reason| |
|
16 * @resolves {A} value |
|
17 * @rejects {B} reason |
|
18 */ |
|
19 |
|
20 "use strict"; |
|
21 |
|
22 this.EXPORTED_SYMBOLS = ["OS"]; |
|
23 |
|
24 const Cu = Components.utils; |
|
25 const Ci = Components.interfaces; |
|
26 |
|
27 let SharedAll = {}; |
|
28 Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll); |
|
29 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this); |
|
30 Cu.import("resource://gre/modules/Timer.jsm", this); |
|
31 |
|
32 XPCOMUtils.defineLazyModuleGetter(this, 'Deprecated', |
|
33 'resource://gre/modules/Deprecated.jsm'); |
|
34 |
|
35 // Boilerplate, to simplify the transition to require() |
|
36 let LOG = SharedAll.LOG.bind(SharedAll, "Controller"); |
|
37 let isTypedArray = SharedAll.isTypedArray; |
|
38 |
|
39 // The constructor for file errors. |
|
40 let SysAll = {}; |
|
41 if (SharedAll.Constants.Win) { |
|
42 Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll); |
|
43 } else if (SharedAll.Constants.libc) { |
|
44 Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll); |
|
45 } else { |
|
46 throw new Error("I am neither under Windows nor under a Posix system"); |
|
47 } |
|
48 let OSError = SysAll.Error; |
|
49 let Type = SysAll.Type; |
|
50 |
|
51 let Path = {}; |
|
52 Cu.import("resource://gre/modules/osfile/ospath.jsm", Path); |
|
53 |
|
54 // The library of promises. |
|
55 Cu.import("resource://gre/modules/Promise.jsm", this); |
|
56 Cu.import("resource://gre/modules/Task.jsm", this); |
|
57 |
|
58 // The implementation of communications |
|
59 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); |
|
60 |
|
61 Cu.import("resource://gre/modules/Services.jsm", this); |
|
62 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this); |
|
63 Cu.import("resource://gre/modules/AsyncShutdown.jsm", this); |
|
64 let Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {}); |
|
65 |
|
66 /** |
|
67 * Constructors for decoding standard exceptions |
|
68 * received from the worker. |
|
69 */ |
|
70 const EXCEPTION_CONSTRUCTORS = { |
|
71 EvalError: function(error) { |
|
72 return new EvalError(error.message, error.fileName, error.lineNumber); |
|
73 }, |
|
74 InternalError: function(error) { |
|
75 return new InternalError(error.message, error.fileName, error.lineNumber); |
|
76 }, |
|
77 RangeError: function(error) { |
|
78 return new RangeError(error.message, error.fileName, error.lineNumber); |
|
79 }, |
|
80 ReferenceError: function(error) { |
|
81 return new ReferenceError(error.message, error.fileName, error.lineNumber); |
|
82 }, |
|
83 SyntaxError: function(error) { |
|
84 return new SyntaxError(error.message, error.fileName, error.lineNumber); |
|
85 }, |
|
86 TypeError: function(error) { |
|
87 return new TypeError(error.message, error.fileName, error.lineNumber); |
|
88 }, |
|
89 URIError: function(error) { |
|
90 return new URIError(error.message, error.fileName, error.lineNumber); |
|
91 }, |
|
92 OSError: function(error) { |
|
93 return OS.File.Error.fromMsg(error); |
|
94 } |
|
95 }; |
|
96 |
|
97 // It's possible for osfile.jsm to get imported before the profile is |
|
98 // set up. In this case, some path constants aren't yet available. |
|
99 // Here, we make them lazy loaders. |
|
100 |
|
101 function lazyPathGetter(constProp, dirKey) { |
|
102 return function () { |
|
103 let path; |
|
104 try { |
|
105 path = Services.dirsvc.get(dirKey, Ci.nsIFile).path; |
|
106 delete SharedAll.Constants.Path[constProp]; |
|
107 SharedAll.Constants.Path[constProp] = path; |
|
108 } catch (ex) { |
|
109 // Ignore errors if the value still isn't available. Hopefully |
|
110 // the next access will return it. |
|
111 } |
|
112 |
|
113 return path; |
|
114 }; |
|
115 } |
|
116 |
|
117 for (let [constProp, dirKey] of [ |
|
118 ["localProfileDir", "ProfLD"], |
|
119 ["profileDir", "ProfD"], |
|
120 ["userApplicationDataDir", "UAppData"], |
|
121 ["winAppDataDir", "AppData"], |
|
122 ["winStartMenuProgsDir", "Progs"], |
|
123 ]) { |
|
124 |
|
125 if (constProp in SharedAll.Constants.Path) { |
|
126 continue; |
|
127 } |
|
128 |
|
129 LOG("Installing lazy getter for OS.Constants.Path." + constProp + |
|
130 " because it isn't defined and profile may not be loaded."); |
|
131 Object.defineProperty(SharedAll.Constants.Path, constProp, { |
|
132 get: lazyPathGetter(constProp, dirKey), |
|
133 }); |
|
134 } |
|
135 |
|
136 /** |
|
137 * Return a shallow clone of the enumerable properties of an object. |
|
138 */ |
|
139 let clone = SharedAll.clone; |
|
140 |
|
141 /** |
|
142 * Extract a shortened version of an object, fit for logging. |
|
143 * |
|
144 * This function returns a copy of the original object in which all |
|
145 * long strings, Arrays, TypedArrays, ArrayBuffers are removed and |
|
146 * replaced with placeholders. Use this function to sanitize objects |
|
147 * if you wish to log them or to keep them in memory. |
|
148 * |
|
149 * @param {*} obj The obj to shorten. |
|
150 * @return {*} array A shorter object, fit for logging. |
|
151 */ |
|
152 function summarizeObject(obj) { |
|
153 if (!obj) { |
|
154 return null; |
|
155 } |
|
156 if (typeof obj == "string") { |
|
157 if (obj.length > 1024) { |
|
158 return {"Long string": obj.length}; |
|
159 } |
|
160 return obj; |
|
161 } |
|
162 if (typeof obj == "object") { |
|
163 if (Array.isArray(obj)) { |
|
164 if (obj.length > 32) { |
|
165 return {"Long array": obj.length}; |
|
166 } |
|
167 return [summarizeObject(k) for (k of obj)]; |
|
168 } |
|
169 if ("byteLength" in obj) { |
|
170 // Assume TypedArray or ArrayBuffer |
|
171 return {"Binary Data": obj.byteLength}; |
|
172 } |
|
173 let result = {}; |
|
174 for (let k of Object.keys(obj)) { |
|
175 result[k] = summarizeObject(obj[k]); |
|
176 } |
|
177 return result; |
|
178 } |
|
179 return obj; |
|
180 } |
|
181 |
|
182 let Scheduler = { |
|
183 |
|
184 /** |
|
185 * |true| once we have sent at least one message to the worker. |
|
186 * This field is unaffected by resetting the worker. |
|
187 */ |
|
188 launched: false, |
|
189 |
|
190 /** |
|
191 * |true| once shutdown has begun i.e. we should reject any |
|
192 * message, including resets. |
|
193 */ |
|
194 shutdown: false, |
|
195 |
|
196 /** |
|
197 * A promise resolved once all operations are complete. |
|
198 * |
|
199 * This promise is never rejected and the result is always undefined. |
|
200 */ |
|
201 queue: Promise.resolve(), |
|
202 |
|
203 /** |
|
204 * Miscellaneous debugging information |
|
205 */ |
|
206 Debugging: { |
|
207 /** |
|
208 * The latest message sent and still waiting for a reply. |
|
209 */ |
|
210 latestSent: undefined, |
|
211 |
|
212 /** |
|
213 * The latest reply received, or null if we are waiting for a reply. |
|
214 */ |
|
215 latestReceived: undefined, |
|
216 |
|
217 /** |
|
218 * Number of messages sent to the worker. This includes the |
|
219 * initial SET_DEBUG, if applicable. |
|
220 */ |
|
221 messagesSent: 0, |
|
222 |
|
223 /** |
|
224 * Total number of messages ever queued, including the messages |
|
225 * sent. |
|
226 */ |
|
227 messagesQueued: 0, |
|
228 |
|
229 /** |
|
230 * Number of messages received from the worker. |
|
231 */ |
|
232 messagesReceived: 0, |
|
233 }, |
|
234 |
|
235 /** |
|
236 * A timer used to automatically shut down the worker after some time. |
|
237 */ |
|
238 resetTimer: null, |
|
239 |
|
240 /** |
|
241 * The worker to which to send requests. |
|
242 * |
|
243 * If the worker has never been created or has been reset, this is a |
|
244 * fresh worker, initialized with osfile_async_worker.js. |
|
245 * |
|
246 * @type {PromiseWorker} |
|
247 */ |
|
248 get worker() { |
|
249 if (!this._worker) { |
|
250 // Either the worker has never been created or it has been reset |
|
251 this._worker = new PromiseWorker( |
|
252 "resource://gre/modules/osfile/osfile_async_worker.js", LOG); |
|
253 } |
|
254 return this._worker; |
|
255 }, |
|
256 |
|
257 _worker: null, |
|
258 |
|
259 /** |
|
260 * Prepare to kill the OS.File worker after a few seconds. |
|
261 */ |
|
262 restartTimer: function(arg) { |
|
263 let delay; |
|
264 try { |
|
265 delay = Services.prefs.getIntPref("osfile.reset_worker_delay"); |
|
266 } catch(e) { |
|
267 // Don't auto-shutdown if we don't have a delay preference set. |
|
268 return; |
|
269 } |
|
270 |
|
271 if (this.resetTimer) { |
|
272 clearTimeout(this.resetTimer); |
|
273 } |
|
274 this.resetTimer = setTimeout( |
|
275 () => Scheduler.kill({reset: true, shutdown: false}), |
|
276 delay |
|
277 ); |
|
278 }, |
|
279 |
|
280 /** |
|
281 * Shutdown OS.File. |
|
282 * |
|
283 * @param {*} options |
|
284 * - {boolean} shutdown If |true|, reject any further request. Otherwise, |
|
285 * further requests will resurrect the worker. |
|
286 * - {boolean} reset If |true|, instruct the worker to shutdown if this |
|
287 * would not cause leaks. Otherwise, assume that the worker will be shutdown |
|
288 * through some other mean. |
|
289 */ |
|
290 kill: function({shutdown, reset}) { |
|
291 return Task.spawn(function*() { |
|
292 |
|
293 yield this.queue; |
|
294 |
|
295 // Enter critical section: no yield in this block |
|
296 // (we want to make sure that we remain the only |
|
297 // request in the queue). |
|
298 |
|
299 if (!this.launched || this.shutdown || !this._worker) { |
|
300 // Nothing to kill |
|
301 this.shutdown = this.shutdown || shutdown; |
|
302 this._worker = null; |
|
303 return null; |
|
304 } |
|
305 |
|
306 // Deactivate the queue, to ensure that no message is sent |
|
307 // to an obsolete worker (we reactivate it in the |finally|). |
|
308 let deferred = Promise.defer(); |
|
309 this.queue = deferred.promise; |
|
310 |
|
311 |
|
312 // Exit critical section |
|
313 |
|
314 let message = ["Meta_shutdown", [reset]]; |
|
315 |
|
316 try { |
|
317 Scheduler.latestReceived = []; |
|
318 Scheduler.latestSent = [Date.now(), ...message]; |
|
319 let promise = this._worker.post(...message); |
|
320 |
|
321 // Wait for result |
|
322 let resources; |
|
323 try { |
|
324 resources = (yield promise).ok; |
|
325 |
|
326 Scheduler.latestReceived = [Date.now(), message]; |
|
327 } catch (ex) { |
|
328 LOG("Could not dispatch Meta_reset", ex); |
|
329 // It's most likely a programmer error, but we'll assume that |
|
330 // the worker has been shutdown, as it's less risky than the |
|
331 // opposite stance. |
|
332 resources = {openedFiles: [], openedDirectoryIterators: [], killed: true}; |
|
333 |
|
334 Scheduler.latestReceived = [Date.now(), message, ex]; |
|
335 } |
|
336 |
|
337 let {openedFiles, openedDirectoryIterators, killed} = resources; |
|
338 if (!reset |
|
339 && (openedFiles && openedFiles.length |
|
340 || ( openedDirectoryIterators && openedDirectoryIterators.length))) { |
|
341 // The worker still holds resources. Report them. |
|
342 |
|
343 let msg = ""; |
|
344 if (openedFiles.length > 0) { |
|
345 msg += "The following files are still open:\n" + |
|
346 openedFiles.join("\n"); |
|
347 } |
|
348 if (openedDirectoryIterators.length > 0) { |
|
349 msg += "The following directory iterators are still open:\n" + |
|
350 openedDirectoryIterators.join("\n"); |
|
351 } |
|
352 |
|
353 LOG("WARNING: File descriptors leaks detected.\n" + msg); |
|
354 } |
|
355 |
|
356 // Make sure that we do not leave an invalid |worker| around. |
|
357 if (killed || shutdown) { |
|
358 this._worker = null; |
|
359 } |
|
360 |
|
361 this.shutdown = shutdown; |
|
362 |
|
363 return resources; |
|
364 |
|
365 } finally { |
|
366 // Resume accepting messages. If we have set |shutdown| to |true|, |
|
367 // any pending/future request will be rejected. Otherwise, any |
|
368 // pending/future request will spawn a new worker if necessary. |
|
369 deferred.resolve(); |
|
370 } |
|
371 |
|
372 }.bind(this)); |
|
373 }, |
|
374 |
|
375 /** |
|
376 * Push a task at the end of the queue. |
|
377 * |
|
378 * @param {function} code A function returning a Promise. |
|
379 * This function will be executed once all the previously |
|
380 * pushed tasks have completed. |
|
381 * @return {Promise} A promise with the same behavior as |
|
382 * the promise returned by |code|. |
|
383 */ |
|
384 push: function(code) { |
|
385 let promise = this.queue.then(code); |
|
386 // By definition, |this.queue| can never reject. |
|
387 this.queue = promise.then(null, () => undefined); |
|
388 // Fork |promise| to ensure that uncaught errors are reported |
|
389 return promise.then(null, null); |
|
390 }, |
|
391 |
|
392 /** |
|
393 * Post a message to the worker thread. |
|
394 * |
|
395 * @param {string} method The name of the method to call. |
|
396 * @param {...} args The arguments to pass to the method. These arguments |
|
397 * must be clonable. |
|
398 * @return {Promise} A promise conveying the result/error caused by |
|
399 * calling |method| with arguments |args|. |
|
400 */ |
|
401 post: function post(method, ...args) { |
|
402 if (this.shutdown) { |
|
403 LOG("OS.File is not available anymore. The following request has been rejected.", |
|
404 method, args); |
|
405 return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method)); |
|
406 } |
|
407 let firstLaunch = !this.launched; |
|
408 this.launched = true; |
|
409 |
|
410 if (firstLaunch && SharedAll.Config.DEBUG) { |
|
411 // If we have delayed sending SET_DEBUG, do it now. |
|
412 this.worker.post("SET_DEBUG", [true]); |
|
413 Scheduler.Debugging.messagesSent++; |
|
414 } |
|
415 |
|
416 // By convention, the last argument of any message may be an |options| object. |
|
417 let options; |
|
418 let methodArgs = args[0]; |
|
419 if (methodArgs) { |
|
420 options = methodArgs[methodArgs.length - 1]; |
|
421 } |
|
422 Scheduler.Debugging.messagesQueued++; |
|
423 return this.push(Task.async(function*() { |
|
424 if (this.shutdown) { |
|
425 LOG("OS.File is not available anymore. The following request has been rejected.", |
|
426 method, args); |
|
427 throw new Error("OS.File has been shut down. Rejecting request to " + method); |
|
428 } |
|
429 |
|
430 // Update debugging information. As |args| may be quite |
|
431 // expensive, we only keep a shortened version of it. |
|
432 Scheduler.Debugging.latestReceived = null; |
|
433 Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(methodArgs)]; |
|
434 |
|
435 // Don't kill the worker just yet |
|
436 Scheduler.restartTimer(); |
|
437 |
|
438 |
|
439 let data; |
|
440 let reply; |
|
441 let isError = false; |
|
442 try { |
|
443 try { |
|
444 data = yield this.worker.post(method, ...args); |
|
445 } finally { |
|
446 Scheduler.Debugging.messagesReceived++; |
|
447 } |
|
448 reply = data; |
|
449 } catch (error) { |
|
450 reply = error; |
|
451 isError = true; |
|
452 if (error instanceof PromiseWorker.WorkerError) { |
|
453 throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data); |
|
454 } |
|
455 if (error instanceof ErrorEvent) { |
|
456 let message = error.message; |
|
457 if (message == "uncaught exception: [object StopIteration]") { |
|
458 isError = false; |
|
459 throw StopIteration; |
|
460 } |
|
461 throw new Error(message, error.filename, error.lineno); |
|
462 } |
|
463 throw error; |
|
464 } finally { |
|
465 Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2); |
|
466 if (isError) { |
|
467 Scheduler.Debugging.latestReceived = [Date.now(), reply.message, reply.fileName, reply.lineNumber]; |
|
468 } else { |
|
469 Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)]; |
|
470 } |
|
471 if (firstLaunch) { |
|
472 Scheduler._updateTelemetry(); |
|
473 } |
|
474 |
|
475 Scheduler.restartTimer(); |
|
476 } |
|
477 |
|
478 // Check for duration and return result. |
|
479 if (!options) { |
|
480 return data.ok; |
|
481 } |
|
482 // Check for options.outExecutionDuration. |
|
483 if (typeof options !== "object" || |
|
484 !("outExecutionDuration" in options)) { |
|
485 return data.ok; |
|
486 } |
|
487 // If data.durationMs is not present, return data.ok (there was an |
|
488 // exception applying the method). |
|
489 if (!("durationMs" in data)) { |
|
490 return data.ok; |
|
491 } |
|
492 // Bug 874425 demonstrates that two successive calls to Date.now() |
|
493 // can actually produce an interval with negative duration. |
|
494 // We assume that this is due to an operation that is so short |
|
495 // that Date.now() is not monotonic, so we round this up to 0. |
|
496 let durationMs = Math.max(0, data.durationMs); |
|
497 // Accumulate (or initialize) outExecutionDuration |
|
498 if (typeof options.outExecutionDuration == "number") { |
|
499 options.outExecutionDuration += durationMs; |
|
500 } else { |
|
501 options.outExecutionDuration = durationMs; |
|
502 } |
|
503 return data.ok; |
|
504 }.bind(this))); |
|
505 }, |
|
506 |
|
507 /** |
|
508 * Post Telemetry statistics. |
|
509 * |
|
510 * This is only useful on first launch. |
|
511 */ |
|
512 _updateTelemetry: function() { |
|
513 let worker = this.worker; |
|
514 let workerTimeStamps = worker.workerTimeStamps; |
|
515 if (!workerTimeStamps) { |
|
516 // If the first call to OS.File results in an uncaught errors, |
|
517 // the timestamps are absent. As this case is a developer error, |
|
518 // let's not waste time attempting to extract telemetry from it. |
|
519 return; |
|
520 } |
|
521 let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS"); |
|
522 HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp); |
|
523 |
|
524 let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS"); |
|
525 HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp); |
|
526 } |
|
527 }; |
|
528 |
|
529 const PREF_OSFILE_LOG = "toolkit.osfile.log"; |
|
530 const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect"; |
|
531 |
|
532 /** |
|
533 * Safely read a PREF_OSFILE_LOG preference. |
|
534 * Returns a value read or, in case of an error, oldPref or false. |
|
535 * |
|
536 * @param bool oldPref |
|
537 * An optional value that the DEBUG flag was set to previously. |
|
538 */ |
|
539 function readDebugPref(prefName, oldPref = false) { |
|
540 let pref = oldPref; |
|
541 try { |
|
542 pref = Services.prefs.getBoolPref(prefName); |
|
543 } catch (x) { |
|
544 // In case of an error when reading a pref keep it as is. |
|
545 } |
|
546 // If neither pref nor oldPref were set, default it to false. |
|
547 return pref; |
|
548 }; |
|
549 |
|
550 /** |
|
551 * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag |
|
552 * appropriately. |
|
553 */ |
|
554 Services.prefs.addObserver(PREF_OSFILE_LOG, |
|
555 function prefObserver(aSubject, aTopic, aData) { |
|
556 SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG); |
|
557 if (Scheduler.launched) { |
|
558 // Don't start the worker just to set this preference. |
|
559 Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]); |
|
560 } |
|
561 }, false); |
|
562 SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false); |
|
563 |
|
564 Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT, |
|
565 function prefObserver(aSubject, aTopic, aData) { |
|
566 SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST); |
|
567 }, false); |
|
568 SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false); |
|
569 |
|
570 |
|
571 /** |
|
572 * If |true|, use the native implementaiton of OS.File methods |
|
573 * whenever possible. Otherwise, force the use of the JS version. |
|
574 */ |
|
575 let nativeWheneverAvailable = true; |
|
576 const PREF_OSFILE_NATIVE = "toolkit.osfile.native"; |
|
577 Services.prefs.addObserver(PREF_OSFILE_NATIVE, |
|
578 function prefObserver(aSubject, aTopic, aData) { |
|
579 nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable); |
|
580 }, false); |
|
581 |
|
582 |
|
583 // Update worker's DEBUG flag if it's true. |
|
584 // Don't start the worker just for this, though. |
|
585 if (SharedAll.Config.DEBUG && Scheduler.launched) { |
|
586 Scheduler.post("SET_DEBUG", [true]); |
|
587 } |
|
588 |
|
589 // Observer topics used for monitoring shutdown |
|
590 const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown"; |
|
591 |
|
592 // Preference used to configure test shutdown observer. |
|
593 const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER = |
|
594 "toolkit.osfile.test.shutdown.observer"; |
|
595 |
|
596 AsyncShutdown.webWorkersShutdown.addBlocker( |
|
597 "OS.File: flush pending requests, warn about unclosed files, shut down service.", |
|
598 () => Scheduler.kill({reset: false, shutdown: true}) |
|
599 ); |
|
600 |
|
601 |
|
602 // Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or |
|
603 // disable the test shutdown event observer. |
|
604 // Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset. |
|
605 // Note: This is meant to be used for testing purposes only. |
|
606 Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER, |
|
607 function prefObserver() { |
|
608 // The temporary phase topic used to trigger the unclosed |
|
609 // phase warning. |
|
610 let TOPIC = null; |
|
611 try { |
|
612 TOPIC = Services.prefs.getCharPref( |
|
613 PREF_OSFILE_TEST_SHUTDOWN_OBSERVER); |
|
614 } catch (x) { |
|
615 } |
|
616 if (TOPIC) { |
|
617 // Generate a phase, add a blocker. |
|
618 // Note that this can work only if AsyncShutdown itself has been |
|
619 // configured for testing by the testsuite. |
|
620 let phase = AsyncShutdown._getPhase(TOPIC); |
|
621 phase.addBlocker( |
|
622 "(for testing purposes) OS.File: warn about unclosed files", |
|
623 () => Scheduler.kill({shutdown: false, reset: false}) |
|
624 ); |
|
625 } |
|
626 }, false); |
|
627 |
|
628 /** |
|
629 * Representation of a file, with asynchronous methods. |
|
630 * |
|
631 * @param {*} fdmsg The _message_ representing the platform-specific file |
|
632 * handle. |
|
633 * |
|
634 * @constructor |
|
635 */ |
|
636 let File = function File(fdmsg) { |
|
637 // FIXME: At the moment, |File| does not close on finalize |
|
638 // (see bug 777715) |
|
639 this._fdmsg = fdmsg; |
|
640 this._closeResult = null; |
|
641 this._closed = null; |
|
642 }; |
|
643 |
|
644 |
|
645 File.prototype = { |
|
646 /** |
|
647 * Close a file asynchronously. |
|
648 * |
|
649 * This method is idempotent. |
|
650 * |
|
651 * @return {promise} |
|
652 * @resolves {null} |
|
653 * @rejects {OS.File.Error} |
|
654 */ |
|
655 close: function close() { |
|
656 if (this._fdmsg != null) { |
|
657 let msg = this._fdmsg; |
|
658 this._fdmsg = null; |
|
659 return this._closeResult = |
|
660 Scheduler.post("File_prototype_close", [msg], this); |
|
661 } |
|
662 return this._closeResult; |
|
663 }, |
|
664 |
|
665 /** |
|
666 * Fetch information about the file. |
|
667 * |
|
668 * @return {promise} |
|
669 * @resolves {OS.File.Info} The latest information about the file. |
|
670 * @rejects {OS.File.Error} |
|
671 */ |
|
672 stat: function stat() { |
|
673 return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then( |
|
674 File.Info.fromMsg |
|
675 ); |
|
676 }, |
|
677 |
|
678 /** |
|
679 * Set the last access and modification date of the file. |
|
680 * The time stamp resolution is 1 second at best, but might be worse |
|
681 * depending on the platform. |
|
682 * |
|
683 * @return {promise} |
|
684 * @rejects {TypeError} |
|
685 * @rejects {OS.File.Error} |
|
686 */ |
|
687 setDates: function setDates(accessDate, modificationDate) { |
|
688 return Scheduler.post("File_prototype_setDates", |
|
689 [this._fdmsg, accessDate, modificationDate], this); |
|
690 }, |
|
691 |
|
692 /** |
|
693 * Read a number of bytes from the file and into a buffer. |
|
694 * |
|
695 * @param {Typed array | C pointer} buffer This buffer will be |
|
696 * modified by another thread. Using this buffer before the |read| |
|
697 * operation has completed is a BAD IDEA. |
|
698 * @param {JSON} options |
|
699 * |
|
700 * @return {promise} |
|
701 * @resolves {number} The number of bytes effectively read. |
|
702 * @rejects {OS.File.Error} |
|
703 */ |
|
704 readTo: function readTo(buffer, options = {}) { |
|
705 // If |buffer| is a typed array and there is no |bytes| options, we |
|
706 // need to extract the |byteLength| now, as it will be lost by |
|
707 // communication. |
|
708 // Options might be a nullish value, so better check for that before using |
|
709 // the |in| operator. |
|
710 if (isTypedArray(buffer) && !(options && "bytes" in options)) { |
|
711 // Preserve reference to option |outExecutionDuration|, if it is passed. |
|
712 options = clone(options, ["outExecutionDuration"]); |
|
713 options.bytes = buffer.byteLength; |
|
714 } |
|
715 // Note: Type.void_t.out_ptr.toMsg ensures that |
|
716 // - the buffer is effectively shared (not neutered) between both |
|
717 // threads; |
|
718 // - we take care of any |byteOffset|. |
|
719 return Scheduler.post("File_prototype_readTo", |
|
720 [this._fdmsg, |
|
721 Type.void_t.out_ptr.toMsg(buffer), |
|
722 options], |
|
723 buffer/*Ensure that |buffer| is not gc-ed*/); |
|
724 }, |
|
725 /** |
|
726 * Write bytes from a buffer to this file. |
|
727 * |
|
728 * Note that, by default, this function may perform several I/O |
|
729 * operations to ensure that the buffer is fully written. |
|
730 * |
|
731 * @param {Typed array | C pointer} buffer The buffer in which the |
|
732 * the bytes are stored. The buffer must be large enough to |
|
733 * accomodate |bytes| bytes. Using the buffer before the operation |
|
734 * is complete is a BAD IDEA. |
|
735 * @param {*=} options Optionally, an object that may contain the |
|
736 * following fields: |
|
737 * - {number} bytes The number of |bytes| to write from the buffer. If |
|
738 * unspecified, this is |buffer.byteLength|. Note that |bytes| is required |
|
739 * if |buffer| is a C pointer. |
|
740 * |
|
741 * @return {number} The number of bytes actually written. |
|
742 */ |
|
743 write: function write(buffer, options = {}) { |
|
744 // If |buffer| is a typed array and there is no |bytes| options, |
|
745 // we need to extract the |byteLength| now, as it will be lost |
|
746 // by communication. |
|
747 // Options might be a nullish value, so better check for that before using |
|
748 // the |in| operator. |
|
749 if (isTypedArray(buffer) && !(options && "bytes" in options)) { |
|
750 // Preserve reference to option |outExecutionDuration|, if it is passed. |
|
751 options = clone(options, ["outExecutionDuration"]); |
|
752 options.bytes = buffer.byteLength; |
|
753 } |
|
754 // Note: Type.void_t.out_ptr.toMsg ensures that |
|
755 // - the buffer is effectively shared (not neutered) between both |
|
756 // threads; |
|
757 // - we take care of any |byteOffset|. |
|
758 return Scheduler.post("File_prototype_write", |
|
759 [this._fdmsg, |
|
760 Type.void_t.in_ptr.toMsg(buffer), |
|
761 options], |
|
762 buffer/*Ensure that |buffer| is not gc-ed*/); |
|
763 }, |
|
764 |
|
765 /** |
|
766 * Read bytes from this file to a new buffer. |
|
767 * |
|
768 * @param {number=} bytes If unspecified, read all the remaining bytes from |
|
769 * this file. If specified, read |bytes| bytes, or less if the file does not |
|
770 * contain that many bytes. |
|
771 * @param {JSON} options |
|
772 * @return {promise} |
|
773 * @resolves {Uint8Array} An array containing the bytes read. |
|
774 */ |
|
775 read: function read(nbytes, options = {}) { |
|
776 let promise = Scheduler.post("File_prototype_read", |
|
777 [this._fdmsg, |
|
778 nbytes, options]); |
|
779 return promise.then( |
|
780 function onSuccess(data) { |
|
781 return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); |
|
782 }); |
|
783 }, |
|
784 |
|
785 /** |
|
786 * Return the current position in the file, as bytes. |
|
787 * |
|
788 * @return {promise} |
|
789 * @resolves {number} The current position in the file, |
|
790 * as a number of bytes since the start of the file. |
|
791 */ |
|
792 getPosition: function getPosition() { |
|
793 return Scheduler.post("File_prototype_getPosition", |
|
794 [this._fdmsg]); |
|
795 }, |
|
796 |
|
797 /** |
|
798 * Set the current position in the file, as bytes. |
|
799 * |
|
800 * @param {number} pos A number of bytes. |
|
801 * @param {number} whence The reference position in the file, |
|
802 * which may be either POS_START (from the start of the file), |
|
803 * POS_END (from the end of the file) or POS_CUR (from the |
|
804 * current position in the file). |
|
805 * |
|
806 * @return {promise} |
|
807 */ |
|
808 setPosition: function setPosition(pos, whence) { |
|
809 return Scheduler.post("File_prototype_setPosition", |
|
810 [this._fdmsg, pos, whence]); |
|
811 }, |
|
812 |
|
813 /** |
|
814 * Flushes the file's buffers and causes all buffered data |
|
815 * to be written. |
|
816 * Disk flushes are very expensive and therefore should be used carefully, |
|
817 * sparingly and only in scenarios where it is vital that data survives |
|
818 * system crashes. Even though the function will be executed off the |
|
819 * main-thread, it might still affect the overall performance of any running |
|
820 * application. |
|
821 * |
|
822 * @return {promise} |
|
823 */ |
|
824 flush: function flush() { |
|
825 return Scheduler.post("File_prototype_flush", |
|
826 [this._fdmsg]); |
|
827 }, |
|
828 |
|
829 /** |
|
830 * Set the file's access permissions. Without any options, the |
|
831 * permissions are set to an approximation of what they would have |
|
832 * been if the file had been created in its current directory in the |
|
833 * "most typical" fashion for the operating system. In the current |
|
834 * implementation, this means that on Unix-like systems (including |
|
835 * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask), |
|
836 * and on Windows, we do nothing. |
|
837 * |
|
838 * This operation is likely to fail if applied to a file that was |
|
839 * not created by the currently running program (more precisely, |
|
840 * if it was created by a program running under a different OS-level |
|
841 * user account). It may also fail, or silently do nothing, if the |
|
842 * filesystem containing the file does not support access permissions. |
|
843 * |
|
844 * @param {*=} options |
|
845 * - {number} unixMode If present, the POSIX file mode is set to exactly |
|
846 * this value, unless |unixHonorUmask| is also |
|
847 * present. |
|
848 * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the |
|
849 * process umask, as open() would have done. |
|
850 */ |
|
851 setPermissions: function setPermissions(options = {}) { |
|
852 return Scheduler.post("File_prototype_setPermissions", |
|
853 [this._fdmsg, options]); |
|
854 } |
|
855 }; |
|
856 |
|
857 /** |
|
858 * Open a file asynchronously. |
|
859 * |
|
860 * @return {promise} |
|
861 * @resolves {OS.File} |
|
862 * @rejects {OS.Error} |
|
863 */ |
|
864 File.open = function open(path, mode, options) { |
|
865 return Scheduler.post( |
|
866 "open", [Type.path.toMsg(path), mode, options], |
|
867 path |
|
868 ).then( |
|
869 function onSuccess(msg) { |
|
870 return new File(msg); |
|
871 } |
|
872 ); |
|
873 }; |
|
874 |
|
875 /** |
|
876 * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name. |
|
877 * |
|
878 * @param {string} path The path to the file. |
|
879 * @param {*=} options Additional options for file opening. This |
|
880 * implementation interprets the following fields: |
|
881 * |
|
882 * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. |
|
883 * If |false| use HEX numbers ie: filename-A65BC0.ext |
|
884 * - {number} maxReadableNumber Used to limit the amount of tries after a failed |
|
885 * file creation. Default is 20. |
|
886 * |
|
887 * @return {Object} contains A file object{file} and the path{path}. |
|
888 * @throws {OS.File.Error} If the file could not be opened. |
|
889 */ |
|
890 File.openUnique = function openUnique(path, options) { |
|
891 return Scheduler.post( |
|
892 "openUnique", [Type.path.toMsg(path), options], |
|
893 path |
|
894 ).then( |
|
895 function onSuccess(msg) { |
|
896 return { |
|
897 path: msg.path, |
|
898 file: new File(msg.file) |
|
899 }; |
|
900 } |
|
901 ); |
|
902 }; |
|
903 |
|
904 /** |
|
905 * Get the information on the file. |
|
906 * |
|
907 * @return {promise} |
|
908 * @resolves {OS.File.Info} |
|
909 * @rejects {OS.Error} |
|
910 */ |
|
911 File.stat = function stat(path, options) { |
|
912 return Scheduler.post( |
|
913 "stat", [Type.path.toMsg(path), options], |
|
914 path).then(File.Info.fromMsg); |
|
915 }; |
|
916 |
|
917 |
|
918 /** |
|
919 * Set the last access and modification date of the file. |
|
920 * The time stamp resolution is 1 second at best, but might be worse |
|
921 * depending on the platform. |
|
922 * |
|
923 * @return {promise} |
|
924 * @rejects {TypeError} |
|
925 * @rejects {OS.File.Error} |
|
926 */ |
|
927 File.setDates = function setDates(path, accessDate, modificationDate) { |
|
928 return Scheduler.post("setDates", |
|
929 [Type.path.toMsg(path), accessDate, modificationDate], |
|
930 this); |
|
931 }; |
|
932 |
|
933 /** |
|
934 * Set the file's access permissions. Without any options, the |
|
935 * permissions are set to an approximation of what they would have |
|
936 * been if the file had been created in its current directory in the |
|
937 * "most typical" fashion for the operating system. In the current |
|
938 * implementation, this means that on Unix-like systems (including |
|
939 * Android, B2G, etc) we set the POSIX file mode to (0666 & ~umask), |
|
940 * and on Windows, we do nothing. |
|
941 * |
|
942 * This operation is likely to fail if applied to a file that was |
|
943 * not created by the currently running program (more precisely, |
|
944 * if it was created by a program running under a different OS-level |
|
945 * user account). It may also fail, or silently do nothing, if the |
|
946 * filesystem containing the file does not support access permissions. |
|
947 * |
|
948 * @param {string} path The path to the file. |
|
949 * |
|
950 * @param {*=} options |
|
951 * - {number} unixMode If present, the POSIX file mode is set to exactly |
|
952 * this value, unless |unixHonorUmask| is also |
|
953 * present. |
|
954 * - {bool} unixHonorUmask If true, any |unixMode| value is modified by the |
|
955 * process umask, as open() would have done. |
|
956 */ |
|
957 File.setPermissions = function setPermissions(path, options = {}) { |
|
958 return Scheduler.post("setPermissions", |
|
959 [Type.path.toMsg(path), options]); |
|
960 }; |
|
961 |
|
962 /** |
|
963 * Fetch the current directory |
|
964 * |
|
965 * @return {promise} |
|
966 * @resolves {string} The current directory, as a path usable with OS.Path |
|
967 * @rejects {OS.Error} |
|
968 */ |
|
969 File.getCurrentDirectory = function getCurrentDirectory() { |
|
970 return Scheduler.post( |
|
971 "getCurrentDirectory" |
|
972 ).then(Type.path.fromMsg); |
|
973 }; |
|
974 |
|
975 /** |
|
976 * Change the current directory |
|
977 * |
|
978 * @param {string} path The OS-specific path to the current directory. |
|
979 * You should use the methods of OS.Path and the constants of OS.Constants.Path |
|
980 * to build OS-specific paths in a portable manner. |
|
981 * |
|
982 * @return {promise} |
|
983 * @resolves {null} |
|
984 * @rejects {OS.Error} |
|
985 */ |
|
986 File.setCurrentDirectory = function setCurrentDirectory(path) { |
|
987 return Scheduler.post( |
|
988 "setCurrentDirectory", [Type.path.toMsg(path)], path |
|
989 ); |
|
990 }; |
|
991 |
|
992 /** |
|
993 * Copy a file to a destination. |
|
994 * |
|
995 * @param {string} sourcePath The platform-specific path at which |
|
996 * the file may currently be found. |
|
997 * @param {string} destPath The platform-specific path at which the |
|
998 * file should be copied. |
|
999 * @param {*=} options An object which may contain the following fields: |
|
1000 * |
|
1001 * @option {bool} noOverwrite - If true, this function will fail if |
|
1002 * a file already exists at |destPath|. Otherwise, if this file exists, |
|
1003 * it will be erased silently. |
|
1004 * |
|
1005 * @rejects {OS.File.Error} In case of any error. |
|
1006 * |
|
1007 * General note: The behavior of this function is defined only when |
|
1008 * it is called on a single file. If it is called on a directory, the |
|
1009 * behavior is undefined and may not be the same across all platforms. |
|
1010 * |
|
1011 * General note: The behavior of this function with respect to metadata |
|
1012 * is unspecified. Metadata may or may not be copied with the file. The |
|
1013 * behavior may not be the same across all platforms. |
|
1014 */ |
|
1015 File.copy = function copy(sourcePath, destPath, options) { |
|
1016 return Scheduler.post("copy", [Type.path.toMsg(sourcePath), |
|
1017 Type.path.toMsg(destPath), options], [sourcePath, destPath]); |
|
1018 }; |
|
1019 |
|
1020 /** |
|
1021 * Move a file to a destination. |
|
1022 * |
|
1023 * @param {string} sourcePath The platform-specific path at which |
|
1024 * the file may currently be found. |
|
1025 * @param {string} destPath The platform-specific path at which the |
|
1026 * file should be moved. |
|
1027 * @param {*=} options An object which may contain the following fields: |
|
1028 * |
|
1029 * @option {bool} noOverwrite - If set, this function will fail if |
|
1030 * a file already exists at |destPath|. Otherwise, if this file exists, |
|
1031 * it will be erased silently. |
|
1032 * |
|
1033 * @returns {Promise} |
|
1034 * @rejects {OS.File.Error} In case of any error. |
|
1035 * |
|
1036 * General note: The behavior of this function is defined only when |
|
1037 * it is called on a single file. If it is called on a directory, the |
|
1038 * behavior is undefined and may not be the same across all platforms. |
|
1039 * |
|
1040 * General note: The behavior of this function with respect to metadata |
|
1041 * is unspecified. Metadata may or may not be moved with the file. The |
|
1042 * behavior may not be the same across all platforms. |
|
1043 */ |
|
1044 File.move = function move(sourcePath, destPath, options) { |
|
1045 return Scheduler.post("move", [Type.path.toMsg(sourcePath), |
|
1046 Type.path.toMsg(destPath), options], [sourcePath, destPath]); |
|
1047 }; |
|
1048 |
|
1049 /** |
|
1050 * Create a symbolic link to a source. |
|
1051 * |
|
1052 * @param {string} sourcePath The platform-specific path to which |
|
1053 * the symbolic link should point. |
|
1054 * @param {string} destPath The platform-specific path at which the |
|
1055 * symbolic link should be created. |
|
1056 * |
|
1057 * @returns {Promise} |
|
1058 * @rejects {OS.File.Error} In case of any error. |
|
1059 */ |
|
1060 if (!SharedAll.Constants.Win) { |
|
1061 File.unixSymLink = function unixSymLink(sourcePath, destPath) { |
|
1062 return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath), |
|
1063 Type.path.toMsg(destPath)], [sourcePath, destPath]); |
|
1064 }; |
|
1065 } |
|
1066 |
|
1067 /** |
|
1068 * Gets the number of bytes available on disk to the current user. |
|
1069 * |
|
1070 * @param {string} Platform-specific path to a directory on the disk to |
|
1071 * query for free available bytes. |
|
1072 * |
|
1073 * @return {number} The number of bytes available for the current user. |
|
1074 * @throws {OS.File.Error} In case of any error. |
|
1075 */ |
|
1076 File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) { |
|
1077 return Scheduler.post("getAvailableFreeSpace", |
|
1078 [Type.path.toMsg(sourcePath)], sourcePath |
|
1079 ).then(Type.uint64_t.fromMsg); |
|
1080 }; |
|
1081 |
|
1082 /** |
|
1083 * Remove an empty directory. |
|
1084 * |
|
1085 * @param {string} path The name of the directory to remove. |
|
1086 * @param {*=} options Additional options. |
|
1087 * - {bool} ignoreAbsent If |true|, do not fail if the |
|
1088 * directory does not exist yet. |
|
1089 */ |
|
1090 File.removeEmptyDir = function removeEmptyDir(path, options) { |
|
1091 return Scheduler.post("removeEmptyDir", |
|
1092 [Type.path.toMsg(path), options], path); |
|
1093 }; |
|
1094 |
|
1095 /** |
|
1096 * Remove an existing file. |
|
1097 * |
|
1098 * @param {string} path The name of the file. |
|
1099 */ |
|
1100 File.remove = function remove(path) { |
|
1101 return Scheduler.post("remove", |
|
1102 [Type.path.toMsg(path)]); |
|
1103 }; |
|
1104 |
|
1105 |
|
1106 |
|
1107 /** |
|
1108 * Create a directory and, optionally, its parent directories. |
|
1109 * |
|
1110 * @param {string} path The name of the directory. |
|
1111 * @param {*=} options Additional options. |
|
1112 * |
|
1113 * - {string} from If specified, the call to |makeDir| creates all the |
|
1114 * ancestors of |path| that are descendants of |from|. Note that |path| |
|
1115 * must be a descendant of |from|, and that |from| and its existing |
|
1116 * subdirectories present in |path| must be user-writeable. |
|
1117 * Example: |
|
1118 * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); |
|
1119 * creates directories profileDir/foo, profileDir/foo/bar |
|
1120 * - {bool} ignoreExisting If |false|, throw an error if the directory |
|
1121 * already exists. |true| by default. Ignored if |from| is specified. |
|
1122 * - {number} unixMode Under Unix, if specified, a file creation mode, |
|
1123 * as per libc function |mkdir|. If unspecified, dirs are |
|
1124 * created with a default mode of 0700 (dir is private to |
|
1125 * the user, the user can read, write and execute). Ignored under Windows |
|
1126 * or if the file system does not support file creation modes. |
|
1127 * - {C pointer} winSecurity Under Windows, if specified, security |
|
1128 * attributes as per winapi function |CreateDirectory|. If |
|
1129 * unspecified, use the default security descriptor, inherited from |
|
1130 * the parent directory. Ignored under Unix or if the file system |
|
1131 * does not support security descriptors. |
|
1132 */ |
|
1133 File.makeDir = function makeDir(path, options) { |
|
1134 return Scheduler.post("makeDir", |
|
1135 [Type.path.toMsg(path), options], path); |
|
1136 }; |
|
1137 |
|
1138 /** |
|
1139 * Return the contents of a file |
|
1140 * |
|
1141 * @param {string} path The path to the file. |
|
1142 * @param {number=} bytes Optionally, an upper bound to the number of bytes |
|
1143 * to read. DEPRECATED - please use options.bytes instead. |
|
1144 * @param {JSON} options Additional options. |
|
1145 * - {boolean} sequential A flag that triggers a population of the page cache |
|
1146 * with data from a file so that subsequent reads from that file would not |
|
1147 * block on disk I/O. If |true| or unspecified, inform the system that the |
|
1148 * contents of the file will be read in order. Otherwise, make no such |
|
1149 * assumption. |true| by default. |
|
1150 * - {number} bytes An upper bound to the number of bytes to read. |
|
1151 * - {string} compression If "lz4" and if the file is compressed using the lz4 |
|
1152 * compression algorithm, decompress the file contents on the fly. |
|
1153 * |
|
1154 * @resolves {Uint8Array} A buffer holding the bytes |
|
1155 * read from the file. |
|
1156 */ |
|
1157 File.read = function read(path, bytes, options = {}) { |
|
1158 if (typeof bytes == "object") { |
|
1159 // Passing |bytes| as an argument is deprecated. |
|
1160 // We should now be passing it as a field of |options|. |
|
1161 options = bytes || {}; |
|
1162 } else { |
|
1163 options = clone(options, ["outExecutionDuration"]); |
|
1164 if (typeof bytes != "undefined") { |
|
1165 options.bytes = bytes; |
|
1166 } |
|
1167 } |
|
1168 |
|
1169 if (options.compression || !nativeWheneverAvailable) { |
|
1170 // We need to use the JS implementation. |
|
1171 let promise = Scheduler.post("read", |
|
1172 [Type.path.toMsg(path), bytes, options], path); |
|
1173 return promise.then( |
|
1174 function onSuccess(data) { |
|
1175 if (typeof data == "string") { |
|
1176 return data; |
|
1177 } |
|
1178 return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); |
|
1179 }); |
|
1180 } |
|
1181 |
|
1182 // Otherwise, use the native implementation. |
|
1183 return Scheduler.push(() => Native.read(path, options)); |
|
1184 }; |
|
1185 |
|
1186 /** |
|
1187 * Find outs if a file exists. |
|
1188 * |
|
1189 * @param {string} path The path to the file. |
|
1190 * |
|
1191 * @return {bool} true if the file exists, false otherwise. |
|
1192 */ |
|
1193 File.exists = function exists(path) { |
|
1194 return Scheduler.post("exists", |
|
1195 [Type.path.toMsg(path)], path); |
|
1196 }; |
|
1197 |
|
1198 /** |
|
1199 * Write a file, atomically. |
|
1200 * |
|
1201 * By opposition to a regular |write|, this operation ensures that, |
|
1202 * until the contents are fully written, the destination file is |
|
1203 * not modified. |
|
1204 * |
|
1205 * Limitation: In a few extreme cases (hardware failure during the |
|
1206 * write, user unplugging disk during the write, etc.), data may be |
|
1207 * corrupted. If your data is user-critical (e.g. preferences, |
|
1208 * application data, etc.), you may wish to consider adding options |
|
1209 * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as |
|
1210 * detailed below. Note that no combination of options can be |
|
1211 * guaranteed to totally eliminate the risk of corruption. |
|
1212 * |
|
1213 * @param {string} path The path of the file to modify. |
|
1214 * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. |
|
1215 * @param {*=} options Optionally, an object determining the behavior |
|
1216 * of this function. This object may contain the following fields: |
|
1217 * - {number} bytes The number of bytes to write. If unspecified, |
|
1218 * |buffer.byteLength|. Required if |buffer| is a C pointer. |
|
1219 * - {string} tmpPath If |null| or unspecified, write all data directly |
|
1220 * to |path|. If specified, write all data to a temporary file called |
|
1221 * |tmpPath| and, once this write is complete, rename the file to |
|
1222 * replace |path|. Performing this additional operation is a little |
|
1223 * slower but also a little safer. |
|
1224 * - {bool} noOverwrite - If set, this function will fail if a file already |
|
1225 * exists at |path|. |
|
1226 * - {bool} flush - If |false| or unspecified, return immediately once the |
|
1227 * write is complete. If |true|, before writing, force the operating system |
|
1228 * to write its internal disk buffers to the disk. This is considerably slower |
|
1229 * (not just for the application but for the whole system) but also safer: |
|
1230 * if the system shuts down improperly (typically due to a kernel freeze |
|
1231 * or a power failure) or if the device is disconnected before the buffer |
|
1232 * is flushed, the file has more chances of not being corrupted. |
|
1233 * - {string} backupTo - If specified, backup the destination file as |backupTo|. |
|
1234 * Note that this function renames the destination file before overwriting it. |
|
1235 * If the process or the operating system freezes or crashes |
|
1236 * during the short window between these operations, |
|
1237 * the destination file will have been moved to its backup. |
|
1238 * |
|
1239 * @return {promise} |
|
1240 * @resolves {number} The number of bytes actually written. |
|
1241 */ |
|
1242 File.writeAtomic = function writeAtomic(path, buffer, options = {}) { |
|
1243 // Copy |options| to avoid modifying the original object but preserve the |
|
1244 // reference to |outExecutionDuration| option if it is passed. |
|
1245 options = clone(options, ["outExecutionDuration"]); |
|
1246 // As options.tmpPath is a path, we need to encode it as |Type.path| message |
|
1247 if ("tmpPath" in options) { |
|
1248 options.tmpPath = Type.path.toMsg(options.tmpPath); |
|
1249 }; |
|
1250 if (isTypedArray(buffer) && (!("bytes" in options))) { |
|
1251 options.bytes = buffer.byteLength; |
|
1252 }; |
|
1253 // Note: Type.void_t.out_ptr.toMsg ensures that |
|
1254 // - the buffer is effectively shared (not neutered) between both |
|
1255 // threads; |
|
1256 // - we take care of any |byteOffset|. |
|
1257 let refObj = {}; |
|
1258 TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj); |
|
1259 let promise = Scheduler.post("writeAtomic", |
|
1260 [Type.path.toMsg(path), |
|
1261 Type.void_t.in_ptr.toMsg(buffer), |
|
1262 options], [options, buffer]); |
|
1263 TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj); |
|
1264 return promise; |
|
1265 }; |
|
1266 |
|
1267 File.removeDir = function(path, options = {}) { |
|
1268 return Scheduler.post("removeDir", |
|
1269 [Type.path.toMsg(path), options], path); |
|
1270 }; |
|
1271 |
|
1272 /** |
|
1273 * Information on a file, as returned by OS.File.stat or |
|
1274 * OS.File.prototype.stat |
|
1275 * |
|
1276 * @constructor |
|
1277 */ |
|
1278 File.Info = function Info(value) { |
|
1279 // Note that we can't just do this[k] = value[k] because our |
|
1280 // prototype defines getters for all of these fields. |
|
1281 for (let k in value) { |
|
1282 if (k != "creationDate") { |
|
1283 Object.defineProperty(this, k, {value: value[k]}); |
|
1284 } |
|
1285 } |
|
1286 Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]}); |
|
1287 }; |
|
1288 File.Info.prototype = SysAll.AbstractInfo.prototype; |
|
1289 |
|
1290 // Deprecated |
|
1291 Object.defineProperty(File.Info.prototype, "creationDate", { |
|
1292 get: function creationDate() { |
|
1293 Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes"); |
|
1294 return this._deprecatedCreationDate; |
|
1295 } |
|
1296 }); |
|
1297 |
|
1298 File.Info.fromMsg = function fromMsg(value) { |
|
1299 return new File.Info(value); |
|
1300 }; |
|
1301 |
|
1302 /** |
|
1303 * Get worker's current DEBUG flag. |
|
1304 * Note: This is used for testing purposes. |
|
1305 */ |
|
1306 File.GET_DEBUG = function GET_DEBUG() { |
|
1307 return Scheduler.post("GET_DEBUG"); |
|
1308 }; |
|
1309 |
|
1310 /** |
|
1311 * Iterate asynchronously through a directory |
|
1312 * |
|
1313 * @constructor |
|
1314 */ |
|
1315 let DirectoryIterator = function DirectoryIterator(path, options) { |
|
1316 /** |
|
1317 * Open the iterator on the worker thread |
|
1318 * |
|
1319 * @type {Promise} |
|
1320 * @resolves {*} A message accepted by the methods of DirectoryIterator |
|
1321 * in the worker thread |
|
1322 * @rejects {StopIteration} If all entries have already been visited |
|
1323 * or the iterator has been closed. |
|
1324 */ |
|
1325 this.__itmsg = Scheduler.post( |
|
1326 "new_DirectoryIterator", [Type.path.toMsg(path), options], |
|
1327 path |
|
1328 ); |
|
1329 this._isClosed = false; |
|
1330 }; |
|
1331 DirectoryIterator.prototype = { |
|
1332 iterator: function () this, |
|
1333 __iterator__: function () this, |
|
1334 |
|
1335 // Once close() is called, _itmsg should reject with a |
|
1336 // StopIteration. However, we don't want to create the promise until |
|
1337 // it's needed because it might never be used. In that case, we |
|
1338 // would get a warning on the console. |
|
1339 get _itmsg() { |
|
1340 if (!this.__itmsg) { |
|
1341 this.__itmsg = Promise.reject(StopIteration); |
|
1342 } |
|
1343 return this.__itmsg; |
|
1344 }, |
|
1345 |
|
1346 /** |
|
1347 * Determine whether the directory exists. |
|
1348 * |
|
1349 * @resolves {boolean} |
|
1350 */ |
|
1351 exists: function exists() { |
|
1352 return this._itmsg.then( |
|
1353 function onSuccess(iterator) { |
|
1354 return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]); |
|
1355 } |
|
1356 ); |
|
1357 }, |
|
1358 /** |
|
1359 * Get the next entry in the directory. |
|
1360 * |
|
1361 * @return {Promise} |
|
1362 * @resolves {OS.File.Entry} |
|
1363 * @rejects {StopIteration} If all entries have already been visited. |
|
1364 */ |
|
1365 next: function next() { |
|
1366 let self = this; |
|
1367 let promise = this._itmsg; |
|
1368 |
|
1369 // Get the iterator, call _next |
|
1370 promise = promise.then( |
|
1371 function withIterator(iterator) { |
|
1372 return self._next(iterator); |
|
1373 }); |
|
1374 |
|
1375 return promise; |
|
1376 }, |
|
1377 /** |
|
1378 * Get several entries at once. |
|
1379 * |
|
1380 * @param {number=} length If specified, the number of entries |
|
1381 * to return. If unspecified, return all remaining entries. |
|
1382 * @return {Promise} |
|
1383 * @resolves {Array} An array containing the |length| next entries. |
|
1384 */ |
|
1385 nextBatch: function nextBatch(size) { |
|
1386 if (this._isClosed) { |
|
1387 return Promise.resolve([]); |
|
1388 } |
|
1389 let promise = this._itmsg; |
|
1390 promise = promise.then( |
|
1391 function withIterator(iterator) { |
|
1392 return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]); |
|
1393 }); |
|
1394 promise = promise.then( |
|
1395 function withEntries(array) { |
|
1396 return array.map(DirectoryIterator.Entry.fromMsg); |
|
1397 }); |
|
1398 return promise; |
|
1399 }, |
|
1400 /** |
|
1401 * Apply a function to all elements of the directory sequentially. |
|
1402 * |
|
1403 * @param {Function} cb This function will be applied to all entries |
|
1404 * of the directory. It receives as arguments |
|
1405 * - the OS.File.Entry corresponding to the entry; |
|
1406 * - the index of the entry in the enumeration; |
|
1407 * - the iterator itself - return |iterator.close()| to stop the loop. |
|
1408 * |
|
1409 * If the callback returns a promise, iteration waits until the |
|
1410 * promise is resolved before proceeding. |
|
1411 * |
|
1412 * @return {Promise} A promise resolved once the loop has reached |
|
1413 * its end. |
|
1414 */ |
|
1415 forEach: function forEach(cb, options) { |
|
1416 if (this._isClosed) { |
|
1417 return Promise.resolve(); |
|
1418 } |
|
1419 |
|
1420 let self = this; |
|
1421 let position = 0; |
|
1422 let iterator; |
|
1423 |
|
1424 // Grab iterator |
|
1425 let promise = this._itmsg.then( |
|
1426 function(aIterator) { |
|
1427 iterator = aIterator; |
|
1428 } |
|
1429 ); |
|
1430 |
|
1431 // Then iterate |
|
1432 let loop = function loop() { |
|
1433 if (self._isClosed) { |
|
1434 return Promise.resolve(); |
|
1435 } |
|
1436 return self._next(iterator).then( |
|
1437 function onSuccess(value) { |
|
1438 return Promise.resolve(cb(value, position++, self)).then(loop); |
|
1439 }, |
|
1440 function onFailure(reason) { |
|
1441 if (reason == StopIteration) { |
|
1442 return; |
|
1443 } |
|
1444 throw reason; |
|
1445 } |
|
1446 ); |
|
1447 }; |
|
1448 |
|
1449 return promise.then(loop); |
|
1450 }, |
|
1451 /** |
|
1452 * Auxiliary method: fetch the next item |
|
1453 * |
|
1454 * @rejects {StopIteration} If all entries have already been visited |
|
1455 * or the iterator has been closed. |
|
1456 */ |
|
1457 _next: function _next(iterator) { |
|
1458 if (this._isClosed) { |
|
1459 return this._itmsg; |
|
1460 } |
|
1461 let self = this; |
|
1462 let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]); |
|
1463 promise = promise.then( |
|
1464 DirectoryIterator.Entry.fromMsg, |
|
1465 function onReject(reason) { |
|
1466 if (reason == StopIteration) { |
|
1467 self.close(); |
|
1468 throw StopIteration; |
|
1469 } |
|
1470 throw reason; |
|
1471 }); |
|
1472 return promise; |
|
1473 }, |
|
1474 /** |
|
1475 * Close the iterator |
|
1476 */ |
|
1477 close: function close() { |
|
1478 if (this._isClosed) { |
|
1479 return Promise.resolve(); |
|
1480 } |
|
1481 this._isClosed = true; |
|
1482 let self = this; |
|
1483 return this._itmsg.then( |
|
1484 function withIterator(iterator) { |
|
1485 // Set __itmsg to null so that the _itmsg getter returns a |
|
1486 // rejected StopIteration promise if it's ever used. |
|
1487 self.__itmsg = null; |
|
1488 return Scheduler.post("DirectoryIterator_prototype_close", [iterator]); |
|
1489 } |
|
1490 ); |
|
1491 } |
|
1492 }; |
|
1493 |
|
1494 DirectoryIterator.Entry = function Entry(value) { |
|
1495 return value; |
|
1496 }; |
|
1497 DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype); |
|
1498 |
|
1499 DirectoryIterator.Entry.fromMsg = function fromMsg(value) { |
|
1500 return new DirectoryIterator.Entry(value); |
|
1501 }; |
|
1502 |
|
1503 File.resetWorker = function() { |
|
1504 return Task.spawn(function*() { |
|
1505 let resources = yield Scheduler.kill({shutdown: false, reset: true}); |
|
1506 if (resources && !resources.killed) { |
|
1507 throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources)); |
|
1508 } |
|
1509 }); |
|
1510 }; |
|
1511 |
|
1512 // Constants |
|
1513 File.POS_START = SysAll.POS_START; |
|
1514 File.POS_CURRENT = SysAll.POS_CURRENT; |
|
1515 File.POS_END = SysAll.POS_END; |
|
1516 |
|
1517 // Exports |
|
1518 File.Error = OSError; |
|
1519 File.DirectoryIterator = DirectoryIterator; |
|
1520 |
|
1521 this.OS = {}; |
|
1522 this.OS.File = File; |
|
1523 this.OS.Constants = SharedAll.Constants; |
|
1524 this.OS.Shared = { |
|
1525 LOG: SharedAll.LOG, |
|
1526 Type: SysAll.Type, |
|
1527 get DEBUG() { |
|
1528 return SharedAll.Config.DEBUG; |
|
1529 }, |
|
1530 set DEBUG(x) { |
|
1531 return SharedAll.Config.DEBUG = x; |
|
1532 } |
|
1533 }; |
|
1534 Object.freeze(this.OS.Shared); |
|
1535 this.OS.Path = Path; |
|
1536 |
|
1537 // Returns a resolved promise when all the queued operation have been completed. |
|
1538 Object.defineProperty(OS.File, "queue", { |
|
1539 get: function() { |
|
1540 return Scheduler.queue; |
|
1541 } |
|
1542 }); |
|
1543 |
|
1544 // Auto-flush OS.File during profile-before-change. This ensures that any I/O |
|
1545 // that has been queued *before* profile-before-change is properly completed. |
|
1546 // To ensure that I/O queued *during* profile-before-change is completed, |
|
1547 // clients should register using AsyncShutdown.addBlocker. |
|
1548 AsyncShutdown.profileBeforeChange.addBlocker( |
|
1549 "OS.File: flush I/O queued before profile-before-change", |
|
1550 // Wait until the latest currently enqueued promise is satisfied/rejected |
|
1551 function() { |
|
1552 let DEBUG = false; |
|
1553 try { |
|
1554 DEBUG = Services.prefs.getBoolPref("toolkit.osfile.debug.failshutdown"); |
|
1555 } catch (ex) { |
|
1556 // Ignore |
|
1557 } |
|
1558 if (DEBUG) { |
|
1559 // Return a promise that will never be satisfied |
|
1560 return Promise.defer().promise; |
|
1561 } else { |
|
1562 return Scheduler.queue; |
|
1563 } |
|
1564 }, |
|
1565 function getDetails() { |
|
1566 let result = { |
|
1567 launched: Scheduler.launched, |
|
1568 shutdown: Scheduler.shutdown, |
|
1569 worker: !!Scheduler._worker, |
|
1570 pendingReset: !!Scheduler.resetTimer, |
|
1571 latestSent: Scheduler.Debugging.latestSent, |
|
1572 latestReceived: Scheduler.Debugging.latestReceived, |
|
1573 messagesSent: Scheduler.Debugging.messagesSent, |
|
1574 messagesReceived: Scheduler.Debugging.messagesReceived, |
|
1575 messagesQueued: Scheduler.Debugging.messagesQueued, |
|
1576 DEBUG: SharedAll.Config.DEBUG |
|
1577 }; |
|
1578 // Convert dates to strings for better readability |
|
1579 for (let key of ["latestSent", "latestReceived"]) { |
|
1580 if (result[key] && typeof result[key][0] == "number") { |
|
1581 result[key][0] = Date(result[key][0]); |
|
1582 } |
|
1583 } |
|
1584 return result; |
|
1585 } |
|
1586 ); |