Reputation: 1244
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:
I think the reason for the problems is the asynchronous nature of the file methods. But this is just a guess.
The question are:
Upvotes: 1
Views: 485
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:
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
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:
cache
the logcache
reached a size limitcache
to a tmp_var
and clear cache
tmp_var
to logfileonwriteend
> 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