Merge pull request #765 from hjubb/restore_log_report

Restore log report
This commit is contained in:
Harris 2021-10-07 23:01:04 +00:00 committed by GitHub
commit d190ac8335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 415 additions and 11 deletions

View File

@ -54,15 +54,19 @@ import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.JobDatabase; import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.LokiAPIDatabase; import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage; import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.dependencies.DatabaseModule; import org.thoughtcrime.securesms.dependencies.DatabaseModule;
import org.thoughtcrime.securesms.groups.OpenGroupManager; import org.thoughtcrime.securesms.groups.OpenGroupManager;
import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.groups.OpenGroupManager;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
import org.thoughtcrime.securesms.jobs.FastJobStorage; import org.thoughtcrime.securesms.jobs.FastJobStorage;
import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.jobs.JobManagerFactories;
import org.thoughtcrime.securesms.logging.AndroidLogger; import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; import org.thoughtcrime.securesms.notifications.BackgroundPollWorker;
import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier;
@ -126,6 +130,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
public Broadcaster broadcaster = null; public Broadcaster broadcaster = null;
private Job firebaseInstanceIdJob; private Job firebaseInstanceIdJob;
private Handler conversationListNotificationHandler; private Handler conversationListNotificationHandler;
private PersistentLogger persistentLogger;
@Inject LokiAPIDatabase lokiAPIDatabase; @Inject LokiAPIDatabase lokiAPIDatabase;
@Inject Storage storage; @Inject Storage storage;
@ -146,6 +151,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
return this.conversationListNotificationHandler; return this.conversationListNotificationHandler;
} }
public PersistentLogger getPersistentLogger() {
return this.persistentLogger;
}
@Override @Override
public void onCreate() { public void onCreate() {
DatabaseModule.init(this); DatabaseModule.init(this);
@ -281,7 +290,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
} }
private void initializeLogging() { private void initializeLogging() {
Log.initialize(new AndroidLogger()); if (persistentLogger == null) {
persistentLogger = new PersistentLogger(this);
}
Log.initialize(new AndroidLogger(), persistentLogger);
} }
private void initializeCrashHandling() { private void initializeCrashHandling() {

View File

@ -123,7 +123,7 @@ class LogFile {
return builder.toString(); return builder.toString();
} }
private String readEntry() throws IOException { String readEntry() throws IOException {
try { try {
Util.readFully(inputStream, ivBuffer); Util.readFully(inputStream, ivBuffer);
Util.readFully(inputStream, intBuffer); Util.readFully(inputStream, intBuffer);

View File

@ -0,0 +1,250 @@
package org.thoughtcrime.securesms.logging;
import android.content.Context;
import androidx.annotation.AnyThread;
import androidx.annotation.WorkerThread;
import org.session.libsignal.utilities.ListenableFuture;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.NoExternalStorageException;
import org.session.libsignal.utilities.SettableFuture;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class PersistentLogger extends Log.Logger {
private static final String TAG = PersistentLogger.class.getSimpleName();
private static final String LOG_V = "V";
private static final String LOG_D = "D";
private static final String LOG_I = "I";
private static final String LOG_W = "W";
private static final String LOG_E = "E";
private static final String LOG_WTF = "A";
private static final String LOG_DIRECTORY = "log";
private static final String FILENAME_PREFIX = "log-";
private static final int MAX_LOG_FILES = 5;
private static final int MAX_LOG_SIZE = 300 * 1024;
private static final int MAX_LOG_EXPORT = 10_000;
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz");
private final Context context;
private final Executor executor;
private final byte[] secret;
private LogFile.Writer writer;
public PersistentLogger(Context context) {
this.context = context.getApplicationContext();
this.secret = LogSecretProvider.getOrCreateAttachmentSecret(context);
this.executor = Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r, "PersistentLogger");
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
executor.execute(this::initializeWriter);
}
@Override
public void v(String tag, String message, Throwable t) {
write(LOG_V, tag, message, t);
}
@Override
public void d(String tag, String message, Throwable t) {
write(LOG_D, tag, message, t);
}
@Override
public void i(String tag, String message, Throwable t) {
write(LOG_I, tag, message, t);
}
@Override
public void w(String tag, String message, Throwable t) {
write(LOG_W, tag, message, t);
}
@Override
public void e(String tag, String message, Throwable t) {
write(LOG_E, tag, message, t);
}
@Override
public void wtf(String tag, String message, Throwable t) {
write(LOG_WTF, tag, message, t);
}
@Override
public void blockUntilAllWritesFinished() {
CountDownLatch latch = new CountDownLatch(1);
executor.execute(latch::countDown);
try {
latch.await();
} catch (InterruptedException e) {
android.util.Log.w(TAG, "Failed to wait for all writes.");
}
}
@WorkerThread
public ListenableFuture<String> getLogs() {
final SettableFuture<String> future = new SettableFuture<>();
executor.execute(() -> {
StringBuilder builder = new StringBuilder();
long entriesWritten = 0;
try {
File[] logs = getSortedLogFiles();
for (int i = logs.length - 1; i >= 0 && entriesWritten <= MAX_LOG_EXPORT; i--) {
try {
LogFile.Reader reader = new LogFile.Reader(secret, logs[i]);
String entry;
while ((entry = reader.readEntry()) != null) {
entriesWritten++;
builder.append(entry).append('\n');
}
} catch (IOException e) {
android.util.Log.w(TAG, "Failed to read log at index " + i + ". Removing reference.");
logs[i].delete();
}
}
future.set(builder.toString());
} catch (NoExternalStorageException e) {
future.setException(e);
}
});
return future;
}
@WorkerThread
private void initializeWriter() {
try {
writer = new LogFile.Writer(secret, getOrCreateActiveLogFile());
} catch (NoExternalStorageException | IOException e) {
android.util.Log.e(TAG, "Failed to initialize writer.", e);
}
}
@AnyThread
private void write(String level, String tag, String message, Throwable t) {
executor.execute(() -> {
try {
if (writer == null) {
return;
}
if (writer.getLogSize() >= MAX_LOG_SIZE) {
writer.close();
writer = new LogFile.Writer(secret, createNewLogFile());
trimLogFilesOverMax();
}
for (String entry : buildLogEntries(level, tag, message, t)) {
writer.writeEntry(entry);
}
} catch (NoExternalStorageException e) {
android.util.Log.w(TAG, "Cannot persist logs.", e);
} catch (IOException e) {
android.util.Log.w(TAG, "Failed to write line. Deleting all logs and starting over.");
deleteAllLogs();
initializeWriter();
}
});
}
private void trimLogFilesOverMax() throws NoExternalStorageException {
File[] logs = getSortedLogFiles();
if (logs.length > MAX_LOG_FILES) {
for (int i = MAX_LOG_FILES; i < logs.length; i++) {
logs[i].delete();
}
}
}
private void deleteAllLogs() {
try {
File[] logs = getSortedLogFiles();
for (File log : logs) {
log.delete();
}
} catch (NoExternalStorageException e) {
android.util.Log.w(TAG, "Was unable to delete logs.", e);
}
}
private File getOrCreateActiveLogFile() throws NoExternalStorageException {
File[] logs = getSortedLogFiles();
if (logs.length > 0) {
return logs[0];
}
return createNewLogFile();
}
private File createNewLogFile() throws NoExternalStorageException {
return new File(getOrCreateLogDirectory(), FILENAME_PREFIX + System.currentTimeMillis());
}
private File[] getSortedLogFiles() throws NoExternalStorageException {
File[] logs = getOrCreateLogDirectory().listFiles();
if (logs != null) {
Arrays.sort(logs, (o1, o2) -> o2.getName().compareTo(o1.getName()));
return logs;
}
return new File[0];
}
private File getOrCreateLogDirectory() throws NoExternalStorageException {
File logDir = new File(context.getCacheDir(), LOG_DIRECTORY);
if (!logDir.exists() && !logDir.mkdir()) {
throw new NoExternalStorageException("Unable to create log directory.");
}
return logDir;
}
private List<String> buildLogEntries(String level, String tag, String message, Throwable t) {
List<String> entries = new LinkedList<>();
Date date = new Date();
entries.add(buildEntry(level, tag, message, date));
if (t != null) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
t.printStackTrace(new PrintStream(outputStream));
String trace = new String(outputStream.toByteArray());
String[] lines = trace.split("\\n");
for (String line : lines) {
entries.add(buildEntry(level, tag, line, date));
}
}
return entries;
}
private String buildEntry(String level, String tag, String message, Date date) {
return DATE_FORMAT.format(date) + ' ' + level + ' ' + tag + ": " + message;
}
}

View File

@ -85,6 +85,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
helpTranslateButton.setOnClickListener { helpTranslate() } helpTranslateButton.setOnClickListener { helpTranslate() }
seedButton.setOnClickListener { showSeed() } seedButton.setOnClickListener { showSeed() }
clearAllDataButton.setOnClickListener { clearAllData() } clearAllDataButton.setOnClickListener { clearAllData() }
supportButton.setOnClickListener { shareLogs() }
val isLightMode = UiModeUtilities.isDayUiMode(this) val isLightMode = UiModeUtilities.isDayUiMode(this)
oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode) oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode)
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
@ -321,6 +322,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
} }
private fun shareLogs() {
ShareLogsDialog().show(supportFragmentManager,"Share Logs Dialog")
}
// endregion // endregion
private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { private inner class DisplayNameEditActionModeCallback: ActionMode.Callback {

View File

@ -0,0 +1,65 @@
package org.thoughtcrime.securesms.preferences
import android.content.Intent
import android.os.Build
import android.view.LayoutInflater
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.dialog_share_logs.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog
import org.thoughtcrime.securesms.providers.BlobProvider
class ShareLogsDialog : BaseDialog() {
private var shareJob: Job? = null
override fun setContentView(builder: AlertDialog.Builder) {
val contentView =
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null)
contentView.cancelButton.setOnClickListener {
dismiss()
}
contentView.shareButton.setOnClickListener {
// start the export and share
shareLogs()
}
builder.setView(contentView)
builder.setCancelable(false)
}
private fun shareLogs() {
shareJob?.cancel()
shareJob = lifecycleScope.launch(Dispatchers.IO) {
val persistentLogger = ApplicationContext.getInstance(context).persistentLogger
try {
val logs = persistentLogger.logs.get()
val fileName = "${Build.MANUFACTURER}-${Build.DEVICE}-API${Build.VERSION.SDK_INT}-v${BuildConfig.VERSION_NAME}.log"
val logUri = BlobProvider().forData(logs.toByteArray())
.withFileName(fileName)
.withMimeType("text/plain")
.createForSingleSessionOnDisk(requireContext(),null)
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, logUri)
type = "text/plain"
}
dismiss()
startActivity(Intent.createChooser(shareIntent, getString(R.string.share)))
} catch (e: Exception) {
Toast.makeText(context,"Error saving logs", Toast.LENGTH_LONG).show()
dismiss()
}
}
}
}

