VulpesOS/gaia/apps/system/js/devtools/logshake.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

307 lines
8.8 KiB
JavaScript
Executable file

/* global dump,
ModalDialog,
MozActivity,
Notification,
NotificationHelper,
Service
*/
(function(exports) {
'use strict';
const DEBUG = false;
/**
* This developer system module captures a snapshot of the current device
* logs as displayed by logcat using DeviceStorage to persist the file for
* future access. It communicates with gecko code running in
* b2g/chrome/content/shell.js using a SystemAppProxy custom event based API.
* It requires the preference 'devtools.logshake.enabled' to be enabled
*
* @class LogShake
*/
function LogShake() {
}
function debug(str) {
if (DEBUG) {
dump('LogShake: ' + str + '\n');
}
}
LogShake.prototype = {
/**
* Start existing, observing for capture-logs events caused by Gecko
* LogShake
*/
start: function() {
Service.request('handleSystemMessageNotification', 'logshake', this);
window.addEventListener('volumeup+volumedown', this);
this.startCaptureLogsListener();
},
/**
* Stop the component, removing all listeners if necessary
*/
stop: function() {
Service.request('unhandleSystemMessageNotification', 'logshake', this);
this.stopCaptureLogsListener();
},
startCaptureLogsListener: function() {
debug('starting captureLogs listener');
window.addEventListener('capture-logs-start', this);
window.addEventListener('capture-logs-success', this);
window.addEventListener('capture-logs-error', this);
},
stopCaptureLogsListener: function() {
debug('stopping captureLogs listener');
window.removeEventListener('capture-logs-start', this);
window.removeEventListener('capture-logs-success', this);
window.removeEventListener('capture-logs-error', this);
},
/**
* Handle a capture-logs-start, capture-logs-success or capture-logs-error
* event, dispatching to the appropriate handler
*/
handleEvent: function(event) {
debug('handling event ' + event.type);
switch(event.type) {
case 'volumeup+volumedown':
this.requestSystemLogs();
break;
case 'capture-logs-start':
this.handleCaptureLogsStart(event);
break;
case 'capture-logs-success':
this.handleCaptureLogsSuccess(event);
break;
case 'capture-logs-error':
this.handleCaptureLogsError(event);
break;
}
},
_shakeId: null,
handleCaptureLogsStart: function(event) {
debug('handling capture-logs-start');
this._shakeId = Date.now();
this._notify('logsSaving');
},
requestSystemLogs: function() {
window.dispatchEvent(new CustomEvent('requestSystemLogs'));
},
/**
* Handle an event of type capture-logs-success where event.detail.logPaths
* is an array of absolute paths to the saved log files.
*/
handleCaptureLogsSuccess: function(event) {
debug('handling capture-logs-success');
navigator.vibrate(100);
this._notify('logsSavedHeader', 'logsSavedBody',
this.triggerShareLogs.bind(this, event.detail.logPaths),
event.detail);
this._shakeId = null;
},
handleCaptureLogsError: function(event) {
debug('handling capture logs error');
var error = event ? event.detail.error : '';
var errorMsg = this.formatError(error);
this._notify('logsSaveError', errorMsg,
this.showErrorMessage.bind('logsSaveError', this, error),
event.detail);
this._shakeId = null;
},
getDeviceStorage: function() {
var storageName = 'sdcard';
var storages = navigator.getDeviceStorages(storageName);
for (var i = 0; i < storages.length; i++) {
if (storages[i].storageName === storageName) {
return storages[i];
}
}
return navigator.getDeviceStorage('sdcard');
},
triggerShareLogs: function(logPaths, notif) {
var logFiles = [];
var storage = this.getDeviceStorage();
var requestsRemaining = logPaths.length;
var self = this;
function onSuccess() {
/* jshint validthis: true */
logFiles.push(this.result);
requestsRemaining -= 1;
if (requestsRemaining === 0) {
var logNames = logFiles.map(function(file) {
// For some reason file.name contains the full path despite
// File's documentation explicitly stating the opposite.
var pathComponents = file.name.split('/');
return pathComponents[pathComponents.length - 1];
});
var activity = new MozActivity({
name: 'share',
data: {
type: 'application/vnd.moz-systemlog',
blobs: logFiles,
filenames: logNames
}
});
activity.onsuccess = function() {
if (!notif) {
return;
}
if ('close' in notif) {
notif.close();
} else {
self.closeSystemMessageNotification(notif);
}
};
}
}
// Only report error one time.
var inError = false;
function onError() {
if (!inError) {
inError = true;
/* jshint validthis: true */
self.showErrorMessage('logsShareError', this.error);
}
}
for (var logPath of logPaths) {
var req = storage.get(logPath);
req.onsuccess = onSuccess;
req.onerror = onError;
}
},
ERRNO_TO_MSG: {
0: 'logsGenericError',
30: 'logsSDCardMaybeShared', // errno: filesystem ro-mounted
'NotFoundError': 'logsNotFoundError'
},
formatError: function(error) {
if (typeof error === 'string') {
return error;
}
if (typeof error === 'object') {
if ('operation' in error) {
return { id: 'logsOperationFailed',
args: { operation: error.operation }
};
}
if ('name' in error && error.name === 'NotFoundError') {
return 'logsNotFoundError';
}
}
return '';
},
showErrorMessage: function(title, error, notif) {
if (notif) {
notif.close();
}
// Do nothing for error string
if (typeof error === 'string') {
return;
}
if (typeof error !== 'object') {
console.warn('Unexpected error type: ' + typeof error);
return;
}
var errorKey;
// Small heuristic for some frequent unix error cases
if ('unixErrno' in error) {
errorKey = error.unixErrno;
} else if ('name' in error) {
errorKey = error.name;
}
debug('errorKey: ' + errorKey);
// Gracefully fallback to a generic error messages if we don't know
// this errno code.
if (!this.ERRNO_TO_MSG[errorKey]) {
errorKey = 0;
}
ModalDialog.alert(title, this.ERRNO_TO_MSG[errorKey], { title: 'ok' });
},
_notify: function(titleL10n, bodyL10n, onclick, dataPayload) {
var payload = {
bodyL10n: bodyL10n,
icon: '/style/notifications/images/bug.png',
tag: 'logshake:' + this._shakeId,
data: {
systemMessageTarget: 'logshake',
logshakePayload: dataPayload || undefined
}
};
NotificationHelper.send(titleL10n, payload).then(notification => {
if (onclick) {
notification.onclick = onclick.bind(this, notification);
}
});
},
handleSystemMessageNotification: function(message) {
debug('Received system message: ' + JSON.stringify(message));
if (!('logshakePayload' in message.data)) {
console.warn('Received logshake system message notification without ' +
'payload, silently discarding.');
return;
}
debug('Message payload: ' + message.data.logshakePayload);
var payload = message.data.logshakePayload;
if ('error' in payload) {
this.showErrorMessage('logsSaveError', payload.error);
} else if ('logPaths' in payload) {
this.triggerShareLogs(payload.logPaths, message);
} else {
console.warn('Unidentified payload: ' + JSON.stringify(payload));
}
},
closeSystemMessageNotification: function(msg) {
Notification.get({ tag: msg.tag }).then(notifs => {
notifs.forEach(notif => {
if (notif.tag) {
// Close notification with the matching tag
if (notif.tag === msg.tag) {
notif.close && notif.close();
}
}
});
});
}
};
exports.LogShake = LogShake;
// XXX: See issue described in screenshot.js
exports.logshake = new LogShake();
exports.logshake.start();
})(window);