Skip to content

Commit

Permalink
Scalability (johnno1962#20)
Browse files Browse the repository at this point in the history
* Unhide only changed files

* Not possible to just skip files.

* Tracking previous interposes no longer required.

* SwiftTrace supporting resilient classes

* Fallback to find class versions

* Avoid infinite loops.

* Notification warning.

* Give user option of replaying injections.
  • Loading branch information
johnno1962 authored May 30, 2021
1 parent eef7a32 commit fa593d5
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 19 deletions.
45 changes: 37 additions & 8 deletions Sources/HotReloading/SwiftInjection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by John Holdsworth on 05/11/2017.
// Copyright © 2017 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftInjection.swift#31 $
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftInjection.swift#40 $
//
// Cut-down version of code injection in Swift. Uses code
// from SwiftEval.swift to recompile and reload class.
Expand Down Expand Up @@ -105,6 +105,22 @@ public class SwiftInjection: NSObject {
return "Injection#\(SwiftEval.instance.injectionNumber)/"
}

class func versions(of aClass: AnyClass) -> [AnyClass] {
let named = class_getName(aClass)
var out = [AnyClass]()
var nc: UInt32 = 0
if let classes = UnsafePointer(objc_copyClassList(&nc)) {
for i in 0 ..< Int(nc) {
if class_getSuperclass(classes[i]) != nil &&
strcmp(named, class_getName(classes[i])) == 0 {
out.append(classes[i])
}
}
free(UnsafeMutableRawPointer(mutating: classes))
}
return out
}

@objc
public class func inject(tmpfile: String) throws {
let newClasses = try SwiftEval.instance.loadAndInject(tmpfile: tmpfile)
Expand All @@ -113,7 +129,17 @@ public class SwiftInjection: NSObject {
var testClasses = [AnyClass]()

for i in 0..<oldClasses.count {
let oldClass: AnyClass = oldClasses[i], newClass: AnyClass = newClasses[i]
var oldClass: AnyClass = oldClasses[i], newClass: AnyClass = newClasses[i]

if oldClass === newClass {
let versions = Self.versions(of: newClass)
if versions.count > 1 {
oldClass = versions.first!
newClass = versions.last!
} else {
print("\(APP_PREFIX)⚠️ Could not find versions of class \(_typeName(newClass)). ⚠️")
}
}

// old-school swizzle Objective-C class & instance methods
injection(swizzle: object_getClass(newClass), onto: object_getClass(oldClass))
Expand Down Expand Up @@ -259,10 +285,11 @@ public class SwiftInjection: NSObject {

for suffix in SwiftTrace.swiftFunctionSuffixes {
findSwiftSymbols(dylib, suffix) { (loadedFunc, symbol, _, _) in
guard let existing = dlsym(main, symbol), existing != loadedFunc,
let current = SwiftTrace.interposed(replacee: existing) else {
guard let existing = dlsym(main, symbol), existing != loadedFunc/*,
let current = SwiftTrace.interposed(replacee: existing)*/ else {
return
}
let current = existing
let method = SwiftMeta.demangle(symbol: symbol) ?? String(cString: symbol)
if detail {
print("\(APP_PREFIX)Replacing \(method)")
Expand All @@ -282,9 +309,10 @@ public class SwiftInjection: NSObject {
#endif
}
}

#if !ORIGINAL_2_2_0_CODE
if SwiftTrace.apply(interposes: interposes, symbols: symbols, onInjection: { header in
if interposes.count != 0 &&
SwiftTrace.apply(interposes: interposes, symbols: symbols, onInjection: { header in
#if !arch(arm64)
let interposed = NSObject.swiftTraceInterposed.bindMemory(to:
[UnsafeRawPointer : UnsafeRawPointer].self, capacity: 1)
Expand All @@ -298,7 +326,7 @@ public class SwiftInjection: NSObject {
}
_ = SwiftTrace.apply(interposes: previous, symbols: symbols)
#endif
}) == 0 && interposes.count != 0 {
}) == 0 {
print("\(APP_PREFIX)⚠️ Injection has failed. Have you added -Xlinker -interposable to your project's \"Other Linker Flags\"? ⚠️")
}
#else
Expand Down Expand Up @@ -354,6 +382,7 @@ public class SwiftInjection: NSObject {
instances to determine which objects to message. \
If this fails, subscribe to the notification \
"INJECTION_BUNDLE_NOTIFICATION" instead.
\(APP_PREFIX)(note: notification may not arrive on the main thread)
""")
sweepWarned = true
}
Expand Down Expand Up @@ -473,7 +502,7 @@ public class SwiftInjection: NSObject {
found = true
}
}

if !found {
print("\(APP_PREFIX)Do you have the right project selected?")
}
Expand Down
9 changes: 6 additions & 3 deletions Sources/HotReloadingGuts/Unhide.mm
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
// (default argument generators) so they can be referenced
// in a file being dynamically loaded.
//
// $Id: //depot/HotReloading/Sources/HotReloadingGuts/Unhide.mm#8 $
// $Id: //depot/HotReloading/Sources/HotReloadingGuts/Unhide.mm#10 $
//

#import <Foundation/Foundation.h>

#import <mach-o/loader.h>
#import <mach-o/nlist.h>
#import <mach-o/stab.h>

#import <sys/stat.h>
#import <string>
#import <map>

Expand All @@ -29,7 +29,7 @@ void unhide_reset(void) {
seen.clear();
}

int unhide_symbols(const char *framework, const char *linkFileList, FILE *log) {
int unhide_symbols(const char *framework, const char *linkFileList, FILE *log, time_t since) {
FILE *linkFiles = fopen(linkFileList, "r");
if (!linkFiles) {
fprintf(log, "unhide: Could not open link file list %s\n", linkFileList);
Expand All @@ -43,6 +43,9 @@ int unhide_symbols(const char *framework, const char *linkFileList, FILE *log) {
buffer[strlen(buffer)-1] = '\000';

@autoreleasepool {
// struct stat info;
// if (stat(buffer, &info) || info.st_mtimespec.tv_sec < since)
// continue;
NSString *file = [NSString stringWithUTF8String:buffer];
NSData *patched = [[NSMutableData alloc] initWithContentsOfFile:file];

Expand Down
4 changes: 2 additions & 2 deletions Sources/HotReloadingGuts/include/InjectionClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by John Holdsworth on 06/11/2017.
// Copyright © 2017 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloadingGuts/include/InjectionClient.h#21 $
// $Id: //depot/HotReloading/Sources/HotReloadingGuts/include/InjectionClient.h#22 $
//
// Shared definitions between server and client.
//
Expand Down Expand Up @@ -98,5 +98,5 @@ typedef NS_ENUM(int, InjectionResponse) {
InjectionExit = ~0
};

extern int unhide_symbols(const char *framework, const char *linkFileList, FILE *log);
extern int unhide_symbols(const char *framework, const char *linkFileList, FILE *log, time_t since);
extern void unhide_reset(void);
1 change: 1 addition & 0 deletions Sources/HotReloadingGuts/include/UserDefaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ static NSString *const UserDefaultsOrderFront = @"orderFront";
static NSString *const UserDefaultsFeedback = @"feedback";
static NSString *const UserDefaultsLookup = @"typeLookup";
static NSString *const UserDefaultsRemote = @"appRemote";
static NSString *const UserDefaultsReplay = @"replayInjections";
5 changes: 3 additions & 2 deletions Sources/injectiond/InjectionServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by John Holdsworth on 06/11/2017.
// Copyright © 2017 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/injectiond/InjectionServer.swift#23 $
// $Id: //depot/HotReloading/Sources/injectiond/InjectionServer.swift#25 $
//

import Cocoa
Expand Down Expand Up @@ -129,7 +129,8 @@ public class InjectionServer: SimpleSocket {
}

guard let executable = readString() else { return }
if false && appDelegate.enableWatcher.state == .on {
if appDelegate.defaults.bool(forKey: UserDefaultsReplay) &&
appDelegate.enableWatcher.state == .on {
let mtime = {
(path: String) -> time_t in
var info = stat()
Expand Down
14 changes: 10 additions & 4 deletions Sources/injectiond/UnhidingEval.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// Created by John Holdsworth on 13/04/2021.
//
// $Id: //depot/HotReloading/Sources/injectiond/UnhidingEval.swift#8 $
// $Id: //depot/HotReloading/Sources/injectiond/UnhidingEval.swift#9 $
//
// Retro-fit Unhide into InjectionIII
//
Expand Down Expand Up @@ -44,13 +44,18 @@ public class UnhidingEval: SwiftEval {
return SwiftEval.instance
}

static var lastProcessed = [URL: time_t]()

let unhideQueue = DispatchQueue(label: "unhide")

var unhidden = false

public override func determineEnvironment(classNameOrFile: String) throws -> (URL, URL) {
let (project, logs) =
try super.determineEnvironment(classNameOrFile: classNameOrFile)

if !unhidden {
unhidden = true
let buildDir = logs.deletingLastPathComponent()
.deletingLastPathComponent().appendingPathComponent("Build")
if let enumerator = FileManager.default
Expand All @@ -59,27 +64,28 @@ public class UnhidingEval: SwiftEval {
let linkFileLists = enumerator
.compactMap { $0 as? String }
.filter { $0.hasSuffix(".LinkFileList") }
DispatchQueue.global(qos: .background).async {
unhideQueue.async {
// linkFileLists sorted to process packages
// first due to Edge case in Fruta example.
let since = Self.lastProcessed[buildDir] ?? 0
for path in linkFileLists.sorted(by: {
($0.hasSuffix(".o.LinkFileList") ? 0 : 1) <
($1.hasSuffix(".o.LinkFileList") ? 0 : 1) }) {
let fileURL = buildDir
.appendingPathComponent(path)
let exported = unhide_symbols(fileURL
.deletingPathExtension().deletingPathExtension()
.lastPathComponent, fileURL.path, log)
.lastPathComponent, fileURL.path, log, since)
if exported != 0 {
let s = exported == 1 ? "" : "s"
print("\(APP_PREFIX)Exported \(exported) default argument\(s) in \(fileURL.lastPathComponent)")
}
}
Self.lastProcessed[buildDir] = time(nil)
unhide_reset()
fclose(log)
}
}
unhidden = true
}

return (project, logs)
Expand Down

0 comments on commit fa593d5

Please sign in to comment.