View File

@ -227,6 +227,18 @@
android:gravity="center" android:gravity="center"
android:text="@string/activity_settings_survey_feedback" /> android:text="@string/activity_settings_survey_feedback" />
<TextView
android:padding="@dimen/small_spacing"
android:id="@+id/supportButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/medium_spacing"
android:textColor="@color/text"
android:textSize="@dimen/medium_font_size"
android:textStyle="bold"
android:gravity="center"
android:text="@string/activity_settings_support" />
<TextView <TextView
android:padding="@dimen/small_spacing" android:padding="@dimen/small_spacing"
android:id="@+id/helpTranslateButton" android:id="@+id/helpTranslateButton"

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/default_dialog_background_inset"
android:gravity="center_horizontal"
android:orientation="vertical"
android:elevation="4dp"
android:padding="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/dialog_share_logs_title"
android:textColor="@color/text"
android:textStyle="bold"
android:textSize="@dimen/medium_font_size" />
<TextView
android:id="@+id/dialogDescriptionText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing"
android:text="@string/dialog_share_logs_explanation"
android:textColor="@color/text"
android:textSize="@dimen/small_font_size"
android:textAlignment="center" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing"
android:orientation="horizontal">
<Button
style="@style/Widget.Session.Button.Dialog.Unimportant"
android:id="@+id/cancelButton"
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
android:text="@string/cancel" />
<Button
style="@style/Widget.Session.Button.Dialog.Unimportant"
android:id="@+id/shareButton"
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/share" />
<com.github.ybq.android.spinkit.SpinKitView
style="@style/SpinKitView.Small.ThreeBounce"
android:id="@+id/progressBar"
android:layout_width="0dp"
android:layout_height="@dimen/small_button_height"
android:layout_weight="1"
app:SpinKit_Color="@color/accent"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>

