Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
152aaf8
Suggested changes.
johnno1962 Apr 21, 2026
8ae418d
Debug only.
johnno1962 Apr 21, 2026
f31c17d
To discuss.
johnno1962 Apr 21, 2026
ec3d144
Compatibility.
johnno1962 Apr 21, 2026
d42ac3d
Tweak titles.
johnno1962 Apr 21, 2026
6156955
Tooltips, more environment vars.
johnno1962 Apr 21, 2026
8b72919
More Tooltips.
johnno1962 Apr 21, 2026
f4eec93
Overdue for reuse.
johnno1962 Apr 21, 2026
d64e83b
Missed the boat reconstructing PR.
johnno1962 Apr 21, 2026
6e980c9
Is client Connected?
johnno1962 Apr 21, 2026
068bbfc
Disarm bear trap, FrontendServer needs to start first.
johnno1962 Apr 21, 2026
735801f
Capturing logs was inhibiting my debug prints.
johnno1962 Apr 21, 2026
c88f585
Update App/InjectionNext/Views/FileWatcherSettingsView.swift
johnno1962 Apr 21, 2026
130f273
Update App/InjectionNext/Views/BuildSystemSettingsView.swift
johnno1962 Apr 21, 2026
a7c60c2
Update App/InjectionNext/ConfigStore.swift
johnno1962 Apr 21, 2026
ff21627
D'oh
johnno1962 Apr 21, 2026
8e92360
Update App/InjectionNext/Views/XcodeSettingsView.swift
johnno1962 Apr 21, 2026
d7282fa
Change default arguments text.
johnno1962 Apr 21, 2026
46641f0
Update App/InjectionNext/Views/StatusMenuView.swift
johnno1962 Apr 21, 2026
5bb066f
Update App/InjectionNext/Views/XcodeSettingsView.swift
johnno1962 Apr 21, 2026
fd973bd
Update App/InjectionNext/AppDelegate.swift
johnno1962 Apr 21, 2026
ec1be47
Move away from print for debugs.
johnno1962 Apr 21, 2026
04c7495
Recover Defaults.swift fro InjectionIII.
johnno1962 Apr 21, 2026
0084513
Debug prints on #DEBUG or INJECTION_DEBUG anv var.
johnno1962 Apr 21, 2026
5714dbf
More env vars, connected status.
johnno1962 Apr 21, 2026
9295705
Tracing preferences.
johnno1962 Apr 21, 2026
13f1a68
docs(todo): flesh out 2.0 Next section with fork-discovered items (#142)
maatheusgois-dd Apr 22, 2026
985e359
Update App/InjectionNext/Defaults.swift
johnno1962 Apr 22, 2026
ac7559a
Update App/InjectionNext/Views/XcodeSettingsView.swift
johnno1962 Apr 22, 2026
982d7f6
Update App/InjectionNext/Views/XcodeSettingsView.swift
johnno1962 Apr 22, 2026
821d6d4
Update App/InjectionNext/ConfigStore.swift
johnno1962 Apr 22, 2026
765220e
For rellease
johnno1962 Apr 22, 2026
8af79c5
Reinstate project path further down pane.
johnno1962 Apr 22, 2026
f74a900
UNSETENV_VALUE
johnno1962 Apr 22, 2026
1028341
Consolidate.
johnno1962 Apr 22, 2026
e4e358b
Update App/InjectionNext/InjectionHybrid.swift
johnno1962 Apr 22, 2026
941e3ea
Enough already.
johnno1962 Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions App/InjectionNext/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
/// Mimics `launchXcodeItem.state` for MonitorXcode.
var launchXcodeItem: CompatMenuItem {
CompatMenuItem(
get: { ConfigStore.shared.isXcodeRunning ? .on : .off },
set: { ConfigStore.shared.isXcodeRunning = ($0 == .on) }
get: { ConfigStore.shared.haveLaunchedXcode ? .on : .off },
set: { ConfigStore.shared.haveLaunchedXcode = ($0 == .on) }
)
}

Expand All @@ -62,7 +62,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

/// Mimics `patchCompilerItem` — a real orphan NSMenuItem so
/// `patchCompilerItem?.title = ...` and `prepareProject(item)` calls compile.
var patchCompilerItem: NSMenuItem { NSMenuItem() }
var patchCompilerItem: NSMenuItem! { NSMenuItem() }

/// Mimics `restartDeviceItem.state`.
var restartDeviceItem: CompatMenuItem {
Expand Down Expand Up @@ -116,11 +116,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
ConfigStore.shared.discoverCodesigningIdentities()

// Start injection server for on-device/sim connections.
if updatePatchUnpatch() == .patched {
_ = FrontendServer.startOnce
}
deviceEnable(nil)

if let xcodePath = NSRunningApplication
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
.first?.bundleURL?.path {
if let xcodePath = MonitorXcode.externalXcode?.bundleURL?.path {
if Defaults.xcodeDefault == nil {
Defaults.xcodeDefault = xcodePath
}
Expand All @@ -138,16 +139,19 @@ class AppDelegate: NSObject, NSApplicationDelegate {
_ = MonitorXcode(args: " '\(project)'")
}

LogManager.shared.startCapturing()
if Defaults.mcpServer {
LogManager.shared.startCapturing()
ControlServer.start()
}
}

// MARK: - Status Icon (bridges to ConfigStore)

func setMenuIcon(_ state: InjectionState) {
ConfigStore.shared.setInjectionState(state)
DispatchQueue.main.async {
ConfigStore.shared.setInjectionState(state)
ConfigStore.shared.isClientConnected = InjectionServer.currentClient != nil
}
}

// MARK: - Actions
Expand Down
95 changes: 77 additions & 18 deletions App/InjectionNext/ConfigStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ final class ConfigStore: ObservableObject {
// MARK: - Injection State (published, not persisted)

@Published var injectionState: InjectionState = .idle
@Published var isXcodeRunning = false
@Published var haveLaunchedXcode = false
@Published var isClientConnected = false
@Published var watchingDirectories: [String] = []

Expand Down Expand Up @@ -275,22 +275,79 @@ final class ConfigStore: ObservableObject {
didSet { ud.set(autoOpenDefaultProject, forKey: "autoOpenDefaultProject") }
}
@Published var preserveStatics: Bool {
didSet { ud.set(preserveStatics, forKey: "preserveStatics") }
didSet { ud.set(preserveStatics, forKey: "preserveStatics"); updateEnvVars() }
}
@Published var disableStandalone: Bool {
@Published var disableStandalone: Bool { // difficult to implement without connection.
didSet { ud.set(disableStandalone, forKey: "disableStandalone") }
}
@Published var genericsMode: GenericsMode {
@Published var genericsMode: GenericsMode { // difficult to implement early.
didSet { ud.set(genericsMode.rawValue, forKey: "genericsMode") }
}
@Published var keyPathsMode: KeyPathsMode {
@Published var keyPathsMode: KeyPathsMode { // difficult to implement early.
didSet { ud.set(keyPathsMode.rawValue, forKey: "keyPathsMode") }
}
@Published var sweepExclude: String {
didSet { ud.set(sweepExclude, forKey: "sweepExclude") }
didSet { ud.set(sweepExclude, forKey: "sweepExclude"); updateEnvVars() }
}
@Published var sweepDetail: Bool {
didSet { ud.set(sweepDetail, forKey: "sweepDetail") }
didSet { ud.set(sweepDetail, forKey: "sweepDetail"); updateEnvVars() }
}

func updateEnvVars() {
let clients = InjectionServer.currentClients
InjectionServer.clientQueue.async {
for client in clients where client != nil {
self.sendEnvVars(to: client!)
}
}
}

func sendEnvVars(to client: InjectionServer) {
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_DETAIL)
client.write(verboseLogging ? "1" : UNSETENV_VALUE)
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_PRESERVE_STATICS)
client.write(preserveStatics ? "1" : UNSETENV_VALUE)
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_SWEEP_DETAIL)
client.write(sweepDetail ? "1" : UNSETENV_VALUE)
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_SWEEP_EXCLUDE)
client.write(sweepExclude != "" ? sweepExclude : UNSETENV_VALUE)
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_BENCH)
client.write(benchmarking ? "1" : UNSETENV_VALUE)
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_TRACE_FILTER)
client.write(traceFilter != "" ? traceFilter : ".")
switch traceMode {
case .injected:
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_TRACE)
client.write("1")
case .all:
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_TRACE_ALL)
client.write("1")
case .off:
break
}
if traceFrameworks != "" {
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_TRACE_FRAMEWORKS)
client.write(traceFrameworks)
}
if traceUIKit != "" {
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_TRACE_UIKIT)
client.write(traceUIKit)
}
if traceLookup {
client.writeCommand(InjectionCommand.setenv.rawValue,
with: INJECTION_TRACE_LOOKUP)
client.write("1")
}
Comment thread
johnno1962 marked this conversation as resolved.
}

