From 895cabee80f42edcbeef8e74f40c9f04ab1fcbc2 Mon Sep 17 00:00:00 2001 From: relikd Date: Sat, 27 Jun 2020 00:40:29 +0200 Subject: [PATCH] Context analysis: +/-5min raw logs --- AppCheck.xcodeproj/project.pbxproj | 4 + main/Assets.xcassets/.DS_Store | Bin 10244 -> 10244 bytes .../jump-to-target.imageset/Contents.json | 23 ++++ .../jump-to-target.imageset/img.png | Bin 0 -> 230 bytes .../jump-to-target.imageset/img@2x.png | Bin 0 -> 409 bytes .../jump-to-target.imageset/img@3x.png | Bin 0 -> 544 bytes main/Base.lproj/Main.storyboard | 73 +++++++++++-- main/DB/DBAppOnly.swift | 32 ++++-- main/Extensions/Generic.swift | 16 ++- .../Analytics/TVCOccurrenceContext.swift | 100 ++++++++++++++++++ main/Requests/TVCHostDetails.swift | 8 +- 11 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 main/Assets.xcassets/jump-to-target.imageset/Contents.json create mode 100644 main/Assets.xcassets/jump-to-target.imageset/img.png create mode 100644 main/Assets.xcassets/jump-to-target.imageset/img@2x.png create mode 100644 main/Assets.xcassets/jump-to-target.imageset/img@3x.png create mode 100644 main/Requests/Analytics/TVCOccurrenceContext.swift diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index f876283..b2cff7e 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 54448A30248647D900771C96 /* Time.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A2F248647D900771C96 /* Time.swift */; }; 54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54448A3124899A4000771C96 /* SearchBarManager.swift */; }; 544C95262407B1C700AB89D0 /* SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544C95252407B1C700AB89D0 /* SharedState.swift */; }; + 544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */; }; 5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */; }; 545DDDCF243E6267003B6544 /* TutorialSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDCE243E6267003B6544 /* TutorialSheet.swift */; }; 545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; }; @@ -191,6 +192,7 @@ 54448A2F248647D900771C96 /* Time.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Time.swift; sourceTree = ""; }; 54448A3124899A4000771C96 /* SearchBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBarManager.swift; sourceTree = ""; }; 544C95252407B1C700AB89D0 /* SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedState.swift; sourceTree = ""; }; + 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCOccurrenceContext.swift; sourceTree = ""; }; 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCRecordingDetails.swift; sourceTree = ""; }; 545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = ""; }; 545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = ""; }; @@ -403,6 +405,7 @@ isa = PBXGroup; children = ( 541FC47724A1453F009154D8 /* VCCoOccurrence.swift */, + 544F911F24A67EC5001D4B00 /* TVCOccurrenceContext.swift */, ); path = Analytics; sourceTree = ""; @@ -852,6 +855,7 @@ 54B34596240F0513004C53CC /* TableView.swift in Sources */, 540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */, 54953E3323DC752E0054345C /* DBCore.swift in Sources */, + 544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */, 54448A30248647D900771C96 /* Time.swift in Sources */, 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */, 54751E512423955100168273 /* URL.swift in Sources */, diff --git a/main/Assets.xcassets/.DS_Store b/main/Assets.xcassets/.DS_Store index a60f8eabc4317410a20c8ed1e03f3f64be89b43e..2e96e05b264f2f2d308ecf9baf47d9b282cb971c 100644 GIT binary patch delta 148 zcmZn(XbG4Q$z8-y%#g~E%uvFRxv_8>`@{y`&FmZ;9E_rq8^wfKGa2$3@+Qj(yhP@2 zo+&<$2}K}8su@LKa;TW%@&dvU!4ZDIRvt+RPp7|1i#Y&^DoQ!OPq`Wz`GkrXK4v^E&%%?uz~5BCoH{-;=fH l?V9{GTYCRKpMFa?;#%hR*AsU*FfcGMc)I$ztaD0e0s#F)V3Ys= literal 0 HcmV?d00001 diff --git a/main/Assets.xcassets/jump-to-target.imageset/img@2x.png b/main/Assets.xcassets/jump-to-target.imageset/img@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f4f949c57483497eeb5f5956dec7b64c29d4b062 GIT binary patch literal 409 zcmeAS@N?(olHy`uVBq!ia0y~yV9)?z4rT@hhTRGt@eB+M#sNMdt_-Au#jI{885kG@ zOM?7@85;ig7rb{6P&mJTenNo3{qrBS8d&C=3EX>=hZ))FQ)dBm7ihS@AlKG+*ZHrF0U<5*FB(g zJ%q(IuOdprT=DJ7RX6L;L>6o~-T2zCUq`twe+rvIs*H{6%{j`#5za3Jyiy{5jLE9a`)Y{_?5}zd-rh zFZyZI_NLnVx2m@M^on@gQ&_8Y#_&c`LPPWvw>fpamY$rG`6J?%Ufy(>dA84<<7{V_ zoxAh>>7I0T-koik!XMWrm?)S?N2aogt6i=;BJA+ZcU^?s(Os*8zOm-8v44BN|HcYX O5PQ1%xvX0mLd+eron2Emdb zzhH*_{~h|z3mDvgU!V|>&@g}fx3^ta7#JA4JzX3_Dj45Rjh%H{fydSHrx-_&n? zt8gpkx{t`G8~V?AkGFd>M6P@MQhNJ2_kSnkf7VYqTwV00=v3{5)3ane-mY_fe{Dt9 z=Vx;S-k!eLCGH>cc~P@YeK6B6VdI@B# zvRc12Pj7RSY4xN(0q0v6hV^E6U4L^R&EO4p;p{I - + @@ -404,11 +404,11 @@ - + - + + + + @@ -512,13 +515,13 @@ - + @@ -638,6 +641,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1244,6 +1298,7 @@ Duration: 60:00 + diff --git a/main/DB/DBAppOnly.swift b/main/DB/DBAppOnly.swift index 10b0286..0bb5edf 100644 --- a/main/DB/DBAppOnly.swift +++ b/main/DB/DBAppOnly.swift @@ -54,6 +54,10 @@ extension SQLiteDatabase { return sqlite3_column_int64($0, 0) }) ?? 0 } + + fileprivate func col_ts(_ stmt: OpaquePointer, _ col: Int32) -> Timestamp { + sqlite3_column_int64(stmt, col) + } } class WhereClauseBuilder: CustomStringConvertible { @@ -105,6 +109,7 @@ struct GroupedDomain { var options: FilterOptions? = nil } typealias GroupedTsOccurrence = (ts: Timestamp, total: Int32, blocked: Int32) +typealias DomainTsPair = (domain: String, ts: Timestamp) extension SQLiteDatabase { @@ -150,7 +155,7 @@ extension SQLiteDatabase { func dnsLogsMinDate() -> Timestamp? { try? run(sql:"SELECT min(ts) FROM heap") { try ifStep($0, SQLITE_ROW) - return sqlite3_column_int64($0, 0) + return col_ts($0, 0) } } @@ -164,8 +169,19 @@ extension SQLiteDatabase { let Where = WhereClauseBuilder().and(in: range).and(min: ts1, max: ts2) return try? run(sql:"SELECT min(rowid), max(rowid) FROM heap \(Where);", bind: Where.bindings) { try ifStep($0, SQLITE_ROW) - let max = sqlite3_column_int64($0, 1) - return (max == 0) ? nil : (sqlite3_column_int64($0, 0), max) + let max = col_ts($0, 1) + return (max == 0) ? nil : (col_ts($0, 0), max) + } + } + + /// Get raw logs between two timestamps. `ts >= ? AND ts <= ?` + /// - Returns: List sorted by `ts` in descending order (newest entries first). + func dnsLogs(between ts1: Timestamp, and ts2: Timestamp) -> [DomainTsPair]? { + try? run(sql: "SELECT fqdn, ts FROM heap WHERE ts >= ? AND ts <= ? ORDER BY ts DESC, rowid ASC;", + bind: [BindInt64(ts1), BindInt64(ts2)]) { + allRows($0) { + (readText($0, 0) ?? "", col_ts($0, 1)) + } } } @@ -192,7 +208,7 @@ extension SQLiteDatabase { GroupedDomain(domain: readText($0, 0) ?? "", total: sqlite3_column_int($0, 1), blocked: sqlite3_column_int($0, 2), - lastModified: sqlite3_column_int64($0, 3)) + lastModified: col_ts($0, 3)) } } } @@ -206,7 +222,7 @@ extension SQLiteDatabase { let Where = WhereClauseBuilder().and(in: range).and("fqdn = ?", BindText(fqdn)) return try? run(sql: "SELECT ts, COUNT(ts), COUNT(opt) FROM heap \(Where) GROUP BY ts ORDER BY ts DESC;", bind: Where.bindings) { allRows($0) { - (sqlite3_column_int64($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2)) + (col_ts($0, 0), sqlite3_column_int($0, 1), sqlite3_column_int($0, 2)) } } } @@ -230,7 +246,7 @@ extension SQLiteDatabase { /// Get sorted, unique list of `ts` with given `fqdn`. func dnsLogsUniqTs(_ fqdn: String) -> [Timestamp]? { try? run(sql: "SELECT DISTINCT ts FROM heap WHERE fqdn = ? ORDER BY ts;", bind: [BindText(fqdn)]) { - allRows($0) { sqlite3_column_int64($0, 0) } + allRows($0) { col_ts($0, 0) } } } @@ -348,9 +364,9 @@ extension SQLiteDatabase { // MARK: read private func readRecording(_ stmt: OpaquePointer) -> Recording { - let end = sqlite3_column_int64(stmt, 2) + let end = col_ts(stmt, 2) return Recording(id: sqlite3_column_int64(stmt, 0), - start: sqlite3_column_int64(stmt, 1), + start: col_ts(stmt, 1), stop: end == 0 ? nil : end, appId: readText(stmt, 3), title: readText(stmt, 4), diff --git a/main/Extensions/Generic.swift b/main/Extensions/Generic.swift index 3bbf75a..1a6b621 100644 --- a/main/Extensions/Generic.swift +++ b/main/Extensions/Generic.swift @@ -16,9 +16,21 @@ struct QLog { } } +// See: https://noahgilmore.com/blog/dark-mode-uicolor-compatibility/ extension UIColor { - static var sysBg: UIColor { get { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } }} - static var sysFg: UIColor { get { if #available(iOS 13.0, *) { return .label } else { return .black } }} + /// `.systemBackground ?? .white` + static var sysBg: UIColor { if #available(iOS 13.0, *) { return .systemBackground } else { return .white } } + /// `.label ?? .black` + static var sysFg: UIColor { if #available(iOS 13.0, *) { return .label } else { return .black } } + /// `.link ?? .systemBlue` + static var sysLink: UIColor { if #available(iOS 13.0, *) { return .link } else { return .systemBlue } } + + /// `.label ?? .black` + static var sysLabel: UIColor { if #available(iOS 13.0, *) { return .label } else { return .black } } + /// `.secondaryLabel ?? rgba(60, 60, 67, 0.6)` + static var sysLabel2: UIColor { if #available(iOS 13.0, *) { return .secondaryLabel } else { return .init(red: 60/255.0, green: 60/255.0, blue: 67/255.0, alpha: 0.6) } } + /// `.tertiaryLabel ?? rgba(60, 60, 67, 0.3)` + static var sysLabel3: UIColor { if #available(iOS 13.0, *) { return .tertiaryLabel } else { return .init(red: 60/255.0, green: 60/255.0, blue: 67/255.0, alpha: 0.3) } } } extension UIEdgeInsets { diff --git a/main/Requests/Analytics/TVCOccurrenceContext.swift b/main/Requests/Analytics/TVCOccurrenceContext.swift new file mode 100644 index 0000000..a20d026 --- /dev/null +++ b/main/Requests/Analytics/TVCOccurrenceContext.swift @@ -0,0 +1,100 @@ +import UIKit + +class TVCOccurrenceContext: UITableViewController { + + var ts: Timestamp! + var domain: String! + + private let dT: Timestamp = 300 // +/- 5 minutes + private lazy var dataSource: [DomainTsPair] = { + var list: [DomainTsPair] = [] + list.append(("[…]", ts + dT)) + list.append(contentsOf: AppDB?.dnsLogs(between: ts - dT, and: ts + dT) ?? []) + list.append(("[…]", ts - dT)) + return list + }() + + override func viewDidLoad() { + navigationItem.title = "± 5 Min Context" + super.viewDidLoad() + } + + override func viewDidAppear(_ animated: Bool) { + jumpToTsZero() + } + + @IBAction private func jumpToTsZero() { + if let i = dataSource.firstIndex(where: { isChoosenOne($0) }) { + tableView.scrollToRow(at: IndexPath(row: i), at: .middle, animated: true) + } + } + + private func isChoosenOne(_ obj: DomainTsPair) -> Bool { + obj.domain == domain && obj.ts == ts + } + + private func firstOrLast(_ row: Int) -> Bool { + row == 0 || row == dataSource.count - 1 + } + + + // MARK: - Table View Data Source + + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "OccurrenceContextCell")! + let src = dataSource[indexPath.row] + cell.detailTextLabel?.text = src.domain + + if firstOrLast(indexPath.row) { + cell.detailTextLabel?.textColor = .sysLabel2 // same as textLabel + } else if isChoosenOne(src) { + cell.detailTextLabel?.textColor = .sysLink + } else { + cell.detailTextLabel?.textColor = .sysLabel + } + + if src.ts > ts { + cell.textLabel?.text = "+ " + TimeFormat.from(src.ts - ts) + } else if src.ts < ts { + cell.textLabel?.text = "− " + TimeFormat.from(ts - src.ts) + } else { + cell.textLabel?.text = "0" + } + //cell.textLabel?.text = String(format: "%+d s", src.ts - ts) + return cell + } + + + // MARK: - Tap to Copy + + private var rowToCopy: Int = Int.max + + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + if firstOrLast(indexPath.row) { return nil } + if rowToCopy == indexPath.row { + UIMenuController.shared.setMenuVisible(false, animated: true) + rowToCopy = Int.max + return nil + } + rowToCopy = indexPath.row + self.becomeFirstResponder() + let cell = tableView.cellForRow(at: indexPath)! + UIMenuController.shared.setTargetRect(cell.bounds, in: cell) + UIMenuController.shared.setMenuVisible(true, animated: true) + return nil + } + + override var canBecomeFirstResponder: Bool { true } + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + action == #selector(UIResponderStandardEditActions.copy) + } + + override func copy(_ sender: Any?) { + guard rowToCopy < dataSource.count else { return } + UIPasteboard.general.string = dataSource[rowToCopy].domain + rowToCopy = Int.max + } +} diff --git a/main/Requests/TVCHostDetails.swift b/main/Requests/TVCHostDetails.swift index 6fccfa7..11448b3 100644 --- a/main/Requests/TVCHostDetails.swift +++ b/main/Requests/TVCHostDetails.swift @@ -14,14 +14,14 @@ class TVCHostDetails: UITableViewController, SyncUpdateDelegate, UITabBarDelegat sync.addObserver(self) // calls `syncUpdate(reset:)` if #available(iOS 10.0, *) { sync.allowPullToRefresh(onTVC: self, forObserver: self) - actionsBar.unselectedItemTintColor = .systemBlue + actionsBar.unselectedItemTintColor = .sysLink } UIDevice.orientationDidChangeNotification.observe(call: #selector(didChangeOrientation), on: self) } // MARK: - Table View Data Source - override func tableView(_ _: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } + override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { dataSource.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "HostDetailCell")! @@ -53,6 +53,10 @@ extension TVCHostDetails { override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "segueAnalysisCoOccurrence" { (segue.destination as? VCCoOccurrence)?.fqdn = fullDomain + } else if let index = tableView.indexPathForSelectedRow?.row { + let tvc = segue.destination as? TVCOccurrenceContext + tvc?.domain = fullDomain + tvc?.ts = dataSource[index].ts } } }