mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Ensure uploaded logs match debug log viewer.
This commit is contained in:
@@ -376,18 +376,28 @@ public class SubmitDebugLogActivity extends BaseActivity {
|
||||
|
||||
switch (mode) {
|
||||
case LOADING:
|
||||
searchNav.setVisibility(View.GONE);
|
||||
saveMenuItem.setVisible(false);
|
||||
searchMenuItem.setVisible(false);
|
||||
progressCard.setVisibility(View.VISIBLE);
|
||||
submitButton.setEnabled(false);
|
||||
logWebView.setAlpha(0.25f);
|
||||
break;
|
||||
case NORMAL:
|
||||
searchNav.setVisibility(View.GONE);
|
||||
saveMenuItem.setVisible(true);
|
||||
searchMenuItem.setVisible(true);
|
||||
progressCard.setVisibility(View.GONE);
|
||||
submitButton.setEnabled(true);
|
||||
logWebView.setAlpha(1f);
|
||||
break;
|
||||
case SUBMITTING:
|
||||
searchMenuItem.setVisible(false);
|
||||
searchNav.setVisibility(View.GONE);
|
||||
saveMenuItem.setVisible(false);
|
||||
progressCard.setVisibility(View.VISIBLE);
|
||||
searchMenuItem.setVisible(false);
|
||||
progressCard.setVisibility(View.GONE);
|
||||
submitButton.setSpinning();
|
||||
logWebView.setAlpha(1f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -447,7 +457,7 @@ public class SubmitDebugLogActivity extends BaseActivity {
|
||||
private void onSubmitClicked() {
|
||||
submitButton.setSpinning();
|
||||
|
||||
viewModel.onSubmitClicked().observe(this, result -> {
|
||||
viewModel.onSubmitClicked(DebugLogsViewer.readLogs(logWebView)).observe(this, result -> {
|
||||
if (result.isPresent()) {
|
||||
presentResultDialog(result.get());
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.logging.Scrubber;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.debuglogsviewer.DebugLogsViewer;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
|
||||
@@ -125,15 +126,8 @@ public class SubmitDebugLogRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits a log with the provided prefix lines.
|
||||
*
|
||||
* @param untilTime Only submit logs from {@link LogDatabase} if they were created before this time. This is our way of making sure that the logs we submit
|
||||
* only include the logs that we've already shown the user. It's possible some old logs may have been trimmed off in the meantime, but no
|
||||
* new ones could pop up.
|
||||
*/
|
||||
public void submitLogWithPrefixLines(long untilTime, @NonNull List<LogLine> prefixLines, @Nullable byte[] trace, Callback<Optional<String>> callback) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(untilTime, prefixLines, trace)));
|
||||
public void submitLogFromReader(DebugLogsViewer.LogReader logReader, @Nullable byte[] trace, Callback<Optional<String>> callback) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogFromReaderInternal(logReader, trace)));
|
||||
}
|
||||
|
||||
public void writeLogToDisk(@NonNull Uri uri, long untilTime, Callback<Boolean> callback) {
|
||||
@@ -168,6 +162,79 @@ public class SubmitDebugLogRepository {
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Optional<String> submitLogFromReaderInternal(DebugLogsViewer.LogReader logReader, @Nullable byte[] trace) {
|
||||
Stopwatch stopwatch = new Stopwatch("log-upload");
|
||||
String traceUrl = null;
|
||||
if (trace != null) {
|
||||
try {
|
||||
traceUrl = uploadContent("application/octet-stream", RequestBody.create(MediaType.get("application/octet-stream"), trace));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Error during trace upload.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
stopwatch.split("trace");
|
||||
|
||||
try {
|
||||
|
||||
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
|
||||
Future<Uri> futureUri = BlobProvider.getInstance()
|
||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||
.withMimeType("application/gzip")
|
||||
.createForSingleSessionOnDiskAsync(context);
|
||||
|
||||
OutputStream gzipOutput = new GZIPOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
||||
|
||||
boolean traceFound = trace == null;
|
||||
String next;
|
||||
while ((next = logReader.nextChunk(10_000)) != null) {
|
||||
if (!traceFound) {
|
||||
int traceIndex = next.indexOf("<binary trace data>");
|
||||
if (traceIndex != -1) {
|
||||
next = next.replace("<binary trace data>", traceUrl);
|
||||
traceFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
gzipOutput.write(next.getBytes());
|
||||
gzipOutput.write("\n".getBytes());
|
||||
}
|
||||
|
||||
StreamUtil.close(gzipOutput);
|
||||
Uri gzipUri = futureUri.get();
|
||||
|
||||
stopwatch.split("body");
|
||||
|
||||
String logUrl = uploadContent("application/gzip", new RequestBody() {
|
||||
@Override
|
||||
public @NonNull MediaType contentType() {
|
||||
return MediaType.get("application/gzip");
|
||||
}
|
||||
|
||||
@Override public long contentLength() {
|
||||
return BlobProvider.getInstance().calculateFileSize(context, gzipUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(@NonNull BufferedSink sink) throws IOException {
|
||||
Source source = Okio.source(BlobProvider.getInstance().getStream(context, gzipUri));
|
||||
sink.writeAll(source);
|
||||
}
|
||||
});
|
||||
|
||||
stopwatch.split("upload");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
BlobProvider.getInstance().delete(context, gzipUri);
|
||||
|
||||
return Optional.of(logUrl);
|
||||
} catch (IOException | RuntimeException | ExecutionException | InterruptedException e) {
|
||||
Log.w(TAG, "Error during log upload.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Optional<String> submitLogInternal(long untilTime, @NonNull List<LogLine> prefixLines, @Nullable byte[] trace) {
|
||||
String traceUrl = null;
|
||||
|
||||
@@ -10,10 +10,9 @@ import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.debuglogsviewer.DebugLogsViewer;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
@@ -120,17 +119,14 @@ public class SubmitDebugLogViewModel extends ViewModel {
|
||||
return mode;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Optional<String>> onSubmitClicked() {
|
||||
@NonNull LiveData<Optional<String>> onSubmitClicked(DebugLogsViewer.LogReader logReader) {
|
||||
mode.postValue(Mode.SUBMITTING);
|
||||
|
||||
MutableLiveData<Optional<String>> result = new MutableLiveData<>();
|
||||
|
||||
// Get prefix lines for submission - this is a quick operation so it's ok to do it here
|
||||
repo.getPrefixLogLines(prefixLines -> {
|
||||
repo.submitLogWithPrefixLines(firstViewTime, prefixLines, trace, value -> {
|
||||
mode.postValue(Mode.NORMAL);
|
||||
result.postValue(value);
|
||||
});
|
||||
repo.submitLogFromReader(logReader, trace, value -> {
|
||||
mode.postValue(Mode.NORMAL);
|
||||
result.postValue(value);
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
@@ -13,6 +13,7 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core-util"))
|
||||
implementation(project(":core-util-jvm"))
|
||||
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
|
||||
@@ -220,4 +220,13 @@ function applyFilter() {
|
||||
editor.setValue(filtered, -1);
|
||||
}
|
||||
|
||||
function readLines(offset, limit) {
|
||||
const lines = logLines.split("\n")
|
||||
if (offset >= lines.length) {
|
||||
return "<<END OF INPUT>>";
|
||||
}
|
||||
|
||||
return lines.slice(offset, offset + limit).join("\n")
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -11,8 +11,10 @@ import android.webkit.ValueCallback
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import kotlinx.coroutines.Runnable
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.function.Consumer
|
||||
|
||||
object DebugLogsViewer {
|
||||
@@ -47,6 +49,27 @@ object DebugLogsViewer {
|
||||
ThreadUtil.runOnMain { webview.evaluateJavascript("editor.insert($escaped); logLines+=$escaped;", null) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun readLogs(webview: WebView): LogReader {
|
||||
var position = 0
|
||||
return LogReader { size ->
|
||||
val latch = CountDownLatch(1)
|
||||
var result: String? = null
|
||||
ThreadUtil.runOnMain {
|
||||
webview.evaluateJavascript("readLines($position, $size)") { value ->
|
||||
// Annoying, string returns from javascript land are always encoded as JSON strings (wrapped in quotes, various strings escaped, etc)
|
||||
val parsed = JSONArray("[$value]").getString(0)
|
||||
result = parsed.takeUnless { it == "<<END OF INPUT>>" }
|
||||
position += size
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
latch.await()
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun scrollToTop(webview: WebView) {
|
||||
webview.evaluateJavascript("editor.scrollToRow(0);", null)
|
||||
@@ -107,4 +130,9 @@ object DebugLogsViewer {
|
||||
fun onSearchClose(webview: WebView) {
|
||||
webview.evaluateJavascript("onSearchClose();", null)
|
||||
}
|
||||
|
||||
fun interface LogReader {
|
||||
/** Returns the next bit of log, containing at most [size] lines (but may be less), or null if there are no logs remaining. */
|
||||
fun nextChunk(size: Int): String?
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user