Gisela
Gisela

Reputation: 1244

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

Testing:

    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:

  1. not all lines are in the log
  2. it is undefined which line happens to be written

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

The question are:

  1. 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?
  2. Do I have to use synchronous versions of file methods?
  3. If yes: Is there any documentation / sample code available?

Upvotes: 1

Views: 485

Answers (2)

Gisela
Gisela

Reputation: 1244

#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:

  1. fopen : opens an existing file or creates a new one, file position at end ("a" for append)
  2. if (fp) : check if file object is created
  3. fprintf : do it!
  4. fclose : close it (and flush contents)

It can be compiled with this command:

gcc -Wall addline.c -o addline

and started like so:

./addline

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 ...

Upvotes: 1

benka
benka

Reputation: 4742

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 logged content.

What I would do is:

  1. cache the log
  2. check if cache reached a size limit
  3. pass content of cache to a tmp_var and clear cache
  4. write tmp_var to logfile
  5. 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.

Upvotes: 1

Related Questions