Add debug log preview
This commit is contained in:
parent
025a9ff9be
commit
ab454da687
@ -1250,14 +1250,20 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
switch action {
|
||||
case .submitDebugLogsAndCrash:
|
||||
addSubmitDebugLogsAction {
|
||||
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
|
||||
DebugLogs(dumper: logDumper).promptToSubmitLogs(
|
||||
from: viewController,
|
||||
supportTag: supportTag,
|
||||
) {
|
||||
owsFail("Exiting after submitting debug logs")
|
||||
}
|
||||
}
|
||||
|
||||
case .submitDebugLogsAndLaunchApp(let window, let launchContext):
|
||||
addSubmitDebugLogsAction { [unowned window] in
|
||||
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
|
||||
DebugLogs(dumper: logDumper).promptToSubmitLogs(
|
||||
from: viewController,
|
||||
supportTag: supportTag,
|
||||
) {
|
||||
ignoreErrorAndLaunchApp(in: window, launchContext: launchContext)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,13 +192,16 @@ class CallQualitySurveyManager {
|
||||
return proto
|
||||
}
|
||||
|
||||
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
|
||||
func submit(
|
||||
rating: CallQualitySurvey.Rating,
|
||||
logsToSubmit logs: DebugLogs?,
|
||||
) {
|
||||
var proto = buildProto(rating: rating)
|
||||
|
||||
Task {
|
||||
if shouldSubmitDebugLogs {
|
||||
if let logs {
|
||||
do {
|
||||
let debugLogURL = try await DebugLogs.uploadLogs(dumper: .fromGlobals())
|
||||
let debugLogURL = try await logs.uploadLogs()
|
||||
proto.debugLogURL = debugLogURL.absoluteString
|
||||
} catch {
|
||||
logger.error("Failed to submit debug logs: \(error)")
|
||||
|
||||
@ -16,10 +16,12 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
private let tableViewController = OWSTableViewController2()
|
||||
|
||||
private var shouldSubmitDebugLogs = false
|
||||
private var logs: DebugLogs
|
||||
|
||||
private let rating: CallQualitySurvey.Rating
|
||||
|
||||
init(rating: CallQualitySurvey.Rating) {
|
||||
self.logs = DebugLogs(dumper: .fromGlobals())
|
||||
self.rating = rating
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
@ -129,7 +131,8 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
let container = UIView()
|
||||
|
||||
let textView = LinkingTextView { [weak self] in
|
||||
self?.showDebugLogPreview()
|
||||
guard let self else { return }
|
||||
self.logs.showPreview(from: self)
|
||||
}
|
||||
textView.attributedText = .composed(of: [
|
||||
OWSLocalizedString(
|
||||
@ -193,12 +196,6 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
private func showDebugLogPreview() {
|
||||
let vc = DebugLogPreviewViewController()
|
||||
let nav = OWSNavigationController(rootViewController: vc)
|
||||
present(nav, animated: true)
|
||||
}
|
||||
|
||||
override func customSheetHeight() -> CGFloat? {
|
||||
let headerHeight = headerContainer.height
|
||||
let collectionViewHeight = tableViewController.tableView.contentSize.height + tableViewController.tableView.contentInset.totalHeight
|
||||
@ -209,7 +206,7 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
|
||||
private func submit() {
|
||||
sheetNav?.submit(
|
||||
rating: self.rating,
|
||||
shouldSubmitDebugLogs: self.shouldSubmitDebugLogs,
|
||||
logsToSubmit: shouldSubmitDebugLogs ? logs : nil,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,10 +82,13 @@ final class CallQualitySurveyNavigationController: UINavigationController {
|
||||
pushViewController(vc, animated: false)
|
||||
}
|
||||
|
||||
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
|
||||
func submit(
|
||||
rating: CallQualitySurvey.Rating,
|
||||
logsToSubmit: DebugLogs?,
|
||||
) {
|
||||
callQualitySurveyManager.submit(
|
||||
rating: rating,
|
||||
shouldSubmitDebugLogs: shouldSubmitDebugLogs,
|
||||
logsToSubmit: logsToSubmit,
|
||||
)
|
||||
let host = presentingViewController
|
||||
dismiss(animated: true) {
|
||||
|
||||
@ -47,11 +47,12 @@ enum ContactSupportActionSheet {
|
||||
let submitWithLogAction = ActionSheetAction(title: submitWithLogTitle, style: .default) { [weak fromViewController] _ in
|
||||
guard let fromViewController else { return }
|
||||
|
||||
let logs = DebugLogs(dumper: logDumper)
|
||||
let emailRequest = SupportEmailModel(
|
||||
userDescription: nil,
|
||||
emojiMood: nil,
|
||||
supportFilter: emailFilter.asString,
|
||||
debugLogPolicy: .requireUpload(logDumper),
|
||||
debugLogPolicy: .requireUpload(logs),
|
||||
hasRecentChallenge: logDumper.challengeReceivedRecently(),
|
||||
)
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import SignalServiceKit
|
||||
import SignalUI
|
||||
import zlib
|
||||
|
||||
public struct DebugLogDumper {
|
||||
struct DebugLogDumper {
|
||||
fileprivate var accountManager: (any TSAccountManager)?
|
||||
fileprivate var appVersion: any AppVersion
|
||||
fileprivate var db: (any DB)?
|
||||
@ -25,7 +25,7 @@ public struct DebugLogDumper {
|
||||
)
|
||||
}
|
||||
|
||||
public func challengeReceivedRecently() -> Bool {
|
||||
func challengeReceivedRecently() -> Bool {
|
||||
guard let db else {
|
||||
return false
|
||||
}
|
||||
@ -57,34 +57,134 @@ public struct DebugLogDumper {
|
||||
}
|
||||
}
|
||||
|
||||
enum DebugLogs {
|
||||
final class DebugLogs {
|
||||
private let dumper: DebugLogDumper
|
||||
private var logsDirPath: String?
|
||||
|
||||
init(dumper: DebugLogDumper) {
|
||||
self.dumper = dumper
|
||||
self.logsDirPath = DebugLogs.collectAndFlushLogs(dumper: dumper)
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let logsDirPath {
|
||||
OWSFileSystem.deleteFile(logsDirPath)
|
||||
}
|
||||
}
|
||||
|
||||
func showPreview(
|
||||
from viewController: UIViewController,
|
||||
onSubmit: (() -> Void)? = nil,
|
||||
onCancel: (() -> Void)? = nil,
|
||||
) {
|
||||
guard let logsDirPath else {
|
||||
Logger.error("No logs path found for preview")
|
||||
handleError(error: .noLogs, viewController: viewController)
|
||||
onCancel?()
|
||||
return
|
||||
}
|
||||
let logFilePaths = ((try? FileManager.default.contentsOfDirectory(atPath: logsDirPath)) ?? []).map {
|
||||
URL(fileURLWithPath: logsDirPath).appendingPathComponent($0).path
|
||||
}
|
||||
let previewVC = DebugLogPreviewViewController(logFilePaths: logFilePaths, onSubmit: onSubmit, onCancel: onCancel)
|
||||
let nav = OWSNavigationController(rootViewController: previewVC)
|
||||
viewController.present(nav, animated: true)
|
||||
}
|
||||
|
||||
/// Presents a log preview with an option to submit. Completion is only
|
||||
/// called if the user submits, after the submission is completed.
|
||||
@MainActor
|
||||
static func submitLogs(supportTag: String? = nil, dumper: DebugLogDumper, completion: (() -> Void)? = nil) {
|
||||
let submitLogsCompletion = {
|
||||
if let completion {
|
||||
// Wait a moment. If the user opens a URL, it needs a moment to complete.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
func promptToSubmitLogs(
|
||||
from viewController: UIViewController,
|
||||
supportTag: String? = nil,
|
||||
completion: (() -> Void)? = nil,
|
||||
) {
|
||||
showPreview(from: viewController, onSubmit: {
|
||||
Task {
|
||||
await viewController.awaitableDismiss(animated: true)
|
||||
await self.submitLogs(supportTag: supportTag)
|
||||
if let completion {
|
||||
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func promptToSubmitLogs(
|
||||
from viewController: UIViewController,
|
||||
supportTag: String? = nil,
|
||||
) async {
|
||||
let didSubmit = await withCheckedContinuation { continuation in
|
||||
showPreview(
|
||||
from: viewController,
|
||||
onSubmit: {
|
||||
continuation.resume(returning: true)
|
||||
},
|
||||
onCancel: {
|
||||
continuation.resume(returning: false)
|
||||
},
|
||||
)
|
||||
}
|
||||
if didSubmit {
|
||||
await viewController.awaitableDismiss(animated: true)
|
||||
await submitLogs(supportTag: supportTag)
|
||||
}
|
||||
}
|
||||
|
||||
enum DebugLogsError: LocalizedError {
|
||||
case noLogs
|
||||
case couldNotPackageLogs
|
||||
case uploadError(zipFilePath: String)
|
||||
|
||||
var errorDescription: String? { localizedErrorMessage }
|
||||
var localizedErrorMessage: String {
|
||||
switch self {
|
||||
case .noLogs:
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_NO_LOGS",
|
||||
comment: "Error indicating that no debug logs could be found.",
|
||||
)
|
||||
case .couldNotPackageLogs:
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS",
|
||||
comment: "Error indicating that the debug logs could not be packaged.",
|
||||
)
|
||||
case .uploadError:
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG",
|
||||
comment: "Error indicating that a debug log could not be uploaded.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func submitLogs(supportTag: String?) async {
|
||||
var supportFilter = "Signal - iOS Debug Log"
|
||||
if let supportTag {
|
||||
supportFilter += " - \(supportTag)"
|
||||
}
|
||||
|
||||
guard let frontmostViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
submitLogsCompletion()
|
||||
return
|
||||
}
|
||||
uploadLogsUsingViewController(frontmostViewController, dumper: dumper) { url in
|
||||
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
submitLogsCompletion()
|
||||
return
|
||||
}
|
||||
|
||||
let url: URL?
|
||||
do {
|
||||
url = try await uploadLogsWithUI(from: frontmostViewController)
|
||||
} catch {
|
||||
self.handleError(error: error, viewController: frontmostViewController)
|
||||
return
|
||||
}
|
||||
guard let url else { return }
|
||||
|
||||
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
return
|
||||
}
|
||||
|
||||
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
||||
let alert = ActionSheetController(
|
||||
title: NSLocalizedString("DEBUG_LOG_ALERT_TITLE", comment: "Title of the debug log alert."),
|
||||
message: NSLocalizedString("DEBUG_LOG_ALERT_MESSAGE", comment: "Message of the debug log alert."),
|
||||
@ -102,10 +202,10 @@ enum DebugLogs {
|
||||
await ComposeSupportEmailOperation.sendEmailWithDefaultErrorHandling(
|
||||
supportFilter: supportFilter,
|
||||
logUrl: url,
|
||||
hasRecentChallenge: dumper.challengeReceivedRecently(),
|
||||
hasRecentChallenge: self.dumper.challengeReceivedRecently(),
|
||||
)
|
||||
}
|
||||
submitLogsCompletion()
|
||||
continuation.resume()
|
||||
},
|
||||
))
|
||||
}
|
||||
@ -118,7 +218,7 @@ enum DebugLogs {
|
||||
handler: { _ in
|
||||
UIPasteboard.general.string = url.absoluteString
|
||||
presentingViewController.presentToast(text: CommonStrings.copiedToClipboardToast, image: .copy)
|
||||
submitLogsCompletion()
|
||||
continuation.resume()
|
||||
},
|
||||
))
|
||||
alert.addAction(ActionSheetAction(
|
||||
@ -131,67 +231,39 @@ enum DebugLogs {
|
||||
AttachmentSharing.showShareUI(
|
||||
for: url.absoluteString,
|
||||
sender: nil,
|
||||
completion: submitLogsCompletion,
|
||||
completion: { continuation.resume() },
|
||||
)
|
||||
},
|
||||
))
|
||||
alert.addAction(ActionSheetAction(
|
||||
title: CommonStrings.cancelButton,
|
||||
style: .cancel,
|
||||
handler: { _ in submitLogsCompletion() },
|
||||
handler: { _ in continuation.resume() },
|
||||
))
|
||||
presentingViewController.presentActionSheet(alert)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func uploadLogsUsingViewController(_ viewController: UIViewController, dumper: DebugLogDumper, completion: @escaping (URL) -> Void) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
ModalActivityIndicatorViewController.present(
|
||||
fromViewController: viewController,
|
||||
private func uploadLogsWithUI(from viewController: UIViewController) async throws(DebugLogsError) -> URL? {
|
||||
return try await ModalActivityIndicatorViewController.presentAndPropagateResult(
|
||||
from: viewController,
|
||||
canCancel: true,
|
||||
asyncBlock: { await _uploadLogs(dumper: dumper, modalActivityIndicator: $0, completion: completion) },
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func _uploadLogs(dumper: DebugLogDumper, modalActivityIndicator: ModalActivityIndicatorViewController, completion: @escaping (URL) -> Void) async {
|
||||
do {
|
||||
let url = try await uploadLogs(dumper: dumper)
|
||||
guard !modalActivityIndicator.wasCancelled else { return }
|
||||
modalActivityIndicator.dismiss {
|
||||
completion(url)
|
||||
}
|
||||
} catch {
|
||||
guard !modalActivityIndicator.wasCancelled else {
|
||||
if let logArchiveOrDirectoryPath = error.logArchiveOrDirectoryPath {
|
||||
OWSFileSystem.deleteFile(logArchiveOrDirectoryPath)
|
||||
) { () throws(DebugLogsError) -> URL? in
|
||||
do throws(DebugLogsError) {
|
||||
return try await self.uploadLogs()
|
||||
} catch {
|
||||
if Task.isCancelled {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
modalActivityIndicator.dismiss {
|
||||
DebugLogs.showFailureAlert(
|
||||
with: error.localizedErrorMessage,
|
||||
logArchiveOrDirectoryPath: error.logArchiveOrDirectoryPath,
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Collecting & uploading
|
||||
|
||||
private struct NoLogsError: Error {
|
||||
var errorString: String {
|
||||
OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_NO_LOGS",
|
||||
comment: "Error indicating that no debug logs could be found.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static func collectLogs() -> Result<String, NoLogsError> {
|
||||
private static func collectLogs() -> String? {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy.MM.dd hh.mm.ss"
|
||||
let dateString = dateFormatter.string(from: Date())
|
||||
@ -203,7 +275,7 @@ enum DebugLogs {
|
||||
|
||||
let logFilePaths = DebugLogger.shared.allLogFilePaths
|
||||
if logFilePaths.isEmpty {
|
||||
return .failure(NoLogsError())
|
||||
return nil
|
||||
}
|
||||
|
||||
for logFilePath in logFilePaths {
|
||||
@ -219,50 +291,44 @@ enum DebugLogs {
|
||||
OWSFileSystem.protectFileOrFolder(atPath: copyFilePath)
|
||||
}
|
||||
|
||||
return .success(zipDirPath)
|
||||
return zipDirPath
|
||||
}
|
||||
|
||||
static func exportLogs() {
|
||||
func exportLogs(viewController: UIViewController) {
|
||||
AssertIsOnMainThread()
|
||||
switch collectLogs() {
|
||||
case let .success(logsDirPath):
|
||||
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) {
|
||||
OWSFileSystem.deleteFile(logsDirPath)
|
||||
}
|
||||
case let .failure(error):
|
||||
Self.showFailureAlert(with: error.errorString, logArchiveOrDirectoryPath: nil)
|
||||
return
|
||||
guard let logsDirPath else {
|
||||
return handleError(
|
||||
error: .noLogs,
|
||||
viewController: viewController,
|
||||
)
|
||||
}
|
||||
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) {
|
||||
OWSFileSystem.deleteFile(logsDirPath)
|
||||
}
|
||||
}
|
||||
|
||||
struct UploadDebugLogError: Error {
|
||||
var localizedErrorMessage: String
|
||||
var logArchiveOrDirectoryPath: String?
|
||||
}
|
||||
|
||||
/// - Note: Various dependencies might not be initialized yet when this
|
||||
/// method is called from the database recovery flow. Notably, the database
|
||||
/// isn't available in that flow.
|
||||
static func uploadLogs(dumper: DebugLogDumper) async throws(UploadDebugLogError) -> URL {
|
||||
// Phase 1: Dump any additional details that are relevant.
|
||||
private static func collectAndFlushLogs(
|
||||
dumper: DebugLogDumper,
|
||||
) -> String? {
|
||||
// Dump any additional details that are relevant.
|
||||
dumper.dump()
|
||||
Logger.info("About to zip debug logs")
|
||||
|
||||
// Phase 2: Flush pending logs to disk.
|
||||
// Flush pending logs to disk.
|
||||
Logger.flush()
|
||||
|
||||
// Phase 3: Make a local copy of all of the log files.
|
||||
let zipDirPath: String
|
||||
switch collectLogs() {
|
||||
case let .success(logsDirPath):
|
||||
zipDirPath = logsDirPath
|
||||
case let .failure(error):
|
||||
throw UploadDebugLogError(localizedErrorMessage: error.errorString)
|
||||
// Make a local copy of all of the log files.
|
||||
return collectLogs()
|
||||
}
|
||||
|
||||
func uploadLogs() async throws(DebugLogsError) -> URL {
|
||||
guard let logsDirPath else {
|
||||
throw DebugLogsError.noLogs
|
||||
}
|
||||
|
||||
// Phase 4: Zip up the log files.
|
||||
let zipDirUrl = URL(fileURLWithPath: zipDirPath)
|
||||
let zipFileUrl = URL(fileURLWithPath: (zipDirPath as NSString).appendingPathExtension("zip")!)
|
||||
// Zip up the log files.
|
||||
let zipDirUrl = URL(fileURLWithPath: logsDirPath)
|
||||
let zipFileUrl = URL(fileURLWithPath: (logsDirPath as NSString).appendingPathExtension("zip")!)
|
||||
let fileCoordinator = NSFileCoordinator()
|
||||
var zipError: NSError?
|
||||
fileCoordinator.coordinate(readingItemAt: zipDirUrl, options: [.forUploading], error: &zipError) { temporaryFileUrl in
|
||||
@ -273,38 +339,44 @@ enum DebugLogs {
|
||||
}
|
||||
}
|
||||
if zipError != nil || !OWSFileSystem.fileOrFolderExists(url: zipFileUrl) {
|
||||
let errorMessage = OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS",
|
||||
comment: "Error indicating that the debug logs could not be packaged.",
|
||||
)
|
||||
throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipDirPath)
|
||||
throw DebugLogsError.couldNotPackageLogs
|
||||
}
|
||||
|
||||
OWSFileSystem.protectFileOrFolder(atPath: zipFileUrl.path)
|
||||
OWSFileSystem.deleteFile(zipDirPath)
|
||||
|
||||
// Phase 5: Upload the log files.
|
||||
// Upload the log files.
|
||||
do {
|
||||
let url = try await DebugLogUploader.uploadFile(fileUrl: zipFileUrl, mimeType: MimeType.applicationZip.rawValue)
|
||||
try OWSFileSystem.deleteFile(url: zipFileUrl)
|
||||
return url
|
||||
} catch {
|
||||
let errorMessage = OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG",
|
||||
comment: "Error indicating that a debug log could not be uploaded.",
|
||||
)
|
||||
throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipFileUrl.path)
|
||||
throw DebugLogsError.uploadError(zipFilePath: zipFileUrl.path)
|
||||
}
|
||||
}
|
||||
|
||||
private static func showFailureAlert(with message: String, logArchiveOrDirectoryPath: String?) {
|
||||
let deleteArchive: (String) -> Void = { filePath in
|
||||
OWSFileSystem.deleteFile(filePath)
|
||||
private func handleError(
|
||||
error: DebugLogsError,
|
||||
viewController: UIViewController,
|
||||
) {
|
||||
let logsPath: String?
|
||||
let completion: (() -> Void)?
|
||||
switch error {
|
||||
case .noLogs:
|
||||
logsPath = nil
|
||||
completion = nil
|
||||
case .couldNotPackageLogs:
|
||||
logsPath = self.logsDirPath
|
||||
completion = nil
|
||||
case .uploadError(let zipFilePath):
|
||||
logsPath = zipFilePath
|
||||
completion = {
|
||||
OWSFileSystem.deleteFile(zipFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
let alert = ActionSheetController(title: nil, message: message)
|
||||
let alert = ActionSheetController(message: error.localizedErrorMessage)
|
||||
|
||||
if let logArchiveOrDirectoryPath {
|
||||
if let logsPath {
|
||||
alert.addAction(.init(
|
||||
title: OWSLocalizedString(
|
||||
"DEBUG_LOG_ALERT_OPTION_EXPORT_LOG_ARCHIVE",
|
||||
@ -312,23 +384,18 @@ enum DebugLogs {
|
||||
),
|
||||
) { _ in
|
||||
AttachmentSharing.showShareUI(
|
||||
for: URL(fileURLWithPath: logArchiveOrDirectoryPath),
|
||||
for: URL(fileURLWithPath: logsPath),
|
||||
sender: nil,
|
||||
completion: {
|
||||
deleteArchive(logArchiveOrDirectoryPath)
|
||||
},
|
||||
completion: completion,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
alert.addAction(.init(title: CommonStrings.okButton) { _ in
|
||||
if let logArchiveOrDirectoryPath {
|
||||
deleteArchive(logArchiveOrDirectoryPath)
|
||||
}
|
||||
completion?()
|
||||
})
|
||||
|
||||
let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts
|
||||
presentingViewController?.presentActionSheet(alert)
|
||||
viewController.presentActionSheet(alert)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -366,11 +366,13 @@ public class NotificationActionHandler {
|
||||
|
||||
@MainActor
|
||||
private class func submitDebugLogs(supportTag: String?) async {
|
||||
await withCheckedContinuation { continuation in
|
||||
DebugLogs.submitLogs(supportTag: supportTag, dumper: .fromGlobals()) {
|
||||
continuation.resume()
|
||||
}
|
||||
guard let viewController = CurrentAppContext().frontmostViewController() else {
|
||||
return
|
||||
}
|
||||
await DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs(
|
||||
from: viewController,
|
||||
supportTag: supportTag,
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@ -148,7 +148,11 @@ class ProvisioningController: NSObject {
|
||||
@objc
|
||||
@MainActor
|
||||
private func submitLogs() {
|
||||
DebugLogs.submitLogs(supportTag: "Onboarding", dumper: .fromGlobals())
|
||||
guard let viewController = CurrentAppContext().frontmostViewController() else {
|
||||
return
|
||||
}
|
||||
let logs = DebugLogs(dumper: .fromGlobals())
|
||||
logs.promptToSubmitLogs(from: viewController, supportTag: "Onboarding")
|
||||
}
|
||||
|
||||
// MARK: - Transitions
|
||||
|
||||
@ -500,7 +500,8 @@ public class RegistrationNavigationController: OWSNavigationController {
|
||||
))
|
||||
self.present(navVc, animated: true)
|
||||
} else {
|
||||
DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals())
|
||||
let logs = DebugLogs(dumper: .fromGlobals())
|
||||
logs.promptToSubmitLogs(from: self, supportTag: "Registration")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@ struct SupportEmailModel {
|
||||
enum LogPolicy {
|
||||
/// Attempt to upload the logs and include the resulting URL in the email body
|
||||
/// If the upload fails for one reason or another, continue anyway
|
||||
case attemptUpload(DebugLogDumper)
|
||||
case attemptUpload(DebugLogs)
|
||||
|
||||
/// Upload the logs. If they fail to upload, fail the operation
|
||||
case requireUpload(DebugLogDumper)
|
||||
case requireUpload(DebugLogs)
|
||||
|
||||
/// Don't upload new logs, instead use the provided link
|
||||
case link(URL)
|
||||
@ -157,15 +157,15 @@ final class ComposeSupportEmailOperation: NSObject {
|
||||
debugUrlString = nil
|
||||
case .link(let url):
|
||||
debugUrlString = url.absoluteString
|
||||
case .attemptUpload(let dumper):
|
||||
case .attemptUpload(let logs):
|
||||
do {
|
||||
debugUrlString = try await uploadDebugLogWithTimeout(dumper: dumper).absoluteString
|
||||
debugUrlString = try await uploadDebugLogWithTimeout(logs: logs).absoluteString
|
||||
} catch {
|
||||
debugUrlString = "[Support note: Log upload failed — \(error.userErrorDescription)]"
|
||||
}
|
||||
case .requireUpload(let dumper):
|
||||
case .requireUpload(let logs):
|
||||
do {
|
||||
debugUrlString = try await uploadDebugLogWithTimeout(dumper: dumper).absoluteString
|
||||
debugUrlString = try await uploadDebugLogWithTimeout(logs: logs).absoluteString
|
||||
} catch {
|
||||
throw EmailError.logUploadFailure(underlyingError: (error as? LocalizedError))
|
||||
}
|
||||
@ -187,17 +187,16 @@ final class ComposeSupportEmailOperation: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadDebugLogWithTimeout(dumper: DebugLogDumper) async throws -> URL {
|
||||
private func uploadDebugLogWithTimeout(logs: DebugLogs) async throws -> URL {
|
||||
do {
|
||||
return try await withCooperativeTimeout(seconds: 60) {
|
||||
do throws(DebugLogs.UploadDebugLogError) {
|
||||
return try await DebugLogs.uploadLogs(dumper: dumper)
|
||||
do throws(DebugLogs.DebugLogsError) {
|
||||
return try await logs.uploadLogs()
|
||||
} catch {
|
||||
// FIXME: Should we do something with the local log file?
|
||||
if let logArchiveOrDirectoryPath = error.logArchiveOrDirectoryPath {
|
||||
_ = OWSFileSystem.deleteFile(logArchiveOrDirectoryPath)
|
||||
if case .uploadError(zipFilePath: let zipPath) = error {
|
||||
OWSFileSystem.deleteFile(zipPath)
|
||||
}
|
||||
throw DebugLogsUploadError(localizedDescription: error.localizedErrorMessage)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch is CooperativeTimeoutError {
|
||||
@ -286,11 +285,3 @@ final class ComposeSupportEmailOperation: NSObject {
|
||||
.joined(separator: "\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugLogsUploadError: Error, LocalizedError, UserErrorDescriptionProvider {
|
||||
let localizedDescription: String
|
||||
|
||||
var errorDescription: String? {
|
||||
localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,11 +214,15 @@ final class ContactSupportViewController: OWSTableViewController2, TextViewWithP
|
||||
|
||||
private func didTapNext() {
|
||||
let logDumper = DebugLogDumper.fromGlobals()
|
||||
var logPolicy: SupportEmailModel.LogPolicy?
|
||||
if debugSwitch.isOn {
|
||||
logPolicy = .attemptUpload(DebugLogs(dumper: logDumper))
|
||||
}
|
||||
let emailRequest = SupportEmailModel(
|
||||
userDescription: descriptionField.text,
|
||||
emojiMood: emojiPicker.selectedMood,
|
||||
supportFilter: selectedFilter.map { "iOS \($0.emailFilterString)" },
|
||||
debugLogPolicy: debugSwitch.isOn ? .attemptUpload(logDumper) : nil,
|
||||
debugLogPolicy: logPolicy,
|
||||
hasRecentChallenge: logDumper.challengeReceivedRecently(),
|
||||
)
|
||||
|
||||
|
||||
@ -9,6 +9,16 @@ import SignalUI
|
||||
final class DebugLogPreviewViewController: OWSViewController {
|
||||
|
||||
private let textView = UITextView()
|
||||
private let logFilePaths: [String]
|
||||
private let onSubmit: (() -> Void)?
|
||||
private let onCancel: (() -> Void)?
|
||||
|
||||
init(logFilePaths: [String], onSubmit: (() -> Void)? = nil, onCancel: (() -> Void)? = nil) {
|
||||
self.logFilePaths = logFilePaths
|
||||
self.onSubmit = onSubmit
|
||||
self.onCancel = onCancel
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@ -22,7 +32,7 @@ final class DebugLogPreviewViewController: OWSViewController {
|
||||
|
||||
view.backgroundColor = .Signal.groupedBackground
|
||||
|
||||
navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: self)
|
||||
navigationItem.rightBarButtonItem = .cancelButton(dismissingFrom: self, completion: onCancel)
|
||||
|
||||
// UITableView does not have the text-rendering optimizations that
|
||||
// UITextView with scrolling enabled has, and the app freezes when
|
||||
@ -64,12 +74,31 @@ final class DebugLogPreviewViewController: OWSViewController {
|
||||
textView.textContainerInset = .init(margin: 20)
|
||||
textView.textContainer.lineFragmentPadding = 0
|
||||
textView.verticalScrollIndicatorInsets = .init(hMargin: 0, vMargin: OWSTableViewController2.cellRounding / 2)
|
||||
|
||||
if let onSubmit {
|
||||
cellContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
|
||||
let submitButton = UIButton(
|
||||
configuration: .largePrimary(title: OWSLocalizedString(
|
||||
"DEBUG_LOG_PREVIEW_SUBMIT_BUTTON",
|
||||
comment: "Button below the debug log preview which continues the submission flow",
|
||||
)),
|
||||
primaryAction: UIAction { _ in
|
||||
onSubmit()
|
||||
},
|
||||
)
|
||||
stackView.addArrangedSubview(submitButton)
|
||||
submitButton.autoPinWidthToSuperviewMargins()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.presentationController?.delegate = self
|
||||
}
|
||||
|
||||
private func loadLogs() {
|
||||
Logger.flush()
|
||||
|
||||
self.textView.text = DebugLogger.shared.allLogFilePaths.reduce(
|
||||
self.textView.text = self.logFilePaths.reduce(
|
||||
into: "",
|
||||
) { partialResult, logFilePath in
|
||||
do {
|
||||
@ -89,3 +118,11 @@ final class DebugLogPreviewViewController: OWSViewController {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
|
||||
extension DebugLogPreviewViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
onCancel?()
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,8 +69,9 @@ final class HelpViewController: OWSTableViewController2 {
|
||||
loggingSection.add(.item(
|
||||
name: OWSLocalizedString("SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", comment: ""),
|
||||
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "submit_debug_log"),
|
||||
actionBlock: {
|
||||
DebugLogs.submitLogs(dumper: .fromGlobals())
|
||||
actionBlock: { [weak self] in
|
||||
guard let self else { return }
|
||||
DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs(from: self)
|
||||
},
|
||||
))
|
||||
contents.add(loggingSection)
|
||||
|
||||
@ -88,8 +88,10 @@ class InternalSettingsViewController: OWSTableViewController2 {
|
||||
))
|
||||
|
||||
if mode == .registration {
|
||||
debugSection.add(.actionItem(withText: "Submit debug logs") {
|
||||
DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals())
|
||||
debugSection.add(.actionItem(withText: "Submit debug logs") { [weak self] in
|
||||
guard let self else { return }
|
||||
let logs = DebugLogs(dumper: .fromGlobals())
|
||||
logs.promptToSubmitLogs(from: self, supportTag: "Registration")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -210,12 +210,10 @@ class DatabaseRecoveryViewController<SetupResult>: OWSViewController {
|
||||
|
||||
@objc
|
||||
private func didRequestToSubmitDebugLogs() {
|
||||
self.dismiss(animated: true) {
|
||||
DebugLogs.submitLogs(
|
||||
supportTag: "LaunchFailure_DatabaseRecoveryFailed",
|
||||
dumper: .preLaunch(),
|
||||
)
|
||||
}
|
||||
DebugLogs(dumper: .preLaunch()).promptToSubmitLogs(
|
||||
from: self,
|
||||
supportTag: "LaunchFailure_DatabaseRecoveryFailed",
|
||||
)
|
||||
}
|
||||
|
||||
private func attemptRecovery() {
|
||||
|
||||
@ -676,7 +676,10 @@ private final class BackupArchiveErrorHeroSheet: HeroSheetViewController {
|
||||
title: "Submit debug log",
|
||||
action: { sheet in
|
||||
sheet.dismiss(animated: true) {
|
||||
DebugLogs.submitLogs(supportTag: "BackupArchive", dumper: .fromGlobals())
|
||||
DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs(
|
||||
from: fromViewController,
|
||||
supportTag: "BackupArchive",
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@ -2749,6 +2749,9 @@
|
||||
/* Header text displayed above the debug log preview */
|
||||
"DEBUG_LOG_PREVIEW_HEADER" = "When submitted, your log will be posted online for 30 days at a unique, unpublished URL.";
|
||||
|
||||
/* Button below the debug log preview which continues the submission flow */
|
||||
"DEBUG_LOG_PREVIEW_SUBMIT_BUTTON" = "Submit";
|
||||
|
||||
/* Title for the debug log preview screen */
|
||||
"DEBUG_LOG_PREVIEW_TITLE" = "Debug Log";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user