#270" style="filled" fillcolor="#e0e0e0" color="#000000"];
269 -> 270 [label="_invalidated" color="#000000" eid="486"];
}
================================================
FILE: Contents/Resources/log.html
================================================
This area is fully editable and can be annotated.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 John Holdsworth
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Package.swift
================================================
// swift-tools-version:5.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
//
// Repo: https://github.com/johnno1962/HotReloading
// $Id: //depot/HotReloading/Package.swift#205 $
//
import PackageDescription
import Foundation
// This means of locating the IP address of developer's
// Mac has been replaced by a multicast implementation.
// If the multicast implementation fails to connect,
// clone the HotReloading project and hardcode the IP
// address of your Mac into the hostname value below.
// Then drag the clone onto your project to have it
// take precedence over the configured version.
var hostname = Host.current().name ?? "localhost"
// hostname = "192.168.0.243" // for example
let simulateDlopenOnDevice = false
let package = Package(
name: "HotReloading",
platforms: [.macOS("10.12"), .iOS("10.0"), .tvOS("10.0")],
products: [
.library(name: "HotReloading", targets: ["HotReloading"]),
.library(name: "HotReloadingGuts", targets: ["HotReloadingGuts"]),
.library(name: "injectiondGuts", targets: ["injectiondGuts"]),
.executable(name: "injectiond", targets: ["injectiond"]),
],
dependencies: [
.package(url: "https://github.com/johnno1962/SwiftTrace",
.upToNextMinor(from: "8.6.1")),
.package(name: "SwiftRegex",
url: "https://github.com/johnno1962/SwiftRegex5",
.upToNextMinor(from: "6.1.2")),
.package(url: "https://github.com/johnno1962/XprobePlugin",
.upToNextMinor(from: "2.9.10")),
.package(name: "RemotePlugin",
url: "https://github.com/johnno1962/Remote",
.upToNextMinor(from: "2.3.5")),
.package(url: "https://github.com/johnno1962/ProfileSwiftUI",
.upToNextMinor(from: "1.1.3")),
// .package(url: "https://github.com/johnno1962/DLKit",
// .upToNextMinor(from: "1.2.1")),
] + (simulateDlopenOnDevice ? [
.package(url: "https://github.com/johnno1962/InjectionScratch",
.upToNextMinor(from: "1.2.13"))] : []),
targets: [
.target(name: "HotReloading", dependencies: ["HotReloadingGuts",
.product(name: "SwiftTraceD", package: "SwiftTrace"),
.product(name: "Xprobe", package: "XprobePlugin"),
.product(name: "SwiftRegex", package: "SwiftRegex"),
"ProfileSwiftUI" /*, "DLKit",
*/] + (simulateDlopenOnDevice ? ["InjectionScratch"] : [])
/*, linkerSettings: [.unsafeFlags([
"-Xlinker", "-interposable", "-undefined", "dynamic_lookup"])]*/),
.target(name: "HotReloadingGuts",
cSettings: [.define("DEVELOPER_HOST", to: "\"\(hostname)\"")]),
.target(name: "injectiondGuts"),
.target(name: "injectiond", dependencies: ["HotReloadingGuts", "injectiondGuts",
.product(name: "SwiftRegex", package: "SwiftRegex"),
.product(name: "XprobeUI", package: "XprobePlugin"),
.product(name: "RemoteUI", package: "RemotePlugin")],
swiftSettings: [.define("INJECTION_III_APP")])],
cxxLanguageStandard: .cxx11
)
================================================
FILE: README.md
================================================
# Yes, HotReloading for Swift, Objective-C & C++!
Note: While this was once a way of using the InjectionIII.app on real devices
and for its development you would not normally need to use this repo any
more as you can use the pre-built bundles using the `copy_bundle.sh`
script. It has also been largely superseded by the newer and simpler
[InjectionNext](https://github.com/johnno1962/InjectionNext) project.
You should only add the HotReloading product to your main target.
This project is the [InjectionIII](https://github.com/johnno1962/InjectionIII) app
for live code updates available as a Swift Package. i.e.:

Then, you can inject function implementations without having to rebuild your app...

To try out an example project that is already set-up, clone this fork of
[SwiftUI-Kit](https://github.com/johnno1962/SwiftUI-Kit).
To use on your project, add this repo as a Swift Package and add
"Other Linker Flags": -Xlinker -interposable. You no longer need
to add a "Run Script" build phase. If want to inject on a device,
see the notes below on how to configure the InjectionIII app.
Note however, on an M1/M2 Mac this project only works with
an iOS/tvOS 14 or later simulator. Also, due to a quirk of how
Xcode how enables a DEBUG build of Swift Packages, your
"configuration" needs to contain the string "Debug".
***Remember not to release your app with this package configured.***
You should see a message that the app is watching for source file
changes in your home directory. You can change this scope by
adding comma separated list in the environment variable
`INJECTION_DIRECTORIES`. Should you want to connect to the
InjectionIII.app when using the simulator, add the environment
variable `INJECTION_DAEMON` to your scheme.
Consult the README of the [InjectionIII](https://github.com/johnno1962/InjectionIII)
project for more information in particular how to use it to inject `SwiftUI` using the
[HotSwiftUI](https://github.com/johnno1962/HotSwiftUI) protocol extension.
### HotReloading using VSCode
It's possible to use HotReloading from inside the VSCode editor and realise a
form of "VScode Previews". Consult [this project](https://github.com/markst/hotreloading-vscode-ios) for the setup required.
### Device Injection
This version of the HotReloading project and it's dependencies now support
injection on a real iOS or tvOS device.
Device injection now connects to the [InjectionIII.app](https://github.com/johnno1962/InjectionIII)
([github release](https://github.com/johnno1962/InjectionIII/releases)
4.6.0 or above) and requires you type the following commands into a Terminal
then restart the app to opt into receiving remote connections from a device:
$ rm ~/Library/Containers/com.johnholdsworth.InjectionIII/Data/Library/Preferences/com.johnholdsworth.InjectionIII.plist
$ defaults write com.johnholdsworth.InjectionIII deviceUnlock any
Note, if you've used the App Store version of InjectionIII in the past,
the binary releases have a different preferences file and the two can
get confused and prevent writing this preference from taking effect.
This is why the first `rm` command above can be necessary. If your
device doesn't connect check the app is listening on port `8899`:
```
% netstat -an | grep LIST | grep 88
tcp4 0 0 127.0.0.1.8898 *.* LISTEN
tcp4 0 0 *.8899 *.* LISTEN
```
If your device still doesn't connect either add an `INJECTION_HOST`
environment variable to your scheme containg the WiFi IP address of
the host you're running the InjectionIII.app on or clone this project and
code your mac's IP address into the `hostname` variable in Package.swift.
Then, drag the clone onto your project to have it take the place of the
configured Swift Package as outlined in [these instructions](https://developer.apple.com/documentation/xcode/editing-a-package-dependency-as-a-local-package).
Note: as the HotReloading package needs to connect a network
socket to your Mac to receive commands and new versions of code, expect
a message the first time you run your app after adding the package
asking you to "Trust" that your app should be allowed to do this.
Likewise, at the Mac end (as the InjectionIII app needs to open
a network port to accept this connection) you may be prompted for
permission if you have the macOS firewall turned on.
For `SwiftUI` you can force screen updates by following the conventions
outlined in the [HotSwiftUI](https://github.com/johnno1962/HotSwiftUI)
project then you can experience something like "Xcode Previews", except
for a fully functional app on an actual device!
### Vapor injection
To use injection with Vapor web server, it is now possible to just
download the [InjectionIII.app](https://github.com/johnno1962/InjectionIII)
and add the following line to be called as the server configures
(when running Vapor from inside Xcode):
```
#if DEBUG && os(macOS)
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endif
```
It will also be necessary to add the following argument to your targets:
```
linkerSettings: [.unsafeFlags(["-Xlinker", "-interposable"],
.when(platforms: [.macOS], configuration: .debug))]
```
As an alternative, you can add this Swift package as a dependency to Vapor's
Package.swift of the "App" target.
### Thanks to...
The App Tracing functionality uses the [OliverLetterer/imp_implementationForwardingToSelector](https://github.com/OliverLetterer/imp_implementationForwardingToSelector) trampoline implementation
via the [SwiftTrace](https://github.com/johnno1962/SwiftTrace) project under an MIT license.
SwiftTrace uses the very handy [https://github.com/facebook/fishhook](https://github.com/facebook/fishhook)
as an alternative to the dyld_dynamic_interpose dynamic loader private api. See the
project source and header file included in the framework for licensing details.
The ["Remote"](https://github.com/johnno1962/Remote) server in this project which
allows you to capture videos from your device includes code adapted from
[acj/TimeLapseBuilder-Swift](https://github.com/acj/TimeLapseBuilder-Swift)
This release includes a very slightly modified version of the excellent
[canviz](https://code.google.com/p/canviz/) library to render "dot" files
in an HTML canvas which is subject to an MIT license. The changes are to pass
through the ID of the node to the node label tag (line 212), to reverse
the rendering of nodes and the lines linking them (line 406) and to
store edge paths so they can be coloured (line 66 and 303) in "canviz-0.1/canviz.js".
It also includes [CodeMirror](http://codemirror.net/) JavaScript editor for
the code to be evaluated in the Xprobe browser under an MIT license.
$Date: 2025/08/03 $
================================================
FILE: Sources/HotReloading/DeviceInjection.swift
================================================
//
// DeviceInjection.swift
//
// Created by John Holdsworth on 17/03/2022.
// Copyright © 2022 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/DeviceInjection.swift#44 $
//
// Code specific to injecting on an actual device.
//
#if DEBUG || !SWIFT_PACKAGE
#if !targetEnvironment(simulator) && SWIFT_PACKAGE && canImport(InjectionScratch)
#if SWIFT_PACKAGE
import SwiftRegex
#endif
extension SwiftInjection {
/// Emulate remaining functions of the dynamic linker.
/// - Parameter pseudoImage: last image read into memory
public class func onDeviceSpecificProcessing(
for pseudoImage: MachImage, _ sweepClasses: [AnyClass]) {
// register types, protocols, conformances...
var section_size: UInt64 = 0
for (section, regsiter) in [
("types", "swift_registerTypeMetadataRecords"),
("protos", "swift_registerProtocols"),
("proto", "swift_registerProtocolConformances")] {
if let section_start =
getsectdatafromheader_64(autoBitCast(pseudoImage),
SEG_TEXT, "__swift5_"+section, §ion_size),
section_size != 0, let call: @convention(c)
(UnsafeRawPointer, UnsafeRawPointer) -> Void =
autoBitCast(dlsym(SwiftMeta.RTLD_DEFAULT, regsiter)) {
call(section_start, section_start+Int(section_size))
}
}
// Redirect symbolic type references to main bundle
reverse_symbolics(pseudoImage)
// Initialise offsets to ivars
adjustIvarOffsets(in: pseudoImage)
// Fixup references to Objective-C classes
fixupObjcClassReferences(in: pseudoImage)
// Fix Objective-C messages to super
var supersSize: UInt64 = 0
if let injectedClass = sweepClasses.first,
let supersSection: UnsafeMutablePointer = autoBitCast(
getsectdatafromheader_64(autoBitCast(pseudoImage), SEG_DATA,
"__objc_superrefs", &supersSize)), supersSize != 0 {
supersSection[0] = injectedClass
}
// Populate "l_got.*" descriptor references
bindDescriptorReferences(in: pseudoImage)
}
struct ObjcClassMetaData {
var metaClass: AnyClass?
var metaData: UnsafeMutablePointer? {
return autoBitCast(metaClass)
}
var superClass: AnyClass?
var superData: UnsafeMutablePointer? {
return autoBitCast(superClass)
}
var methodCache: UnsafeMutableRawPointer
var bits: uintptr_t
var data: UnsafeMutablePointer?
}
public class func fillinObjcClassMetadata(in pseudoImage: MachImage) {
func getClass(_ sym: UnsafePointer)
-> UnsafeMutablePointer? {
return autoBitCast(dlsym(SwiftMeta.RTLD_DEFAULT, sym))
}
var sectionSize: UInt64 = 0
let info = getsectdatafromheader_64(autoBitCast(pseudoImage),
SEG_DATA, "__objc_imageinfo", §ionSize)
let metaNSObject = getClass("OBJC_CLASS_$_NSObject")
let emptyCache = dlsym(SwiftMeta.RTLD_DEFAULT, "_objc_empty_cache")!
func fillin(newClass: UnsafeRawPointer, symname: UnsafePointer) {
let metaData:
UnsafeMutablePointer = autoBitCast(newClass)
if let oldClass = getClass(symname) {
metaData.pointee.methodCache = emptyCache
metaData.pointee.superClass = oldClass.pointee.superClass
metaData.pointee.metaData?.pointee.methodCache = emptyCache
metaData.pointee.metaData?.pointee.metaClass =
metaNSObject?.pointee.metaClass
metaData.pointee.metaData?.pointee.superClass =
oldClass.pointee.metaClass // should be super of metaclass..
if registerClasses, #available(macOS 10.10, iOS 8.0, tvOS 9.0, *) {
detail("\(newClass): \(metaData.pointee) -> " +
"\((metaData.pointee.metaData ?? metaData).pointee)")
// _objc_realizeClassFromSwift(autoBitCast(aClass), oldClass)
objc_readClassPair(autoBitCast(newClass), autoBitCast(info))
} else {
// Fallback on earlier versions
}
}
}
SwiftTrace.forAllClasses(bundlePath: searchLastLoaded()) {
(aClass, stop) in
var info = Dl_info()
let address: UnsafeRawPointer = autoBitCast(aClass)
if fast_dladdr(address, &info) != 0, let symname = info.dli_sname {
fillin(newClass: address, symname: symname)
}
}
}
// Used to enumerate methods
// on an "unrealised" class.
struct ObjcMethodMetaData {
let name: UnsafePointer
let type: UnsafePointer
let impl: IMP
}
struct ObjcMethodListMetaData {
let flags: Int32, methodCount: Int32
var firstMethod: ObjcMethodMetaData
}
struct ObjcReadOnlyMetaData {
let skip: (Int32, Int32, Int32, Int32) = (0, 0, 0, 0)
let names: (UnsafeRawPointer?, UnsafePointer?)
let methods: UnsafeMutablePointer?
}
public class func onDevice(swizzle oldClass: AnyClass,
from newClass: AnyClass) -> Int {
var swizzled = 0
let metaData: UnsafePointer = autoBitCast(newClass)
if !class_isMetaClass(oldClass), // class methods...
let metaClass = metaData.pointee.metaClass,
let metaOldClass = object_getClass(oldClass) {
swizzled += onDevice(swizzle: metaOldClass, from: metaClass)
}
let swiftBits: uintptr_t = 0x3
guard let roData: UnsafePointer =
autoBitCast(autoBitCast(metaData.pointee.data) & ~swiftBits),
let methodInfo = roData.pointee.methods else { return swizzled }
withUnsafePointer(to: &methodInfo.pointee.firstMethod) {
methods in
for i in 0 ..< Int(methodInfo.pointee.methodCount) {
let selector = sel_registerName(methods[i].name)
let method = class_getInstanceMethod(oldClass, selector)
let existing = method.flatMap { method_getImplementation($0) }
traceAndReplace(existing, replacement: autoBitCast(methods[i].impl),
objcMethod: method, objcClass: newClass) {
(replacement: IMP) -> String? in
if class_replaceMethod(oldClass, selector, replacement,
methods[i].type) != replacement {
swizzled += 1
return "Swizzled"
}
return nil
}
}
}
return swizzled
}
public class func adjustIvarOffsets(in pseudoImage: MachImage) {
var ivarOffsetPtr: UnsafeMutablePointer!
// Objective-C source version
pseudoImage.symbols(withPrefix: "_OBJC_IVAR_$_") {
(address, symname, suffix) in
if let classname = strdup(suffix),
var ivarname = strchr(classname, Int32(UInt8(ascii: "."))) {
ivarname[0] = 0
ivarname += 1
if let oldClass = objc_getClass(classname) as? AnyClass,
let ivar = class_getInstanceVariable(oldClass, ivarname) {
ivarOffsetPtr = autoBitCast(address)
ivarOffsetPtr.pointee = ivar_getOffset(ivar)
detail(String(cString: classname)+"." +
String(cString: ivarname) +
" offset: \(ivarOffsetPtr.pointee)")
}
free(classname)
} else {
log("⚠️ Could not parse ivar: \(String(cString: symname))")
}
}
// Swift source version
findHiddenSwiftSymbols(searchLastLoaded(), "Wvd", .any) {
(address, symname, _, _) -> Void in
if let fieldInfo = SwiftMeta.demangle(symbol: symname),
let (classname, ivarname): (String, String) =
fieldInfo[#"direct field offset for (\S+)\.\(?(\w+) "#],
let oldClass = objc_getClass(classname) as? AnyClass,
let ivar = class_getInstanceVariable(oldClass, ivarname),
get_protection(autoBitCast(address)) & VM_PROT_WRITE != 0 {
ivarOffsetPtr = autoBitCast(address)
ivarOffsetPtr.pointee = ivar_getOffset(ivar)
detail(classname+"."+ivarname +
" direct offset: \(ivarOffsetPtr.pointee)")
} else {
log("⚠️ Could not parse ivar: \(String(cString: symname))")
}
}
}
/// Fixup references to Objective-C classes on device
public class func fixupObjcClassReferences(in pseudoImage: MachImage) {
var sectionSize: UInt64 = 0
if let classNames = objcClassRefs as? [String], classNames.first != "",
let classRefsSection: UnsafeMutablePointer = autoBitCast(
getsectdatafromheader_64(autoBitCast(pseudoImage),
SEG_DATA, "__objc_classrefs", §ionSize)) {
let nClassRefs = Int(sectionSize)/MemoryLayout.size
let objcClasses = classNames.compactMap {
return dlsym(SwiftMeta.RTLD_DEFAULT, "OBJC_CLASS_$_"+$0)
}
if nClassRefs == objcClasses.count {
for i in 0 ..< nClassRefs {
classRefsSection[i] = autoBitCast(objcClasses[i])
}
} else {
log("⚠️ Number of class refs \(nClassRefs) does not equal \(classNames)")
}
}
}
/// Populate "l_got.*" external references to "descriptors"
/// - Parameter pseudoImage: lastLoadedImage
public class func bindDescriptorReferences(in pseudoImage: MachImage) {
if let descriptorSyms = descriptorRefs as? [String],
descriptorSyms.first != "" {
var forces: UnsafeRawPointer?
let forcePrefix = "__swift_FORCE_LOAD_$_"
let forcePrefixLen = strlen(forcePrefix)
fast_dlscan(pseudoImage, .any, { symname in
return strncmp(symname, forcePrefix, forcePrefixLen) == 0
}) { value, symname, _, _ in
forces = value
}
if var descriptorRefs:
UnsafeMutablePointer = autoBitCast(forces) {
for descriptorSym in descriptorSyms {
descriptorRefs = descriptorRefs.advanced(by: 1)
if let value = dlsym(SwiftMeta.RTLD_DEFAULT, descriptorSym),
descriptorRefs.pointee == nil {
descriptorRefs.pointee = value
} else {
detail("⚠️ Could not bind " + describeImageSymbol(descriptorSym))
}
}
} else {
log("⚠️ Could not locate descriptors section")
}
}
}
}
#endif
#endif
================================================
FILE: Sources/HotReloading/DynamicCast.swift
================================================
//
// DynamicCast.swift
// InjectionIII
//
// Created by John Holdsworth on 02/24/2021.
// Copyright © 2021 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/DynamicCast.swift#12 $
//
// Dynamic casting in an "as?" expression to a type that has been injected.
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
public func injection_dynamicCast(inp: UnsafeRawPointer,
out: UnsafeMutablePointer,
from: Any.Type, to: Any.Type, size: size_t) -> Bool {
let toName = _typeName(to)
// print("HERE \(inp) \(out) \(_typeName(from)) \(toName) \(size)")
let to = toName.hasPrefix("__C.") ? to :
SwiftMeta.lookupType(named: toName, protocols: true) ?? to
return DynamicCast.original_dynamicCast?(inp, out,
autoBitCast(from), autoBitCast(to), size) ?? false
}
class DynamicCast {
typealias injection_dynamicCast_t = @convention(c)
(_ inp: UnsafeRawPointer,
_ out: UnsafeMutablePointer,
_ from: UnsafeRawPointer, _ to: UnsafeRawPointer,
_ size: size_t) -> Bool
static let swift_dynamicCast = strdup("swift_dynamicCast")!
static let original_dynamicCast: injection_dynamicCast_t? =
autoBitCast(dlsym(SwiftMeta.RTLD_DEFAULT, swift_dynamicCast))
static var hooked_dynamicCast: UnsafeMutableRawPointer? = {
let module = _typeName(DynamicCast.self)
.components(separatedBy: ".")[0]
return dlsym(SwiftMeta.RTLD_DEFAULT,
"$s\(module.count)\(module)" +
"21injection_dynamicCast" +
"3inp3out4from2to4sizeSbSV_" +
"SpySVGypXpypXpSitF")
}()
static var rebinds = original_dynamicCast != nil &&
hooked_dynamicCast != nil ? [
rebinding(name: swift_dynamicCast,
replacement: hooked_dynamicCast!,
replaced: nil)] : []
static var hook_appDynamicCast: Void = {
appBundleImages { imageName, header, slide in
rebind_symbols_image(autoBitCast(header), slide,
&rebinds, rebinds.count)
}
}()
static func hook_lastInjected() {
_ = DynamicCast.hook_appDynamicCast
let lastLoaded = _dyld_image_count()-1
rebind_symbols_image(
UnsafeMutableRawPointer(mutating: lastPseudoImage() ??
_dyld_get_image_header(lastLoaded)),
lastPseudoImage() != nil ? 0 :
_dyld_get_image_vmaddr_slide(lastLoaded),
&rebinds, rebinds.count)
}
}
#endif
================================================
FILE: Sources/HotReloading/FileWatcher.swift
================================================
//
// FileWatcher.swift
// InjectionIII
//
// Created by John Holdsworth on 08/03/2015.
// Copyright (c) 2015 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/FileWatcher.swift#49 $
//
// Started out as an abstraction to watch files under a directory.
// "Enhanced" to extract the last modified build log directory by
// backdating the event stream to just before the app launched.
//
#if DEBUG || !SWIFT_PACKAGE
#if targetEnvironment(simulator) && !APP_SANDBOXED || os(macOS)
import Foundation
public class FileWatcher: NSObject {
public typealias InjectionCallback = (_ filesChanged: NSArray, _ ideProcPath: String) -> Void
static var INJECTABLE_PATTERN = try! NSRegularExpression(
pattern: "[^~]\\.(mm?|cpp|swift|storyboard|xib)$")
static let logsPref = "HotReloadingBuildLogsDir"
static var derivedLog =
UserDefaults.standard.string(forKey: logsPref) {
didSet {
UserDefaults.standard.set(derivedLog, forKey: logsPref)
}
}
var initStream: ((FSEventStreamEventId) -> Void)!
var eventsStart =
FSEventStreamEventId(kFSEventStreamEventIdSinceNow)
#if SWIFT_PACKAGE
var eventsToBackdate: UInt64 = 10_000
#else
var eventsToBackdate: UInt64 = 50_000
#endif
var fileEvents: FSEventStreamRef! = nil
var callback: InjectionCallback
var context = FSEventStreamContext()
@objc public init(roots: [String], callback: @escaping InjectionCallback,
runLoop: CFRunLoop? = nil) {
self.callback = callback
super.init()
#if os(macOS)
context.info = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
#else
guard let FSEventStreamCreate = FSEventStreamCreate else {
fatalError("Could not locate FSEventStreamCreate")
}
#endif
initStream = { [weak self] since in
guard let self = self else { return }
let fileEvents = FSEventStreamCreate(kCFAllocatorDefault,
{ (streamRef: FSEventStreamRef,
clientCallBackInfo: UnsafeMutableRawPointer?,
numEvents: Int, eventPaths: UnsafeMutableRawPointer,
eventFlags: UnsafePointer,
eventIds: UnsafePointer) in
#if os(macOS)
let watcher = unsafeBitCast(clientCallBackInfo, to: FileWatcher.self)
#else
guard let watcher = watchers[streamRef] else { return }
#endif
// Check that the event flags include an item renamed flag, this helps avoid
// unnecessary injection, such as triggering injection when switching between
// files in Xcode.
for i in 0 ..< numEvents {
let flag = Int(eventFlags[i])
if (flag & (kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemModified)) != 0 {
let changes = unsafeBitCast(eventPaths, to: NSArray.self)
if CFRunLoopGetCurrent() != CFRunLoopGetMain() {
return watcher.filesChanged(changes: changes)
}
DispatchQueue.main.async {
watcher.filesChanged(changes: changes)
}
return
}
}
},
&self.context, roots as CFArray, since, 0.1,
FSEventStreamCreateFlags(kFSEventStreamCreateFlagUseCFTypes |
kFSEventStreamCreateFlagFileEvents))!
#if !os(macOS)
watchers[fileEvents] = self
#endif
FSEventStreamScheduleWithRunLoop(fileEvents, runLoop ?? CFRunLoopGetMain(),
"kCFRunLoopDefaultMode" as CFString)
_ = FSEventStreamStart(fileEvents)
self.fileEvents = fileEvents
}
initStream(eventsStart)
}
func filesChanged(changes: NSArray) {
var changed = Set()
#if !INJECTION_III_APP
let eventId = FSEventStreamGetLatestEventId(fileEvents)
if eventId != kFSEventStreamEventIdSinceNow &&
eventsStart == kFSEventStreamEventIdSinceNow {
eventsStart = eventId
FSEventStreamStop(fileEvents)
initStream(max(0, eventsStart-eventsToBackdate))
return
}
#endif
for path in changes {
guard var path = path as? String else { continue }
#if !INJECTION_III_APP
if path.hasSuffix(".xcactivitylog") &&
path.contains("/Logs/Build/") {
Self.derivedLog = path
}
if eventId <= eventsStart { continue }
#endif
if Self.INJECTABLE_PATTERN.firstMatch(in: path,
range: NSMakeRange(0, path.utf16.count)) != nil &&
path.range(of: "DerivedData/|InjectionProject/|.DocumentRevisions-|@__swiftmacro_|main.mm?$",
options: .regularExpression) == nil {
if let absolute = try? URL(fileURLWithPath: path)
.resourceValues(forKeys: [.canonicalPathKey])
.canonicalPath {
path = absolute
}
changed.insert(path)
}
}
if changed.count != 0 {
var path = ""
#if os(macOS)
if let application = NSWorkspace.shared.frontmostApplication {
path = getProcPath(pid: application.processIdentifier)
}
#endif
callback(Array(changed) as NSArray, path)
}
}
#if os(macOS)
deinit {
FSEventStreamStop(fileEvents)
FSEventStreamInvalidate(fileEvents)
FSEventStreamRelease(fileEvents)
#if DEBUG
NSLog("\(self).deinit()")
#endif
}
func getProcPath(pid: pid_t) -> String {
let pathBuffer = UnsafeMutablePointer.allocate(capacity: Int(MAXPATHLEN))
defer {
pathBuffer.deallocate()
}
proc_pidpath(pid, pathBuffer, UInt32(MAXPATHLEN))
let path = String(cString: pathBuffer)
return path
}
#endif
}
#if !os(macOS) // Yes, this api is available inside the simulator...
typealias FSEventStreamRef = OpaquePointer
typealias ConstFSEventStreamRef = OpaquePointer
struct FSEventStreamContext {
var version: CFIndex = 0
var info: UnsafeRawPointer?
var retain: UnsafeRawPointer?
var release: UnsafeRawPointer?
var copyDescription: UnsafeRawPointer?
}
typealias FSEventStreamCreateFlags = UInt32
typealias FSEventStreamEventId = UInt64
typealias FSEventStreamEventFlags = UInt32
typealias FSEventStreamCallback = @convention(c) (ConstFSEventStreamRef, UnsafeMutableRawPointer?, Int, UnsafeMutableRawPointer, UnsafePointer, UnsafePointer) -> Void
#if true // avoid linker flags -undefined dynamic_lookup
let RTLD_DEFAULT = UnsafeMutableRawPointer(bitPattern: -2)
let FSEventStreamCreate = unsafeBitCast(dlsym(RTLD_DEFAULT, "FSEventStreamCreate"), to: (@convention(c) (_ allocator: CFAllocator?, _ callback: FSEventStreamCallback, _ context: UnsafeMutableRawPointer?, _ pathsToWatch: CFArray, _ sinceWhen: FSEventStreamEventId, _ latency: CFTimeInterval, _ flags: FSEventStreamCreateFlags) -> FSEventStreamRef?)?.self)
let FSEventStreamScheduleWithRunLoop = unsafeBitCast(dlsym(RTLD_DEFAULT, "FSEventStreamScheduleWithRunLoop"), to: (@convention(c) (_ streamRef: FSEventStreamRef, _ runLoop: CFRunLoop, _ runLoopMode: CFString) -> Void).self)
let FSEventStreamStart = unsafeBitCast(dlsym(RTLD_DEFAULT, "FSEventStreamStart"), to: (@convention(c) (_ streamRef: FSEventStreamRef) -> Bool).self)
let FSEventStreamGetLatestEventId = unsafeBitCast(dlsym(RTLD_DEFAULT, "FSEventStreamGetLatestEventId"), to: (@convention(c) (_ streamRef: FSEventStreamRef) -> FSEventStreamEventId).self)
let FSEventStreamStop = unsafeBitCast(dlsym(RTLD_DEFAULT, "FSEventStreamStop"), to: (@convention(c) (_ streamRef: FSEventStreamRef) -> Void).self)
#else
@_silgen_name("FSEventStreamCreate")
func FSEventStreamCreate(_ allocator: CFAllocator?, _ callback: FSEventStreamCallback, _ context: UnsafeMutablePointer?, _ pathsToWatch: CFArray, _ sinceWhen: FSEventStreamEventId, _ latency: CFTimeInterval, _ flags: FSEventStreamCreateFlags) -> FSEventStreamRef?
@_silgen_name("FSEventStreamScheduleWithRunLoop")
func FSEventStreamScheduleWithRunLoop(_ streamRef: FSEventStreamRef, _ runLoop: CFRunLoop, _ runLoopMode: CFString)
@_silgen_name("FSEventStreamStart")
func FSEventStreamStart(_ streamRef: FSEventStreamRef) -> Bool
#endif
let kFSEventStreamEventIdSinceNow: UInt64 = 18446744073709551615
let kFSEventStreamCreateFlagUseCFTypes: FSEventStreamCreateFlags = 1
let kFSEventStreamCreateFlagFileEvents: FSEventStreamCreateFlags = 16
let kFSEventStreamEventFlagItemRenamed = 0x00000800
let kFSEventStreamEventFlagItemModified = 0x00001000
fileprivate var watchers = [FSEventStreamRef: FileWatcher]()
#endif
#endif
#endif
================================================
FILE: Sources/HotReloading/InjectionClient.swift
================================================
//
// InjectionClient.swift
// InjectionIII
//
// Created by John Holdsworth on 02/24/2021.
// Copyright © 2021 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/InjectionClient.swift#91 $
//
// Client app side of HotReloading started by +load
// method in HotReloadingGuts/ClientBoot.mm
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
#if SWIFT_PACKAGE
#if canImport(InjectionScratch)
import InjectionScratch
#endif
import Xprobe
import ProfileSwiftUI
public struct HotReloading {
public static var stack: Void {
injection_stack()
}
}
#endif
#if os(macOS)
let isVapor = true
#else
let isVapor = dlsym(SwiftMeta.RTLD_DEFAULT, VAPOR_SYMBOL) != nil
#endif
@objc(InjectionClient)
public class InjectionClient: SimpleSocket, InjectionReader {
let injectionQueue = isVapor ? DispatchQueue(label: "InjectionQueue") : .main
var appVersion: String?
open func log(_ msg: String) {
print(APP_PREFIX+msg)
}
#if canImport(InjectionScratch)
func next(scratch: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
return scratch
}
#endif
public override func runInBackground() {
let builder = SwiftInjectionEval.sharedInstance()
builder.tmpDir = NSTemporaryDirectory()
write(INJECTION_SALT)
write(INJECTION_KEY)
let frameworksPath = Bundle.main.privateFrameworksPath!
write(builder.tmpDir)
write(builder.arch)
let executable = Bundle.main.executablePath!
write(executable)
#if canImport(InjectionScratch)
var isiOSAppOnMac = false
if #available(iOS 14.0, *) {
isiOSAppOnMac = ProcessInfo.processInfo.isiOSAppOnMac
}
if !isiOSAppOnMac, let scratch = loadScratchImage(nil, 0, self, nil) {
log("⚠️ You are using device injection which is very much a work in progress. Expect the unexpected.")
writeCommand(InjectionResponse
.scratchPointer.rawValue, with: nil)
writePointer(next(scratch: scratch))
}
#endif
builder.forceUnhide = { self.writeCommand(InjectionResponse
.forceUnhide.rawValue, with: nil) }
builder.tmpDir = readString() ?? "/tmp"
builder.createUnhider(executable: executable,
SwiftInjection.objcClassRefs,
SwiftInjection.descriptorRefs)
if getenv(INJECTION_UNHIDE) != nil {
builder.legacyUnhide = true
writeCommand(InjectionResponse.legacyUnhide.rawValue, with: "1")
}
var frameworkPaths = [String: String]()
let isPlugin = builder.tmpDir == "/tmp"
if (!isPlugin) {
var frameworks = [String]()
var sysFrameworks = [String]()
for i in stride(from: _dyld_image_count()-1, through: 0, by: -1) {
guard let imageName = _dyld_get_image_name(i),
strstr(imageName, ".framework/") != nil else {
continue
}
let imagePath = String(cString: imageName)
let frameworkName = URL(fileURLWithPath: imagePath).lastPathComponent
frameworkPaths[frameworkName] = imagePath
if imagePath.hasPrefix(frameworksPath) {
frameworks.append(frameworkName)
} else {
sysFrameworks.append(frameworkName)
}
}
writeCommand(InjectionResponse.frameworkList.rawValue, with:
frameworks.joined(separator: FRAMEWORK_DELIMITER))
write(sysFrameworks.joined(separator: FRAMEWORK_DELIMITER))
write(SwiftInjection.packageNames()
.joined(separator: FRAMEWORK_DELIMITER))
}
var codesignStatusPipe = [Int32](repeating: 0, count: 2)
pipe(&codesignStatusPipe)
let reader = SimpleSocket(socket: codesignStatusPipe[0])
let writer = SimpleSocket(socket: codesignStatusPipe[1])
builder.signer = { dylib -> Bool in
self.writeCommand(InjectionResponse.getXcodeDev.rawValue,
with: builder.xcodeDev)
self.writeCommand(InjectionResponse.sign.rawValue, with: dylib)
return reader.readString() == "1"
}
SwiftTrace.swizzleFactory = SwiftTrace.LifetimeTracker.self
if let projectRoot = getenv(INJECTION_PROJECT_ROOT) {
writeCommand(InjectionResponse.projectRoot.rawValue,
with: String(cString: projectRoot))
}
if let derivedData = getenv(INJECTION_DERIVED_DATA) {
writeCommand(InjectionResponse.derivedData.rawValue,
with: String(cString: derivedData))
}
// Find client platform
#if os(macOS) || targetEnvironment(macCatalyst)
var platform = "Mac"
#elseif os(tvOS)
var platform = "AppleTV"
#elseif os(visionOS)
var platform = "XR"
#elseif os(watchOS)
var platform = "Watch"
#else
var platform = "iPhone"
#endif
#if targetEnvironment(simulator)
platform += "Simulator"
#else
platform += "OS"
#endif
#if os(macOS)
platform += "X"
#endif
writeCommand(InjectionResponse.platform.rawValue, with: platform)
commandLoop:
while true {
let commandInt = readInt()
guard let command = InjectionCommand(rawValue: commandInt) else {
log("Invalid commandInt: \(commandInt)")
break
}
switch command {
case .EOF:
log("EOF received from server..")
break commandLoop
case .signed:
writer.write(readString() ?? "0")
case .traceFramework:
let frameworkName = readString() ?? "Misssing framework"
if let frameworkPath = frameworkPaths[frameworkName] {
print("\(APP_PREFIX)Tracing %s\n", frameworkPath)
_ = SwiftTrace.interposeMethods(inBundlePath: frameworkPath,
packageName: nil)
SwiftTrace.trace(bundlePath:frameworkPath)
} else {
log("Tracing package \(frameworkName)")
let mainBundlePath = Bundle.main.executablePath ?? "Missing"
_ = SwiftTrace.interposeMethods(inBundlePath: mainBundlePath,
packageName: frameworkName)
}
filteringChanged()
default:
process(command: command, builder: builder)
}
}
builder.forceUnhide = {}
log("\(APP_NAME) disconnected.")
}
func process(command: InjectionCommand, builder: SwiftEval) {
switch command {
case .vaccineSettingChanged:
if let data = readString()?.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
builder.vaccineEnabled = json[UserDefaultsVaccineEnabled] as! Bool
}
case .connected:
let projectFile = readString() ?? "Missing project"
log("\(APP_NAME) connected \(projectFile)")
builder.projectFile = projectFile
builder.derivedLogs = nil;
case .watching:
log("Watching files under the directory \(readString() ?? "Missing directory")")
case .log:
log(readString() ?? "Missing log message")
case .ideProcPath:
builder.lastIdeProcPath = readString() ?? ""
case .invalid:
log("⚠️ Server has rejected your connection. Are you running InjectionIII.app or start_daemon.sh from the right directory? ⚠️")
case .quietInclude:
SwiftTrace.traceFilterInclude = readString()
case .include:
SwiftTrace.traceFilterInclude = readString()
filteringChanged()
case .exclude:
SwiftTrace.traceFilterExclude = readString()
filteringChanged()
case .feedback:
SwiftInjection.traceInjection = readString() == "1"
case .lookup:
SwiftTrace.typeLookup = readString() == "1"
if SwiftTrace.swiftTracing {
log("Discovery of target app's types switched \(SwiftTrace.typeLookup ? "on" : "off")");
}
case .trace:
if SwiftTrace.traceMainBundleMethods() == 0 {
log("⚠️ Tracing Swift methods can only work if you have -Xlinker -interposable to your project's Debug \"Other Linker Flags\"")
} else {
log("Added trace to methods in main bundle")
}
filteringChanged()
case .untrace:
SwiftTrace.removeAllTraces()
case .traceUI:
if SwiftTrace.traceMainBundleMethods() == 0 {
log("⚠️ Tracing Swift methods can only work if you have -Xlinker -interposable to your project's Debug \"Other Linker Flags\"")
}
SwiftTrace.traceMainBundle()
log("Added trace to methods in main bundle")
filteringChanged()
case .traceUIKit:
DispatchQueue.main.sync {
let OSView: AnyClass = (objc_getClass("UIView") ??
objc_getClass("NSView")) as! AnyClass
log("Adding trace to the framework containg \(OSView), this will take a while...")
SwiftTrace.traceBundle(containing: OSView)
log("Completed adding trace.")
}
filteringChanged()
case .traceSwiftUI:
if let bundleOfAnyTextStorage = swiftUIBundlePath() {
log("Adding trace to SwiftUI calls.")
_ = SwiftTrace.interposeMethods(inBundlePath: bundleOfAnyTextStorage, packageName:nil)
filteringChanged()
} else {
log("Your app doesn't seem to use SwiftUI.")
}
case .uninterpose:
SwiftTrace.revertInterposes()
SwiftTrace.removeAllTraces()
log("Removed all traces (and injections).")
break;
case .stats:
let top = 200;
print("""
\(APP_PREFIX)Sorted top \(top) elapsed time/invocations by method
\(APP_PREFIX)=================================================
""")
SwiftInjection.dumpStats(top:top)
needsTracing()
case .callOrder:
print("""
\(APP_PREFIX)Function names in the order they were first called:
\(APP_PREFIX)===================================================
""")
for signature in SwiftInjection.callOrder() {
print(signature)
}
needsTracing()
case .fileOrder:
print("""
\(APP_PREFIX)Source files in the order they were first referenced:
\(APP_PREFIX)=====================================================
\(APP_PREFIX)(Order the source files should be compiled in target)
""")
SwiftInjection.fileOrder()
needsTracing()
case .counts:
print("""
\(APP_PREFIX)Counts of live objects by class:
\(APP_PREFIX)================================
""")
SwiftInjection.objectCounts()
needsTracing()
case .fileReorder:
writeCommand(InjectionResponse.callOrderList.rawValue,
with:SwiftInjection.callOrder().joined(separator: CALLORDER_DELIMITER))
needsTracing()
case .copy:
if let data = readData() {
injectionQueue.async {
var err: String?
do {
builder.injectionNumber += 1
try data.write(to: URL(fileURLWithPath: "\(builder.tmpfile).dylib"))
try SwiftInjection.inject(tmpfile: builder.tmpfile)
} catch {
self.log("⚠️ Injection error: \(error)")
err = "\(error)"
}
let response: InjectionResponse = err != nil ? .error : .complete
self.writeCommand(response.rawValue, with: err)
}
}
case .pseudoUnlock:
#if canImport(InjectionScratch)
presentInjectionScratch(readString() ?? "")
#endif
case .objcClassRefs:
if let array = readString()?
.components(separatedBy: ",") as NSArray?,
let mutable = array.mutableCopy() as? NSMutableArray {
SwiftInjection.objcClassRefs = mutable
}
case .descriptorRefs:
if let array = readString()?
.components(separatedBy: ",") as NSArray?,
let mutable = array.mutableCopy() as? NSMutableArray {
SwiftInjection.descriptorRefs = mutable
}
case .setXcodeDev:
if let xcodeDev = readString() {
builder.xcodeDev = xcodeDev
}
case .appVersion:
appVersion = readString()
writeCommand(InjectionResponse.buildCache.rawValue,
with: builder.buildCacheFile)
case .profileUI:
DispatchQueue.main.async {
ProfileSwiftUI.profile()
}
default:
processOnMainThread(command: command, builder: builder)
}
}
func processOnMainThread(command: InjectionCommand, builder: SwiftEval) {
guard let changed = self.readString() else {
log("⚠️ Could not read changed filename?")
return
}
#if canImport(InjectionScratch)
if command == .pseudoInject,
let imagePointer = self.readPointer() {
var percent = 0.0
pushPseudoImage(changed, imagePointer)
guard let imageEnd = loadScratchImage(imagePointer,
self.readInt(), self, &percent) else { return }
DispatchQueue.main.async {
do {
builder.injectionNumber += 1
let tmpfile = String(cString: searchLastLoaded())
let newClasses = try SwiftEval.instance.extractClasses(dl: UnsafeMutableRawPointer(bitPattern: ~0)!, tmpfile: tmpfile)
try SwiftInjection.inject(tmpfile: tmpfile, newClasses: newClasses)
} catch {
NSLog("Pseudo: \(error)")
}
if percent > 75 {
print(String(format: "\(APP_PREFIX)You have used %.1f%% of InjectionScratch space.", percent))
}
self.writeCommand(InjectionResponse.scratchPointer.rawValue, with: nil)
self.writePointer(self.next(scratch: imageEnd))
}
return
}
#endif
injectionQueue.async {
var err: String?
switch command {
case .load:
do {
builder.injectionNumber += 1
try SwiftInjection.inject(tmpfile: changed)
let countKey = "__injectionsPerformed", howOften = 100
let count = UserDefaults.standard.integer(forKey: countKey)+1
UserDefaults.standard.set(count, forKey: countKey)
if count % howOften == 0 && getenv("INJECTION_SPONSOR") == nil {
SwiftInjection.log("""
ℹ️ Seems like you're using injection quite a bit. \
Have you considered sponsoring the project at \
https://github.com/johnno1962/\(APP_NAME) or \
asking your boss if they should? (This message \
prints every \(howOften) injections.)
""")
}
} catch {
err = error.localizedDescription
}
case .inject:
if changed.hasSuffix("storyboard") || changed.hasSuffix("xib") {
#if os(iOS) || os(tvOS)
if !NSObject.injectUI(changed) {
err = "Interface injection failed"
}
#else
err = "Interface injection not available on macOS."
#endif
} else {
builder.forceUnhide = { builder.startUnhide() }
SwiftInjection.inject(classNameOrFile: changed)
}
#if SWIFT_PACKAGE
case .xprobe:
Xprobe.connect(to: nil, retainObjects:true)
Xprobe.search("")
case .eval:
let parts = changed.components(separatedBy:"^")
guard let pathID = Int(parts[0]) else { break }
self.writeCommand(InjectionResponse.pause.rawValue, with:"5")
if let object = (xprobePaths[pathID] as? XprobePath)?
.object() as? NSObject, object.responds(to: Selector(("swiftEvalWithCode:"))),
let code = (parts[3] as NSString).removingPercentEncoding,
object.swiftEval(code: code) {
} else {
self.log("Xprobe: Eval only works on NSObject subclasses where the source file has the same name as the class and is in your project.")
}
Xprobe.write("$('BUSY\(pathID)').hidden = true; ")
#endif
default:
self.log("⚠️ Unimplemented command: #\(command.rawValue). " +
"Are you running the most recent versions?")
}
let response: InjectionResponse = err != nil ? .error : .complete
self.writeCommand(response.rawValue, with: err)
}
}
func needsTracing() {
if !SwiftTrace.swiftTracing {
log("⚠️ You need to have traced something to gather stats.")
}
}
func filteringChanged() {
if SwiftTrace.swiftTracing {
let exclude = SwiftTrace.traceFilterExclude
if let include = SwiftTrace.traceFilterInclude {
print(String(format: exclude != nil ?
"\(APP_PREFIX)Filtering trace to include methods matching '%@' but not '%@'." :
"\(APP_PREFIX)Filtering trace to include methods matching '%@'.",
include, exclude != nil ? exclude! : ""))
} else {
print(String(format: exclude != nil ?
"\(APP_PREFIX)Filtering trace to exclude methods matching '%@'." :
"\(APP_PREFIX)Not filtering trace (Menu Item: 'Set Filters')",
exclude != nil ? exclude! : ""))
}
}
}
}
#endif
================================================
FILE: Sources/HotReloading/InjectionStats.swift
================================================
//
// InjectionStats.swift
//
// Created by John Holdsworth on 26/10/2022.
// Copyright © 2022 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/InjectionStats.swift#4 $
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
extension SwiftInjection {
@objc public class func dumpStats(top: Int) {
let invocationCounts = SwiftTrace.invocationCounts()
for (method, elapsed) in SwiftTrace.sortedElapsedTimes(onlyFirst: top) {
print("\(String(format: "%.1f", elapsed*1000.0))ms/\(invocationCounts[method] ?? 0)\t\(method)")
}
}
@objc public class func callOrder() -> [String] {
return SwiftTrace.callOrder().map { $0.signature }
}
@objc public class func fileOrder() {
let builder = SwiftEval.sharedInstance()
let signatures = callOrder()
guard let projectRoot = builder.projectFile.flatMap({
URL(fileURLWithPath: $0).deletingLastPathComponent().path+"/"
}),
let (_, logsDir) =
try? builder.determineEnvironment(classNameOrFile: "") else {
log("File ordering not available.")
return
}
let tmpfile = builder.tmpDir+"/eval101"
var found = false
SwiftEval.uniqueTypeNames(signatures: signatures) { typeName in
if !typeName.contains("("), let (_, foundSourceFile) =
try? builder.findCompileCommand(logsDir: logsDir,
classNameOrFile: typeName, tmpfile: tmpfile) {
print(foundSourceFile
.replacingOccurrences(of: projectRoot, with: ""))
found = true
}
}
if !found {
log("Do you have the right project selected?")
}
}
@objc public class func packageNames() -> [String] {
var packages = Set()
for suffix in SwiftTrace.traceableFunctionSuffixes {
findSwiftSymbols(Bundle.main.executablePath!, suffix) {
(_, symname: UnsafePointer, _, _) in
if let sym = SwiftMeta.demangle(symbol: String(cString: symname)),
!sym.hasPrefix("(extension in "),
let endPackage = sym.firstIndex(of: ".") {
packages.insert(sym[..<(endPackage+0)])
}
}
}
return Array(packages)
}
@objc public class func objectCounts() {
for (className, count) in SwiftTrace.liveObjects
.map({(_typeName(autoBitCast($0.key)), $0.value.count)})
.sorted(by: {$0.0 < $1.0}) {
print("\(count)\t\(className)")
}
}
}
#endif
================================================
FILE: Sources/HotReloading/ObjcInjection.swift
================================================
//
// ObjcInjection.swift
//
// Created by John Holdsworth on 17/03/2022.
// Copyright © 2022 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/ObjcInjection.swift#23 $
//
// Code specific to "classic" Objective-C method swizzling.
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
extension SwiftInjection.MachImage {
func symbols(withPrefix: UnsafePointer,
apply: @escaping (UnsafeRawPointer, UnsafePointer,
UnsafePointer) -> Void) {
let prefixLen = strlen(withPrefix)
fast_dlscan(self, .any, {
return strncmp($0, withPrefix, prefixLen) == 0}) {
(address, symname, _, _) in
apply(address, symname, symname + prefixLen - 1)
}
}
}
extension SwiftInjection {
public typealias MachImage = UnsafePointer
/// New method of swizzling based on symbol names
/// - Parameters:
/// - oldClass: original class to be swizzled
/// - tmpfile: no longer used
/// - Returns: # methods swizzled
public class func injection(swizzle oldClass: AnyClass, tmpfile: String) -> Int {
var methodCount: UInt32 = 0, swizzled = 0
if let methods = class_copyMethodList(oldClass, &methodCount) {
for i in 0 ..< Int(methodCount) {
swizzled += swizzle(oldClass: oldClass,
selector: method_getName(methods[i]), tmpfile)
}
free(methods)
}
return swizzled
}
/// Swizzle the newly loaded implementation of a selector onto oldClass
/// - Parameters:
/// - oldClass: orignal class to be swizzled
/// - selector: method selector to be swizzled
/// - tmpfile: no longer used
/// - Returns: # methods swizzled
public class func swizzle(oldClass: AnyClass, selector: Selector,
_ tmpfile: String) -> Int {
var swizzled = 0
if let method = class_getInstanceMethod(oldClass, selector),
let existing = unsafeBitCast(method_getImplementation(method),
to: UnsafeMutableRawPointer?.self),
let selsym = originalSym(for: existing) {
if let replacement = fast_dlsym(lastLoadedImage(), selsym) {
traceAndReplace(existing, replacement: replacement,
objcMethod: method, objcClass: oldClass) {
(replacement: IMP) -> String? in
if class_replaceMethod(oldClass, selector, replacement,
method_getTypeEncoding(method)) != nil {
swizzled += 1
return "Swizzled"
}
return nil
}
} else {
detail("⚠️ Swizzle failed "+describeImageSymbol(selsym))
}
}
return swizzled
}
/// Fallback to make sure at least the @objc func injected() and viewDidLoad() methods are swizzled
public class func swizzleBasics(oldClass: AnyClass, tmpfile: String) -> Int {
var swizzled = swizzle(oldClass: oldClass, selector: injectedSEL, tmpfile)
#if os(iOS) || os(tvOS)
swizzled += swizzle(oldClass: oldClass, selector: viewDidLoadSEL, tmpfile)
#endif
return swizzled
}
/// Original Objective-C swizzling
/// - Parameters:
/// - oldClass: Original class to be swizzle
/// - newClass: Newly loaded class
/// - Returns: # of methods swizzled
public class func injection(swizzle oldClass: AnyClass?,
from newClass: AnyClass?) -> Int {
var methodCount: UInt32 = 0, swizzled = 0
if let methods = class_copyMethodList(newClass, &methodCount) {
for i in 0 ..< Int(methodCount) {
let selector = method_getName(methods[i])
let replacement = method_getImplementation(methods[i])
guard let method = class_getInstanceMethod(oldClass, selector) ??
class_getInstanceMethod(newClass, selector),
let existing = i < 0 ? nil : method_getImplementation(method) else {
continue
}
traceAndReplace(existing, replacement: autoBitCast(replacement),
objcMethod: methods[i], objcClass: newClass) {
(replacement: IMP) -> String? in
if class_replaceMethod(oldClass, selector, replacement,
method_getTypeEncoding(methods[i])) != replacement {
swizzled += 1
return "Swizzled"
}
return nil
}
}
free(methods)
}
return swizzled
}
}
#endif
================================================
FILE: Sources/HotReloading/ReducerInjection.swift
================================================
//
// ReducerInjection.swift
//
// Created by John Holdsworth on 09/06/2022.
// Copyright © 2022 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/ReducerInjection.swift#12 $
//
// Support for injecting "The Composble Architecture" Reducers using TCA fork:
// https://github.com/thebrowsercompany/swift-composable-architecture/tree/develop
// Top level Reducer var initialisations are wrapped in ARCInjectable() call.
// Reducers are now deprecated in favour of using the new "ReducerProtocol".
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
extension NSObject {
@objc
public func registerInjectableTCAReducer(_ symbol: String) {
SwiftInjection.injectableReducerSymbols.insert(symbol)
}
}
extension SwiftInjection {
static var injectableReducerSymbols = Set()
static var checkReducerInitializers: Void = {
var expectedInjectableReducerSymbols = Set()
findHiddenSwiftSymbols(searchBundleImages(), "Reducer_WZ", .any) {
_, symname, _, _ in
expectedInjectableReducerSymbols.insert(String(cString: symname))
}
for symname in expectedInjectableReducerSymbols
.subtracting(injectableReducerSymbols) {
let sym = SwiftMeta.demangle(symbol: symname) ?? symname
let variable = sym.components(separatedBy: " ").last ?? sym
log("⚠️ \(variable) is not injectable (or unused), wrap it with ARCInjectable")
}
}()
/// Support for re-initialising "The Composable Architecture", "Reducer"
/// variables declared at the top level. Requires custom version of TCA:
/// https://github.com/thebrowsercompany/swift-composable-architecture/tree/develop
public class func reinitializeInjectedReducers(_ tmpfile: String,
reinitialized: UnsafeMutablePointer<[SymbolName]>) {
_ = checkReducerInitializers
findHiddenSwiftSymbols(searchLastLoaded(), "_WZ", .local) {
accessor, symname, _, _ in
if injectableReducerSymbols.contains(String(cString: symname)) {
typealias OneTimeInitialiser = @convention(c) () -> Void
let reinitialise: OneTimeInitialiser = autoBitCast(accessor)
reinitialise()
reinitialized.pointee.append(symname)
}
}
}
}
#endif
================================================
FILE: Sources/HotReloading/StandaloneInjection.swift
================================================
//
// StandaloneInjection.swift
//
// Created by John Holdsworth on 15/03/2022.
// Copyright © 2022 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/StandaloneInjection.swift#77 $
//
// Standalone version of the HotReloading version of the InjectionIII project
// https://github.com/johnno1962/InjectionIII. This file allows you to
// add HotReloading to a project without having to add a "Run Script"
// build phase to run the daemon process.
//
// The most recent change was for the InjectionIII.app injection bundles
// to fall back to this implementation if the user is not running the app.
// This was made possible by using the FileWatcher to find the build log
// directory in DerivedData of the most recently built project.
//
#if DEBUG || !SWIFT_PACKAGE
#if targetEnvironment(simulator) && !APP_SANDBOXED || os(macOS)
#if canImport(UIKit)
import UIKit
#endif
@objc(StandaloneInjection)
class StandaloneInjection: InjectionClient {
static var singleton: StandaloneInjection?
var watchers = [FileWatcher]()
override func runInBackground() {
let builder = SwiftInjectionEval.sharedInstance()
builder.tmpDir = NSTemporaryDirectory()
#if SWIFT_PACKAGE
let swiftTracePath = String(cString: swiftTrace_path())
// convert SwiftTrace path into path to logs.
builder.derivedLogs = swiftTracePath.replacingOccurrences(of:
#"SourcePackages/checkouts/SwiftTrace/SwiftTraceGutsD?/SwiftTrace.mm$"#,
with: "Logs/Build", options: .regularExpression)
if builder.derivedLogs == swiftTracePath {
log("⚠️ HotReloading could find log directory from: \(swiftTracePath)")
builder.derivedLogs = nil // let FileWatcher find logs
}
#endif
signal(SIGPIPE, { _ in print(APP_PREFIX+"⚠️ SIGPIPE") })
builder.signer = { _ in
#if os(tvOS)
let dylib = builder.tmpfile+".dylib"
let codesign = """
(export CODESIGN_ALLOCATE=\"\(builder.xcodeDev
)/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate\"; \
if /usr/bin/file \"\(dylib)\" | /usr/bin/grep ' shared library ' >/dev/null; \
then /usr/bin/codesign --force -s - \"\(dylib)\";\
else exit 1; fi) >>/tmp/hot_reloading.log 2>&1
"""
return builder.shell(command: codesign)
#else
return true
#endif
}
builder.debug = { (what: Any...) in
//print("\(APP_PREFIX)***** %@", what.map {"\($0)"}.joined(separator: " "))
}
builder.forceUnhide = { builder.startUnhide() }
builder.bazelLight = true
let home = NSHomeDirectory()
.replacingOccurrences(of: #"(/Users/[^/]+).*"#, with: "$1",
options: .regularExpression)
setenv("USER_HOME", home, 1)
var dirs = [home]
let library = home+"/Library"
if let extra = getenv(INJECTION_DIRECTORIES) {
dirs = String(cString: extra).components(separatedBy: ",")
.map { $0[#"^~"#, substitute: home] } // expand ~ in paths
if builder.derivedLogs == nil && dirs.allSatisfy({
$0 != home && !$0.hasPrefix(library) }) {
log("⚠️ INJECTION_DIRECTORIES should contain ~/Library")
dirs.append(library)
}
}
var lastInjected = [String: TimeInterval]()
if getenv(INJECTION_REPLAY) != nil {
injectionQueue.sync {
_ = SwiftInjection.replayInjections()
}
}
let isVapor = injectionQueue != .main
let holdOff = 1.0, minInterval = 0.33 // seconds
let firstInjected = Date.timeIntervalSinceReferenceDate + holdOff
watchers.append(FileWatcher(roots: dirs,
callback: { filesChanged, idePath in
#if canImport(UIKit) && !os(watchOS)
if UIApplication.shared.applicationState != .active { return }
#endif
builder.lastIdeProcPath = idePath
if builder.derivedLogs == nil {
if let lastBuilt = FileWatcher.derivedLog {
builder.derivedLogs = URL(fileURLWithPath: lastBuilt)
.deletingLastPathComponent().path
self.log("Using logs: \(lastBuilt).")
} else {
self.log("⚠️ Build log for project not found. " +
"Please edit a file and build it.")
return
}
}
for changed in filesChanged {
guard let changed = changed as? String,
!changed.hasPrefix(library) && !changed.contains("/."),
Date.timeIntervalSinceReferenceDate -
lastInjected[changed, default: firstInjected] >
minInterval else {
continue
}
if changed.hasSuffix(".storyboard") ||
changed.hasSuffix(".xib") {
#if os(iOS) || os(tvOS)
if !NSObject.injectUI(changed) {
self.log("⚠️ Interface injection failed")
}
#endif
} else {
SwiftInjection.inject(classNameOrFile: changed)
}
lastInjected[changed] = Date.timeIntervalSinceReferenceDate
}
}, runLoop: isVapor ? CFRunLoopGetCurrent() : nil))
log("Standalone \(APP_NAME) available for sources under \(dirs)")
if #available(iOS 14.0, tvOS 14.0, *) {
} else {
log("ℹ️ HotReloading not available on Apple Silicon before iOS 14.0")
}
if let executable = Bundle.main.executablePath {
builder.createUnhider(executable: executable,
SwiftInjection.objcClassRefs,
SwiftInjection.descriptorRefs)
}
Self.singleton = self
if isVapor {
CFRunLoopRun()
}
}
var swiftTracing: String?
func maybeTrace() {
if let pattern = getenv(INJECTION_TRACE)
.flatMap({String(cString: $0)}), pattern != swiftTracing {
SwiftTrace.typeLookup = getenv(INJECTION_LOOKUP) != nil
SwiftInjection.traceInjection = true
if pattern != "" {
// This alone will not work for non-final class methods.
_ = SwiftTrace.interpose(aBundle: searchBundleImages(),
methodName: pattern)
}
swiftTracing = pattern
}
}
}
#endif
#endif
================================================
FILE: Sources/HotReloading/SwiftEval.swift
================================================
//
// SwiftEval.swift
// InjectionBundle
//
// Created by John Holdsworth on 02/11/2017.
// Copyright © 2017 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftEval.swift#302 $
//
// Basic implementation of a Swift "eval()" including the
// mechanics of recompiling a class and loading the new
// version used in the associated injection version.
// Used as the basis of a new version of Injection.
//
#if DEBUG || !SWIFT_PACKAGE
#if arch(x86_64) || arch(i386) || arch(arm64) // simulator/macOS only
import Foundation
#if SWIFT_PACKAGE
@_exported import HotReloadingGuts
#elseif !INJECTION_III_APP
private let APP_PREFIX = "💉 ",
INJECTION_DERIVED_DATA = "INJECTION_DERIVED_DATA"
#endif
#if !INJECTION_III_APP
#if canImport(SwiftTraceD)
import SwiftTraceD
#endif
@objc protocol SwiftEvalImpl {
@objc optional func evalImpl(_ptr: UnsafeMutableRawPointer)
}
extension NSObject {
private static var lastEvalByClass = [String: String]()
@objc public func swiftEval(code: String) -> Bool {
if let closure = swiftEval("{\n\(code)\n}", type: (() -> ())?.self) {
closure()
return true
}
return false
}
/// eval() for String value
@objc public func swiftEvalString(contents: String) -> String {
return swiftEval("""
"\(contents)"
""", type: String.self)
}
/// eval() for value of any type
public func swiftEval(_ expression: String, type: T.Type) -> T {
let oldClass: AnyClass = object_getClass(self)!
let className = "\(oldClass)"
let extra = """
extension \(className) {
@objc func evalImpl(_ptr: UnsafeMutableRawPointer) {
func xprint(_ str: T) {
if let xprobe = NSClassFromString("Xprobe") {
#if swift(>=4.0)
_ = (xprobe as AnyObject).perform(Selector(("xlog:")), with: "\\(str)")
#elseif swift(>=3.0)
Thread.detachNewThreadSelector(Selector(("xlog:")), toTarget:xprobe, with:"\\(str)" as NSString)
#else
NSThread.detachNewThreadSelector(Selector("xlog:"), toTarget:xprobe, withObject:"\\(str)" as NSString)
#endif
}
}
#if swift(>=3.0)
struct XprobeOutputStream: TextOutputStream {
var out = ""
mutating func write(_ string: String) {
out += string
}
}
func xdump(_ arg: T) {
var stream = XprobeOutputStream()
dump(arg, to: &stream)
xprint(stream.out)
}
#endif
let _ptr = _ptr.assumingMemoryBound(to: (\(type)).self)
_ptr.pointee = \(expression)
}
}
"""
// update evalImpl to implement expression
if NSObject.lastEvalByClass[className] != expression {
do {
let tmpfile = try SwiftEval.instance.rebuildClass(oldClass: oldClass,
classNameOrFile: className, extra: extra)
if let newClass = try SwiftEval.instance
.loadAndInject(tmpfile: tmpfile, oldClass: oldClass).first {
if NSStringFromClass(newClass) != NSStringFromClass(oldClass) {
NSLog("Class names different. Have the right class been loaded?")
}
// swizzle new version of evalImpl onto class
let selector = #selector(SwiftEvalImpl.evalImpl(_ptr:))
if let newMethod = class_getInstanceMethod(newClass, selector) {
class_replaceMethod(oldClass, selector,
method_getImplementation(newMethod),
method_getTypeEncoding(newMethod))
NSObject.lastEvalByClass[className] = expression
}
}
}
catch {
}
}
// call patched evalImpl to realise expression
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
bzero(ptr, MemoryLayout.size)
if NSObject.lastEvalByClass[className] == expression {
unsafeBitCast(self, to: SwiftEvalImpl.self).evalImpl?(_ptr: ptr)
}
let out = ptr.pointee
ptr.deallocate()
return out
}
}
#endif
extension StringProtocol {
subscript(range: NSRange) -> String? {
return Range(range, in: String(self)).flatMap { String(self[$0]) }
}
func escaping(_ chars: String = "' {}()&*",
with template: String = "\\$0") -> String {
return self.replacingOccurrences(of: "[\(chars)]",
with: template.replacingOccurrences(of: #"\"#, with: "\\\\"),
options: [.regularExpression])
}
func unescape() -> String {
return replacingOccurrences(of: #"\\(.)"#, with: "$1",
options: .regularExpression)
}
}
@objc(SwiftEval)
public class SwiftEval: NSObject {
static var instance = SwiftEval()
static let bundleLink = "/tmp/injection.link"
@objc public class func sharedInstance() -> SwiftEval {
return instance
}
@objc public var signer: ((_: String) -> Bool)?
@objc public var vaccineEnabled: Bool = false
// client specific info
@objc public var frameworks = Bundle.main.privateFrameworksPath
?? Bundle.main.bundlePath + "/Frameworks"
#if arch(arm64)
@objc public var arch = "arm64"
#elseif arch(x86_64)
@objc public var arch = "x86_64"
#else
@objc public var arch = "i386"
#endif
var forceUnhide = {}
var legacyUnhide = false
var objectUnhider: ((String) -> Void)?
var linkerOptions = ""
let bazelWorkspace = "IGNORE_WORKSPACE"
let skipBazelLinking = "--skip_linking"
var bazelLight = getenv("INJECTION_BAZEL") != nil ||
UserDefaults.standard.bool(forKey: "bazelLight")
var moduleLibraries = Set()
/// Additional logging to /tmp/hot\_reloading.log for "HotReloading" version of injection.
var debug = { (what: Any...) in
if getenv("INJECTION_DEBUG") != nil || getenv("DERIVED_LOGS") != nil {
NSLog("\(APP_PREFIX)***** %@", what.map {"\($0)"}.joined(separator: " "))
}
}
// Xcode related info
@objc public var xcodeDev = "/Applications/Xcode.app/Contents/Developer" {
willSet(newValue) {
if newValue != xcodeDev {
print(APP_PREFIX+"Selecting Xcode \(newValue)")
}
}
}
@objc public var projectFile: String?
@objc public var derivedLogs: String?
@objc public var tmpDir = "/tmp" {
didSet {
// SwiftEval.buildCacheFile = "\(tmpDir)/eval_builds.plist"
}
}
@objc public var injectionNumber = 100
@objc public var lastIdeProcPath = ""
var tmpfile: String { URL(fileURLWithPath: tmpDir)
.appendingPathComponent("eval\(injectionNumber)").path }
var logfile: String { "\(tmpfile).log" }
var cmdfile: String { URL(fileURLWithPath: tmpDir)
.appendingPathComponent("command.sh").path
}
/// Error handler
@objc public var evalError = {
(_ message: String) -> Error in
print(APP_PREFIX+(message.hasPrefix("Compiling") ?"":"⚠️ ")+message)
return NSError(domain: "SwiftEval", code: -1,
userInfo: [NSLocalizedDescriptionKey: message])
}
func scriptError(_ what: String) -> Error {
var log = (try? String(contentsOfFile: logfile)) ??
"Could not read log file '\(logfile)'"
if log.contains(".h' file not found") {
log += "\(APP_PREFIX)⚠️ Adjust the \"Header Search Paths\" in your project's Build Settings"
}
return evalError("""
\(what) failed (see: \(cmdfile))
\(log)
""")
}
var compileByClass = [String: (String, String)]()
static let simulatorCacheFile = "/tmp/iOS_Simulator_builds.plist"
#if os(macOS) || targetEnvironment(macCatalyst)
var buildCacheFile = "/tmp/macOS_builds.plist"
#elseif os(tvOS)
var buildCacheFile = "/tmp/tvOS_builds.plist"
#elseif os(visionOS)
var buildCacheFile = "/tmp/xrOS_builds.plist"
#elseif targetEnvironment(simulator)
var buildCacheFile = SwiftEval.simulatorCacheFile
#else
var buildCacheFile = "/tmp/iOS_builds.plist"
#endif
lazy var longTermCache =
NSMutableDictionary(contentsOfFile: buildCacheFile) ?? NSMutableDictionary()
public func determineEnvironment(classNameOrFile: String) throws -> (URL, URL) {
// Largely obsolete section used find Xcode paths from source file being injected.
let sourceURL = URL(fileURLWithPath:
classNameOrFile.hasPrefix("/") ? classNameOrFile : #file)
debug("Project file:", projectFile ?? "nil")
guard let derivedData = getenv(INJECTION_DERIVED_DATA).flatMap({
let url = URL(fileURLWithPath: String(cString: $0))
guard FileManager.default.fileExists(atPath: url.path) else {
_ = evalError("Invalid path in \(INJECTION_DERIVED_DATA): "+url.path)
return nil }
return url }) ??
findDerivedData(url: URL(fileURLWithPath: NSHomeDirectory())) ??
(projectFile == nil ? findDerivedData(url: sourceURL) :
findDerivedData(url: URL(fileURLWithPath: projectFile!))) else {
throw evalError("""
Could not locate derived data. Is the project under your \
home directory? If you are using a custom derived data path, \
add it as an environment variable \(INJECTION_DERIVED_DATA) \
in your scheme.
""")
}
debug("DerivedData:", derivedData.path)
guard let (projectFile, logsDir) =
derivedLogs.flatMap({
(findProject(for: sourceURL, derivedData:derivedData)?
.projectFile ?? URL(fileURLWithPath: "/tmp/x.xcodeproj"),
URL(fileURLWithPath: $0)) }) ??
projectFile
.flatMap({ logsDir(project: URL(fileURLWithPath: $0), derivedData: derivedData) })
.flatMap({ (URL(fileURLWithPath: projectFile!), $0) }) ??
findProject(for: sourceURL, derivedData: derivedData) else {
throw evalError("""
Could not locate containing project or it's logs.
For a macOS app you need to turn off the App Sandbox.
Are using a custom DerivedData path? This is not supported.
""")
}
if false == (try? String(contentsOf: projectFile
.appendingPathComponent("project.pbxproj")))?.contains("-interposable") {
print(APP_PREFIX+"""
⚠️ Project file \(projectFile.path) does not contain the -interposable \
linker flag. In order to be able to inject methods of structs and final \
classes, please add \"Other Linker Flags\" -Xlinker -interposable for Debug builds only.
""")
}
return (projectFile, logsDir)
}
public func actualCase(path: String) -> String? {
let fm = FileManager.default
if fm.fileExists(atPath: path) {
return path
}
var out = ""
for component in path.split(separator: "/") {
var real: String?
if fm.fileExists(atPath: out+"/"+component) {
real = String(component)
} else {
guard let contents = try? fm.contentsOfDirectory(atPath: "/"+out) else {
return nil
}
real = contents.first { $0.lowercased() == component.lowercased() }
}
guard let found = real else {
return nil
}
out += "/" + found
}
return out
}
let detectFilepaths = try! NSRegularExpression(pattern: #"(/(?:[^\ ]*\\.)*[^\ ]*) "#)
@objc public func rebuildClass(oldClass: AnyClass?,
classNameOrFile: String, extra: String?) throws -> String {
let (projectFile, logsDir) = try
determineEnvironment(classNameOrFile: classNameOrFile)
let projectRoot = projectFile.deletingLastPathComponent().path
// if self.projectFile == nil { self.projectFile = projectFile.path }
// locate compile command for class
injectionNumber += 1
if projectFile.lastPathComponent == bazelWorkspace,
let dylib = try bazelLight(projectRoot: projectRoot,
recompile: classNameOrFile) {
return dylib
}
guard var (compileCommand, sourceFile) = try
compileByClass[classNameOrFile] ??
(longTermCache[classNameOrFile] as? String)
.flatMap({ ($0, classNameOrFile) }) ??
findCompileCommand(logsDir: logsDir,
classNameOrFile: classNameOrFile, tmpfile: tmpfile) else {
throw evalError("""
Could not locate compile command for "\(classNameOrFile)" in \
\(logsDir.path)/.\nThis could be due to one of the following:
1. Injection does not work with Whole Module Optimization.
2. There are restrictions on characters allowed in paths.
3. File paths in the simulator are case sensitive.
4. The modified source file is not in the current project.
5. The source file is an XCTest that has not been run yet.
6. Xcode has removed the build logs. Edit a file and re-run \
or try a build clean then rebuild to make logs available or \
consult: "\(cmdfile)".
7. If you're using Xcode 16.3+, Swift compilation details are no \
longer logged by default. See the note in the project README. \
You'll need to add a EMIT_FRONTEND_COMMAND_LINES custom \
build setting to your project to continue using InjectionIII.
Whatever the problem, if you see this error it may be worth \
trying the start-over implementation https://github.com/johnno1962/InjectionNext.
""")
}
sourceFile += "" // remove warning
#if targetEnvironment(simulator)
// Normalise paths in compile command with the actual casing
// of files as the simulator has a case-sensitive file system.
for filepath in detectFilepaths.matches(in: compileCommand, options: [],
range: NSMakeRange(0, compileCommand.utf16.count))
.compactMap({ compileCommand[$0.range(at: 1)] }) {
let unescaped = filepath.unescape()
if let normalised = actualCase(path: unescaped) {
let escaped = normalised.escaping("' ${}()&*~")
if filepath != escaped {
print("""
\(APP_PREFIX)Mapped: \(filepath)
\(APP_PREFIX)... to: \(escaped)
""")
compileCommand = compileCommand
.replacingOccurrences(of: filepath, with: escaped,
options: .caseInsensitive)
}
}
}
#endif
// load and patch class source if there is an extension to add
let filemgr = FileManager.default, backup = sourceFile + ".tmp"
if extra != nil {
guard var classSource = try? String(contentsOfFile: sourceFile) else {
throw evalError("Could not load source file \(sourceFile)")
}
let changesTag = "// extension added to implement eval"
classSource = classSource.components(separatedBy: "\n\(changesTag)\n")[0] + """
\(changesTag)
\(extra!)
"""
debug(classSource)
// backup original and compile patched class source
if !filemgr.fileExists(atPath: backup) {
try! filemgr.moveItem(atPath: sourceFile, toPath: backup)
}
try! classSource.write(toFile: sourceFile, atomically: true, encoding: .utf8)
}
defer {
if extra != nil {
try! filemgr.removeItem(atPath: sourceFile)
try! filemgr.moveItem(atPath: backup, toPath: sourceFile)
}
}
// Extract object path (overidden in UnhidingEval.swift for Xcode 13)
let objectFile = xcode13Fix(sourceFile: sourceFile,
compileCommand: &compileCommand)
_ = evalError("Compiling \(sourceFile)")
let isBazelCompile = compileCommand.contains(skipBazelLinking)
if isBazelCompile, arch == "x86_64", !compileCommand.contains(arch) {
compileCommand = compileCommand
.replacingOccurrences(of: #"(--cpu=ios_)\w+"#,
with: "$1\(arch)", options: .regularExpression)
}
if !sourceFile.hasSuffix(".swift") {
compileCommand += " -Xclang -fno-validate-pch"
}
debug("Final command:", compileCommand, "-->", objectFile)
guard shell(command: """
(cd "\(projectRoot.escaping("$"))" && \
\(compileCommand) >\"\(logfile)\" 2>&1)
""") || isBazelCompile else {
if longTermCache[classNameOrFile] != nil {
updateLongTermCache(remove: classNameOrFile)
do {
return try rebuildClass(oldClass: oldClass,
classNameOrFile: classNameOrFile, extra: extra)
} catch {
#if true || !os(macOS)
_ = evalError("Recompilation failing, are you renaming/adding " +
"files? Build your project to generate a new Xcode build " +
"log and try injecting again or relauch your app.")
throw error
#else
// Retry again with new build log in case of added/renamed files...
_ = evalError("Compilation failed, rebuilding \(projectFile.path)")
_ = shell(command: """
/usr/bin/osascript -e 'tell application "Xcode"
set targetProject to active workspace document
if (build targetProject) is equal to "Build succeeded" then
end if
end tell'
""")
return try rebuildClass(oldClass: oldClass,
classNameOrFile: classNameOrFile, extra: extra)
#endif
}
}
throw scriptError("Re-compilation")
}
compileByClass[classNameOrFile] = (compileCommand, sourceFile)
if longTermCache[classNameOrFile] as? String != compileCommand &&
classNameOrFile.hasPrefix("/") {//&& scanTime > slowLogScan {
longTermCache[classNameOrFile] = compileCommand
updateLongTermCache()
}
if isBazelCompile {
let projectRoot = objectFile // returned by xcode13Fix()
return try bazelLink(in: projectRoot, since: sourceFile,
compileCommand: compileCommand)
}
// link resulting object file to create dynamic library
_ = objectUnhider?(objectFile)
var speclib = ""
if sourceFile.contains("Spec.") && (try? String(
contentsOfFile: classNameOrFile))?.contains("Quick") == true {
speclib = logsDir.path+"/../../Build/Products/"+Self.quickFiles
}
try link(dylib: "\(tmpfile).dylib", compileCommand: compileCommand,
contents: "\"\(objectFile)\" \(speclib)")
return tmpfile
}
func updateLongTermCache(remove: String? = nil) {
if let source = remove {
compileByClass.removeValue(forKey: source)
longTermCache.removeObject(forKey: source)
// longTermCache.removeAllObjects()
// compileByClass.removeAll()
}
longTermCache.write(toFile: buildCacheFile,
atomically: false)
}
// Implementations provided in UnhidingEval.swift
func bazelLight(projectRoot: String, recompile sourceFile: String) throws -> String? {
throw evalError("No bazel support")
}
func bazelLink(in projectRoot: String, since sourceFile: String,
compileCommand: String) throws -> String {
throw evalError("No bazel support")
}
static let quickFiles = getenv("INJECTION_QUICK_FILES").flatMap {
String(cString: $0) } ?? "Debug-*/{Quick*,Nimble,Cwl*}.o"
static let quickDylib = "_spec.dylib"
static let dylibDelim = "==="
static let parsePlatform = try! NSRegularExpression(pattern:
#"-(?:isysroot|sdk)(?: |"\n")((\#(fileNameRegex)/Contents/Developer)/Platforms/(\w+)\.platform\#(fileNameRegex)\#\.sdk)"#)
func link(dylib: String, compileCommand: String, contents: String,
cd: String = "") throws {
var platform: String
switch buildCacheFile {
case Self.simulatorCacheFile: platform = "iPhoneSimulator"
case "/tmp/xrOS_builds.plist": platform = "XRSimulator"
case "/tmp/tvOS_builds.plist": platform = "AppleTVSimulator"
case "/tmp/macOS_builds.plist": platform = "MacOSX"
default: platform = "iPhoneOS"
}
var sdk = "\(xcodeDev)/Platforms/\(platform).platform/Developer/SDKs/\(platform).sdk"
if let match = Self.parsePlatform.firstMatch(in: compileCommand,
options: [], range: NSMakeRange(0, compileCommand.utf16.count)) {
func extract(group: Int, into: inout String) {
if let range = Range(match.range(at: group), in: compileCommand) {
into = compileCommand[range]
.replacingOccurrences(of: #"\\(.)"#, with: "$1",
options: .regularExpression)
}
}
extract(group: 1, into: &sdk)
extract(group: 2, into: &xcodeDev)
extract(group: 4, into: &platform)
} else if compileCommand.contains(".swift ") {
_ = evalError("Unable to parse SDK from: \(compileCommand)")
}
var osSpecific = ""
switch platform {
case "iPhoneSimulator":
osSpecific = "-mios-simulator-version-min=9.0"
case "iPhoneOS":
osSpecific = "-miphoneos-version-min=9.0"
case "AppleTVSimulator":
osSpecific = "-mtvos-simulator-version-min=9.0"
case "AppleTVOS":
osSpecific = "-mtvos-version-min=9.0"
case "MacOSX":
let target = compileCommand
.replacingOccurrences(of: #"^.*( -target \S+).*$"#,
with: "$1", options: .regularExpression)
osSpecific = "-mmacosx-version-min=10.11"+target
case "XRSimulator": fallthrough case "XROS":
osSpecific = ""
#if os(watchOS)
case "WatchSimulator":
osSpecific = ""
#endif
default:
_ = evalError("Invalid platform \(platform)")
// -Xlinker -bundle_loader -Xlinker \"\(Bundle.main.executablePath!)\""
}
let toolchain = xcodeDev+"/Toolchains/XcodeDefault.xctoolchain"
let cd = cd == "" ? "" : "cd \"\(cd)\" && "
if cd != "" && !contents.contains(arch) {
_ = evalError("Modified object files \(contents) not built for architecture \(arch)")
}
guard shell(command: """
\(cd)"\(toolchain)/usr/bin/clang" -arch "\(arch)" \
-Xlinker -dylib -isysroot "__PLATFORM__" \
-L"\(toolchain)/usr/lib/swift/\(platform.lowercased())" \(osSpecific) \
-undefined dynamic_lookup -dead_strip -Xlinker -objc_abi_version \
-Xlinker 2 -Xlinker -interposable\(linkerOptions) -fobjc-arc \
-fprofile-instr-generate \(contents) -L "\(frameworks)" -F "\(frameworks)" \
-rpath "\(frameworks)" -o \"\(dylib)\" >>\"\(logfile)\" 2>&1
""".replacingOccurrences(of: "__PLATFORM__", with: sdk)) else {
throw scriptError("Linking")
}
// codesign dylib
if signer != nil {
guard dylib.hasSuffix(Self.quickDylib) ||
buildCacheFile == Self.simulatorCacheFile ||
signer!("\(injectionNumber).dylib") else {
#if SWIFT_PACKAGE
throw evalError("Codesign failed. Consult /tmp/hot_reloading.log or Console.app")
#else
throw evalError("Codesign failed, consult Console. If you are using macOS 11+, Please download a new release from https://github.com/johnno1962/InjectionIII/releases")
#endif
}
}
else {
#if os(iOS)
// have to delegate code signing to macOS "signer" service
guard (try? String(contentsOf: URL(string: "http://localhost:8899\(tmpfile).dylib")!)) != nil else {
throw evalError("Codesign failed. Is 'signer' daemon running?")
}
#else
guard shell(command: """
export CODESIGN_ALLOCATE=\(xcodeDev)/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate; codesign --force -s '-' "\(tmpfile).dylib"
""") else {
throw evalError("Codesign failed")
}
#endif
}
// Rewrite dylib to prevent macOS 10.15+ from quarantining it
let url = URL(fileURLWithPath: dylib)
let dylib = try Data(contentsOf: url)
try FileManager.default.removeItem(at: url)
try dylib.write(to: url)
}
/// Regex for path argument, perhaps containg escaped spaces
static let argumentRegex = #"[^\s\\]*(?:\\.[^\s\\]*)*"#
/// Regex to extract filename base, perhaps containg escaped spaces
static let fileNameRegex = #"/(\#(argumentRegex))\.\w+"#
/// Extract full file path and name either quoted or escaped
static let filePathRegex =
#""/[^"]*\#(fileNameRegex)"|/\#(argumentRegex)"#
// Overridden in UnhidingEval.swift
func xcode13Fix(sourceFile: String,
compileCommand: inout String) -> String {
// Trim off junk at end of compile command
if sourceFile.hasSuffix(".swift") {
compileCommand = compileCommand.replacingOccurrences(
of: " -o (\(Self.filePathRegex))",
with: "", options: .regularExpression)
.components(separatedBy: " -index-system-modules")[0]
} else {
compileCommand = compileCommand
.components(separatedBy: " -o ")[0]
}
if compileCommand.contains("/bazel ") {
// force ld to fail as it is not needed
compileCommand += " --linkopt=-Wl,"+skipBazelLinking
// return path to workspace instead of object file
return compileCommand[#"^cd "([^"]+)""#] ?? "dir?"
}
let objectFile = "/tmp/injection_\(injectionNumber).o"
compileCommand += " -o "+objectFile
unlink(objectFile)
return objectFile
}
func createUnhider(executable: String, _ objcClassRefs: NSMutableArray,
_ descriptorRefs: NSMutableArray) {
}
#if !INJECTION_III_APP
lazy var loadXCTest: () = {
#if targetEnvironment(simulator)
#if os(macOS)
let sdk = "MacOSX"
#elseif os(tvOS)
let sdk = "AppleTVSimulator"
#elseif targetEnvironment(simulator)
let sdk = "iPhoneSimulator"
#else
let sdk = "iPhoneOS"
#endif
let platform = "\(xcodeDev)/Platforms/\(sdk).platform/Developer/"
guard FileManager.default.fileExists(atPath: platform) else { return }
if dlopen(platform+"Library/Frameworks/XCTest.framework/XCTest", RTLD_LAZY) == nil {
debug(String(cString: dlerror()))
}
if dlopen(platform+"usr/lib/libXCTestSwiftSupport.dylib", RTLD_LAZY) == nil {
debug(String(cString: dlerror()))
}
#else
let copiedFrameworks = Bundle.main.bundlePath+"/iOSInjection.bundle/Frameworks/"
for fw in ["XCTestCore", "XCUnit", "XCUIAutomation", "XCTest"] {
if dlopen(copiedFrameworks+fw+".framework/\(fw)", RTLD_LAZY) == nil {
debug(String(cString: dlerror()))
}
}
if dlopen(copiedFrameworks+"libXCTestSwiftSupport.dylib", RTLD_LAZY) == nil {
debug(String(cString: dlerror()))
}
#endif
}()
lazy var loadTestsBundle: () = {
do {
guard let testsBundlePath = try testsBundlePath() else {
debug("Tests bundle wasn't found - did you run the tests target before running the application?")
return
}
guard let bundle = Bundle(path: testsBundlePath), bundle.load() else {
debug("Failed loading tests bundle")
return
}
} catch {
debug("Error while searching for the tests bundle: \(error)")
return
}
}()
func testsBundlePath() throws -> String? {
guard let pluginsDirectory = Bundle.main.path(forResource: "PlugIns", ofType: nil) else {
return nil
}
let bundlePaths: [String] = try FileManager.default.contentsOfDirectory(atPath: pluginsDirectory)
.filter { $0.hasSuffix(".xctest") }
.map { directoryName in
return "\(pluginsDirectory)/\(directoryName)"
}
if bundlePaths.count > 1 {
debug("Found more than one tests bundle, using the first one")
}
return bundlePaths.first
}
@objc func loadAndInject(tmpfile: String, oldClass: AnyClass? = nil)
throws -> [AnyClass] {
print("\(APP_PREFIX)Loading .dylib ...")
// load patched .dylib into process with new version of class
var dl: UnsafeMutableRawPointer?
for dylib in "\(tmpfile).dylib".components(separatedBy: Self.dylibDelim) {
if let object = NSData(contentsOfFile: dylib),
memmem(object.bytes, object.count, "XCTest", 6) != nil ||
memmem(object.bytes, object.count, "Quick", 5) != nil,
object.count != 0 {
_ = loadXCTest
_ = loadTestsBundle
}
#if canImport(SwiftTrace) || canImport(SwiftTraceD)
dl = fast_dlopen(dylib, RTLD_NOW)
#else
dl = dlopen(dylib, RTLD_NOW)
#endif
guard dl != nil else {
var error = String(cString: dlerror())
if error.contains("___llvm_profile_runtime") {
error += """
\n\(APP_PREFIX)⚠️ Loading .dylib has failed, try turning off \
collection of test coverage in your scheme
"""
} else if error.contains("ymbol not found") {
error += """
\n\(APP_PREFIX)⚠️ Loading .dylib has failed, This is likely \
because Swift code being injected references a function \
using a default argument or a member with access control \
that is too restrictive or perhaps an XCTest that depends on \
code not normally linked into your application. Rebuilding and \
re-running your project (without a build clean) can resolve this.
"""
forceUnhide()
} else if error.contains("code signature invalid") {
error += """
\n\(APP_PREFIX)⚠️ Loading .dylib has failed due to invalid code signing.
\(APP_PREFIX)Add the following as a Run Script/Build Phase:
defaults write com.johnholdsworth.InjectionIII "$PROJECT_FILE_PATH" "$EXPANDED_CODE_SIGN_IDENTITY"
"""
} else if error.contains("rying to load an unsigned library") {
error += """
\n\(APP_PREFIX)⚠️ Loading .dylib in Xcode 15+ requires code signing.
\(APP_PREFIX)You will need to run the InjectionIII.app
"""
} else if error.contains("incompatible platform") {
error += """
\n\(APP_PREFIX)⚠️ Clean build folder when switching platform
"""
}
throw evalError("dlopen() error: \(error)")
}
}
if oldClass != nil {
// find patched version of class using symbol for existing
var info = Dl_info()
guard dladdr(unsafeBitCast(oldClass, to: UnsafeRawPointer.self), &info) != 0 else {
throw evalError("Could not locate class symbol")
}
debug(String(cString: info.dli_sname))
guard let newSymbol = dlsym(dl, info.dli_sname) else {
throw evalError("Could not locate newly loaded class symbol")
}
return [unsafeBitCast(newSymbol, to: AnyClass.self)]
}
else {
// grep out symbols for classes being injected from object file
return try extractClasses(dl: dl!, tmpfile: tmpfile)
}
}
#endif
func startUnhide() {
}
// Overridden by SwiftInjectionEval subclass for injection
@objc func extractClasses(dl: UnsafeMutableRawPointer,
tmpfile: String) throws -> [AnyClass] {
guard shell(command: """
\(xcodeDev)/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm \(tmpfile).o | \
grep -E ' S _OBJC_CLASS_\\$_| _(_T0|\\$S|\\$s).*CN$' | awk '{print $3}' \
>\(tmpfile).classes
""") else {
throw evalError("Could not list class symbols")
}
guard var classSymbolNames = (try? String(contentsOfFile:
"\(tmpfile).classes"))?.components(separatedBy: "\n") else {
throw evalError("Could not load class symbol list")
}
classSymbolNames.removeLast()
return Set(classSymbolNames.compactMap {
dlsym(dl, String($0.dropFirst())) })
.map { unsafeBitCast($0, to: AnyClass.self) }
}
func findCompileCommand(logsDir: URL, classNameOrFile: String, tmpfile: String)
throws -> (compileCommand: String, sourceFile: String)? {
// path to project can contain spaces and '$&(){}
// Objective-C paths can only contain space and '
// project file itself can only contain spaces
let isFile = classNameOrFile.hasPrefix("/")
let sourceRegex = isFile ?
#"\Q\#(classNameOrFile)\E"# : #"/\#(classNameOrFile)\.\w+"#
let swiftEscaped = (isFile ? "" : #"[^"]*?"#) + sourceRegex.escaping("'$", with: #"\E\\*$0\Q"#)
let objcEscaped = (isFile ? "" :
#"(?:/(?:[^/\\]*\\.)*[^/\\ ]+)+"#) + sourceRegex.escaping()
var regexp = #" -(?:primary-file|c(?/dev/null |" or die "gnozip";
sub actualPath {
\#(actualPath)
}
sub recoverFilelist {
my ($filemap) = $_[0] =~ / -output-file-map (\#(
Self.argumentRegex)) /;
$filemap =~ s/\\//g;
return if ! -s $filemap;
my $file_handle = IO::File->new( "< $filemap" )
or die "Could not open filemap '$filemap'";
my $json_text = join'', $file_handle->getlines();
return unless index($json_text, '\#(sourceName)') > 0;
my $json_map = decode_json( $json_text, { utf8 => 1 } );
my $swift_sources = join "\n", map { actualPath($_) } keys %$json_map;
mkdir "/tmp/filelists";
my $filelist = '/tmp/filelists/\#(sourceName)';
unlink $filelist;
my $listfile = IO::File->new( "> $filelist" )
or die "Could not open list file '$filelist'";
binmode $listfile, ':utf8';
$listfile->print( $swift_sources );
$listfile->close();
return $filelist;
}
# grep the log until there is a match
my ($realPath, $command, $filelist);
while (defined (my $line = )) {
if ($line =~ /^\s*cd /) {
$realPath = $line;
}
elsif ($line =~ m@\#(regexp.escaping("\"$")
.escaping("@", with: #"\E\$0\Q"#)
)@oi and $line \#(filterWatchOS)
and $line =~ "\#(arch)"\#(swiftpm)) {
# found compile command..
# may need to recover file list
my ($flarg) = $line =~ / -filelist (\#(
Self.argumentRegex))/;
if ($flarg && ! -s $flarg) {
while (defined (my $line2 = )) {
if (my ($fl) = recoverFilelist($line2)) {
$filelist = $fl;
last;
}
}
}
if ($realPath and (undef, $realPath) =
$realPath =~ /cd (\"?)(.*?)\1\r/) {
$realPath =~ s/\\([^\$])/$1/g;
$line = "cd \"$realPath\"; $line";
}
# find last
$command = $line;
#exit 0;
}
elsif (my ($bazel, $dir) = $line =~ /^Running "([^"]+)".* (?:patching output for workspace root|with project path) at ("[^"]+")/) {
$command = "cd $dir && $bazel";
last;
}
elsif (my ($identity, $bundle) = $line =~ m@/usr/bin/codesign --force --sign (\S+) --entitlements \#(Self.argumentRegex) .+ (\#(Self.argumentRegex))@) {
$bundle =~ s/\\(.)/$1/g;
unlink "\#(Self.bundleLink)";
symlink $bundle, "\#(Self.bundleLink)";
system "rm -f ~/Library/Containers/com.johnholdsworth.InjectionIII/Data/Library/Preferences/com.johnholdsworth.InjectionIII.plist"
if $identity ne "-";
system (qw(/usr/bin/env defaults write com.johnholdsworth.InjectionIII),
'\#(projectFile?.escaping("'") ?? "current project")', $identity);
}
elsif (!$filelist &&
index($line, " -output-file-map ") > 0 and
my ($fl) = recoverFilelist($line)) {
$filelist = $fl;
}
}
if ($command) {
my ($flarg) = $command =~ / -filelist (\#(
Self.argumentRegex))/;
if ($flarg && $filelist && ! -s $flarg) {
$command =~ s/( -filelist )(\#(
Self.argumentRegex)) /$1'$filelist' /;
}
print $command;
exit 0;
}
# class/file not found
exit 1;
"""#.write(toFile: "\(tmpfile).pl",
atomically: false, encoding: .utf8)
guard shell(command: """
# search through build logs, most recent first
cp \(cmdfile) \(cmdfile).save 2>/dev/null ; \
cd "\(logsDir.path.escaping("$"))" &&
for log in `ls -t *.xcactivitylog`; do
#echo "Scanning $log"
/usr/bin/env perl "\(tmpfile).pl" "$log" \
>"\(tmpfile).sh" 2>>"\(tmpfile).err" && exit 0
done
exit 1;
""") else {
#if targetEnvironment(simulator)
if #available(iOS 14.0, tvOS 14.0, *) {
} else {
print(APP_PREFIX+"""
⚠️ Injection unable to search logs. \
Try a more recent iOS 14+ simulator \
or, download a release directly from \
https://github.com/johnno1962/InjectionIII/releases
""")
}
#endif
if let log = try? String(contentsOfFile: "\(tmpfile).err"),
!log.isEmpty {
_ = evalError("stderr contains: "+log)
}
return nil
}
var compileCommand: String
do {
compileCommand = try String(contentsOfFile: "\(tmpfile).sh")
} catch {
throw evalError("""
Error reading \(tmpfile).sh, scanCommand: \(cmdfile)
""")
}
// // escape ( & ) outside quotes
// .replacingOccurrences(of: "[()](?=(?:(?:[^\"]*\"){2})*[^\"]$)", with: "\\\\$0", options: [.regularExpression])
// (logs of new build system escape ', $ and ")
debug("Found command:", compileCommand)
compileCommand = compileCommand.replacingOccurrences(of:
#"builtin-swift(DriverJob|Task)Execution --|-frontend-parseable-output|\r$"#,
with: "", options: .regularExpression)
// // remove excess escaping in new build system (no linger necessary)
// .replacingOccurrences(of: #"\\([\"'\\])"#, with: "$1", options: [.regularExpression])
// these files may no longer exist
.replacingOccurrences(of:
#" -(pch-output-dir|supplementary-output-file-map|index-store-path|Xcc -ivfsstatcache -Xcc) \#(Self.argumentRegex) "#,
with: " ", options: .regularExpression)
// Strip junk with Xcode 16.3 and EMIT_FRONTEND_COMMAND_LINES
.replacingOccurrences(of: #"^(.*?"; )?\S*?"(?=/)"#,
with: "", options: .regularExpression)
debug("Replaced command:", compileCommand)
if isFile {
return (compileCommand, classNameOrFile)
}
// for eval() extract full path to file from compile command
let fileExtractor: NSRegularExpression
regexp = regexp.escaping("$")
do {
fileExtractor = try NSRegularExpression(pattern: regexp, options: [])
}
catch {
throw evalError("Regexp parse error: \(error) -- \(regexp)")
}
guard let matches = fileExtractor
.firstMatch(in: compileCommand, options: [],
range: NSMakeRange(0, compileCommand.utf16.count)),
var sourceFile = compileCommand[matches.range(at: 1)] ??
compileCommand[matches.range(at: 2)] else {
throw evalError("Could not locate source file \(compileCommand) -- \(regexp)")
}
sourceFile = actualCase(path: sourceFile.unescape()) ?? sourceFile
return (compileCommand, sourceFile)
}
func getAppCodeDerivedData(procPath: String) -> String {
//Default with current year
let derivedDataPath = { (year: Int, pathSelector: String) -> String in
"Library/Caches/\(year > 2019 ? "JetBrains/" : "")\(pathSelector)/DerivedData"
}
let year = Calendar.current.component(.year, from: Date())
let month = Calendar.current.component(.month, from: Date())
let defaultPath = derivedDataPath(year, "AppCode\(month / 4 == 0 ? year - 1 : year).\(month / 4 + (month / 4 == 0 ? 3 : 0))")
var plistPath = URL(fileURLWithPath: procPath)
plistPath.deleteLastPathComponent()
plistPath.deleteLastPathComponent()
plistPath = plistPath.appendingPathComponent("Info.plist")
guard let dictionary = NSDictionary(contentsOf: plistPath) as? Dictionary else { return defaultPath }
guard let jvmOptions = dictionary["JVMOptions"] as? Dictionary else { return defaultPath }
guard let properties = jvmOptions["Properties"] as? Dictionary else { return defaultPath }
guard let pathSelector: String = properties["idea.paths.selector"] as? String else { return defaultPath }
let components = pathSelector.replacingOccurrences(of: "AppCode", with: "").components(separatedBy: ".")
guard components.count == 2 else { return defaultPath }
guard let realYear = Int(components[0]) else { return defaultPath }
return derivedDataPath(realYear, pathSelector)
}
func findDerivedData(url: URL) -> URL? {
if url.path == "/" {
return nil
}
var relativeDirs = ["DerivedData", "build/DerivedData"]
if lastIdeProcPath.lowercased().contains("appcode") {
relativeDirs.append(getAppCodeDerivedData(procPath: lastIdeProcPath))
} else {
relativeDirs.append("Library/Developer/Xcode/DerivedData")
}
for relative in relativeDirs {
let derived = url.appendingPathComponent(relative)
if FileManager.default.fileExists(atPath: derived.path) {
return derived
}
}
return findDerivedData(url: url.deletingLastPathComponent())
}
func findProject(for source: URL, derivedData: URL) -> (projectFile: URL, logsDir: URL)? {
let dir = source.deletingLastPathComponent()
if dir.path == "/" {
return nil
}
if bazelLight {
let workspaceURL = dir.deletingLastPathComponent()
.appendingPathComponent(bazelWorkspace)
if FileManager.default.fileExists(atPath: workspaceURL.path) {
return (workspaceURL, derivedLogs.flatMap({
URL(fileURLWithPath: $0)}) ?? dir)
}
}
var candidate = findProject(for: dir, derivedData: derivedData)
if let files =
try? FileManager.default.contentsOfDirectory(atPath: dir.path),
let project = Self.projects(in: files)?.first,
let logsDir = logsDir(project: dir.appendingPathComponent(project), derivedData: derivedData),
mtime(logsDir) > candidate.flatMap({ mtime($0.logsDir) }) ?? 0 {
candidate = (dir.appendingPathComponent(project), logsDir)
}
return candidate
}
class func projects(in files: [String]) -> [String]? {
return names(withSuffix: ".xcworkspace", in: files) ??
names(withSuffix: ".xcodeproj", in: files) ??
names(withSuffix: "Package.swift", in: files)
}
class func names(withSuffix ext: String, in files: [String]) -> [String]? {
let matches = files.filter { $0.hasSuffix(ext) }
return matches.count != 0 ? matches : nil
}
func mtime(_ url: URL) -> time_t {
var info = stat()
return stat(url.path, &info) == 0 ? info.st_mtimespec.tv_sec : 0
}
func logsDir(project: URL, derivedData: URL) -> URL? {
let filemgr = FileManager.default
var projectPrefix = project.deletingPathExtension().lastPathComponent
if project.lastPathComponent == "Package.swift" {
projectPrefix = project.deletingLastPathComponent().lastPathComponent
}
projectPrefix = projectPrefix.replacingOccurrences(of: #"\s+"#, with: "_",
options: .regularExpression, range: nil)
let derivedDirs = (try? filemgr
.contentsOfDirectory(atPath: derivedData.path)) ?? []
let namedDirs = derivedDirs
.filter { $0.starts(with: projectPrefix + "-") }
var possibleDerivedData = (namedDirs.isEmpty ? derivedDirs : namedDirs)
.map { derivedData.appendingPathComponent($0 + "/Logs/Build") }
possibleDerivedData.append(project.deletingLastPathComponent()
.appendingPathComponent("DerivedData/\(projectPrefix)/Logs/Build"))
debug("Possible DerivedDatas: \(possibleDerivedData)")
// use most recentry modified
return possibleDerivedData
.filter { filemgr.fileExists(atPath: $0.path) }
.sorted { mtime($0) > mtime($1) }
.first
}
class func uniqueTypeNames(signatures: [String], exec: (String) -> Void) {
var typesSearched = Set()
for signature in signatures {
let parts = signature.components(separatedBy: ".")
if parts.count < 3 {
continue
}
let typeName = parts[1]
if typesSearched.insert(typeName).inserted {
exec(typeName)
}
}
}
func shell(command: String) -> Bool {
try! command.write(toFile: cmdfile, atomically: false, encoding: .utf8)
debug(command)
#if os(macOS)
let task = Process()
task.launchPath = "/bin/bash"
task.arguments = [cmdfile]
task.launch()
task.waitUntilExit()
let status = task.terminationStatus
#else
let status = runner.run(script: cmdfile)
#endif
return status == EXIT_SUCCESS
}
#if !os(macOS)
lazy var runner = ScriptRunner()
class ScriptRunner {
let commandsOut: UnsafeMutablePointer
let statusesIn: UnsafeMutablePointer
init() {
let ForReading = 0, ForWriting = 1
var commandsPipe = [Int32](repeating: 0, count: 2)
var statusesPipe = [Int32](repeating: 0, count: 2)
pipe(&commandsPipe)
pipe(&statusesPipe)
var envp = [UnsafeMutablePointer?](repeating: nil, count: 2)
if let home = getenv("USER_HOME") {
envp[0] = strdup("HOME=\(home)")!
}
if fork() == 0 {
let commandsIn = fdopen(commandsPipe[ForReading], "r")
let statusesOut = fdopen(statusesPipe[ForWriting], "w")
var buffer = [Int8](repeating: 0, count: Int(MAXPATHLEN))
close(commandsPipe[ForWriting])
close(statusesPipe[ForReading])
setbuf(statusesOut, nil)
while let script = fgets(&buffer, Int32(buffer.count), commandsIn) {
script[strlen(script)-1] = 0
let pid = fork()
if pid == 0 {
var argv = [UnsafeMutablePointer?](repeating: nil, count: 3)
argv[0] = strdup("/bin/bash")!
argv[1] = strdup(script)!
_ = execve(argv[0], &argv, &envp)
fatalError("execve() fails \(String(cString: strerror(errno)))")
}
var status: Int32 = 0
while waitpid(pid, &status, 0) == -1 {}
fputs("\(status)\n", statusesOut)
}
exit(0)
}
commandsOut = fdopen(commandsPipe[ForWriting], "w")
statusesIn = fdopen(statusesPipe[ForReading], "r")
close(commandsPipe[ForReading])
close(statusesPipe[ForWriting])
setbuf(commandsOut, nil)
}
func run(script: String) -> Int32 {
fputs("\(script)\n", commandsOut)
var buffer = [Int8](repeating: 0, count: 20)
fgets(&buffer, Int32(buffer.count), statusesIn)
let status = atoi(buffer)
return status >> 8 | status & 0xff
}
}
#endif
#if DEBUG
deinit {
print("\(self).deinit()")
}
#endif
}
@_silgen_name("fork")
func fork() -> Int32
@_silgen_name("execve")
func execve(_ __file: UnsafePointer!,
_ __argv: UnsafePointer?>!,
_ __envp: UnsafePointer?>!) -> Int32
#endif
#endif
================================================
FILE: Sources/HotReloading/SwiftInjection.swift
================================================
//
// SwiftInjection.swift
// InjectionBundle
//
// Created by John Holdsworth on 05/11/2017.
// Copyright © 2017 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftInjection.swift#223 $
//
// Cut-down version of code injection in Swift. Uses code
// from SwiftEval.swift to recompile and reload class.
//
// There is a lot of history in this file. Originaly injection for Swift
// worked by patching the vtable of non final classes which worked fairly
// well but then we discovered "interposing" which is a mechanisim used by
// the dynamic linker to resolve references to system frameworks that can
// be used to rebind symbols at run time if you use the -interposable linker
// flag. This meant we were able to support injecting final methods of classes
// and methods of structs and enums. The code still updates the vtable though.
//
// A more recent change is to better supprt injection of generic classes
// and classes that inherit from generics which causes problems (crashes) in
// the Objective-C runtime. As one can't anticipate the specialisation of
// a generic in use from the object file (.dylib) alone, the patching of the
// vtable has been moved to the being a part of the sweep which means you need
// to have a live object of that specialisation for class injection to work.
//
// Support was also added to use injection with projects using "The Composable
// Architecture" (TCA) though you need to use a modified version of the repo:
// https://github.com/thebrowsercompany/swift-composable-architecture/tree/develop
//
// InjectionIII.app now supports injection of class methods, getters and setters
// and can maintain the values of top level and static variables when they are
// injected instead of their being reinitialised as the object file is reloaded.
//
// Which Swift symbols can be patched or interposed is now centralised and
// configurable using the closure SwiftTrace.injectableSymbol which has been
// extended to include async functions which, while they can be injected, can
// never be traced due to changes in the stack layout when using co-routines.
//
#if DEBUG || !SWIFT_PACKAGE
#if arch(x86_64) || arch(i386) || arch(arm64) // simulator/macOS only
import Foundation
#if SWIFT_PACKAGE
@_exported import SwiftTraceD
#else
@_exported import SwiftTrace
#endif
#if os(iOS) || os(tvOS)
import UIKit
extension UIViewController {
/// inject a UIView controller and redraw
public func injectVC() {
injectSelf()
for subview in self.view.subviews {
subview.removeFromSuperview()
}
if let sublayers = self.view.layer.sublayers {
for sublayer in sublayers {
sublayer.removeFromSuperlayer()
}
}
viewDidLoad()
}
}
#endif
extension NSObject {
public func injectSelf() {
if let oldClass: AnyClass = object_getClass(self) {
SwiftInjection.inject(oldClass: oldClass, classNameOrFile: "\(oldClass)")
}
}
@objc
public class func inject(file: String) {
SwiftInjection.inject(classNameOrFile: file)
}
}
@objc(SwiftInjection)
public class SwiftInjection: NSObject {
public typealias SymbolName = UnsafePointer
@objc static var traceInjection = false
static let testQueue = DispatchQueue(label: "INTestQueue")
static let injectedSEL = #selector(SwiftInjected.injected)
#if os(iOS) || os(tvOS)
static let viewDidLoadSEL = #selector(UIViewController.viewDidLoad)
#endif
static let notification = Notification.Name(INJECTION_BUNDLE_NOTIFICATION)
static var injectionDetail = getenv(INJECTION_DETAIL) != nil
static let registerClasses = false && SwiftTrace.deviceInjection
static var objcClassRefs = NSMutableArray()
static var descriptorRefs = NSMutableArray()
static var injectedPrefix: String {
return "#\(SwiftEval.instance.injectionNumber-100)/"
}
open class func log(_ what: Any...) {
print(APP_PREFIX+what.map {"\($0)"}.joined(separator: " "))
}
open class func detail(_ msg: @autoclosure () -> String) {
if injectionDetail {
log(msg())
}
}
@objc
open class func inject(oldClass: AnyClass? = nil, classNameOrFile: String) {
do {
let tmpfile = try SwiftEval.instance.rebuildClass(oldClass: oldClass,
classNameOrFile: classNameOrFile, extra: nil)
try inject(tmpfile: tmpfile)
}
catch {
SwiftEval.instance.updateLongTermCache(remove: classNameOrFile)
}
}
@objc
open class func replayInjections() -> Int {
do {
func mtime(_ path: String) -> time_t {
return SwiftEval.instance.mtime(URL(fileURLWithPath: path))
}
let execBuild = mtime(Bundle.main.executablePath!)
while true {
SwiftEval.instance.injectionNumber += 1
let tmpfile = SwiftEval.instance.tmpfile
if mtime("\(tmpfile).dylib") < execBuild {
SwiftEval.instance.injectionNumber -= 1
break
}
try inject(tmpfile: tmpfile)
}
}
catch {
}
return SwiftEval.instance.injectionNumber
}
open class func versions(of aClass: AnyClass) -> [AnyClass] {
var out = [AnyClass](), nc: UInt32 = 0, info = Dl_info()
if let classes = UnsafePointer(objc_copyClassList(&nc)) {
let named = _typeName(aClass)
for i in 0 ..< Int(nc) {
if class_getSuperclass(classes[i]) != nil && classes[i] != aClass,
_typeName(classes[i]) == named,
!(registerClasses &&
dladdr(autoBitCast(classes[i]), &info) != 0 &&
strcmp(info.dli_sname, "injected_code") == 0) {
out.append(classes[i])
}
}
free(UnsafeMutableRawPointer(mutating: classes))
}
return out
}
@objc
open class func inject(tmpfile: String) throws {
try inject(tmpfile: tmpfile, newClasses:
SwiftEval.instance.loadAndInject(tmpfile: tmpfile))
}
@objc
open class func inject(tmpfile: String, newClasses: [AnyClass]) throws {
var totalPatched = 0, totalSwizzled = 0
var injectedGenerics = Set()
var injectedClasses = [AnyClass]()
var sweepClasses = [AnyClass]()
var testClasses = [AnyClass]()
injectionDetail = getenv(INJECTION_DETAIL) != nil
SwiftTrace.preserveStatics = getenv(INJECTION_PRESERVE_STATICS) != nil
if getenv(INJECTION_TRACE) != nil {
traceInjection = true
SwiftTrace.typeLookup = true
}
// Determine any generic classes being injected.
findSwiftSymbols(searchLastLoaded(), "CMa") {
accessor, symname, _, _ in
if let demangled = SwiftMeta.demangle(symbol: symname),
let genericClassName = demangled[safe: (.last(of: " ")+1)...],
!genericClassName.hasPrefix("__C.") {
injectedGenerics.insert(genericClassName)
}
}
#if !targetEnvironment(simulator) && SWIFT_PACKAGE && canImport(InjectionScratch)
if let pseudoImage = lastPseudoImage() {
fillinObjcClassMetadata(in: pseudoImage)
}
#endif
// First, the old way for non-generics
for var newClass: AnyClass in newClasses {
let className = _typeName(newClass)
detail("Processing class \(className)")
var oldClasses = versions(of: newClass)
injectedGenerics.remove(className)
if oldClasses.isEmpty {
var info = Dl_info()
if dladdr(autoBitCast(newClass), &info) != 0,
let symbol = info.dli_sname,
let oldClass = dlsym(SwiftMeta.RTLD_MAIN_ONLY, symbol) {
oldClasses.append(autoBitCast(oldClass))
}
}
sweepClasses += oldClasses
for var oldClass: AnyClass in oldClasses {
let oldClassName = _typeName(oldClass) +
String(format: " %p", unsafeBitCast(oldClass, to: uintptr_t.self))
#if true
let patched = patchSwiftVtable(oldClass: oldClass, newClass: newClass)
#else
let patched = newPatchSwiftVtable(oldClass: oldClass, tmpfile: tmpfile)
#endif
if patched != 0 {
totalPatched += patched
let existingClass = unsafeBitCast(oldClass, to:
UnsafeMutablePointer.self)
let classMetadata = unsafeBitCast(newClass, to:
UnsafeMutablePointer.self)
// Old mechanism for Swift equivalent of "Swizzling".
if classMetadata.pointee.ClassAddressPoint != existingClass.pointee.ClassAddressPoint {
log("""
⚠️ Mixing Xcode versions across injection. This may work \
but "Clean Builder Folder" when switching Xcode versions. \
To clear the cache: rm \(SwiftEval.instance.buildCacheFile)
""")
} else
if classMetadata.pointee.ClassSize != existingClass.pointee.ClassSize {
log("""
⚠️ Adding or [re]moving methods of non-final class \
\(oldClass)[\(existingClass.pointee.ClassSize)],\
\(newClass)[\(classMetadata.pointee.ClassSize)] \
is not supported. Your application will likely crash. \
Paradoxically, you can avoid this by making the class \
you are trying to inject (and add methods to) "final". ⚠️
""")
}
}
// Is there a generic superclass?
if inheritedGeneric(anyType: oldClass) {
// fallback to limited processing avoiding objc runtime.
// (object_getClass() and class_copyMethodList() crash)
let swizzled = swizzleBasics(oldClass: oldClass, tmpfile: tmpfile)
totalSwizzled += swizzled
detail("Injected class '\(oldClassName)' (\(patched),\(swizzled)).")
continue
}
if oldClass == newClass {
if oldClasses.count > 1 {
oldClass = oldClasses.first!
newClass = oldClasses.last!
} else {
log("⚠️ Could not find versions of class \(_typeName(newClass)). ⚠️")
}
}
var swizzled: Int
if !SwiftTrace.deviceInjection || registerClasses {
// old-school swizzle Objective-C class & instance methods
swizzled = injection(swizzle: object_getClass(oldClass),
from: object_getClass(newClass)) +
injection(swizzle: oldClass, from: newClass)
} else {
#if !targetEnvironment(simulator) && SWIFT_PACKAGE && canImport(InjectionScratch)
swizzled = onDevice(swizzle: oldClass, from: newClass)
#else
swizzled = injection(swizzle: object_getClass(oldClass)!,
tmpfile: tmpfile) +
injection(swizzle: oldClass, tmpfile: tmpfile)
#endif
}
totalSwizzled += swizzled
detail("Patched class '\(oldClassName)' (\(patched),\(swizzled))")
}
if let XCTestCase = objc_getClass("XCTestCase") as? AnyClass,
isSubclass(newClass, of: XCTestCase) {
testClasses.append(newClass)
}
injectedClasses.append(newClass)
}
#if !SWIFT_PACKAGE
let patchedGenerics = hookedPatch(of: injectedGenerics, tmpfile: tmpfile)
totalPatched += patchedGenerics.count
sweepClasses += patchedGenerics
#endif
// (Reverse) interposing, reducers, operation on a device etc.
let totalInterposed = newerProcessing(tmpfile: tmpfile, sweepClasses)
lastLoadedImage().symbols(withPrefix: "__OBJC_$_CATEGORY_") {_,_,_ in
totalSwizzled += 1
}
if totalPatched + totalSwizzled + totalInterposed + testClasses.count == 0 {
log("⚠️ Injection may have failed. Have you added -Xlinker -interposable (for the Debug configuration only, without double quotes and on separate lines) to the \"Other Linker Flags\" of the executable and frameworks? ⚠️")
}
DispatchQueue.main.async {
// Thanks https://github.com/johnno1962/injectionforxcode/pull/234
if !testClasses.isEmpty {
testQueue.async {
testQueue.suspend()
let timer = Timer(timeInterval: 0, repeats:false, block: { _ in
for newClass in testClasses {
NSObject.runXCTestCase(newClass)
}
testQueue.resume()
})
RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
}
} else { // implement class and instance injected() methods
if !SwiftTrace.deviceInjection {
typealias ClassIMP = @convention(c) (AnyClass, Selector) -> ()
for cls in injectedClasses {
if let classMethod = class_getClassMethod(cls, injectedSEL) {
let classIMP = method_getImplementation(classMethod)
unsafeBitCast(classIMP, to: ClassIMP.self)(cls, injectedSEL)
}
}
}
performSweep(oldClasses: sweepClasses, tmpfile,
getenv(INJECTION_OF_GENERICS) != nil ? injectedGenerics : [])
NotificationCenter.default.post(name: notification, object: sweepClasses)
}
}
}
open class func isSubclass(_ subClass: AnyClass, of aClass: AnyClass) -> Bool {
var subClass: AnyClass? = subClass
repeat {
if subClass == aClass {
return true
}
subClass = class_getSuperclass(subClass)
} while subClass != nil
return false
}
open class func inheritedGeneric(anyType: Any.Type) -> Bool {
var inheritedGeneric: AnyClass? = anyType as? AnyClass
if class_getSuperclass(inheritedGeneric) == nil {
return true
}
while let parent = inheritedGeneric {
if _typeName(parent).hasSuffix(">") {
return true
}
inheritedGeneric = class_getSuperclass(parent)
}
return false
}
open class func newerProcessing(tmpfile: String,
_ sweepClasses: [AnyClass]) -> Int {
// new mechanism for injection of Swift functions,
// using "interpose" API from dynamic loader along
// with -Xlinker -interposable "Other Linker Flags".
let interposed = Set(interpose(functionsIn: "\(tmpfile).dylib"))
if interposed.count != 0 {
for symname in interposed {
detail("Interposed "+describeImageSymbol(symname))
}
log("Interposed \(interposed.count) function references.")
}
#if !targetEnvironment(simulator) && SWIFT_PACKAGE && canImport(InjectionScratch)
if let pseudoImage = lastPseudoImage() {
onDeviceSpecificProcessing(for: pseudoImage, sweepClasses)
}
#endif
// Can prevent statics from re-initializing on injection
reverseInterposeStaticsAddressors(tmpfile)
// log any types being injected
var ntypes = 0, npreviews = 0
findSwiftSymbols(searchLastLoaded(), "N") {
(typePtr, symbol, _, _) in
if let existing: Any.Type =
autoBitCast(dlsym(SwiftMeta.RTLD_DEFAULT, symbol)) {
let name = _typeName(existing)
if name.hasSuffix("_Previews") {
npreviews += 1
}
if name.hasSuffix("PreviewRegistryfMu_") {
return
}
ntypes += 1
log("Injected type #\(ntypes) '\(name)'")
if SwiftTrace.deviceInjection {
SwiftMeta.cloneValueWitness(from: existing, onto: autoBitCast(typePtr))
}
let newSize = SwiftMeta.sizeof(anyType: autoBitCast(typePtr))
if newSize != 0 && newSize != SwiftMeta.sizeof(anyType: existing) {
log("⚠️ Size of value type \(_typeName(existing)) has changed (\(newSize) != \(SwiftMeta.sizeof(anyType: existing))). You cannot inject changes to memory layout. This will likely just crash. ⚠️")
}
}
}
if false && npreviews > 0 && ntypes > 2 && SwiftTrace.deviceInjection {
log("⚠️ Device injection may fail if you have more than one type from the injected file referred to in a SwiftUI View.")
}
if getenv(INJECTION_DYNAMIC_CAST) != nil {
// Cater for dynamic cast (i.e. as?) to types that have been injected.
DynamicCast.hook_lastInjected()
}
var reducers = [SymbolName]()
if !injectableReducerSymbols.isEmpty {
reinitializeInjectedReducers(tmpfile, reinitialized: &reducers)
let s = reducers.count == 1 ? "" : "s"
log("Overrode \(reducers.count) reducer"+s)
}
return interposed.count + reducers.count
}
#if true // Original version of vtable patch, headed for retirement..
/// Patch entries in vtable of existing class to be that in newly loaded version of class for non-final methods
class func patchSwiftVtable(oldClass: AnyClass, newClass: AnyClass) -> Int {
// overwrite Swift vtable of existing class with implementations from new class
let existingClass = unsafeBitCast(oldClass, to:
UnsafeMutablePointer.self)
let classMetadata = unsafeBitCast(newClass, to:
UnsafeMutablePointer.self)
// Is this a Swift class?
// Reference: https://github.com/apple/swift/blob/master/include/swift/ABI/Metadata.h#L1195
let oldSwiftCondition = classMetadata.pointee.Data & 0x1 == 1
let newSwiftCondition = classMetadata.pointee.Data & 0x3 != 0
guard newSwiftCondition || oldSwiftCondition else { return 0 }
var patched = 0
#if true // supplimented by "interpose" code
// vtable still needs to be patched though for non-final methods
func byteAddr(_ location: UnsafeMutablePointer) -> UnsafeMutablePointer {
return location.withMemoryRebound(to: UInt8.self, capacity: 1) { $0 }
}
let vtableOffset = byteAddr(&existingClass.pointee.IVarDestroyer) - byteAddr(existingClass)
#if false
// Old mechanism for Swift equivalent of "Swizzling".
if classMetadata.pointee.ClassSize != existingClass.pointee.ClassSize {
log("⚠️ Adding or [re]moving methods on non-final classes is not supported. Your application will likely crash. ⚠️")
}
// original injection implementaion for Swift.
let vtableLength = Int(existingClass.pointee.ClassSize -
existingClass.pointee.ClassAddressPoint) - vtableOffset
memcpy(byteAddr(existingClass) + vtableOffset,
byteAddr(classMetadata) + vtableOffset, vtableLength)
#else
// new version only copying only symbols that are functions.
let newTable = (byteAddr(classMetadata) + vtableOffset)
.withMemoryRebound(to: SwiftTrace.SIMP?.self, capacity: 1) { $0 }
SwiftTrace.iterateMethods(ofClass: oldClass) {
(name, slotIndex, vtableSlot, stop) in
if let replacement = SwiftTrace.interposed(replacee:
autoBitCast(newTable[slotIndex] ?? vtableSlot.pointee)),
autoBitCast(vtableSlot.pointee) != replacement {
traceAndReplace(vtableSlot.pointee,
replacement: replacement, name: name) {
(replacement: UnsafeMutableRawPointer) -> String? in
vtableSlot.pointee = autoBitCast(replacement)
if autoBitCast(vtableSlot.pointee) == replacement { ////
patched += 1
return newTable[slotIndex] != nil ? "Patched" : "Populated"
}
return nil
}
}
}
#endif
#endif
return patched
}
#endif
/// Newer way to patch vtable looking up existing entries individually in newly loaded dylib.
open class func newPatchSwiftVtable(oldClass: AnyClass,// newClass: AnyClass?,
tmpfile: String) -> Int {
var patched = 0
SwiftTrace.forEachVTableEntry(ofClass: oldClass) {
(symname, slotIndex, vtableSlot, stop) in
let existing: UnsafeMutableRawPointer = autoBitCast(vtableSlot.pointee)
guard let replacement = fast_dlsym(lastLoadedImage(), symname) ??
(dlsym(SwiftMeta.RTLD_DEFAULT, symname) ??
findSwiftSymbol(searchBundleImages(), symname, .any)).flatMap({
autoBitCast(SwiftTrace.interposed(replacee: $0)) }) else {
log("⚠️ Class patching failed to lookup " +
describeImageSymbol(symname))
return
}
if replacement != existing {
traceAndReplace(existing, replacement: replacement, symname: symname) {
(replacement: UnsafeMutableRawPointer) -> String? in
vtableSlot.pointee = autoBitCast(replacement)
if autoBitCast(vtableSlot.pointee) == replacement {
patched += 1
return "Patched"
}
return nil
}
}
}
return patched
}
/// Pop a trace on a newly injected method and convert the pointer type while you're at it
open class func traceInjected(replacement: IN, name: String? = nil,
symname: UnsafePointer? = nil,
objcMethod: Method? = nil, objcClass: AnyClass? = nil)
-> UnsafeRawPointer {
if traceInjection || SwiftTrace.isTracing,
let name = name ??
symname.flatMap({ SwiftMeta.demangle(symbol: $0) }) ??
objcMethod.flatMap({ NSStringFromSelector(method_getName($0)) }),
!name.contains(".unsafeMutableAddressor :"),
let tracer = SwiftTrace.trace(name: injectedPrefix + name,
objcMethod: objcMethod, objcClass: objcClass,
original: autoBitCast(replacement)) {
return autoBitCast(tracer)
}
return autoBitCast(replacement)
}
/// All implementation replacements go through this function which can also apply a trace
/// - Parameters:
/// - existing: implementation being replaced
/// - replacement: new implementation
/// - name: demangled symbol for trace
/// - symname: raw symbol nme
/// - objcMethod: used for trace
/// - objcClass: used for trace
/// - apply: closure to apply replacement
open class func traceAndReplace(_ existing: E,
replacement: UnsafeRawPointer,
name: String? = nil, symname: UnsafePointer? = nil,
objcMethod: Method? = nil, objcClass: AnyClass? = nil,
apply: (O) -> String?) {
let traced = traceInjected(replacement: replacement, name: name,
symname: symname, objcMethod: objcMethod, objcClass: objcClass)
// injecting getters returning generics best avoided for some reason.
if let getted = name?[safe: .last(of: ".getter : ", end: true)...] {
if getted.hasSuffix(">") { return }
if let type = SwiftMeta.lookupType(named: getted),
inheritedGeneric(anyType: type) {
return
}
}
if let success = apply(autoBitCast(traced)) {
detail("\(success) \(autoBitCast(existing) as UnsafeRawPointer) -> \(replacement) " + describeImagePointer(replacement))
}
}
/// Resolve a perhaps traced function back to name of original symbol
open class func originalSym(for existing: UnsafeMutableRawPointer) -> SymbolName? {
var info = Dl_info()
if fast_dladdr(existing, &info) != 0 {
return info.dli_sname
} else if let swizzle = SwiftTrace.originalSwizzle(for: autoBitCast(existing)),
fast_dladdr(autoBitCast(swizzle.implementation), &info) != 0 {
return info.dli_sname
}
return nil
}
/// If class is a generic, patch its specialised vtable and basic selectors
open class func patchGenerics(oldClass: AnyClass, tmpfile: String,
injectedGenerics: Set,
patched: inout Set) -> Bool {
if let genericClassName = _typeName(oldClass)[safe: ..<(.first(of: "<"))],
injectedGenerics.contains(genericClassName) {
if patched.insert(autoBitCast(oldClass)).inserted {
let patched = newPatchSwiftVtable(oldClass: oldClass, tmpfile: tmpfile)
let swizzled = swizzleBasics(oldClass: oldClass, tmpfile: tmpfile)
log("Injected generic '\(oldClass)' (\(patched),\(swizzled))")
}
return oldClass.instancesRespond(to: injectedSEL)
}
return false
}
@objc(vaccine:)
open class func performVaccineInjection(_ object: AnyObject) {
#if !os(watchOS)
let vaccine = Vaccine()
vaccine.performInjection(on: object)
#endif
}
#if os(iOS) || os(tvOS)
@objc(flash:)
open class func flash(vc: UIViewController) {
DispatchQueue.main.async {
let v = UIView(frame: vc.view.frame)
v.backgroundColor = .white
v.alpha = 0.3
vc.view.addSubview(v)
UIView.animate(withDuration: 0.2,
delay: 0.0,
options: UIView.AnimationOptions.curveEaseIn,
animations: {
v.alpha = 0.0
}, completion: { _ in v.removeFromSuperview() })
}
}
#endif
}
@objc
public class SwiftInjectionEval: UnhidingEval {
@objc override open class func sharedInstance() -> SwiftEval {
SwiftEval.instance = SwiftInjectionEval()
return SwiftEval.instance
}
@objc override open func extractClasses(dl: UnsafeMutableRawPointer,
tmpfile: String) throws -> [AnyClass] {
var classes = [AnyClass]()
SwiftTrace.forAllClasses(bundlePath: searchLastLoaded()) {
aClass, stop in
classes.append(aClass)
}
if classes.count > 0 && !SwiftTrace.deviceInjection {
print("\(APP_PREFIX)Loaded .dylib - Ignore any duplicate class warning ⬆️")
}
#if false // Just too dubious
// Determine any generic classes being injected.
// (Done as part of sweep in the end.)
findSwiftSymbols(searchLastLoaded(), "CMa") {
accessor, _, _, _ in
struct Something {}
typealias AF = @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> UnsafeRawPointer
let tmd: Any.Type = Void.self
let tmd0 = unsafeBitCast(tmd, to: UnsafeRawPointer.self)
let tmd1 = unsafeBitCast(accessor, to: AF.self)(tmd0, tmd0)
let tmd2 = unsafeBitCast(tmd1, to: Any.Type.self)
if let genericClass = tmd2 as? AnyClass {
classes.append(genericClass)
}
}
#endif
return classes
}
}
#endif
#endif
================================================
FILE: Sources/HotReloading/SwiftInterpose.swift
================================================
//
// SwiftInterpose.swift
//
// Created by John Holdsworth on 25/04/2022.
//
// Interpose processing (-Xlinker -interposable).
//
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftInterpose.swift#11 $
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
extension SwiftInjection {
/// "Interpose" all function definitions in a dylib onto the main executable
public class func interpose(functionsIn dylib: String) -> [UnsafePointer] {
var symbols = [UnsafePointer]()
#if false // DLKit based interposing
// ... doesn't play well with tracing.
var replacements = [UnsafeMutableRawPointer]()
// Find all definitions of Swift functions and ...
// SwiftUI body properties defined in the new dylib.
for (symbol, value, _) in DLKit.lastImage
.swiftSymbols(withSuffixes: injectableSuffixes) {
guard var replacement = value else {
continue
}
let method = symbol.demangled ?? String(cString: symbol)
if detail {
log("Replacing \(method)")
}
if traceInjection || SwiftTrace.isTracing, let tracer = SwiftTrace
.trace(name: injectedPrefix+method, original: replacement) {
replacement = autoBitCast(tracer)
}
symbols.append(symbol)
replacements.append(replacement)
}
// Rebind all references in all images in the app bundle
// to function symbols defined in the last loaded dylib
// to the new implementations in the newly loaded dylib.
DLKit.appImages[symbols] = replacements
#else // Current SwiftTrace based code...
let main = dlopen(nil, RTLD_NOW)
var interposes = [dyld_interpose_tuple]()
#if false
let suffixesToInterpose = SwiftTrace.traceableFunctionSuffixes
// Oh alright, interpose all property getters..
.map { $0 == "Qrvg" ? "g" : $0 }
// and class/static members
.flatMap { [$0, $0+"Z"] }
for suffix in suffixesToInterpose {
findSwiftSymbols(dylib, suffix) { (loadedFunc, symbol, _, _) in
// interposing was here ...
}
}
#endif
filterImageSymbols(ST_LAST_IMAGE, .any, SwiftTrace.injectableSymbol) {
(loadedFunc, symbol, _, _) in
guard let existing = dlsym(main, symbol) ??
findSwiftSymbol(searchBundleImages(), symbol, .any),
existing != loadedFunc/*,
let current = SwiftTrace.interposed(replacee: existing)*/ else {
return
}
let current = existing
traceAndReplace(current, replacement: loadedFunc, symname: symbol) {
(replacement: UnsafeMutableRawPointer) -> String? in
interposes.append(dyld_interpose_tuple(replacement: replacement,
replacee: current))
symbols.append(symbol)
return nil //"Interposing"
}
#if ORIGINAL_2_2_0_CODE
SwiftTrace.interposed[existing] = loadedFunc
SwiftTrace.interposed[current] = loadedFunc
#endif
}
#if !ORIGINAL_2_2_0_CODE
//// if interposes.count == 0 { return [] }
var rebindings = SwiftTrace.record(interposes: interposes, symbols: symbols)
return SwiftTrace.apply(rebindings: &rebindings,
onInjection: { (header, slide) in
var info = Dl_info()
// Need to apply previous interposes
// to the newly loaded dylib as well.
var previous = SwiftTrace.initialRebindings
var already = Set()
let interposed = NSObject.swiftTraceInterposed.bindMemory(to:
[UnsafeRawPointer : UnsafeRawPointer].self, capacity: 1)
for (replacee, _) in interposed.pointee {
if let replacement = SwiftTrace.interposed(replacee: replacee),
already.insert(replacement).inserted,
dladdr(replacee, &info) != 0, let symname = info.dli_sname {
previous.append(rebinding(name: symname, replacement:
UnsafeMutableRawPointer(mutating: replacement),
replaced: nil))
}
}
rebind_symbols_image(UnsafeMutableRawPointer(mutating: header),
slide, &previous, previous.count)
})
#else // ORIGINAL_2_2_0_CODE replaced by fishhook now
// Using array of new interpose structs
interposes.withUnsafeBufferPointer { interps in
var mostRecentlyLoaded = true
// Apply interposes to all images in the app bundle
// as well as the most recently loaded "new" dylib.
appBundleImages { image, header in
if mostRecentlyLoaded {
// Need to apply all previous interposes
// to the newly loaded dylib as well.
var previous = Array()
for (replacee, replacement) in SwiftTrace.interposed {
previous.append(dyld_interpose_tuple(
replacement: replacement, replacee: replacee))
}
previous.withUnsafeBufferPointer {
interps in
dyld_dynamic_interpose(header,
interps.baseAddress!, interps.count)
}
mostRecentlyLoaded = false
}
// patch out symbols defined by new dylib.
dyld_dynamic_interpose(header,
interps.baseAddress!, interps.count)
// print("Patched \(String(cString: image))")
}
}
#endif
#endif
}
/// Interpose references to witness tables, meta data and perhps static variables
/// to those in main bundle to have them not re-initialise again on each injection.
static func reverseInterposeStaticsAddressors(_ tmpfile: String) {
var staticsAccessors = [rebinding]()
var already = Set()
var symbolSuffixes = ["Wl"] // Witness table accessors
if false && SwiftTrace.deviceInjection {
symbolSuffixes.append("Ma") // meta data accessors
}
if SwiftTrace.preserveStatics {
symbolSuffixes.append("vau") // static variable "mutable addressors"
}
for suffix in symbolSuffixes {
findHiddenSwiftSymbols(searchLastLoaded(), suffix, .any) {
accessor, symname, _, _ in
var original = dlsym(SwiftMeta.RTLD_MAIN_ONLY, symname)
if original == nil {
original = findSwiftSymbol(searchBundleImages(), symname, .any)
if original != nil && !already.contains(original!) {
detail("Recovered top level variable with private scope " +
describeImagePointer(original!))
}
}
guard original != nil, already.insert(original!).inserted else {
return
}
detail("Reverse interposing \(original!) <- \(accessor) " +
describeImagePointer(original!))
staticsAccessors.append(rebinding(name: symname,
replacement: original!, replaced: nil))
}
}
let injectedImage = _dyld_image_count()-1 // last injected
let interposed = SwiftTrace.apply(
rebindings: &staticsAccessors, count: staticsAccessors.count,
header: lastPseudoImage() ?? _dyld_get_image_header(injectedImage),
slide: lastPseudoImage() != nil ? 0 :
_dyld_get_image_vmaddr_slide(injectedImage))
for symname in interposed {
detail("Reverse interposed "+describeImageSymbol(symname))
}
if interposed.count != staticsAccessors.count && injectionDetail {
let succeeded = Set(interposed)
for attemped in staticsAccessors.map({ $0.name }) {
if !succeeded.contains(attemped) {
log("Reverse interposing \(interposed.count)/\(staticsAccessors.count) failed for \(describeImageSymbol(attemped))")
}
}
}
}
}
#endif
================================================
FILE: Sources/HotReloading/SwiftKeyPath.swift
================================================
//
// SwiftKeyPath.swift
//
// Created by John Holdsworth on 20/03/2024.
// Copyright © 2024 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftKeyPath.swift#34 $
//
// Key paths weren't made to be injected as their underlying types can change.
// This is particularly evident in code that uses "The Composable Architecture".
// This code maintains a cache of previously allocated key paths using a unique
// identifier of the calling site so they remain invariant over an injection.
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
private struct ViewBodyKeyPaths {
typealias KeyPathFunc = @convention(c) (UnsafeMutableRawPointer,
UnsafeRawPointer) -> UnsafeRawPointer
static let keyPathFuncName = "swift_getKeyPath"
static var save_getKeyPath: KeyPathFunc!
static var cache = [String: ViewBodyKeyPaths]()
static var lastInjectionNumber = SwiftEval().injectionNumber
static var hasInjected = false
var lastOffset = 0
var keyPathNumber = 0
var recycled = false
var keyPaths = [UnsafeRawPointer]()
}
@_cdecl("hookKeyPaths")
public func hookKeyPaths(original: UnsafeMutableRawPointer,
replacer: UnsafeMutableRawPointer) {
print(APP_PREFIX+"ℹ️ Intercepting keypaths for when their types are injected." +
" Set an env var INJECTION_NOKEYPATHS in your scheme to prevent this.")
ViewBodyKeyPaths.save_getKeyPath = autoBitCast(original)
var keyPathRebinding = [rebinding(name: strdup(ViewBodyKeyPaths.keyPathFuncName),
replacement: replacer, replaced: nil)]
SwiftTrace.initialRebindings += keyPathRebinding
_ = SwiftTrace.apply(rebindings: &keyPathRebinding)
}
@_cdecl("injection_getKeyPath")
public func injection_getKeyPath(pattern: UnsafeMutableRawPointer,
arguments: UnsafeRawPointer) -> UnsafeRawPointer {
if ViewBodyKeyPaths.lastInjectionNumber != SwiftEval.instance.injectionNumber {
ViewBodyKeyPaths.lastInjectionNumber = SwiftEval.instance.injectionNumber
for key in ViewBodyKeyPaths.cache.keys {
ViewBodyKeyPaths.cache[key]?.keyPathNumber = 0
ViewBodyKeyPaths.cache[key]?.recycled = false
}
ViewBodyKeyPaths.hasInjected = true
}
var info = Dl_info()
for caller in Thread.callStackReturnAddresses.dropFirst() {
guard let caller = caller.pointerValue,
dladdr(caller, &info) != 0, let symbol = info.dli_sname,
let callerDecl = SwiftMeta.demangle(symbol: symbol) else {
continue
}
if !callerDecl.hasSuffix(".body.getter : some") {
break
}
// identify caller site
var relevant: [String] = callerDecl[#"(closure #\d+ |in \S+ : some)"#]
if relevant.isEmpty {
relevant = [callerDecl]
}
let callerKey = relevant.joined() + ".keyPath#"
// print(callerSym, ins)
var body = ViewBodyKeyPaths.cache[callerKey] ?? ViewBodyKeyPaths()
// reset keyPath counter ?
let offset = caller-info.dli_saddr
if offset <= body.lastOffset {
body.keyPathNumber = 0
body.recycled = false
}
body.lastOffset = offset
// print(">>", offset, body.keyPathNumber)
// extract cached keyPath or create
let keyPath: UnsafeRawPointer
if body.keyPathNumber < body.keyPaths.count && ViewBodyKeyPaths.hasInjected {
SwiftInjection.detail("Recycling \(callerKey)\(body.keyPathNumber)")
keyPath = body.keyPaths[body.keyPathNumber]
body.recycled = true
} else {
keyPath = ViewBodyKeyPaths.save_getKeyPath(pattern, arguments)
if body.keyPaths.count == body.keyPathNumber {
body.keyPaths.append(keyPath)
}
if body.recycled {
SwiftInjection.log("""
⚠️ New key path expression introduced over injection. \
This will likely fail and you'll have to restart your \
application.
""")
}
}
body.keyPathNumber += 1
ViewBodyKeyPaths.cache[callerKey] = body
_ = Unmanaged.fromOpaque(keyPath).retain()
return keyPath
}
return ViewBodyKeyPaths.save_getKeyPath(pattern, arguments)
}
#endif
================================================
FILE: Sources/HotReloading/SwiftSweeper.swift
================================================
//
// SwiftSweeper.swift
//
// Created by John Holdsworth on 15/04/2021.
//
// The implementation of the memeory sweep to search for
// instance of classes that have been injected in order
// to be able to send them the @objc injected message.
//
// $Id: //depot/HotReloading/Sources/HotReloading/SwiftSweeper.swift#23 $
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
#if os(macOS)
import Cocoa
typealias OSApplication = NSApplication
#elseif !os(watchOS)
import UIKit
typealias OSApplication = UIApplication
#endif
@objc public protocol SwiftInjected {
@objc optional func injected()
}
extension SwiftInjection {
static let debugSweep = getenv(INJECTION_SWEEP_DETAIL) != nil
static let sweepExclusions = { () -> NSRegularExpression? in
if let exclusions = getenv(INJECTION_SWEEP_EXCLUDE) {
let pattern = String(cString: exclusions)
do {
let filter = try NSRegularExpression(pattern: pattern, options: [])
print(APP_PREFIX+"⚠️ Excluding types matching '\(pattern)' from sweep")
return filter
} catch {
print(APP_PREFIX+"⚠️ Invalid sweep filter pattern \(error): \(pattern)")
}
}
return nil
}()
static var sweepWarned = false
public class func performSweep(oldClasses: [AnyClass], _ tmpfile: String,
_ injectedGenerics: Set) {
var injectedClasses = [AnyClass]()
for cls in oldClasses {
if class_getInstanceMethod(cls, injectedSEL) != nil {
injectedClasses.append(cls)
if !sweepWarned {
log("""
As class \(cls) has an @objc injected() \
method, \(APP_NAME) will perform a "sweep" of live \
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
}
let kvoName = "NSKVONotifying_" + NSStringFromClass(cls)
if let kvoCls = NSClassFromString(kvoName) {
injectedClasses.append(kvoCls)
}
}
}
// implement -injected() method using sweep of objects in application
if !injectedClasses.isEmpty || !injectedGenerics.isEmpty {
log("Starting sweep \(injectedClasses), \(injectedGenerics)...")
#if !os(watchOS)
let app = OSApplication.shared
let seeds: [Any] = [app.delegate as Any] + app.windows
#else
let seeds = [Any]()
#endif
var patched = Set()
SwiftSweeper(instanceTask: {
(instance: AnyObject) in
if let instanceClass = object_getClass(instance),
injectedClasses.contains(where: { $0 == instanceClass }) ||
!injectedGenerics.isEmpty &&
patchGenerics(oldClass: instanceClass, tmpfile: tmpfile,
injectedGenerics: injectedGenerics, patched: &patched) {
let proto = unsafeBitCast(instance, to: SwiftInjected.self)
if SwiftEval.sharedInstance().vaccineEnabled {
performVaccineInjection(instance)
proto.injected?()
return
}
proto.injected?()
#if os(iOS) || os(tvOS)
if let vc = instance as? UIViewController {
flash(vc: vc)
}
#endif
}
}).sweepValue(seeds)
}
}
}
class SwiftSweeper {
static var current: SwiftSweeper?
let instanceTask: (AnyObject) -> Void
var seen = [UnsafeRawPointer: Bool]()
init(instanceTask: @escaping (AnyObject) -> Void) {
self.instanceTask = instanceTask
SwiftSweeper.current = self
}
func sweepValue(_ value: Any, _ containsType: Bool = false) {
/// Skip values that cannot be cast into `AnyObject` because they end up being `nil`
/// Fixes a potential crash that the value is not accessible during injection.
// print(value)
guard !containsType && value as? AnyObject != nil else { return }
let mirror = Mirror(reflecting: value)
if var style = mirror.displayStyle {
if _typeName(mirror.subjectType).hasPrefix("Swift.ImplicitlyUnwrappedOptional<") {
style = .optional
}
switch style {
case .set, .collection:
let containsType = _typeName(type(of: value)).contains(".Type")
if SwiftInjection.debugSweep {
print("Sweeping collection:", _typeName(type(of: value)))
}
for (_, child) in mirror.children {
sweepValue(child, containsType)
}
return
case .dictionary:
for (_, child) in mirror.children {
for (_, element) in Mirror(reflecting: child).children {
sweepValue(element)
}
}
return
case .class:
sweepInstance(value as AnyObject)
return
case .optional, .enum:
if let evals = mirror.children.first?.value {
sweepValue(evals)
}
case .tuple, .struct:
sweepMembers(value)
@unknown default:
break
}
}
}
func sweepInstance(_ instance: AnyObject) {
let reference = unsafeBitCast(instance, to: UnsafeRawPointer.self)
if seen[reference] == nil {
seen[reference] = true
if let filter = SwiftInjection.sweepExclusions {
let typeName = _typeName(type(of: instance))
if filter.firstMatch(in: typeName,
range: NSMakeRange(0, typeName.utf16.count)) != nil {
return
}
}
if SwiftInjection.debugSweep {
print("Sweeping instance \(reference) of class \(type(of: instance))")
}
sweepMembers(instance)
instance.legacySwiftSweep?()
instanceTask(instance)
}
}
func sweepMembers(_ instance: Any) {
var mirror: Mirror? = Mirror(reflecting: instance)
while mirror != nil {
for (name, value) in mirror!.children
where name?.hasSuffix("Type") != true {
sweepValue(value)
}
mirror = mirror!.superclassMirror
}
}
}
extension NSObject {
@objc func legacySwiftSweep() {
var icnt: UInt32 = 0, cls: AnyClass? = object_getClass(self)
while cls != nil && cls != NSObject.self && cls != NSURL.self {
let className = NSStringFromClass(cls!)
if className.hasPrefix("_") || className.hasPrefix("WK") ||
className.hasPrefix("NS") && className != "NSWindow" {
return
}
if let ivars = class_copyIvarList(cls, &icnt) {
let object = UInt8(ascii: "@")
for i in 0 ..< Int(icnt) {
if /*let name = ivar_getName(ivars[i])
.flatMap({ String(cString: $0)}),
sweepExclusions?.firstMatch(in: name,
range: NSMakeRange(0, name.utf16.count)) == nil,*/
let type = ivar_getTypeEncoding(ivars[i]), type[0] == object {
(unsafeBitCast(self, to: UnsafePointer.self) + ivar_getOffset(ivars[i]))
.withMemoryRebound(to: AnyObject?.self, capacity: 1) {
// print("\($0.pointee) \(self) \(name): \(String(cString: type))")
if let obj = $0.pointee {
SwiftSweeper.current?.sweepInstance(obj)
}
}
}
}
free(ivars)
}
cls = class_getSuperclass(cls)
}
}
}
extension NSSet {
@objc override func legacySwiftSweep() {
self.forEach { SwiftSweeper.current?.sweepInstance($0 as AnyObject) }
}
}
extension NSArray {
@objc override func legacySwiftSweep() {
self.forEach { SwiftSweeper.current?.sweepInstance($0 as AnyObject) }
}
}
extension NSDictionary {
@objc override func legacySwiftSweep() {
self.allValues.forEach { SwiftSweeper.current?.sweepInstance($0 as AnyObject) }
}
}
#endif
================================================
FILE: Sources/HotReloading/UnhidingEval.swift
================================================
//
// UnhidingEval.swift
//
// Created by John Holdsworth on 13/04/2021.
//
// $Id: //depot/HotReloading/Sources/HotReloading/UnhidingEval.swift#27 $
//
// Retro-fit Unhide into InjectionIII
//
// Unhiding is a work-around for swift giving "hidden" visibility
// to default argument generators which are called when code uses
// a default argument. "Hidden" visibility is somewhere between a
// public and private declaration where the symbol doesn't become
// part of the Swift ABI but is nevertheless required at call sites.
// This causes problems for injection as "hidden" symbols are not
// available outside the framework or executable that defines them.
// So, a dynamically loading version of a source file that uses a
// default argument cannot load due to not seeing the symbol.
//
// This file calls a piece of C++ in Unhide.mm which scans all the object
// files of a project looking for symbols for default argument generators
// that are hidden and makes them public by clearing the N_PEXT flag on
// the symbol type. Ideally this would happen between compiling and linking
// But as it is not possible to add a build phase between compiling and
// linking you have to build again for the object file to be linked into
// the app executable or framework. This isn't ideal but is about as
// good as it gets, resolving the injection of files that use default
// arguments with the minimum disruption to the build process. This
// file inserts this process when injection is used to keep the files
// declaring the defaut argument patched sometimes giving an error that
// asks the user to run the app again and retry.
//
#if DEBUG || !SWIFT_PACKAGE
import Foundation
#if SWIFT_PACKAGE
import SwiftRegex
#endif
@objc
public class UnhidingEval: SwiftEval {
@objc public override class func sharedInstance() -> SwiftEval {
SwiftEval.instance = UnhidingEval()
return SwiftEval.instance
}
static let unhideQueue = DispatchQueue(label: "unhide")
static var lastProcessed = [URL: time_t]()
var unhidden = false
var buildDir: URL?
public override func determineEnvironment(classNameOrFile: String) throws -> (URL, URL) {
let (project, logs) =
try super.determineEnvironment(classNameOrFile: classNameOrFile)
buildDir = logs.deletingLastPathComponent()
.deletingLastPathComponent().appendingPathComponent("Build")
if legacyUnhide {
startUnhide()
}
return (project, logs)
}
override func startUnhide() {
if !unhidden, let buildDir = buildDir {
unhidden = true
Self.unhideQueue.async {
if let enumerator = FileManager.default
.enumerator(atPath: buildDir.path),
let log = fopen("/tmp/unhide.log", "w") {
// linkFileLists contain the list of object files.
var linkFileLists = [String](), frameworks = [String]()
for path in enumerator.compactMap({ $0 as? String }) {
if path.hasSuffix(".LinkFileList") {
linkFileLists.append(path)
} else if path.hasSuffix(".framework") {
frameworks.append(path)
}
}
// 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, since)
if exported != 0 {
let s = exported == 1 ? "" : "s"
print("\(APP_PREFIX)Exported \(exported) default argument\(s) in \(fileURL.lastPathComponent)")
}
}
#if false // never implemented
for framework in frameworks {
let fileURL = buildDir
.appendingPathComponent(framework)
let frameworkName = fileURL
.deletingPathExtension().lastPathComponent
let exported = unhide_framework(fileURL
.appendingPathComponent(frameworkName).path, log)
if exported != 0 {
let s = exported == 1 ? "" : "s"
print("\(APP_PREFIX)Exported \(exported) symbol\(s) in framework \(frameworkName)")
}
}
#endif
Self.lastProcessed[buildDir] = time(nil)
unhide_reset()
fclose(log)
}
}
}
}
// This was required for Xcode13's new "optimisation" to compile
// more than one primary file in a single compiler invocation.
override func xcode13Fix(sourceFile: String,
compileCommand: inout String) -> String {
let sourceName = URL(fileURLWithPath: sourceFile)
.deletingPathExtension().lastPathComponent.escaping().lowercased()
let hasFileList = compileCommand.contains(" -filelist ")
var nPrimaries = 0
// ensure there is only ever one -primary-file argument and object file
// avoids shuffling of object files due to how the compiler is coded
compileCommand[#" -primary-file (\#(Self.filePathRegex+Self.fileNameRegex))"#] = {
(groups: [String], stop) -> String in
// debug("PF: \(sourceName) \(groups)")
nPrimaries += 1
return groups[2].lowercased() == sourceName ||
groups[3].lowercased() == sourceName ?
groups[0] : hasFileList ? "" : " "+groups[1]
}
// // Xcode 13 can have multiple primary files but implements default
// // arguments in a way that no longer requires they be "unhidden".
// if nPrimaries < 2 {
// startUnhide()
// }
// The number of these options must match the number of -primary-file arguments
// which has just been changed to only ever be one so, strip them out
let toRemove = #" -(serialize-diagnostics|emit-(module(-doc|-source-info)?|(reference-)?dependencies|const-values)|index-unit-output)-path "#
compileCommand = compileCommand
.replacingOccurrences(of: toRemove + Self.argumentRegex,
with: "", options: .regularExpression)
debug("Uniqued command:", compileCommand)
// Replace path(s) of all object files to a single one
return super.xcode13Fix(sourceFile: sourceFile,
compileCommand: &compileCommand)
}
/// Per-object file version of unhiding on injection to export some symbols
/// - Parameters:
/// - executable: Path to app executable to extract module name
/// - objcClassRefs: Array to accumulate class referrences
/// - descriptorRefs: Array to accumulate "l.got" references to "fixup"
override func createUnhider(executable: String, _ objcClassRefs: NSMutableArray,
_ descriptorRefs: NSMutableArray) {
let appModule = URL(fileURLWithPath: executable)
.lastPathComponent.replacingOccurrences(of: " ", with: "_")
let appPrefix = "$s\(appModule.count)\(appModule)"
objectUnhider = { object_file in
let logfile = "/tmp/unhide_object.log"
if let log = fopen(logfile, "w") {
setbuf(log, nil)
objcClassRefs.removeAllObjects()
descriptorRefs.removeAllObjects()
unhide_object(object_file, appPrefix, log,
objcClassRefs, descriptorRefs)
// self.log("Unhidden: \(object_file) -- \(appPrefix) -- \(self.objcClassRefs)")
} else {
// self.log("Could not log to \(logfile)")
}
}
}
// Non-essential functionality moved here...
@objc public func rebuild(storyboard: String) throws {
let (_, logsDir) = try determineEnvironment(classNameOrFile: storyboard)
injectionNumber += 1
// messy but fast
guard shell(command: """
# search through build logs, most recent first
cd "\(logsDir.path.escaping("$"))" &&
for log in `ls -t *.xcactivitylog`; do
#echo "Scanning $log"
/usr/bin/env perl <(cat <<'PERL'
use English;
use strict;
# line separator in Xcode logs
$INPUT_RECORD_SEPARATOR = "\\r";
# format is gzip
open GUNZIP, "/usr/bin/gunzip <\\"$ARGV[0]\\" 2>/dev/null |" or die;
# grep the log until to find codesigning for product path
my $realPath;
while (defined (my $line = )) {
if ($line =~ /^\\s*cd /) {
$realPath = $line;
}
elsif (my ($product) = $line =~ m@/usr/bin/ibtool.*? --link (([^\\ ]+\\\\ )*\\S+\\.app)@o) {
print $product;
exit 0;
}
}
# class/file not found
exit 1;
PERL
) "$log" >"\(tmpfile).sh" && exit 0
done
exit 1;
""") else {
throw scriptError("Locating storyboard compile")
}
guard let resources = try? String(contentsOfFile: "\(tmpfile).sh")
.trimmingCharacters(in: .whitespaces) else {
throw scriptError("Locating product")
}
guard shell(command: """
(cd "\(resources.unescape().escaping("$"))" && for i in 1 2 3 4 5; \
do if (find . -name '*.nib' -a -newer "\(storyboard)" | \
grep .nib >/dev/null); then break; fi; sleep 1; done; \
while (ps auxww | grep -v grep | grep "/ibtool " >/dev/null); do sleep 1; done; \
for i in `find . -name '*.nib'`; do cp -rf "$i" "\(
Bundle.main.bundlePath)/$i"; done >"\(logfile)" 2>&1)
""") else {
throw scriptError("Re-compilation")
}
_ = evalError("Copied \(storyboard)")
}
// Assorted bazel code moved out of SwiftEval.swift
override func bazelLink(in projectRoot: String, since sourceFile: String,
compileCommand: String) throws -> String {
guard var objects = bazelFiles(under: projectRoot, where: """
-newer "\(sourceFile)" -a -name '*.o' | \
egrep '(_swift_incremental|_objs)/' | grep -v /external/
""") else {
throw evalError("Finding Objects failed. Did you actually make a change to \(sourceFile) and does it compile? InjectionIII does not support whole module optimization. (check logfile: \(logfile))")
}
debug(bazelLight, projectRoot, objects)
// precendence to incrementally compiled
let incremental = objects.filter({ $0.contains("_swift_incremental") })
if incremental.count > 0 {
objects = incremental
}
#if true
// With WMO, which modifies all object files
// we need a way to filter them down to that
// of the source file and its related module
// library to provide shared "hidden" symbols.
// We use the related Swift "output_file_map"
// for the module name to include its library.
if objects.count > 1 {
let objectSet = Set(objects)
if let maps = bazelFiles(under: projectRoot,
where: "-name '*output_file_map*.json'") {
let relativePath = sourceFile .replacingOccurrences(
of: projectRoot+"/", with: "")
for map in maps {
if let data = try? Data(contentsOf: URL(
fileURLWithPath: projectRoot)
.appendingPathComponent(map)),
let json = try? JSONSerialization.jsonObject(
with: data, options: []) as? [String: Any],
let info = json[relativePath] as? [String: String] {
if let object = info["object"],
objectSet.contains(object) {
objects = [object]
if let module: String =
map[#"(\w+)\.output_file_map"#] {
for lib in bazelFiles(under: projectRoot,
where: "-name 'lib\(module).a'") ?? [] {
moduleLibraries.insert(lib)
}
}
break
}
}
}
} else {
_ = evalError("Error reading maps")
}
}
#endif
objects += moduleLibraries
debug(objects)
try link(dylib: "\(tmpfile).dylib", compileCommand: compileCommand,
contents: objects.map({ "\"\($0)\""}).joined(separator: " "),
cd: projectRoot)
return tmpfile
}
func bazelFiles(under projectRoot: String, where clause: String,
ext: String = "files") -> [String]? {
let list = "\(tmpfile).\(ext)"
if shell(command: """
cd "\(projectRoot)" && \
find bazel-out/* \(clause) >"\(list)" 2>>\"\(logfile)\"
"""), let files = (try? String(contentsOfFile: list))?
.components(separatedBy: "\n").dropLast() {
return Array(files)
}
return nil
}
override func bazelLight(projectRoot: String, recompile sourceFile: String) throws -> String? {
let relativePath = sourceFile.replacingOccurrences(of:
projectRoot+"/", with: "")
let bazelRulesSwift = projectRoot +
"/bazel-out/../external/build_bazel_rules_swift"
let paramsScanner = tmpDir + "/bazel.pl"
debug(projectRoot, relativePath, bazelRulesSwift, paramsScanner)
if !sourceFile.hasSuffix(".swift") {
throw evalError("Only Swift sources can be standalone injected with bazel")
}
try #"""
use JSON::PP;
use English;
use strict;
my ($params, $relative) = @ARGV;
my $args = join('', (IO::File->new( "< $params" )
or die "Could not open response '$params'")->getlines());
my ($filemap) = $args =~ /"-output-file-map"\n"([^"]+)"/;
my $file_handle = IO::File->new( "< $filemap" )
or die "Could not open filemap '$filemap'";
my $json_text = join'', $file_handle->getlines();
my $json_map = decode_json( $json_text, { utf8 => 1 } );
if (my $info = $json_map->{$relative}) {
$args =~ s/"-(emit-(module|objc-header)-path"\n"[^"]+)"\n//g;
my $paramscopy = "$params.copy";
my $paramsfile = IO::File->new("> $paramscopy");
binmode $paramsfile, ':utf8';
$paramsfile->print($args);
$paramsfile->close();
print "$paramscopy\n$info->{object}\n";
exit 0;
}
# source file not found
exit 1;
"""#.write(toFile: paramsScanner,
atomically: false, encoding: .utf8)
let errfile = "\(tmpfile).err"
guard shell(command: """
# search through bazel args, most recent first
cd "\(bazelRulesSwift)" 2>"\(errfile)" &&
grep module_name_ tools/worker/swift_runner.h >/dev/null 2>>"\(errfile)" ||
(git apply -v <<'BAZEL_PATCH' 2>>"\(errfile)" && echo "⚠️ bazel patched, restart app" >>"\(errfile)" && exit 1) &&
diff --git a/tools/worker/swift_runner.cc b/tools/worker/swift_runner.cc
index 535dad0..19e1a6d 100644
--- a/tools/worker/swift_runner.cc
+++ b/tools/worker/swift_runner.cc
@@ -369,6 +369,11 @@ std::vector SwiftRunner::ParseArguments(Iterator itr) {
arg = *it;
output_file_map_path_ = arg;
out_args.push_back(arg);
+ } else if (arg == "-module-name") {
+ ++it;
+ arg = *it;
+ module_name_ = arg;
+ out_args.push_back(arg);
} else if (arg == "-index-store-path") {
++it;
arg = *it;
@@ -410,12 +415,28 @@ std::vector SwiftRunner::ProcessArguments(
++it;
}
+ auto copy = "/tmp/bazel_"+module_name_+".params";
+ unlink(copy.c_str());
if (force_response_file_) {
// Write the processed args to the response file, and push the path to that
// file (preceded by '@') onto the arg list being returned.
auto new_file = WriteResponseFile(response_file_args);
new_args.push_back("@" + new_file->GetPath());
+
+ // patch to retain swiftc arguments file
+ link(new_file->GetPath().c_str(), copy.c_str());
temp_files_.push_back(std::move(new_file));
+ } else if (FILE *fp = fopen("/tmp/forced_params.txt", "w+")) {
+ // alternate patch to capture arguments file
+ for (auto &a : args_destination) {
+ const char *carg = a.c_str();
+ fprintf(fp, "%s\\n", carg);
+ if (carg[0] != '@')
+ continue;
+ link(carg+1, copy.c_str());
+ fprintf(fp, "Linked %s to %s\\n", copy.c_str(), carg+1);
+ }
+ fclose(fp);
}
return new_args;
diff --git a/tools/worker/swift_runner.h b/tools/worker/swift_runner.h
index 952c593..35cf055 100644
--- a/tools/worker/swift_runner.h
+++ b/tools/worker/swift_runner.h
@@ -153,6 +153,9 @@ class SwiftRunner {
// The index store path argument passed to the runner
std::string index_store_path_;
+ // Swift modue name from -module-name
+ std::string module_name_ = "Unknown";
+
// The path of the global index store when using
// swift.use_global_index_store. When set, this is passed to `swiftc` as the
// `-index-store-path`. After running `swiftc` `index-import` copies relevant
BAZEL_PATCH
cd "\(projectRoot)" 2>>"\(errfile)" &&
for params in `ls -t /tmp/bazel_*.params 2>>"\(errfile)"`; do
#echo "Scanning $params"
/usr/bin/env perl "\(paramsScanner)" "$params" "\(relativePath)" \
>"\(tmpfile).sh" 2>>"\(errfile)" && exit 0
done
exit 1;
"""), let returned = (try? String(contentsOfFile: "\(tmpfile).sh"))?
.components(separatedBy: "\n") else {
if let log = try? String(contentsOfFile: errfile), log != "" {
throw evalError(log.contains("ls: /tmp/bazel_*.params") ? """
\(log)Response files not available (see: \(cmdfile))
Edit and save a swift source file and restart app.
""" : """
Locating response file failed (see: \(cmdfile))
\(log)
""")
}
return nil
}
let params = returned[0]
_ = evalError("Compiling using parameters from \(params)")
guard shell(command: """
cd "\(projectRoot)" && \
chmod +w `find bazel-out/* -name '*.o'`; \
xcrun swiftc @\(params) >\"\(logfile)\" 2>&1
""") || shell(command: """
cd `readlink "\(projectRoot)/bazel-out"`/.. && \
chmod +w `find bazel-out/* -name '*.o'`; \
xcrun swiftc @\(params) >>\"\(logfile)\" 2>&1
"""),
let compileCommand = try? String(contentsOfFile: params) else {
throw scriptError("Recompiling")
}
return try bazelLink(in: projectRoot, since: sourceFile,
compileCommand: compileCommand)
}
}
#endif
================================================
FILE: Sources/HotReloading/Vaccine.swift
================================================
#if (DEBUG || !SWIFT_PACKAGE) && !os(watchOS)
#if os(macOS)
import Cocoa
typealias View = NSView
typealias ViewController = NSViewController
typealias ScrollView = NSScrollView
#else
import UIKit
typealias View = UIView
typealias ViewController = UIViewController
typealias ScrollView = UIScrollView
fileprivate extension ViewController {
func viewWillAppear() { self.viewWillAppear(false) }
func viewDidAppear() { self.viewDidAppear(false) }
}
#endif
extension View {
func subviewsRecursive() -> [View] {
return subviews + subviews.flatMap { $0.subviewsRecursive() }
}
}
class Vaccine {
func performInjection(on object: AnyObject) {
switch object {
case let viewController as ViewController:
let snapshotView: View? = createSnapshotViewIfNeeded(for: viewController)
if viewController.nibName == nil {
CATransaction.begin()
CATransaction.setAnimationDuration(1.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
defer { CATransaction.commit() }
}
let oldScrollViews = indexScrollViews(on: viewController)
#if os(macOS)
reload(viewController.parent ?? viewController)
#else
// Opt-out from performing Vaccine reloads on parent
// if the parent is either a navigation controller or
// a tab bar controller.
if let parentViewController = viewController.parent {
if parentViewController is UINavigationController {
reload(viewController)
} else if parentViewController is UITabBarController {
reload(viewController)
} else {
reload(parentViewController)
}
} else {
reload(viewController)
}
#endif
syncOldScrollViews(oldScrollViews, with: indexScrollViews(on: viewController))
cleanSnapshotViewIfNeeded(snapshotView, viewController: viewController)
case let view as View:
reload(view)
default:
break
}
}
private func reload(_ viewController: ViewController) {
viewController.view.subviews.forEach { $0.removeFromSuperview() }
clean(view: viewController.view)
viewController.loadView()
viewController.viewDidLoad()
viewController.viewWillAppear()
viewController.viewDidAppear()
refreshSubviews(on: viewController.view)
}
private func reload(_ view: View) {
let selector = _Selector("loadView")
guard view.responds(to: selector) == true else { return }
#if os(macOS)
view.animator().perform(selector)
#else
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.allowAnimatedContent,
.beginFromCurrentState,
.layoutSubviews], animations: {
view.perform(selector)
}, completion: nil)
#endif
}
private func createSnapshotViewIfNeeded(for viewController: ViewController) -> View? {
if let snapshotView = createSnapshot(from: viewController.view), viewController.nibName == nil {
#if os(macOS)
viewController.view.window?.contentView?.addSubview(snapshotView)
#else
let maskView = UIView()
maskView.frame.size = snapshotView.frame.size
maskView.frame.origin.y = viewController.navigationController?.navigationBar.frame.maxY ?? 0
maskView.backgroundColor = .white
snapshotView.mask = maskView
viewController.view.window?.addSubview(snapshotView)
#endif
return snapshotView
}
return nil
}
private func cleanSnapshotViewIfNeeded(_ snapshotView: View?, viewController: ViewController) {
if let snapshotView = snapshotView, viewController.nibName == nil {
#if os(macOS)
NSAnimationContext.runAnimationGroup({ (context) in
context.allowsImplicitAnimation = true
context.duration = 0.25
snapshotView.animator().alphaValue = 0.0
}, completionHandler: {
snapshotView.removeFromSuperview()
})
#else
UIView.animate(withDuration: 0.25,
delay: 0.0,
options: [.allowAnimatedContent,
.beginFromCurrentState,
.layoutSubviews],
animations: {
snapshotView.alpha = 0.0
}) { _ in
snapshotView.removeFromSuperview()
}
#endif
}
}
private func clean(view: View) {
view.subviews.forEach { $0.removeFromSuperview() }
#if os(macOS)
if let sublayers = view.layer?.sublayers {
sublayers.forEach { $0.removeFromSuperlayer() }
}
#else
if let sublayers = view.layer.sublayers {
sublayers.forEach { $0.removeFromSuperlayer() }
}
#endif
}
private func refreshSubviews(on view: View) {
#if os(macOS)
view.subviewsRecursive().forEach { view in
(view as? NSTableView)?.reloadData()
(view as? NSCollectionView)?.reloadData()
view.needsLayout = true
view.layout()
view.needsDisplay = true
view.display()
}
#else
view.subviewsRecursive().forEach { view in
(view as? UITableView)?.reloadData()
(view as? UICollectionView)?.reloadData()
view.setNeedsLayout()
view.layoutIfNeeded()
view.setNeedsDisplay()
}
#endif
}
private func indexScrollViews(on viewController: ViewController) -> [ScrollView] {
var scrollViews = [ScrollView]()
for case let scrollView as ScrollView in viewController.view.subviews {
scrollViews.append(scrollView)
}
if let parentViewController = viewController.parent {
for case let scrollView as ScrollView in parentViewController.view.subviews {
scrollViews.append(scrollView)
}
}
for childViewController in viewController.children {
for case let scrollView as ScrollView in childViewController.view.subviews {
scrollViews.append(scrollView)
}
}
return scrollViews
}
private func syncOldScrollViews(_ oldScrollViews: [ScrollView], with newScrollViews: [ScrollView]) {
for (offset, scrollView) in newScrollViews.enumerated() {
if offset < oldScrollViews.count {
let oldScrollView = oldScrollViews[offset]
if type(of: scrollView) == type(of: oldScrollView) {
#if os(macOS)
scrollView.contentView.scroll(to: oldScrollView.documentVisibleRect.origin)
#else
scrollView.contentOffset = oldScrollView.contentOffset
#endif
}
}
}
}
private func createSnapshot(from view: View) -> View? {
#if os(macOS)
let snapshot = NSImageView()
snapshot.image = view.snapshot
snapshot.frame.size = view.frame.size
return snapshot
#else
return view.snapshotView(afterScreenUpdates: true)
#endif
}
private func _Selector(_ string: String) -> Selector {
return Selector(string)
}
}
#if os(macOS)
fileprivate extension NSView {
var snapshot: NSImage {
guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { return NSImage() }
cacheDisplay(in: bounds, to: bitmapRep)
let image = NSImage()
image.addRepresentation(bitmapRep)
bitmapRep.size = bounds.size.doubleScale()
return image
}
}
fileprivate extension CGSize {
func doubleScale() -> CGSize {
return CGSize(width: width * 2, height: height * 2)
}
}
#endif
#endif
================================================
FILE: Sources/HotReloadingGuts/ClientBoot.mm
================================================
//
// ClientBoot.mm
// InjectionIII
//
// Created by John Holdsworth on 02/24/2021.
// Copyright © 2021 John Holdsworth. All rights reserved.
//
// $Id: //depot/HotReloading/Sources/HotReloadingGuts/ClientBoot.mm#131 $
//
// Initiate connection to server side of InjectionIII/HotReloading.
//
#if DEBUG || !SWIFT_PACKAGE
#import "InjectionClient.h"
#import
#import
#import "SimpleSocket.h"
#import
#ifndef INJECTION_III_APP
NSString *INJECTION_KEY = @__FILE__;
#endif
#if defined(DEBUG) || defined(INJECTION_III_APP)
static SimpleSocket *injectionClient;
NSString *injectionHost = @"127.0.0.1";
static dispatch_once_t onlyOneClient;
@implementation SimpleSocket(Connect)
+ (void)backgroundConnect:(const char *)host {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
if (SimpleSocket *client = [[self class] connectTo:[NSString
stringWithFormat:@"%s%@", host, @INJECTION_ADDRESS]])
dispatch_once(&onlyOneClient, ^{
injectionHost = [NSString stringWithUTF8String:host];
[injectionClient = client run];
});
});
}
@end
@interface BundleInjection: NSObject
@end
@implementation BundleInjection
extern "C" {
extern void hookKeyPaths(void *original, void *replacement);
extern const void *swift_getKeyPath(void *, const void *);
extern const void *injection_getKeyPath(void *, const void *);
extern void injection_hookGenerics(void *original, void *replacement);
extern Class swift_allocateGenericClassMetadata(void *, void *, void *);
extern Class injection_allocateGenericClassMetadata(void *, void *, void *);
}
+ (void)load {
// If the custom TEST_HOST used to load XCTestBundle, and this custom TEST_HOST is a normal iOS/macOS/tvOS app,
// then XCTestCase can instantiate NSWindow / UIWindow, make it key and visible, pause and wait for
// standard expectation – XCTestExpectation. This untypical flow used for per-screen UI development without need
// to launch full app. To support this flow we are allowing injection by respecting dedicated environment variable.
bool shouldUseInTests = getenv(INJECTION_USEINTESTS) != nullptr;
// See: https://forums.developer.apple.com/forums/thread/761439
bool isPreviewsDetected = getenv("XCODE_RUNNING_FOR_PREVIEWS") != nullptr;
// See: https://github.com/pointfreeco/swift-issue-reporting/blob/main/Sources/IssueReporting/IsTesting.swift#L29
bool isTestsDetected = getenv("XCTestBundlePath") ||
getenv("XCTestSessionIdentifier") ||
getenv("XCTestConfigurationFilePath");
if (isPreviewsDetected || (isTestsDetected && !shouldUseInTests))
return; // inhibit in previews or when running tests, unless explicitly enabled in tests.
#if !(SWIFT_PACKAGE && TARGET_OS_OSX)
if (!getenv(INJECTION_NOKEYPATHS) && (getenv(INJECTION_KEYPATHS)
#if !SWIFT_PACKAGE
|| dlsym(RTLD_DEFAULT, "$s22ComposableArchitecture6LoggerCN")
#endif
))
hookKeyPaths((void *)swift_getKeyPath, (void *)injection_getKeyPath);
#endif
#if !SWIFT_PACKAGE
if (!getenv(INJECTION_NOGENERICS) && !getenv(INJECTION_OF_GENERICS))
injection_hookGenerics((void *)swift_allocateGenericClassMetadata,
(void *)injection_allocateGenericClassMetadata);
#else
printf(APP_PREFIX"⚠️ Define env var " INJECTION_OF_GENERICS
" in your scheme to inject generic classes.\n");
#endif
if (Class clientClass = objc_getClass("InjectionClient"))
[self performSelectorInBackground:@selector(tryConnect:)
withObject:clientClass];
}
+ (void)tryConnect:(Class)clientClass {
NSString *socketAddr = @INJECTION_ADDRESS;
__unused const char *buildPhase = APP_PREFIX"You'll need to be running "
"a recent copy of the InjectionIII.app downloaded from "
"https://github.com/johnno1962/InjectionIII/releases\n"
APP_PREFIX"And have typed: defaults write com.johnholdsworth.InjectionIII deviceUnlock any\n";
BOOL isVapor = dlsym(RTLD_DEFAULT, VAPOR_SYMBOL) != nullptr;
#if TARGET_IPHONE_SIMULATOR || TARGET_OS_OSX
#if 0 && !defined(INJECTION_III_APP)
BOOL isiOSAppOnMac = false;
if (@available(iOS 14.0, *)) {
isiOSAppOnMac = [NSProcessInfo processInfo].isiOSAppOnMac;
}
if (!isiOSAppOnMac && !isVapor && !getenv(INJECTION_DAEMON))
if (Class standalone = objc_getClass("StandaloneInjection")) {
[[standalone new] run];
return;
}
#endif
#elif TARGET_OS_IPHONE
const char *envHost = getenv(INJECTION_HOST);
if (envHost)
[clientClass backgroundConnect:envHost];
#ifdef DEVELOPER_HOST
[clientClass backgroundConnect:DEVELOPER_HOST];
if (!isdigit(DEVELOPER_HOST[0]) && !envHost)
printf(APP_PREFIX"Sending broadcast packet to connect to your development host %s.\n"
APP_PREFIX"If this fails, hardcode your Mac's IP address in HotReloading/Package.swift\n"
" or add an environment variable " INJECTION_HOST
" with this value.\n%s", DEVELOPER_HOST, buildPhase);
#endif
if (!(@available(iOS 14.0, *) && [NSProcessInfo processInfo].isiOSAppOnMac))
injectionHost = [clientClass
getMulticastService:HOTRELOADING_MULTICAST port:HOTRELOADING_PORT
message:APP_PREFIX"Connecting to %s (%s)...\n"];
socketAddr = [injectionHost stringByAppendingString:@HOTRELOADING_PORT];
if (injectionClient)
return;
#endif
for (int retry=0, retrys=1; retry
#include
#include
#include
#include
#if 0
#define SLog NSLog
#else
#define SLog while(0) NSLog
#endif
#define MAX_PACKET 16384
typedef union {
struct {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
};
struct sockaddr_storage any;
struct sockaddr_in ip4;
struct sockaddr addr;
} sockaddr_union;
@implementation SimpleSocket
+ (int)error:(NSString *)message {
NSLog([@"%@/" stringByAppendingString:message],
self, strerror(errno));
return -1;
}
+ (void)startServer:(NSString *)address {
[self performSelectorInBackground:@selector(runServer:) withObject:address];
}
+ (void)forEachInterface:(void (^)(ifaddrs *ifa, in_addr_t addr, in_addr_t mask))handler {
ifaddrs *addrs;
if (getifaddrs(&addrs) < 0) {
[self error:@"Could not getifaddrs: %s"];
return;
}
for (ifaddrs *ifa = addrs; ifa; ifa = ifa->ifa_next)
if (ifa->ifa_addr->sa_family == AF_INET)
handler(ifa, ((struct sockaddr_in *)ifa->ifa_addr)->sin_addr.s_addr,
((struct sockaddr_in *)ifa->ifa_netmask)->sin_addr.s_addr);
freeifaddrs(addrs);
}
+ (void)runServer:(NSString *)address {
sockaddr_union serverAddr;
[self parseV4Address:address into:&serverAddr.any];
int serverSocket = [self newSocket:serverAddr.sa_family];
if (serverSocket < 0)
return;
if (bind(serverSocket, &serverAddr.addr, serverAddr.sa_len) < 0)
[self error:@"Could not bind service socket: %s"];
else if (listen(serverSocket, 5) < 0)
[self error:@"Service socket would not listen: %s"];
else
while (TRUE) {
sockaddr_union clientAddr;
socklen_t addrLen = sizeof clientAddr;
int clientSocket = accept(serverSocket, &clientAddr.addr, &addrLen);
if (clientSocket > 0) {
int yes = 1;
if (setsockopt(clientSocket, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof yes) < 0)
[self error:@"Could not set SO_NOSIGPIPE: %s"];
@autoreleasepool {
struct sockaddr_in *v4Addr = &clientAddr.ip4;
NSLog(@"Connection from %s:%d\n",
inet_ntoa(v4Addr->sin_addr), ntohs(v4Addr->sin_port));
SimpleSocket *client = [[self alloc] initSocket:clientSocket];
client.isLocalClient =
v4Addr->sin_addr.s_addr == htonl(INADDR_LOOPBACK);
[self forEachInterface:^(ifaddrs *ifa, in_addr_t addr, in_addr_t mask) {
if (v4Addr->sin_addr.s_addr == addr)
client.isLocalClient = TRUE;
}];
[client run];
}
}
else
[NSThread sleepForTimeInterval:.5];
}
}
+ (instancetype)connectTo:(NSString *)address {
sockaddr_union serverAddr;
[self parseV4Address:address into:&serverAddr.any];
int clientSocket = [self newSocket:serverAddr.sa_family];
if (clientSocket < 0)
return nil;
if (connect(clientSocket, &serverAddr.addr, serverAddr.sa_len) < 0) {
[self error:@"Could not connect: %s"];
return nil;
}
return [[self alloc] initSocket:clientSocket];
}
+ (int)newSocket:(sa_family_t)addressFamily {
int newSocket, yes = 1;
if ((newSocket = socket(addressFamily, SOCK_STREAM, 0)) < 0)
[self error:@"Could not open service socket: %s"];
else if (setsockopt(newSocket, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof yes) < 0)
[self error:@"Could not set SO_REUSEADDR: %s"];
else if (setsockopt(newSocket, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof yes) < 0)
[self error:@"Could not set SO_NOSIGPIPE: %s"];
else if (setsockopt(newSocket, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof yes) < 0)
[self error:@"Could not set TCP_NODELAY: %s"];
else if (fcntl(newSocket, F_SETFD, FD_CLOEXEC) < 0)
[self error:@"Could not set FD_CLOEXEC: %s"];
else
return newSocket;
return -1;
}
/**
* Available formats
* @"[:]"
* where can be NNN.NNN.NNN.NNN or hostname, empty for localhost or * for all interfaces
* The default port is 80 or a specific number to bind or an empty string to allocate any port
*/
+ (BOOL)parseV4Address:(NSString *)address into:(struct sockaddr_storage *)serverAddr {
NSArray *parts = [address componentsSeparatedByString:@":"];
struct sockaddr_in *v4Addr = (struct sockaddr_in *)serverAddr;
bzero(v4Addr, sizeof *v4Addr);
v4Addr->sin_family = AF_INET;
v4Addr->sin_len = sizeof *v4Addr;
v4Addr->sin_port = htons(parts.count > 1 ? parts[1].intValue : 80);
const char *host = parts[0].UTF8String;
if (!host[0])
v4Addr->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
else if (host[0] == '*')
v4Addr->sin_addr.s_addr = htonl(INADDR_ANY);
else if (isdigit(host[0]))
v4Addr->sin_addr.s_addr = inet_addr(host);
else if (struct hostent *hp = gethostbyname2(host, v4Addr->sin_family))
memcpy(&v4Addr->sin_addr, hp->h_addr, hp->h_length);
else {
[self error:[NSString stringWithFormat:@"Unable to look up host for %@", address]];
return FALSE;
}
return TRUE;
}
- (instancetype)initSocket:(int)socket {
if ((self = [super init])) {
clientSocket = socket;
}
return self;
}
- (void)run {
[self performSelectorInBackground:@selector(runInBackground) withObject:nil];
}
- (void)runInBackground {
[[self class] error:@"-[SimpleSocket runInBackground] not implemented in subclass"];
}
typedef ssize_t (*io_func)(int, void *, size_t);
- (BOOL)perform:(io_func)io ofBytes:(const void *)buffer
length:(size_t)length cmd:(SEL)cmd {
size_t bytes, ptr = 0;
SLog(@"#%d %s %lu [%p] %s", clientSocket, io == read ?
"<-" : "->", length, buffer, sel_getName(cmd));
while (ptr < length && (bytes = io(clientSocket,
(char *)buffer+ptr, MIN(length-ptr, MAX_PACKET))) > 0)
ptr += bytes;
if (ptr < length) {
NSLog(@"[%@ %s:%p length:%lu] error: %lu %s",
self, sel_getName(cmd), buffer, length, ptr, strerror(errno));
return FALSE;
}
return TRUE;
}
- (BOOL)readBytes:(void *)buffer length:(size_t)length cmd:(SEL)cmd {
return [self perform:read ofBytes:buffer length:length cmd:cmd];
}
- (int)readInt {
int32_t anint = ~0;
if (![self readBytes:&anint length:sizeof anint cmd:_cmd])
return ~0;
SLog(@"#%d <- %d", clientSocket, anint);
return anint;
}
- (void *)readPointer {
void *aptr = (void *)~0;
if (![self readBytes:&aptr length:sizeof aptr cmd:_cmd])
return aptr;
SLog(@"#%d <- %p", clientSocket, aptr);
return aptr;
}
- (NSData *)readData {
size_t length = [self readInt];
void *bytes = malloc(length);
if (!bytes || ![self readBytes:bytes length:length cmd:_cmd])
return nil;
return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:YES];
}
- (NSString *)readString {
NSString *str = [[NSString alloc] initWithData:[self readData]
encoding:NSUTF8StringEncoding];
SLog(@"#%d <- %d '%@'", clientSocket, (int)str.length, str);
return str;
}
- (BOOL)writeBytes:(const void *)buffer length:(size_t)length cmd:(SEL)cmd {
return [self perform:(io_func)write ofBytes:buffer length:length cmd:cmd];
}
- (BOOL)writeInt:(int)length {
SLog(@"#%d %d ->", clientSocket, length);
return [self writeBytes:&length length:sizeof length cmd:_cmd];
}
- (BOOL)writePointer:(void *)ptr {
SLog(@"#%d %p ->", clientSocket, ptr);
return [self writeBytes:&ptr length:sizeof ptr cmd:_cmd];
}
- (BOOL)writeData:(NSData *)data {
uint32_t length = (uint32_t)data.length;
SLog(@"#%d [%d] ->", clientSocket, length);
return [self writeInt:length] &&
[self writeBytes:data.bytes length:length cmd:_cmd];
}
- (BOOL)writeString:(NSString *)string {
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
SLog(@"#%d %d '%@' ->", clientSocket, (int)data.length, string);
return [self writeData:data];
}
- (BOOL)writeCommand:(int)command withString:(NSString *)string {
return [self writeInt:command] &&
(!string || [self writeString:string]);
}
- (void)dealloc {
close(clientSocket);
}
/// Hash used to differentiate HotReloading users on network.
/// Derived from path to source file in project's DerivedData.
+ (int)multicastHash {
#ifdef INJECTION_III_APP
const char *key = [[NSBundle bundleForClass:self]
.infoDictionary[@"UserHome"] UTF8String] ?:
NSHomeDirectory().UTF8String;
#else
NSString *file = [NSString stringWithUTF8String:__FILE__];
const char *key = [file
stringByReplacingOccurrencesOfString: @"(/Users/[^/]+).*"
withString: @"$1" options: NSRegularExpressionSearch
range: NSMakeRange(0, file.length)].UTF8String;
#endif
int hash = 0;
for (size_t i=0, len = strlen(key); i> 24) {
case 10: // mobile network
// case 172: // hotspot
case 127: // loopback
return;
}
int idx = if_nametoindex(ifa->ifa_name);
setsockopt(multicastSocket, IPPROTO_IP, IP_BOUND_IF, &idx, sizeof idx);
addr.sin_addr.s_addr = laddr | ~nmask;
printf("Broadcasting to %s.%d:%s to locate InjectionIII host...\n",
ifa->ifa_name, idx, inet_ntoa(addr.sin_addr));
if (sendto(multicastSocket, &msgbuf, sizeof msgbuf, 0,
(struct sockaddr *)&addr, sizeof addr) < 0)
[self error:@"Could not send broadcast ping: %s"];
}];
socklen_t addrlen = sizeof addr;
while (recvfrom(multicastSocket, &msgbuf, sizeof msgbuf, 0,
(struct sockaddr *)&addr, &addrlen) < sizeof msgbuf) {
[self error:@"%s: Error receiving from broadcast: %s"];
sleep(1);
}
const char *ipaddr = inet_ntoa(addr.sin_addr);
printf(format, msgbuf.host, ipaddr);
close(multicastSocket);
return [NSString stringWithUTF8String:ipaddr];
}
@end
#endif
================================================
FILE: Sources/HotReloadingGuts/Unhide.mm
================================================
//
// Unhide.mm
//
// Created by John Holdsworth on 07/03/2021.
//
// Removes "hidden" visibility for certain Swift symbols
// (default argument generators) so they can be referenced
// in a file being dynamically loaded.
//
// $Id: //depot/HotReloading/Sources/HotReloadingGuts/Unhide.mm#52 $
//
#if DEBUG || !SWIFT_PACKAGE
#import
#import
#import
#import
#import
#import
#import
#import
#import