// MARK: - Devices
Expand All @@ -317,19 +374,19 @@ final class ConfigStore: ObservableObject {
// MARK: - Tracing

@Published var traceMode: TraceMode {
didSet { ud.set(traceMode.rawValue, forKey: "traceMode") }
didSet { ud.set(traceMode.rawValue, forKey: "traceMode"); updateEnvVars() }
}
@Published var traceFilter: String {
didSet { ud.set(traceFilter, forKey: "traceFilter") }
didSet { ud.set(traceFilter, forKey: "traceFilter"); updateEnvVars() }
}
@Published var traceFrameworks: String {
didSet { ud.set(traceFrameworks, forKey: "traceFrameworks") }
didSet { ud.set(traceFrameworks, forKey: "traceFrameworks"); updateEnvVars() }
}
@Published var traceLookup: Bool {
didSet { ud.set(traceLookup, forKey: "traceLookup") }
didSet { ud.set(traceLookup, forKey: "traceLookup"); updateEnvVars() }
}
@Published var traceUIKit: String {
didSet { ud.set(traceUIKit, forKey: "traceUIKit") }
didSet { ud.set(traceUIKit, forKey: "traceUIKit"); updateEnvVars() }
}

// MARK: - File Watcher
Expand All @@ -352,10 +409,10 @@ final class ConfigStore: ObservableObject {
// MARK: - Advanced

@Published var verboseLogging: Bool {
didSet { ud.set(verboseLogging, forKey: "verboseLogging") }
didSet { ud.set(verboseLogging, forKey: "verboseLogging") ; updateEnvVars() }
}
@Published var benchmarking: Bool {
didSet { ud.set(benchmarking, forKey: "benchmarking") }
didSet { ud.set(benchmarking, forKey: "benchmarking"); updateEnvVars() }
}
@Published var dlOpenMode: DLOpenMode {
didSet {
Expand Down Expand Up @@ -427,9 +484,7 @@ final class ConfigStore: ObservableObject {

// Auto-detect running Xcode on launch
if ud.string(forKey: "XcodePath") == nil,
let runningXcode = NSRunningApplication
.runningApplications(withBundleIdentifier: "com.apple.dt.Xcode")
.first?.bundleURL?.path {
let runningXcode = MonitorXcode.externalXcode?.bundleURL?.path {
self.xcodePath = runningXcode
}

Expand Down Expand Up @@ -485,7 +540,11 @@ final class ConfigStore: ObservableObject {
// MARK: - Actions

func setInjectionState(_ state: InjectionState) {
DispatchQueue.main.async { self.injectionState = state }
if Thread.isMainThread {
injectionState = state
} else {
DispatchQueue.main.async { [weak self] in self?.injectionState = state }
}
}

func updateWatchingDirectories() {
Expand Down
66 changes: 66 additions & 0 deletions App/InjectionNext/Defaults.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// Defaults.swift
// InjectionNext
//
// Created by John Holdsworth on 24/07/2024.
// Copyright © 2024 John Holdsworth. All rights reserved.
//

import Foundation

struct Defaults {
/// App defaults for persistent state
static let userDefaults = UserDefaults.standard
static let xcodePathDefault = "XcodePath"
static let librariesDefault = "libraries"
static let codesigningDefault = "codesigningIdentity"
private static let xcodeRestartDefault = "xcodeRestartDefault"
static var xcodePath: String {
xcodeDefault ?? "/Applications/Xcode.app" }
static var xcodeDefault: String? {
get {
userDefaults.string(forKey: xcodePathDefault)
}
set {
userDefaults.setValue(newValue,
forKey: xcodePathDefault)
}
}
static var deviceLibraries: String {
get {
userDefaults.string(forKey: librariesDefault) ??
"-framework XCTest -lXCTestSwiftSupport"
}
set {
userDefaults.setValue(newValue,
forKey: librariesDefault)
}
}
static var codesigningIdentity: String? {
get {
userDefaults.string(forKey: codesigningDefault)
}
set {
userDefaults.setValue(newValue,
forKey: codesigningDefault)
}
}

static var xcodeRestart: Bool {
get {
if userDefaults.value(forKey: xcodeRestartDefault) == nil { return true }
return userDefaults.bool(forKey: xcodeRestartDefault)
}
set {
userDefaults.setValue(newValue,
forKey: xcodeRestartDefault)
}
}
static let projectPathDefault = "projectPath"
static var projectPath: String? {
get {
userDefaults.string(forKey: projectPathDefault)
}
}
static var mcpServer = userDefaults.bool(forKey: "mcpServer")
}
Comment thread
johnno1962 marked this conversation as resolved.
10 changes: 5 additions & 5 deletions App/InjectionNext/FrontendServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class FrontendServer: SimpleSocket {
recompiler.store(compilation: compile, for: source)
}
recompiler.modified = false
print("Loaded \(recompiler.compilations.count) \(platform) commands.")
debug("Loaded \(recompiler.compilations.count) \(platform) commands.")
}
} catch {
InjectionServer.error("Unable to read commands cache: \(error).")
Expand All @@ -90,7 +90,7 @@ class FrontendServer: SimpleSocket {
if let error = Popen.system("gzip -f "+cache.path, errors: true) {
InjectionServer.error("Unable to zip commands cache: \(error)")
} else {
print("Cached \(commands.count) \(platform) commands")
debug("Cached \(commands.count) \(platform) commands")
}
recompiler.modified = false
} catch {
Expand Down Expand Up @@ -173,7 +173,7 @@ class FrontendServer: SimpleSocket {
}
#endif

print("Updating \(parser.args.count) args for \(parser.platform)/" +
debug("Updating \(parser.args.count) args for \(parser.platform)/" +
URL(fileURLWithPath: source).lastPathComponent)
recompiler.store(compilation: update, for: source)
}
Expand Down Expand Up @@ -305,7 +305,7 @@ extension AppDelegate {
.fileExists(atPath: FrontendServer.patched) ?
FrontendServer.State.patched : .unpatched
DispatchQueue.main.async {
self.patchCompilerItem.title = state.rawValue
self.patchCompilerItem?.title = state.rawValue
if state == .patched {
_ = FrontendServer.startOnce
}
Expand Down Expand Up @@ -334,7 +334,7 @@ extension AppDelegate {

"""
if changes?.pointee != before {
print("Patched", source)
debug("Patched", source)
}

if (patched.contains("class AppDelegate") ||
Expand Down
2 changes: 1 addition & 1 deletion App/InjectionNext/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>14083</string>
<string>14194</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
Expand Down
22 changes: 14 additions & 8 deletions App/InjectionNext/InjectionHybrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,17 @@ class InjectionHybrid: InjectionBase {
setenv(INJECTION_DIRECTORIES, watchPaths, 1)
Reloader.injectionQueue = .main
super.init()
// Extend FileWatcher pattern to detect git lock files
FileWatcher.INJECTABLE_PATTERN = try! NSRegularExpression(
pattern: #"[^~]\.(mm?|cpp|cc|swift|lock|o)$"#)
do {
// Extend FileWatcher pattern to detect git lock files
FileWatcher.INJECTABLE_PATTERN = try NSRegularExpression(
pattern: ConfigStore.shared.injectablePattern)
} catch {
InjectionServer.error("Invalid file pattern: \(error)")
}
}

/// Called from file watcher when file is edited.
override func inject(source: String) {
guard MonitorXcode.runningXcode == nil else { return }
// Detect git lock files - record path for later checking
if source.hasSuffix(".lock") &&
source.contains("/.git/") {
Expand Down Expand Up @@ -164,11 +167,14 @@ class HybridCompiler: NextCompiler {
static var liteRecompiler = Recompiler()

override func recompile(source: String, platform: String) -> String? {
let oldCache = Reloader.cacheFile
Reloader.sdk = platform // Select commands cache file.
if oldCache != Reloader.cacheFile { Self.liteRecompiler = Recompiler() }
let connected = InjectionServer.currentClient != nil,
oldCache = Reloader.cacheFile
Reloader.sdk = platform // Switch commands cache file.
if oldCache != Reloader.cacheFile && connected {
Self.liteRecompiler = Recompiler()
}
return Self.liteRecompiler.recompile(source: source, platformFilter:
"SDKs/"+platform, dylink: false)
connected ? "SDKs/"+platform : "", dylink: false)
}

override func link(object: String, dylib: String, arch: String) -> (String, Double)? {
Expand Down
Loading