Skip to content

Commit 44dbd99

Browse files
committed
feat: Add diagnostics tool with blockcheck integration
- Added Diagnostics button in footer - Separate diagnostics window with scan levels (Quick/Standard/Force) - Domain input for testing specific sites - Copy results with visual feedback - Real-time output streaming from blockcheck.sh
1 parent 0bce2b4 commit 44dbd99

1 file changed

Lines changed: 223 additions & 1 deletion

File tree

Sources/main.swift

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import AppKit
55
struct DarkwareZapretApp: App {
66
@StateObject private var zapretManager = ZapretManager()
77
@StateObject private var installerManager = InstallerManager()
8+
@StateObject private var diagnosticsManager = DiagnosticsManager()
89

910
var body: some Scene {
1011
MenuBarExtra {
11-
ContentView(zapretManager: zapretManager, installerManager: installerManager)
12+
ContentView(zapretManager: zapretManager, installerManager: installerManager, diagnosticsManager: diagnosticsManager)
1213
} label: {
1314
Image(systemName: zapretManager.isRunning ? "checkmark.shield.fill" : "xmark.shield.fill")
1415
}
@@ -19,6 +20,7 @@ struct DarkwareZapretApp: App {
1920
struct ContentView: View {
2021
@ObservedObject var zapretManager: ZapretManager
2122
@ObservedObject var installerManager: InstallerManager
23+
@ObservedObject var diagnosticsManager: DiagnosticsManager
2224

2325
var body: some View {
2426
VStack(spacing: 0) {
@@ -151,6 +153,19 @@ struct ContentView: View {
151153
.font(.subheadline)
152154
.foregroundStyle(.primary)
153155

156+
if installerManager.isInstalled {
157+
Text("")
158+
.foregroundStyle(.tertiary)
159+
.padding(.horizontal, 4)
160+
161+
Button("Diagnostics") {
162+
openDiagnosticsWindow()
163+
}
164+
.buttonStyle(.plain)
165+
.font(.subheadline)
166+
.foregroundStyle(.blue)
167+
}
168+
154169
Spacer()
155170

156171
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Dev"
@@ -180,6 +195,20 @@ struct ContentView: View {
180195
}
181196
}
182197
}
198+
199+
private func openDiagnosticsWindow() {
200+
let window = NSWindow(
201+
contentRect: NSRect(x: 0, y: 0, width: 650, height: 500),
202+
styleMask: [.titled, .closable, .resizable],
203+
backing: .buffered,
204+
defer: false
205+
)
206+
window.title = "Diagnostics"
207+
window.contentView = NSHostingView(rootView: DiagnosticsView(diagnosticsManager: diagnosticsManager))
208+
window.center()
209+
window.makeKeyAndOrderFront(nil)
210+
NSApp.activate(ignoringOtherApps: true)
211+
}
183212
}
184213

185214
// Strategies
@@ -451,3 +480,196 @@ class InstallerManager: ObservableObject {
451480
}
452481
}
453482
}
483+
484+
// MARK: - Diagnostics
485+
486+
enum ScanLevel: String, CaseIterable, Identifiable {
487+
case quick = "Quick"
488+
case standard = "Standard"
489+
case force = "Force"
490+
491+
var id: String { rawValue }
492+
}
493+
494+
@MainActor
495+
class DiagnosticsManager: ObservableObject {
496+
@Published var isRunning = false
497+
@Published var output = ""
498+
@Published var scanLevel: ScanLevel = .quick
499+
@Published var testDomain = "discord.com"
500+
501+
private var process: Process?
502+
503+
func runDiagnostics() {
504+
guard !isRunning else { return }
505+
isRunning = true
506+
output = "Starting diagnostics...\n"
507+
508+
let zapretPath = "/opt/darkware-zapret"
509+
let blockcheckPath = "\(zapretPath)/blockcheck.sh"
510+
511+
// Check if blockcheck exists
512+
guard FileManager.default.fileExists(atPath: blockcheckPath) else {
513+
output += "Error: blockcheck.sh not found at \(blockcheckPath)\n"
514+
isRunning = false
515+
return
516+
}
517+
518+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
519+
guard let self = self else { return }
520+
521+
let process = Process()
522+
process.executableURL = URL(fileURLWithPath: "/bin/bash")
523+
process.arguments = ["-c", """
524+
cd "\(zapretPath)" && \
525+
BATCH=1 \
526+
SKIP_PKTWS=1 \
527+
SCANLEVEL=\(self.scanLevel.rawValue.lowercased()) \
528+
DOMAINS="\(self.testDomain)" \
529+
./blockcheck.sh 2>&1
530+
"""]
531+
532+
process.currentDirectoryURL = URL(fileURLWithPath: zapretPath)
533+
534+
let pipe = Pipe()
535+
process.standardOutput = pipe
536+
process.standardError = pipe
537+
538+
self.process = process
539+
540+
pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
541+
let data = handle.availableData
542+
if data.isEmpty { return }
543+
if let str = String(data: data, encoding: .utf8) {
544+
DispatchQueue.main.async {
545+
self?.output += str
546+
}
547+
}
548+
}
549+
550+
do {
551+
try process.run()
552+
process.waitUntilExit()
553+
} catch {
554+
DispatchQueue.main.async {
555+
self.output += "\nError running diagnostics: \(error.localizedDescription)\n"
556+
}
557+
}
558+
559+
DispatchQueue.main.async {
560+
self.output += "\n--- Diagnostics finished ---\n"
561+
self.isRunning = false
562+
self.process = nil
563+
}
564+
}
565+
}
566+
567+
func stopDiagnostics() {
568+
process?.terminate()
569+
isRunning = false
570+
output += "\n--- Diagnostics cancelled ---\n"
571+
}
572+
573+
func copyResults() {
574+
NSPasteboard.general.clearContents()
575+
NSPasteboard.general.setString(output, forType: .string)
576+
}
577+
}
578+
579+
struct DiagnosticsView: View {
580+
@ObservedObject var diagnosticsManager: DiagnosticsManager
581+
@Environment(\.dismiss) var dismiss
582+
@State private var copied = false
583+
584+
var body: some View {
585+
VStack(spacing: 0) {
586+
// Settings
587+
588+
// Settings
589+
HStack {
590+
Text("Domain:")
591+
.font(.subheadline)
592+
TextField("Domain to test", text: $diagnosticsManager.testDomain)
593+
.textFieldStyle(.roundedBorder)
594+
.frame(width: 150)
595+
.disabled(diagnosticsManager.isRunning)
596+
597+
Spacer()
598+
599+
Text("Level:")
600+
.font(.subheadline)
601+
Picker("", selection: $diagnosticsManager.scanLevel) {
602+
ForEach(ScanLevel.allCases) { level in
603+
Text(level.rawValue).tag(level)
604+
}
605+
}
606+
.pickerStyle(.menu)
607+
.frame(width: 120)
608+
.disabled(diagnosticsManager.isRunning)
609+
}
610+
.padding(.horizontal)
611+
.padding(.vertical, 8)
612+
613+
Divider()
614+
615+
// Output
616+
ScrollViewReader { proxy in
617+
ScrollView {
618+
Text(diagnosticsManager.output)
619+
.font(.system(.caption, design: .monospaced))
620+
.frame(maxWidth: .infinity, alignment: .leading)
621+
.textSelection(.enabled)
622+
.id("output")
623+
}
624+
.onChange(of: diagnosticsManager.output) { _ in
625+
withAnimation {
626+
proxy.scrollTo("output", anchor: .bottom)
627+
}
628+
}
629+
}
630+
.frame(maxHeight: .infinity)
631+
.padding(8)
632+
.background(Color(NSColor.textBackgroundColor))
633+
634+
Divider()
635+
636+
// Actions
637+
HStack {
638+
if diagnosticsManager.isRunning {
639+
Button("Stop") {
640+
diagnosticsManager.stopDiagnostics()
641+
}
642+
.buttonStyle(.borderedProminent)
643+
.tint(.red)
644+
645+
ProgressView()
646+
.controlSize(.small)
647+
.padding(.leading, 8)
648+
} else {
649+
Button("Run Diagnostics") {
650+
diagnosticsManager.runDiagnostics()
651+
}
652+
.buttonStyle(.borderedProminent)
653+
}
654+
655+
Spacer()
656+
657+
Button {
658+
diagnosticsManager.copyResults()
659+
copied = true
660+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
661+
copied = false
662+
}
663+
} label: {
664+
HStack(spacing: 4) {
665+
Image(systemName: copied ? "checkmark" : "doc.on.doc")
666+
Text(copied ? "Copied!" : "Copy Results")
667+
}
668+
}
669+
.disabled(diagnosticsManager.output.isEmpty)
670+
}
671+
.padding()
672+
}
673+
.frame(width: 600, height: 500)
674+
}
675+
}

0 commit comments

Comments
 (0)