Add Send Logs command. Closes #6710

Signed-off-by: Pedro Pombeiro <pombeirp@users.noreply.github.com>
This commit is contained in:
Pedro Pombeiro 2018-12-15 19:57:00 +01:00
parent cdf8dc043a
commit 9d21cf143e
No known key found for this signature in database
GPG Key ID: A65DEB11E4BBC647
21 changed files with 429 additions and 51 deletions

View File

@ -91,6 +91,17 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" /> <service android:name="io.invertase.firebase.messaging.RNFirebaseBackgroundMessagingService" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="android:authorities">
<meta-data
tools:replace="android:resource"
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@ -30,7 +30,6 @@ import im.status.ethereum.module.StatusThreadPoolExecutor;
public class MainActivity extends ReactActivity public class MainActivity extends ReactActivity
implements ActivityCompat.OnRequestPermissionsResultCallback{ implements ActivityCompat.OnRequestPermissionsResultCallback{
private static final int PERMISSION_REQUEST_CAMERA = 0;
@Nullable private PermissionListener mPermissionListener; @Nullable private PermissionListener mPermissionListener;

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="logs" path="logs" />
<!-- NOTE: Ensure that the paths here take into account those of npm dependencies as well, e.g. node_modules/react-native-image-crop-picker/android/src/main/res/xml/provider_paths.xml -->
<external-path name="external_files" path="."/>
</paths>

View File

@ -9,4 +9,4 @@ endif(APPLE)
if(SCRIPT AND EXISTS ${CMAKE_SOURCE_DIR}/CMakeModules/${SCRIPT}.cmake.in) if(SCRIPT AND EXISTS ${CMAKE_SOURCE_DIR}/CMakeModules/${SCRIPT}.cmake.in)
configure_file(${CMAKE_SOURCE_DIR}/CMakeModules/${SCRIPT}.cmake.in ${SCRIPT}.cmake @ONLY) configure_file(${CMAKE_SOURCE_DIR}/CMakeModules/${SCRIPT}.cmake.in ${SCRIPT}.cmake @ONLY)
include(${CMAKE_CURRENT_BINARY_DIR}/${SCRIPT}.cmake) include(${CMAKE_CURRENT_BINARY_DIR}/${SCRIPT}.cmake)
endif(SCRIPT) endif()

View File

@ -14,6 +14,10 @@ macro(import_qt_modules)
#message("${mod}_INCLUDE_DIRS: include_directories(${${mod}_INCLUDE_DIRS})") #message("${mod}_INCLUDE_DIRS: include_directories(${${mod}_INCLUDE_DIRS})")
include_directories(${${mod}_INCLUDE_DIRS}) include_directories(${${mod}_INCLUDE_DIRS})
if (${COMP} STREQUAL "Quick")
# We need to include the private headers for QZipWriter. If in the future we can't use that class anymore, we can always resort to the QuaZIP OSS library
include_directories(${${mod}_PRIVATE_INCLUDE_DIRS})
endif()
list(APPEND QT5_LIBRARIES ${${mod}_LIBRARIES}) list(APPEND QT5_LIBRARIES ${${mod}_LIBRARIES})
list(APPEND QT5_CFLAGS ${${mod}_EXECUTABLE_COMPILE_FLAGS}) list(APPEND QT5_CFLAGS ${${mod}_EXECUTABLE_COMPILE_FLAGS})

View File

@ -14,7 +14,6 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
set(APP_NAME "reportApp") set(APP_NAME "reportApp")
set(MAIN_CPP_SOURCE reportpublisher.cpp set(MAIN_CPP_SOURCE reportpublisher.cpp
reportpublisher.cpp
main.cpp) main.cpp)
add_executable( add_executable(
@ -23,7 +22,7 @@ add_executable(
main.qrc main.qrc
) )
set(USED_QT_MODULES Core Qml Quick WebSockets Svg) set(USED_QT_MODULES Core Qml Quick WebSockets Widgets Svg)
qt5_use_modules(${APP_NAME} ${USED_QT_MODULES}) qt5_use_modules(${APP_NAME} ${USED_QT_MODULES})

View File

@ -1,12 +1,17 @@
package im.status.ethereum.module; package im.status.ethereum.module;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.*; import android.os.*;
import android.view.WindowManager; import android.support.v4.content.FileProvider ;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.WindowManager;
import android.util.Log; import android.util.Log;
import android.webkit.CookieManager; import android.webkit.CookieManager;
import android.webkit.CookieSyncManager; import android.webkit.CookieSyncManager;
@ -16,17 +21,25 @@ import com.facebook.react.bridge.*;
import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.github.status_im.status_go.Statusgo; import com.github.status_im.status_go.Statusgo;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Stack;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.json.JSONObject; import org.json.JSONObject;
import org.json.JSONException; import org.json.JSONException;
@ -36,11 +49,12 @@ import javax.annotation.Nullable;
class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ConnectorHandler { class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ConnectorHandler {
private static final String TAG = "StatusModule"; private static final String TAG = "StatusModule";
private static final String logsZipFileName = "Status-debug-logs.zip";
private static final String gethLogFileName = "geth.log";
private static final String statusLogFileName = "Status.log";
private final static int TESTNET_NETWORK_ID = 3; private final static int TESTNET_NETWORK_ID = 3;
private HashMap<String, Callback> callbacks = new HashMap<>();
private static StatusModule module; private static StatusModule module;
private ServiceConnector status = null; private ServiceConnector status = null;
private ExecutorService executor = null; private ExecutorService executor = null;
@ -114,10 +128,15 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params); this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params);
} }
private static String prepareLogsFile() { private File getLogsFile() {
String gethLogFileName = "geth.log"; final File pubDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File pubDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); final File logFile = new File(pubDirectory, gethLogFileName);
File logFile = new File(pubDirectory, gethLogFileName);
return logFile;
}
private String prepareLogsFile(final Context context) {
final File logFile = getLogsFile();
try { try {
logFile.setReadable(true); logFile.setReadable(true);
@ -149,7 +168,8 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
// retrieve parameters from app config, that will be applied onto the Go-side config later on // retrieve parameters from app config, that will be applied onto the Go-side config later on
final String absDataDirPath = pathCombine(absRootDirPath, jsonConfig.getString("DataDir")); final String absDataDirPath = pathCombine(absRootDirPath, jsonConfig.getString("DataDir"));
final Boolean logEnabled = jsonConfig.getBoolean("LogEnabled"); final Boolean logEnabled = jsonConfig.getBoolean("LogEnabled");
final String gethLogFilePath = logEnabled ? prepareLogsFile() : null; final Context context = this.getReactApplicationContext();
final String gethLogFilePath = logEnabled ? prepareLogsFile(context) : null;
jsonConfig.put("DataDir", absDataDirPath); jsonConfig.put("DataDir", absDataDirPath);
jsonConfig.put("KeyStoreDir", absKeystoreDirPath); jsonConfig.put("KeyStoreDir", absKeystoreDirPath);
@ -456,6 +476,156 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
StatusThreadPoolExecutor.getInstance().execute(r); StatusThreadPoolExecutor.getInstance().execute(r);
} }
private Boolean zip(File[] _files, File zipFile, Stack<String> errorList) {
final int BUFFER = 0x8000;
try {
BufferedInputStream origin = null;
FileOutputStream dest = new FileOutputStream(zipFile);
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
byte data[] = new byte[BUFFER];
for (int i = 0; i < _files.length; i++) {
final File file = _files[i];
if (file == null || !file.exists()) {
continue;
}
Log.v("Compress", "Adding: " + file.getAbsolutePath());
try {
FileInputStream fi = new FileInputStream(file);
origin = new BufferedInputStream(fi, BUFFER);
ZipEntry entry = new ZipEntry(file.getName());
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER)) != -1) {
out.write(data, 0, count);
}
origin.close();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
errorList.push(e.getMessage());
}
}
out.close();
return true;
} catch (Exception e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
return false;
}
}
private void dumpAdbLogsTo(final FileOutputStream statusLogStream) throws IOException {
final String filter = "logcat -d -b main ReactNativeJS:D StatusModule:D StatusService:D StatusNativeLogs:D *:S";
final java.lang.Process p = Runtime.getRuntime().exec(filter);
final java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(p.getInputStream()));
final java.io.BufferedWriter out = new java.io.BufferedWriter(new java.io.OutputStreamWriter(statusLogStream));
String line;
while ((line = in.readLine()) != null) {
out.write(line);
out.newLine();
}
out.close();
in.close();
}
private void showErrorMessage(final String message) {
final Activity activity = getCurrentActivity();
new AlertDialog.Builder(activity)
.setTitle("Error")
.setMessage(message)
.setNegativeButton("Exit", new DialogInterface.OnClickListener() {
public void onClick(final DialogInterface dialog, final int id) {
dialog.dismiss();
}
}).show();
}
@ReactMethod
public void sendLogs(final String dbJson) {
Log.d(TAG, "sendLogs");
if (!checkAvailability()) {
return;
}
final Context context = this.getReactApplicationContext();
final File logsTempDir = new File(context.getCacheDir(), "logs"); // This path needs to be in sync with android/app/src/main/res/xml/file_provider_paths.xml
logsTempDir.mkdir();
final File dbFile = new File(logsTempDir, "db.json");
try {
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(dbFile));
outputStreamWriter.write(dbJson);
outputStreamWriter.close();
}
catch (IOException e) {
Log.e(TAG, "File write failed: " + e.toString());
showErrorMessage(e.getLocalizedMessage());
}
final File zipFile = new File(logsTempDir, logsZipFileName);
final File statusLogFile = new File(logsTempDir, statusLogFileName);
final File gethLogFile = getLogsFile();
try {
if (zipFile.exists() || zipFile.createNewFile()) {
final long usableSpace = zipFile.getUsableSpace();
if (usableSpace < 20 * 1024 * 1024) {
final String message = String.format("Insufficient space available on device (%s) to write logs.\nPlease free up some space.", android.text.format.Formatter.formatShortFileSize(context, usableSpace));
Log.e(TAG, message);
showErrorMessage(message);
return;
}
}
dumpAdbLogsTo(new FileOutputStream(statusLogFile));
final Stack<String> errorList = new Stack<String>();
final Boolean zipped = zip(new File[] {dbFile, gethLogFile, statusLogFile}, zipFile, errorList);
if (zipped && zipFile.exists()) {
Log.d(TAG, "Sending " + zipFile.getAbsolutePath() + " file through share intent");
final String providerName = context.getPackageName() + ".provider";
final Activity activity = getCurrentActivity();
zipFile.setReadable(true, false);
final Uri dbJsonURI = FileProvider.getUriForFile(activity, providerName, zipFile);
Intent intentShareFile = new Intent(Intent.ACTION_SEND);
intentShareFile.setType("application/json");
intentShareFile.putExtra(Intent.EXTRA_STREAM, dbJsonURI);
SimpleDateFormat dateFormatGmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormatGmt.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
intentShareFile.putExtra(Intent.EXTRA_SUBJECT, "Status.im logs");
intentShareFile.putExtra(Intent.EXTRA_TEXT,
String.format("Logs from %s GMT\n\nThese logs have been generated automatically by the user's request for debugging purposes.\n\n%s",
dateFormatGmt.format(new java.util.Date()),
errorList));
activity.startActivity(Intent.createChooser(intentShareFile, "Share Debug Logs"));
} else {
Log.d(TAG, "File " + zipFile.getAbsolutePath() + " does not exist");
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
showErrorMessage(e.getLocalizedMessage());
e.printStackTrace();
return;
}
finally {
dbFile.delete();
statusLogFile.delete();
zipFile.deleteOnExit();
}
}
@ReactMethod @ReactMethod
public void addPeer(final String enode, final Callback callback) { public void addPeer(final String enode, final Callback callback) {
Log.d(TAG, "addPeer"); Log.d(TAG, "addPeer");

View File

@ -13,6 +13,8 @@
#include "eventdispatcher.h" #include "eventdispatcher.h"
#include <QDebug> #include <QDebug>
#include <QMessageBox>
#include <QStorageInfo>
#include <QJsonDocument> #include <QJsonDocument>
#include <QByteArray> #include <QByteArray>
#include <QVariantMap> #include <QVariantMap>
@ -22,6 +24,9 @@
#include "libstatus.h" #include "libstatus.h"
extern QString getDataStoragePath();
extern QString getLogFilePath();
namespace { namespace {
struct RegisterQMLMetaType { struct RegisterQMLMetaType {
RegisterQMLMetaType() { RegisterQMLMetaType() {
@ -92,7 +97,7 @@ void RCTStatus::startNode(QString configString) {
if (!relativeDataDirPath.startsWith("/")) if (!relativeDataDirPath.startsWith("/"))
relativeDataDirPath.prepend("/"); relativeDataDirPath.prepend("/");
QString rootDirPath = getRootDirPath(); QString rootDirPath = getDataStoragePath();
QDir rootDir(rootDirPath); QDir rootDir(rootDirPath);
QString absDataDirPath = rootDirPath + relativeDataDirPath; QString absDataDirPath = rootDirPath + relativeDataDirPath;
QDir dataDir(absDataDirPath); QDir dataDir(absDataDirPath);
@ -100,10 +105,11 @@ void RCTStatus::startNode(QString configString) {
dataDir.mkpath("."); dataDir.mkpath(".");
} }
d_gethLogFilePath = dataDir.absoluteFilePath("geth.log");
configJSON["DataDir"] = absDataDirPath; configJSON["DataDir"] = absDataDirPath;
configJSON["BackupDisabledDataDir"] = absDataDirPath; configJSON["BackupDisabledDataDir"] = absDataDirPath;
configJSON["KeyStoreDir"] = rootDir.absoluteFilePath("keystore"); configJSON["KeyStoreDir"] = rootDir.absoluteFilePath("keystore");
configJSON["LogFile"] = dataDir.absoluteFilePath("geth.log"); configJSON["LogFile"] = d_gethLogFilePath;
const QJsonDocument& updatedJsonDoc = QJsonDocument::fromVariant(configJSON); const QJsonDocument& updatedJsonDoc = QJsonDocument::fromVariant(configJSON);
qCInfo(RCTSTATUS) << "::startNode updated configString: " << updatedJsonDoc.toVariant().toMap(); qCInfo(RCTSTATUS) << "::startNode updated configString: " << updatedJsonDoc.toVariant().toMap();
@ -141,6 +147,97 @@ void RCTStatus::notifyUsers(QString token, QString payloadJSON, QString tokensJS
} }
#include <QApplication>
#include <QProcess>
#include <QStandardPaths>
#include <QMessageBox>
#include <QtGui/private/qzipwriter_p.h>
void showFileInGraphicalShell(QWidget *parent, const QFileInfo &fileInfo)
{
// Mac, Windows support folder or file.
#ifdef Q_OS_WIN
const QString explorer = QStandardPaths::findExecutable(QLatin1String("explorer.exe"));
if (explorer.isEmpty()) {
QMessageBox::warning(parent,
QApplication::translate("Core::Internal",
"Launching Windows Explorer Failed"),
QApplication::translate("Core::Internal",
"Could not find explorer.exe in path to launch Windows Explorer."));
return;
}
QStringList param;
if (!fileInfo.isDir())
param += QLatin1String("/select,");
param += QDir::toNativeSeparators(fileInfo.canonicalFilePath());
QProcess::startDetached(explorer, param);
#elif defined(Q_OS_MAC)
QStringList scriptArgs;
scriptArgs << QLatin1String("-e")
<< QString::fromLatin1("tell application \"Finder\" to reveal POSIX file \"%1\"")
.arg(fileInfo.canonicalFilePath());
QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
scriptArgs.clear();
scriptArgs << QLatin1String("-e")
<< QLatin1String("tell application \"Finder\" to activate");
QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
#else
// we cannot select a file here, because no file browser really supports it...
const QString folder = fileInfo.isDir() ? fileInfo.absoluteFilePath() : fileInfo.dir().absolutePath();
QProcess browserProc;
browserProc.setProgram("xdg-open");
browserProc.setArguments(QStringList(folder));
bool success = browserProc.startDetached();
const QString error = QString::fromLocal8Bit(browserProc.readAllStandardError());
success = success && error.isEmpty();
if (!success) {
QMessageBox::warning(parent, "Launching Explorer Failed", error);
return;
}
#endif
}
void RCTStatus::sendLogs(QString dbJSON) {
Q_D(RCTStatus);
qCDebug(RCTSTATUS) << "::sendLogs call - logFilePath:" << getLogFilePath()
<< "d_gethLogFilePath:" << d_gethLogFilePath
<< "dbJSON:" << dbJSON;
QString tmpDirName("Status.im");
QDir tmpDir(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
if (!tmpDir.mkpath(tmpDirName)) {
qCWarning(RCTSTATUS) << "::sendLogs could not create temp dir:" << tmpDirName;
return;
}
// Check that at least 20MB are available for log generation
QStorageInfo storage(tmpDir);
if (storage.bytesAvailable() < 20 * 1024 * 1024) {
QMessageBox dlg;
dlg.warning(QApplication::activeWindow(),
"Error", QString("Insufficient storage space available in %1 for log generation. Please free up some space.").arg(storage.rootPath()),
QMessageBox::Close);
return;
}
QFile zipFile(tmpDir.absoluteFilePath(tmpDirName + QDir::separator() + "Status-debug-logs.zip"));
QZipWriter zipWriter(&zipFile);
QFile gethLogFile(d_gethLogFilePath);
QFile logFile(getLogFilePath());
zipWriter.addFile("db.json", dbJSON.toUtf8());
if (gethLogFile.exists()) {
zipWriter.addFile(QFileInfo(gethLogFile).fileName(), &gethLogFile);
}
if (logFile.exists()) {
zipWriter.addFile(QFileInfo(logFile).fileName(), &logFile);
}
zipWriter.close();
showFileInGraphicalShell(QApplication::activeWindow(), QFileInfo(zipFile));
}
void RCTStatus::addPeer(QString enode, double callbackId) { void RCTStatus::addPeer(QString enode, double callbackId) {
Q_D(RCTStatus); Q_D(RCTStatus);
qCDebug(RCTSTATUS) << "::addPeer call - callbackId:" << callbackId; qCDebug(RCTSTATUS) << "::addPeer call - callbackId:" << callbackId;
@ -177,7 +274,7 @@ void RCTStatus::verify(QString address, QString password, double callbackId) {
Q_D(RCTStatus); Q_D(RCTStatus);
qCInfo(RCTSTATUS) << "::verify call - callbackId:" << callbackId; qCInfo(RCTSTATUS) << "::verify call - callbackId:" << callbackId;
QtConcurrent::run([&](QString address, QString password, double callbackId) { QtConcurrent::run([&](QString address, QString password, double callbackId) {
QDir rootDir(getRootDirPath()); QDir rootDir(getDataStoragePath());
QString keystorePath = rootDir.absoluteFilePath("keystore"); QString keystorePath = rootDir.absoluteFilePath("keystore");
const char* result = VerifyAccountPassword(keystorePath.toUtf8().data(), address.toUtf8().data(), password.toUtf8().data()); const char* result = VerifyAccountPassword(keystorePath.toUtf8().data(), address.toUtf8().data(), password.toUtf8().data());
logStatusGoResult("::verify VerifyAccountPassword", result); logStatusGoResult("::verify VerifyAccountPassword", result);
@ -334,17 +431,3 @@ void RCTStatus::updateMailservers(QString enodes, double callbackId) {
d->bridge->invokePromiseCallback(callbackId, QVariantList{result}); d->bridge->invokePromiseCallback(callbackId, QVariantList{result});
}, enodes, callbackId); }, enodes, callbackId);
} }
QString RCTStatus::getRootDirPath() {
QString statusDataDir = qgetenv("STATUS_DATA_DIR");
QString rootDirPath;
if (!statusDataDir.isEmpty()) {
rootDirPath = statusDataDir;
}
else {
rootDirPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
}
return rootDirPath;
}

View File

@ -39,6 +39,7 @@ public:
Q_INVOKABLE void stopNode(); Q_INVOKABLE void stopNode();
Q_INVOKABLE void createAccount(QString password, double callbackId); Q_INVOKABLE void createAccount(QString password, double callbackId);
Q_INVOKABLE void notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId); Q_INVOKABLE void notifyUsers(QString token, QString payloadJSON, QString tokensJSON, double callbackId);
Q_INVOKABLE void sendLogs(QString dbJSON);
Q_INVOKABLE void addPeer(QString enode, double callbackId); Q_INVOKABLE void addPeer(QString enode, double callbackId);
Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId); Q_INVOKABLE void recoverAccount(QString passphrase, QString password, double callbackId);
Q_INVOKABLE void login(QString address, QString password, double callbackId); Q_INVOKABLE void login(QString address, QString password, double callbackId);
@ -75,9 +76,9 @@ private Q_SLOTS:
private: private:
void logStatusGoResult(const char* methodName, const char* result); void logStatusGoResult(const char* methodName, const char* result);
QString getRootDirPath();
QScopedPointer<RCTStatusPrivate> d_ptr; QScopedPointer<RCTStatusPrivate> d_ptr;
QString d_gethLogFilePath;
}; };
#endif // RCTSTATUS_H #endif // RCTSTATUS_H

View File

@ -196,6 +196,16 @@ RCT_EXPORT_METHOD(notifyUsers:(NSString *)message
#endif #endif
} }
////////////////////////////////////////////////////////////////////
#pragma mark - SendLogs method
//////////////////////////////////////////////////////////////////// sendLogs
RCT_EXPORT_METHOD(sendLogs:(NSString *)dbJson) {
// TODO: Implement SendLogs for iOS
#if DEBUG
NSLog(@"SendLogs() method called, not implemented");
#endif
}
//////////////////////////////////////////////////////////////////// addPeer //////////////////////////////////////////////////////////////////// addPeer
RCT_EXPORT_METHOD(addPeer:(NSString *)enode RCT_EXPORT_METHOD(addPeer:(NSString *)enode
callback:(RCTResponseSenderBlock)callback) { callback:(RCTResponseSenderBlock)callback) {

View File

@ -24,6 +24,7 @@
[status-im.hardwallet.core :as hardwallet] [status-im.hardwallet.core :as hardwallet]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.init.core :as init] [status-im.init.core :as init]
[status-im.utils.logging.core :as logging]
[status-im.log-level.core :as log-level] [status-im.log-level.core :as log-level]
[status-im.mailserver.core :as mailserver] [status-im.mailserver.core :as mailserver]
[status-im.network.core :as network] [status-im.network.core :as network]
@ -543,6 +544,13 @@
(fn [cofx [_ url data modal?]] (fn [cofx [_ url data modal?]]
(extensions.registry/install cofx url data modal?))) (extensions.registry/install cofx url data modal?)))
;; logging module
(handlers/register-handler-fx
:logging.ui/send-logs-pressed
(fn [cofx _]
(logging/send-logs cofx)))
;; log-level module ;; log-level module
(handlers/register-handler-fx (handlers/register-handler-fx

View File

@ -2,17 +2,20 @@
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[status-im.accounts.update.core :as accounts.update] [status-im.accounts.update.core :as accounts.update]
[status-im.react-native.js-dependencies :as rn-dependencies] [status-im.react-native.js-dependencies :as rn-dependencies]
[status-im.node.core :as node]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.utils.fx :as fx])) [status-im.utils.fx :as fx]))
(fx/defn save-log-level (fx/defn save-log-level
[{:keys [db now] :as cofx} log-level] [{:keys [db now] :as cofx} log-level]
(let [settings (get-in db [:account/account :settings])] (let [settings (get-in db [:account/account :settings])
(accounts.update/update-settings cofx new-settings (if log-level
(if log-level
(assoc settings :log-level log-level) (assoc settings :log-level log-level)
(dissoc settings :log-level)) (dissoc settings :log-level))]
{:success-event [:accounts.update.callback/save-settings-success]}))) (accounts.update/update-settings cofx
new-settings
(when (not= (node/get-log-level settings) (node/get-log-level new-settings))
{:success-event [:accounts.update.callback/save-settings-success]}))))
(fx/defn show-change-log-level-confirmation (fx/defn show-change-log-level-confirmation
[{:keys [db]} {:keys [name value] :as log-level}] [{:keys [db]} {:keys [name value] :as log-level}]

View File

@ -54,6 +54,9 @@
(defn notify-users [m callback] (defn notify-users [m callback]
(native-module/notify-users m callback)) (native-module/notify-users m callback))
(defn send-logs [dbJson]
(native-module/send-logs dbJson))
(defn add-peer [enode callback] (defn add-peer [enode callback]
(native-module/add-peer enode callback)) (native-module/add-peer enode callback))

View File

@ -87,6 +87,10 @@
(when status (when status
(call-module #(.notifyUsers status message payload tokens on-result)))) (call-module #(.notifyUsers status message payload tokens on-result))))
(defn send-logs [dbJson]
(when status
(call-module #(.sendLogs status dbJson))))
(defn add-peer [enode on-result] (defn add-peer [enode on-result]
(when (and @node-started status) (when (and @node-started status)
(call-module #(.addPeer status enode on-result)))) (call-module #(.addPeer status enode on-result))))

View File

@ -72,6 +72,12 @@
[limit nodes] [limit nodes]
(take limit (shuffle nodes))) (take limit (shuffle nodes)))
(defn get-log-level
[account-settings]
(or (:log-level account-settings)
(if utils.platform/desktop? ""
config/log-level-status-go)))
(defn- get-account-node-config [db address] (defn- get-account-node-config [db address]
(let [accounts (get db :accounts/accounts) (let [accounts (get db :accounts/accounts)
current-fleet-key (fleet/current-fleet db address) current-fleet-key (fleet/current-fleet db address)
@ -85,9 +91,7 @@
:installation-id (get db :accounts/new-installation-id)} :installation-id (get db :accounts/new-installation-id)}
(get accounts address)) (get accounts address))
use-custom-bootnodes (get-in settings [:bootnodes network]) use-custom-bootnodes (get-in settings [:bootnodes network])
log-level (or (:log-level settings) log-level (get-log-level settings)]
(if utils.platform/desktop? ""
config/log-level-status-go))]
(cond-> (get-in networks [network :config]) (cond-> (get-in networks [network :config])
:always :always
(get-base-node-config) (get-base-node-config)

View File

@ -183,6 +183,19 @@
:margin-bottom 8 :margin-bottom 8
:background-color colors/gray-light}) :background-color colors/gray-light})
(defn adv-settings-row-text [color]
{:color color
:font-size 14})
(defn adv-settings-row [active?]
{:justify-content :space-between
:flex-direction :row
:padding-horizontal 24
:height 56
:align-self :stretch
:align-items :center
:background-color (if active? colors/gray-lighter colors/white)})
(def pair-button (def pair-button
{:margin-left 32}) {:margin-left 32})

View File

@ -7,6 +7,8 @@
[status-im.utils.utils :as utils] [status-im.utils.utils :as utils]
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.utils.logging.core :as logging]
[status-im.utils.platform :as platform]
[status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.icons.vector-icons :as vector-icons]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.utils.gfycat.core :as gfy] [status-im.utils.gfycat.core :as gfy]
@ -135,15 +137,21 @@
(views/defview logging-display [] (views/defview logging-display []
(views/letsubs [logging-enabled [:settings/logging-enabled]] (views/letsubs [logging-enabled [:settings/logging-enabled]]
[react/view {:style (styles/profile-row false)} [react/view
[react/text {:style (assoc (styles/profile-row-text colors/black) [react/view {:style (styles/adv-settings-row false)}
[react/text {:style (assoc (styles/adv-settings-row-text colors/black)
:font-size 14)} (i18n/label :t/logging-enabled)] :font-size 14)} (i18n/label :t/logging-enabled)]
[react/switch {:on-tint-color colors/blue [react/switch {:on-tint-color colors/blue
:value logging-enabled :value logging-enabled
:on-value-change #(re-frame/dispatch [:log-level.ui/logging-enabled (not logging-enabled)])}]])) :on-value-change #(re-frame/dispatch [:log-level.ui/logging-enabled (not logging-enabled)])}]]
[react/view {:style (styles/adv-settings-row false)}
[react/touchable-highlight {:on-press #(re-frame/dispatch [:logging.ui/send-logs-pressed])}
[react/text {:style (styles/adv-settings-row-text colors/red)}
(i18n/label :t/send-logs)]]]]))
(views/defview advanced-settings [] (views/defview advanced-settings []
(views/letsubs [current-mailserver-id [:mailserver/current-id] (views/letsubs [installations [:pairing/installations]
current-mailserver-id [:mailserver/current-id]
mailservers [:mailserver/fleet-mailservers] mailservers [:mailserver/fleet-mailservers]
mailserver-state [:mailserver/state] mailserver-state [:mailserver/state]
node-status [:node-status] node-status [:node-status]
@ -197,21 +205,21 @@
[vector-icons/icon :icons/qr {:style {:tint-color colors/blue}}]]]]) [vector-icons/icon :icons/qr {:style {:tint-color colors/blue}}]]]])
(defn help-item [help-open?] (defn help-item [help-open?]
[react/touchable-highlight {:style (styles/profile-row help-open?) [react/touchable-highlight {:style (styles/adv-settings-row help-open?)
:on-press #(re-frame/dispatch [:navigate-to (if help-open? :home :help-center)])} :on-press #(re-frame/dispatch [:navigate-to (if help-open? :home :help-center)])}
[react/view {:style styles/adv-settings} [react/view {:style styles/adv-settings}
[react/text {:style (styles/profile-row-text colors/black) [react/text {:style (styles/adv-settings-row-text colors/black)
:font (if help-open? :medium :default)} :font (if help-open? :medium :default)}
(i18n/label :t/help-center)] (i18n/label :t/help-center)]
[vector-icons/icon :icons/forward {:style {:tint-color colors/gray}}]]]) [vector-icons/icon :icons/forward {:style {:tint-color colors/gray}}]]])
(defn advanced-settings-item [adv-settings-open?] (defn advanced-settings-item [adv-settings-open?]
[react/touchable-highlight {:style (styles/profile-row adv-settings-open?) [react/touchable-highlight {:style (styles/adv-settings-row adv-settings-open?)
:on-press #(do :on-press #(do
(re-frame/dispatch [:navigate-to (if adv-settings-open? :home :advanced-settings)]) (re-frame/dispatch [:navigate-to (if adv-settings-open? :home :advanced-settings)])
(re-frame/dispatch [:load-debug-metrics]))} (re-frame/dispatch [:load-debug-metrics]))}
[react/view {:style styles/adv-settings} [react/view {:style styles/adv-settings}
[react/text {:style (styles/profile-row-text colors/black) [react/text {:style (styles/adv-settings-row-text colors/black)
:font (if adv-settings-open? :medium :default)} :font (if adv-settings-open? :medium :default)}
(i18n/label :t/advanced-settings)] (i18n/label :t/advanced-settings)]
[vector-icons/icon :icons/forward {:style {:tint-color colors/gray}}]]]) [vector-icons/icon :icons/forward {:style {:tint-color colors/gray}}]]])

View File

@ -38,6 +38,14 @@
(defstyle my-profile-settings-logout (defstyle my-profile-settings-logout
{:min-width "50%"}) {:min-width "50%"})
(defstyle my-profile-settings-send-logs-wrapper
{:flex-direction :row
:justify-content :space-between
:align-items :center})
(defstyle my-profile-settings-send-logs
{:min-width "50%"})
(def advanced-button (def advanced-button
{:margin-top 16 {:margin-top 16
:margin-bottom 12}) :margin-bottom 12})

View File

@ -170,6 +170,13 @@
{:label-kw :t/log-level {:label-kw :t/log-level
:action-fn #(re-frame/dispatch [:navigate-to :log-level-settings]) :action-fn #(re-frame/dispatch [:navigate-to :log-level-settings])
:accessibility-label :log-level-settings-button}] :accessibility-label :log-level-settings-button}]
(when (and dev-mode? (not platform/ios?))
[react/view styles/my-profile-settings-send-logs-wrapper
[react/view styles/my-profile-settings-send-logs
[profile.components/settings-item {:label-kw :t/send-logs
:destructive? true
:hide-arrow? true
:action-fn #(re-frame/dispatch [:logging.ui/send-logs-pressed])}]]])
[profile.components/settings-item-separator] [profile.components/settings-item-separator]
[profile.components/settings-item [profile.components/settings-item
{:label-kw :t/fleet {:label-kw :t/fleet

View File

@ -0,0 +1,36 @@
(ns status-im.utils.logging.core
(:require [re-frame.core :as re-frame]
[status-im.native-module.core :as status]
[status-im.utils.fx :as fx]
[status-im.utils.types :as types]))
(fx/defn send-logs
[{:keys [db] :as cofx}]
;; TODO: Add message explaining db export
(let [db-json (types/clj->json (select-keys db [:app-state
:current-chat-id
:discover-current-dapp
:discover-search-tags
:discoveries
:initial-props
:keyboard-height
:keyboard-max-height
:navigation-stack
:network
:network-status
:peers-count
:peers-summary
:status-module-initialized?
:sync-state
:tab-bar-visible?
:view-id
:chat/cooldown-enabled?
:chat/cooldowns
:chat/last-outgoing-message-sent-at
:chat/spam-messages-frequency
:chats/loading?
:desktop/desktop
:dimensions/window
:my-profile/editing?
:node/status]))]
(status/send-logs db-json)))

View File

@ -261,6 +261,7 @@
"logout": "Log out", "logout": "Log out",
"status-not-sent-tap": "Not confirmed. Tap for options", "status-not-sent-tap": "Not confirmed. Tap for options",
"status-not-sent-click": "Not confirmed. Click for options", "status-not-sent-click": "Not confirmed. Click for options",
"send-logs": "Send logs",
"edit-network-config": "Edit network config", "edit-network-config": "Edit network config",
"clear-history-confirmation": "Clear history?", "clear-history-confirmation": "Clear history?",
"connect": "Connect", "connect": "Connect",