Problems using phonegap / cordova file plugin part 2 - synchronicity


I want to add some simple logging capabilities to my cordova app.

So I added the file plugin, implemented a super simple log method and tested.

My configuration:

$ cordova --version 3.5.0-0.2.7 $ cordova plugins org.apache.cordova.file 1.3.0 "File"

The test device is a Huawei u8850, running Android 2.3.5

The Logger:

window.MyLog = { log: function(line){ window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, function(FS) { FS.root.getFile('the_log3.txt', {"create":true, "exclusive":false}, function(fileEntry) { fileEntry.createWriter( function(writer) { console.log(line); writer.seek(writer.length); // append to eof writer.write(line + '\n'); // write the line }, fail); }, fail); }, fail); } };


MyLog.log('A ' + new Date().toLocaleTimeString()); MyLog.log('B ' + new Date().toLocaleTimeString()); MyLog.log('C ' + new Date().toLocaleTimeString());

I started the app 3 times, expected sth. like

A 16:03:47 B 16:03:47 C 16:03:47 A 16:04:34 B 16:04:34 C 16:04:34 A 16:04:41 B 16:04:41 C 16:04:41

but got this

C 16:03:47 C 16:04:34 A 16:04:41

There are 2 Problems:

<ol><li>not all lines are in the log</li> <li>it is undefined which line happens to be written</li> </ol>

I think the reason for the problems is the asynchronous nature of the file methods. But this is just a guess.

The question are:

<ol><li>How can I implement a method to append lines to a text file, assuring that no line is missing and the order of the lines is the same as the calls to the method?</li> <li>Do I have to use synchronous versions of file methods?</li> <li>If yes: Is there any documentation / sample code available?</li> </ol>


Probably you were trying to write before previous writes finished, and fired the onwriteend event.

As posted in my answer on your first question, it is a good idea to implement a caching function.

So all your logs will be stored in a temporery cache. Every time you add something to this cache you will check the size of it, once it reaches a limit defined by you, call a logDumping method, which will be the actual write to the log file.

When you call the dump method you can pass the log content to your writer and also empty your cache so you can store new content in it and it won't get overlapped with already <em>logged</em> content.

What I would do is:

<ol><li>cache the log</li> <li>check if cache reached a size limit</li> <li>pass content of cache to a tmp_var and clear cache</li> <li>write tmp_var to logfile</li> <li>check onwriteend > if successful clear tmp_var, if there was an error you can write your tmp_var back to your actual cache (hence not loosing any data), and try writing the content of your cache to your logfile again.</li> </ol>


#include "stdio.h" void appendLine(char* path_to_file, char* line) { FILE* fp = fopen(path_to_file, "a"); if (fp) { fprintf(fp, "%s\n", line); fclose(fp); } } int main(){ appendLine("logfile.txt", "One Line"); appendLine("logfile.txt", "Another Line"); return 0; }

this is written in an ancient language called "C".

Just 4 lines of code:

<ol><li>fopen : opens an existing file or creates a new one, file position at end ("a" for append)</li> <li>if (fp) : check if file object is created</li> <li>fprintf : do it!</li> <li>fclose : close it (and flush contents)</li> </ol>

It can be compiled with this command:

gcc -Wall addline.c -o addline

and started like so:


the output will be like that:

One Line Another Line

Unfortunately this cannot be done when writing a JS based application with phonegap / cordova.

For unknown reasons a simple synchronous interface for basic file operations is not part of phonegaps core implementation, additionally is not included in the 'file' plugin.

So I spent some time to implement a simple TextFile wrapper to do the dirty work, so a client app can use simple commands like these:

// a single object is the wrapper: var textFile; function init() { // ... // initialize the (log) file // the file will be created if doesn't exists // when sth. goes wrong, the callback has an error message textFile = new TextFile('logfile.txt', function(ok, msg){ if (!ok) { alert('logging not available' + (msg ? '\nError: ' + msg : '')); textFile = null; } else { textFile.writeLine('start logging ...'); start(); } }, { // optional options, currently just one property 'clear' is supported // if set to true, the file will be emptied at start clear: true } ); // later, use methods // append some lines textFile.writeLine('a line'); textFile.writeLine('another line'); // get content, callback needed since it is not synchronous textFile.getContent(function(text){ // show text }); // empty file textFile.clear(); // remove file textFile.remove();

I use jquery for merging options - if jquery is not appropriate, just replace the $.extend method.

here's the code:

/** * Created by martin on 9/3/14. * * requires the file plugin: * * cordova plugin add org.apache.cordova.file * * implemented and tested with * cordova-3.5.0-0.2.7 * on * Android 2.3.5 */ (function(){ 'use strict'; // these values are NOT part of FileWriter, so we had to define them: const INIT = 0; const WRITING = 1; const DONE = 2; function errMessage(code){ var msg = ''; switch (code) { case FileError.QUOTA_EXCEEDED_ERR: msg = 'QUOTA_EXCEEDED_ERR'; break; case FileError.NOT_FOUND_ERR: msg = 'NOT_FOUND_ERR'; break; case FileError.SECURITY_ERR: msg = 'SECURITY_ERR'; break; case FileError.INVALID_MODIFICATION_ERR: msg = 'INVALID_MODIFICATION_ERR'; break; case FileError.INVALID_STATE_ERR: msg = 'INVALID_STATE_ERR'; break; default: msg = 'Unknown Error'; break; }; return msg; } /** * * @param fileName : the name of the file for which the TextFile is created * @param cb : function, is called when * - TextFile is created, parameter: boolean true * - TextFile is not created, parameters: boolean false, string error_message * @param options : object with single property * - boolean clear - truncates the file, defaults to false * @constructor */ window.TextFile = function(fileName, cb, options){ this.writer = null; this.queue = []; this.fileEntry = null; this._initialize(fileName, cb, options); } var files = {}; TextFile.prototype = { // pseudo private method, called form constructor _initialize: function(fileName, cb, options){ this.options = $.extend({startMsg: null, clear: false}, options) if (files.fileName) { cb(false, 'TextFile[' + fileName + '] already in use'); } files.fileName = true; var that = this; window.requestFileSystem( LocalFileSystem.PERSISTENT, 0, function(FS) { FS.root.getFile( fileName, { 'create': true, 'exclusive': false }, function(fileEntry) { that.fileEntry = fileEntry; fileEntry.createWriter( function(writer) { that.writer = writer; writer.seek(writer.length); writer.onwriteend = function(){ if (that.queue.length > 0){ // sth in the queue var item = that.queue[0]; switch (item.type) { case 'w': writer.write(item.line + '\n'); break; case 't': writer.truncate(0); break; case 'g': that._readContent(item.cb); break; default: throw 'unknown type ' + item.type; } // get rid of processed item that.queue.splice(0, 1); } } if (that.options.clear) { that.clear(); } cb(true); } ); }, function(err){ cb(false, errMessage(err.code)) } ); }, function(){ cb(false, errMessage(err.code)); } ); }, // internal use _readContent: function(cb){ this.fileEntry.file(function(file) { var reader = new FileReader(); reader.onloadend = function(e) { var res = this.result; cb(res); }; reader.readAsText(file); }, function(err) { cb('got an error: ' + errMessage(err.code)); } ); }, // reads whole file, content is sent to client with callback getContent: function(cb){ if (this.writer.readyState !== WRITING) { this._readContent(cb); } else { this.queue.push({type: 'g', cb:cb}); } }, // set file size to zero clear: function() { if (this.writer.readyState !== WRITING) { this.writer.truncate(0); } else { this.queue.push({type: 't'}); } }, // removes file remove: function(cb){ this.fileEntry.remove( function(){ if (cb) { cb(true); } }, function(err){ if (cb) { cb(false,errMessage(err.code)); } } ); }, // append single line to file writeLine: function(line){ if (this.writer.readyState !== WRITING) { this.writer.write(line + '\n'); } else { this.queue.push({type: 'w', line: line}); } } }; })();

Maybe this is useful for others, struggling with the same problem ...


  • cordova, android app how to create subfolder
  • Wikipedia API for python
  • JUnit - Comparing Arrays of objects
  • Rails 4, Asset Compiling
  • how to merge two linear regression prediction models (each per data frame's subset) into one co
  • java: console application main thread spawns a key listener thread
  • Bind a function to multiple dynamically created buttons in kivy?
  • How well does Entity Framework 6 support .NET 4.0?
  • TypeError: object is not a function showing at express
  • How do you Authenticate a Logic app microsoft.web/connections connection with code
  • Get Network Interface Names and Set All to DHCP Batch Script
  • REGEXP_REPLACE pattern has to be const? Comparing strings in BigQuery
  • Scrapy + Selenium + Datepicker
  • How to order the ties in data so that the previously observed value appears first
  • why 'read' command in shell script is missing initial characters? [duplicate]
  • SqlDatasource select parameters
  • Laravel 5 - Cache remember doesn't work
  • Expression.Call GroupBy then Select and Count()?
  • Filtering out choiceless polls in the Django tutorial causes polls in the index to duplicate
  • How to run Daphne Server (Django Channels) & workers in the background?
  • Adding Dynamic Row and Data on Checkbox Click
  • dmtracedump doesn't work, HELP!
  • Bundling python(“.py”)files along with java class files for a web application
  • Facebook friend list in Facebook Android SDK 3.14
  • How can I filter an array of dictionaries in 'updateSearchResultsForSearchController' to s
  • Multiple canvases (pages) in Fabric.js
  • Extracting a small subset of data from XMLs
  • Pick Out Specific Number from Array? [duplicate]
  • content must have a ListView whose id attribute is 'android.R.id.list'
  • separate tokens in batch file
  • LINQ to populate treeview based upon grouping
  • Find angle of point on circle
  • Ajax call on Multiple selection in Select box
  • Typeahead.js does give me suggestions but doesn't select them
  • how do i compare two rows and store the similarities of the two rows in another column
  • how to get the location(lat/lng) on google maps v3 from the location(x,y)
  • How to call jQuery function in HTML returned by AJAX
  • Send array to next viewcontroller iOs xcode [duplicate]
  • ssh remote server login script