Source: js/devtools/logshake.js

  1. /* global dump,
  2. ModalDialog,
  3. MozActivity,
  4. Notification,
  5. Service
  6. */
  7. (function(exports) {
  8. 'use strict';
  9. const DEBUG = false;
  10. /**
  11. * This developer system module captures a snapshot of the current device
  12. * logs as displayed by logcat using DeviceStorage to persist the file for
  13. * future access. It communicates with gecko code running in
  14. * b2g/chrome/content/shell.js using a SystemAppProxy custom event based API.
  15. * It requires the preference 'devtools.logshake' to be enabled
  16. *
  17. * @class LogShake
  18. */
  19. function LogShake() {
  20. }
  21. function debug(str) {
  22. if (DEBUG) {
  23. dump('LogShake: ' + str + '\n');
  24. }
  25. }
  26. LogShake.prototype = {
  27. /**
  28. * Start existing, observing for capture-logs events caused by Gecko
  29. * LogShake
  30. */
  31. start: function() {
  32. Service.request('handleSystemMessageNotification', 'logshake', this);
  33. window.addEventListener('volumeup+volumedown', this);
  34. this.startCaptureLogsListener();
  35. },
  36. /**
  37. * Stop the component, removing all listeners if necessary
  38. */
  39. stop: function() {
  40. Service.request('unhandleSystemMessageNotification', 'logshake', this);
  41. this.stopCaptureLogsListener();
  42. },
  43. startCaptureLogsListener: function() {
  44. debug('starting captureLogs listener');
  45. window.addEventListener('capture-logs-start', this);
  46. window.addEventListener('capture-logs-success', this);
  47. window.addEventListener('capture-logs-error', this);
  48. },
  49. stopCaptureLogsListener: function() {
  50. debug('stopping captureLogs listener');
  51. window.removeEventListener('capture-logs-start', this);
  52. window.removeEventListener('capture-logs-success', this);
  53. window.removeEventListener('capture-logs-error', this);
  54. },
  55. /**
  56. * Handle a capture-logs-start, capture-logs-success or capture-logs-error
  57. * event, dispatching to the appropriate handler
  58. */
  59. handleEvent: function(event) {
  60. debug('handling event ' + event.type);
  61. switch(event.type) {
  62. case 'volumeup+volumedown':
  63. this.requestSystemLogs();
  64. break;
  65. case 'capture-logs-start':
  66. this.handleCaptureLogsStart(event);
  67. break;
  68. case 'capture-logs-success':
  69. this.handleCaptureLogsSuccess(event);
  70. break;
  71. case 'capture-logs-error':
  72. this.handleCaptureLogsError(event);
  73. break;
  74. }
  75. },
  76. _shakeId: null,
  77. handleCaptureLogsStart: function(event) {
  78. debug('handling capture-logs-start');
  79. this._shakeId = Date.now();
  80. this._notify('logsSaving', '');
  81. },
  82. requestSystemLogs: function() {
  83. window.dispatchEvent(new CustomEvent('requestSystemLogs'));
  84. },
  85. /**
  86. * Handle an event of type capture-logs-success. event.detail.locations is
  87. * an array of absolute paths to the saved log files, and
  88. * event.detail.logFolder is the folder name where the logs are located.
  89. */
  90. handleCaptureLogsSuccess: function(event) {
  91. debug('handling capture-logs-success');
  92. navigator.vibrate(100);
  93. this._notify('logsSaved', 'logsSavedBody',
  94. this.triggerShareLogs.bind(this, event.detail.logFilenames),
  95. event.detail);
  96. this._shakeId = null;
  97. },
  98. handleCaptureLogsError: function(event) {
  99. debug('handling capture logs error');
  100. var error = event ? event.detail.error : '';
  101. var errorMsg = this.formatError(error);
  102. this._notify('logsSaveError', errorMsg,
  103. this.showErrorMessage.bind(this, error),
  104. event.detail);
  105. this._shakeId = null;
  106. },
  107. getDeviceStorage: function() {
  108. var storageName = 'sdcard';
  109. var storages = navigator.getDeviceStorages(storageName);
  110. for (var i = 0; i < storages.length; i++) {
  111. if (storages[i].storageName === storageName) {
  112. return storages[i];
  113. }
  114. }
  115. return navigator.getDeviceStorage('sdcard');
  116. },
  117. triggerShareLogs: function(logFilenames, notif) {
  118. var logFiles = [];
  119. var storage = this.getDeviceStorage();
  120. var requestsRemaining = logFilenames.length;
  121. var self = this;
  122. if (notif) {
  123. notif.close();
  124. }
  125. function onSuccess() {
  126. /* jshint validthis: true */
  127. logFiles.push(this.result);
  128. requestsRemaining -= 1;
  129. if (requestsRemaining === 0) {
  130. var logNames = logFiles.map(function(file) {
  131. // For some reason file.name contains the full path despite
  132. // File's documentation explicitly stating the opposite.
  133. var pathComponents = file.name.split('/');
  134. return pathComponents[pathComponents.length - 1];
  135. });
  136. /* jshint nonew: false */
  137. new MozActivity({
  138. name: 'share',
  139. data: {
  140. type: 'application/vnd.moz-systemlog',
  141. blobs: logFiles,
  142. filenames: logNames
  143. }
  144. });
  145. }
  146. }
  147. function onError() {
  148. /* jshint validthis: true */
  149. self.handleCaptureLogsError({detail: {error: this.error}});
  150. }
  151. for (var logFilename of logFilenames) {
  152. var req = storage.get(logFilename);
  153. req.onsuccess = onSuccess;
  154. req.onerror = onError;
  155. }
  156. },
  157. ERRNO_TO_MSG: {
  158. 0: 'logsGenericError',
  159. 30: 'logsSDCardMaybeShared' // errno: filesystem ro-mounted
  160. },
  161. formatError: function(error) {
  162. if (typeof error === 'string') {
  163. return error;
  164. }
  165. if (typeof error === 'object') {
  166. if ('operation' in error) {
  167. return navigator.mozL10n.get('logsOperationFailed',
  168. { operation: error.operation });
  169. }
  170. }
  171. return '';
  172. },
  173. showErrorMessage: function(error, notif) {
  174. if (notif) {
  175. notif.close();
  176. }
  177. // Do nothing for error string
  178. if (typeof error === 'string') {
  179. return;
  180. }
  181. if (typeof error !== 'object') {
  182. console.warn('Unexpected error type: ' + typeof error);
  183. return;
  184. }
  185. // Small heuristic for some frequent unix error cases
  186. if ('unixErrno' in error) {
  187. var errno = error.unixErrno;
  188. debug('errno: ' + errno);
  189. // Gracefully fallback to a generic error messages if we don't know
  190. // this errno code.
  191. if (!this.ERRNO_TO_MSG[errno]) {
  192. errno = 0;
  193. }
  194. ModalDialog.alert('logsSaveError',
  195. this.ERRNO_TO_MSG[errno], { title: 'ok' });
  196. }
  197. },
  198. _notify: function(titleId, body, onclick, dataPayload) {
  199. var title = navigator.mozL10n.get(titleId) || titleId;
  200. var payload = {
  201. body: navigator.mozL10n.get(body) || body,
  202. tag: 'logshake:' + this._shakeId,
  203. data: {
  204. systemMessageTarget: 'logshake',
  205. logshakePayload: dataPayload || undefined
  206. }
  207. };
  208. var notification = new Notification(title, payload);
  209. if (onclick) {
  210. notification.onclick = onclick.bind(this, notification);
  211. }
  212. },
  213. handleSystemMessageNotification: function(message) {
  214. debug('Received system message: ' + JSON.stringify(message));
  215. this.closeSystemMessageNotification(message);
  216. if (!('logshakePayload' in message.data)) {
  217. console.warn('Received logshake system message notification without ' +
  218. 'payload, silently discarding.');
  219. return;
  220. }
  221. debug('Message payload: ' + message.data.logshakePayload);
  222. var payload = message.data.logshakePayload;
  223. if ('error' in payload) {
  224. this.showErrorMessage(payload.error);
  225. } else if ('logFilenames' in payload) {
  226. this.triggerShareLogs(payload.logFilenames);
  227. } else {
  228. console.warn('Unidentified payload: ' + JSON.stringify(payload));
  229. }
  230. },
  231. closeSystemMessageNotification: function(msg) {
  232. Notification.get({ tag: msg.tag }).then(notifs => {
  233. notifs.forEach(notif => {
  234. if (notif.tag) {
  235. // Close notification with the matching tag
  236. if (notif.tag === msg.tag) {
  237. notif.close && notif.close();
  238. }
  239. }
  240. });
  241. });
  242. }
  243. };
  244. exports.LogShake = LogShake;
  245. // XXX: See issue described in screenshot.js
  246. exports.logshake = new LogShake();
  247. exports.logshake.start();
  248. })(window);