View File

@ -899,5 +899,8 @@
<string name="delete_message_for_everyone">Delete for everyone</string> <string name="delete_message_for_everyone">Delete for everyone</string>
<string name="delete_message_for_me_and_recipient">Delete for me and %s</string> <string name="delete_message_for_me_and_recipient">Delete for me and %s</string>
<string name="activity_settings_survey_feedback">Feedback/Survey</string> <string name="activity_settings_survey_feedback">Feedback/Survey</string>
<string name="activity_settings_support">Support</string>
<string name="dialog_share_logs_title">Share Logs</string>
<string name="dialog_share_logs_explanation">Would you like to export your application logs to be able to share for troubleshooting?</string>
</resources> </resources>

View File

@ -19,7 +19,6 @@ class JobQueue : JobDelegate {
private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>() private val jobTimestampMap = ConcurrentHashMap<Long, AtomicInteger>()
private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val rxDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() private val txDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
private val attachmentDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
private val scope = GlobalScope + SupervisorJob() private val scope = GlobalScope + SupervisorJob()
private val queue = Channel<Job>(UNLIMITED) private val queue = Channel<Job>(UNLIMITED)
private val pendingJobIds = mutableSetOf<String>() private val pendingJobIds = mutableSetOf<String>()
@ -39,18 +38,15 @@ class JobQueue : JobDelegate {
scope.launch { scope.launch {
val rxQueue = Channel<Job>(capacity = 4096) val rxQueue = Channel<Job>(capacity = 4096)
val txQueue = Channel<Job>(capacity = 4096) val txQueue = Channel<Job>(capacity = 4096)
val attachmentQueue = Channel<Job>(capacity = 4096)
val receiveJob = processWithDispatcher(rxQueue, rxDispatcher) val receiveJob = processWithDispatcher(rxQueue, rxDispatcher)
val txJob = processWithDispatcher(txQueue, txDispatcher) val txJob = processWithDispatcher(txQueue, txDispatcher)
val attachmentJob = processWithDispatcher(attachmentQueue, attachmentDispatcher)
while (isActive) { while (isActive) {
for (job in queue) { for (job in queue) {
when (job) { when (job) {
is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> txQueue.send(job) is NotifyPNServerJob, is AttachmentUploadJob, is MessageSendJob -> txQueue.send(job)
is AttachmentDownloadJob -> attachmentQueue.send(job) is MessageReceiveJob, is TrimThreadJob, is BatchMessageReceiveJob, is AttachmentDownloadJob-> rxQueue.send(job)
is MessageReceiveJob, is BatchMessageReceiveJob, is TrimThreadJob -> rxQueue.send(job)
else -> throw IllegalStateException("Unexpected job type.") else -> throw IllegalStateException("Unexpected job type.")
} }
} }
@ -59,7 +55,6 @@ class JobQueue : JobDelegate {
// The job has been cancelled // The job has been cancelled
receiveJob.cancel() receiveJob.cancel()
txJob.cancel() txJob.cancel()
attachmentJob.cancel()
} }
} }

View File

@ -10,7 +10,6 @@ import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.crypto.getRandomElementOrNull import org.session.libsignal.crypto.getRandomElementOrNull
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.successBackground
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledFuture
@ -82,7 +81,6 @@ class ClosedGroupPollerV2 {
val limit: Long = 12 * 60 * 60 * 1000 val limit: Long = 12 * 60 * 60 * 1000
val a = (Companion.maxPollInterval - minPollInterval).toDouble() / limit.toDouble() val a = (Companion.maxPollInterval - minPollInterval).toDouble() / limit.toDouble()
val nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval val nextPollInterval = a * min(timeSinceLastMessage, limit) + minPollInterval
Log.d("Loki", "Next poll interval for closed group with public key: $groupPublicKey is ${nextPollInterval / 1000} s.")
executorService?.schedule({ executorService?.schedule({
poll(groupPublicKey).success { poll(groupPublicKey).success {
pollRecursively(groupPublicKey) pollRecursively(groupPublicKey)
@ -108,7 +106,7 @@ class ClosedGroupPollerV2 {
} }
} }
promise.fail { promise.fail {
Log.d("Loki", "Polling failed for closed group with public key: $groupPublicKey due to error: $it.") Log.d("Loki", "Polling failed for closed group due to error: $it.")
} }
return promise.map { } return promise.map { }
} }