Repository: johnno1962/HotReloading Branch: main Commit: b8a80e54f268 Files: 55 Total size: 423.3 KB Directory structure: gitextract_6t44zbop/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── Contents/ │ ├── Info.plist │ ├── PkgInfo │ └── Resources/ │ ├── Base.lproj/ │ │ ├── MainMenu.nib │ │ ├── RMWindowController.nib │ │ ├── XprobeConsole.nib │ │ └── XprobePluginMenuController.nib │ ├── Credits.rtf │ ├── InjectionBusy.tif │ ├── InjectionError.tif │ ├── InjectionIdle.tif │ ├── InjectionOK.tif │ ├── LICENSE │ ├── README.md │ ├── SwiftTrace.h │ ├── fishhook.h │ ├── graph.gv │ └── log.html ├── LICENSE ├── Package.swift ├── README.md ├── Sources/ │ ├── HotReloading/ │ │ ├── DeviceInjection.swift │ │ ├── DynamicCast.swift │ │ ├── FileWatcher.swift │ │ ├── InjectionClient.swift │ │ ├── InjectionStats.swift │ │ ├── ObjcInjection.swift │ │ ├── ReducerInjection.swift │ │ ├── StandaloneInjection.swift │ │ ├── SwiftEval.swift │ │ ├── SwiftInjection.swift │ │ ├── SwiftInterpose.swift │ │ ├── SwiftKeyPath.swift │ │ ├── SwiftSweeper.swift │ │ ├── UnhidingEval.swift │ │ └── Vaccine.swift │ ├── HotReloadingGuts/ │ │ ├── ClientBoot.mm │ │ ├── SimpleSocket.mm │ │ ├── Unhide.mm │ │ └── include/ │ │ ├── InjectionClient.h │ │ ├── SimpleSocket.h │ │ └── UserDefaults.h │ ├── injectiond/ │ │ ├── AppDelegate.swift │ │ ├── DeviceServer.swift │ │ ├── Experimental.swift │ │ ├── InjectionServer.swift │ │ ├── UpdateCheck.swift │ │ └── main.swift │ └── injectiondGuts/ │ ├── SignerService.m │ └── include/ │ ├── SignerService.h │ └── Xcode.h ├── copy_bundle.sh ├── fix_previews.sh └── start_daemon.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: johnno1962 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ## Obj-C/Swift specific *.hmap ## App packaging *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins Package.resolved # *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project .swiftpm .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build/ # Accio dependency management Dependencies/ .accio/ # fastlane # # It is recommended to not store the screenshots in the git repo. # Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ ================================================ FILE: Contents/Info.plist ================================================ BuildMachineOSBuild 20D74 CFBundleDevelopmentRegion en CFBundleExecutable InjectionIII CFBundleIconFile App.icns CFBundleIdentifier com.johnholdsworth.InjectionIII CFBundleInfoDictionaryVersion 6.0 CFBundleName 🔥 HotReloading CFBundlePackageType APPL CFBundleShortVersionString 2.6.0 CFBundleSupportedPlatforms MacOSX CFBundleVersion 6076 DTCompiler com.apple.compilers.llvm.clang.1_0 DTPlatformBuild 12D4e DTPlatformName macosx DTPlatformVersion 11.1 DTSDKBuild 20C63 DTSDKName macosx11.1 DTXcode 1240 DTXcodeBuild 12D4e LSApplicationCategoryType public.app-category.developer-tools LSMinimumSystemVersion 10.12 LSUIElement NSHumanReadableCopyright Copyright © 2017-20 John Holdsworth. All rights reserved. NSMainNibFile MainMenu NSPrincipalClass NSApplication NSServices NSMenuItem default Injection Goto NSMessage injectionGoto NSPortName InjectionIII NSSendTypes NSStringPboardType SMPrivilegedExecutables com.johnholdsworth.InjectionIII.Helper identifier com.johnholdsworth.InjectionIII.Helper ================================================ FILE: Contents/PkgInfo ================================================ APPL???? ================================================ FILE: Contents/Resources/Credits.rtf ================================================ {\rtf1\ansi\ansicpg1252\cocoartf1561\cocoasubrtf600 {\fonttbl\f0\fswiss\fcharset0 Helvetica;\f1\fnil\fcharset0 Menlo-Regular;\f2\fmodern\fcharset0 Courier; \f3\fnil\fcharset0 HelveticaNeue;} {\colortbl;\red255\green255\blue255;\red63\green110\blue116;\red255\green255\blue255;\red83\green98\blue108; \red0\green0\blue0;\red131\green108\blue40;\red14\green14\blue255;} {\*\expandedcolortbl;;\csgenericrgb\c24700\c43100\c45600;\csgenericrgb\c100000\c100000\c100000;\csgenericrgb\c32549\c38431\c42353; \csgenericrgb\c0\c0\c0;\csgenericrgb\c51200\c42300\c15700;\csgenericrgb\c5500\c5500\c100000;} \paperw11900\paperh16840\vieww9600\viewh8400\viewkind0 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0 \f0\fs24 \cf0 InjectionIII.app is a mostly Swift rewrite of {\field{\*\fldinst{HYPERLINK "https://github.com/johnno1962/injectionforxcode"}}{\fldrslt \f1\fs22 injectionforxcode}} that runs in the menu bar. It works slightly differently from previous versions of injection in that it uses a file watcher to detect when a user saves a file in the current project to inject it. You can either use the "Start Injection" menu item to bootstrap injection into your application or include the following code in your application:\ \ \pard\tx543\pardeftab543\pardirnatural\partightenfactor0 \f2 \cf2 \cb3 #if DEBUG\cf0 \ \cf2 Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()\cf0 \ \cf2 //for tvOS:\cf0 \ \cf2 Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()\cf0 \ \cf2 //Or for macOS:\cf0 \ \cf2 Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()\cf0 \ \cf2 #endif\cf0 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardeftab543\partightenfactor0 \f0 \cf0 \cb1 \ Or, to use with Xcode 10:\ \ \pard\tx543\pardeftab543\pardirnatural\partightenfactor0 \f2 \cf2 \cb3 #if DEBUG\cf0 \ \cf2 Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection10.bundle")?.load()\cf0 \ \cf2 //for tvOS:\cf0 \ \cf2 Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection10.bundle")?.load()\cf0 \ \cf2 //Or for macOS:\cf0 \ \cf2 Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection10.bundle")?.load()\cf0 \ \cf2 #endif\cf0 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardeftab543\partightenfactor0 \f0 \cf0 \cb1 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0 \cf0 For further help go to {\field{\*\fldinst{HYPERLINK "https://github.com/johnno1962/InjectionIII"}}{\fldrslt https://github.com/johnno1962/InjectionIII}} or {\field{\*\fldinst{HYPERLINK "http://johnholdsworth.com/injection.html"}}{\fldrslt http://johnholdsworth.com/injection.html}}\ \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 \f1\fs22 \cf0 Copyright (C) 2016-7 John Holdsworth {\field{\*\fldinst{HYPERLINK "mailto:injectionIII@johnholdsworth.com"}}{\fldrslt injectionIII@johnholdsworth.com}} {\field{\*\fldinst{HYPERLINK "https://twitter.com/Injection4Xcode"}}{\fldrslt \fs24 \cf4 \expnd0\expndtw0\kerning0 @Injection4Xcode}}\ \ \pard\pardeftab720\partightenfactor0 \fs24 \cf0 \expnd0\expndtw0\kerning0 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.\ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 \fs22 \cf0 \kerning1\expnd0\expndtw0 \ \pard\tx543\pardeftab543\pardirnatural\partightenfactor0 \f3\fs26 \cf5 \cb3 This release includes a very slightly modified version of the excellent \f0\fs24 \cf0 \ \f3\fs26 \cf6 [\cf5 canviz\cf6 ](\cf7 https://code.google.com/p/canviz/\cf6 )\cf5 library to render "dot" files \f0\fs24 \cf0 \ \f3\fs26 \cf5 in an HTML canvas which is subject to an MIT license. The changes are to pass \f0\fs24 \cf0 \ \f3\fs26 \cf5 through the ID of the node to the node label tag (line 212), to reverse \f0\fs24 \cf0 \ \f3\fs26 \cf5 the rendering of nodes and the lines linking them (line 406) and to \f0\fs24 \cf0 \ \f3\fs26 \cf5 store edge paths so they can be colored (line 66 and 303) in "canviz-0.1/canviz.js". \f0\fs24 \cf0 \ \ \f3\fs26 \cf5 It now also includes \cf6 [\cf5 CodeMirror\cf6 ](\cf7 http://codemirror.net/\cf6 )\cf5 JavaScript editor \f0\fs24 \cf0 \ \f3\fs26 \cf5 for the code to be evaluated using injection under an MIT license. \f0\fs24 \cf0 \ } ================================================ FILE: Contents/Resources/LICENSE ================================================ MIT License Copyright (c) 2017 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: Contents/Resources/README.md ================================================ # InjectionIII - overdue Swift rewrite of InjectionForXcode ![Icon](http://johnholdsworth.com/Syringe_128.png) Code injection allows you to update the implementation of functions and any method of a class, struct or enum incrementally in the iOS simulator without having to rebuild or restart your application. This saves the developer a significant amount of time tweaking code or iterating over a design. This start-over implementation of [Injection for Xcode](https://github.com/johnno1962/injectionforxcode) has been built into a standalone app: `InjectionIII.app` which runs in the status bar and is [available from the Mac App Store](https://itunes.apple.com/app/injectioniii/id1380446739?mt=12). This README includes descriptions of some newer features that are only available in more recent releases of the InjectionIII.app [available on github](https://github.com/johnno1962/InjectionIII/releases). You will need to use one of these releases for Apple Silicon or if you have upgraded to Big Sur due to changes to macOS codesigning that affect the sandboxed App Store version of the app. ![Icon](http://johnholdsworth.com/InjectionUI.gif) `InjectionIII.app` needs an Xcode 10.2 or greater at the path `/Applications/Xcode.app` , works for `Swift` and `Objective-C` and can be used alongside [AppCode](https://www.jetbrains.com/help/objc/create-a-swiftui-application.html) or by using the [AppCode Plugin](https://github.com/johnno1962/InjectionIII/blob/master/AppCodePlugin/INSTALL.md). To understand how InjectionIII works and the techniques it uses consult the book [Swift Secrets](http://books.apple.com/us/book/id1551005489). ### Getting Started To use injection, download the app from the App Store and run it. Then, you must add `"-Xlinker -interposable"` (without the double quotes) to your project's `"Other Linker Flags"` for the Debug target (qualified by the simulator SDK to avoid complications with bitcode). Finally, add one of the following to your application delegate's `applicationDidFinishLaunching:` Xcode 10.2 and later (Swift 5+): ```Swift #if DEBUG Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load() //for tvOS: Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load() //Or for macOS: Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load() #endif ``` Adding one of these lines loads a bundle included in the `InjectionIII.app`'s resources which connects over a localhost socket to the InjectionII app which runs on the task bar. Once injection is connected, you'll be prompted to select the directory containing the project file for the app you wish to inject. This starts a `file watcher` for that directory inside the Mac app so whenever you save to disk a Swift (or Objective-C) source in the project, the target app is messaged through the socket to compile, link, dynamically load and update the implementation of methods in the file being injected. If your project is organised across multiple directories or the project file is not at the root of the source tree you can add other directories to be watched for file changes using the "Add Directory" menu item. This list resets when you select a new project. The file watcher can be disabled & enabled while the app is running using the status bar men. While the file watcher is disabled you can still force injections through manually using a hotkey `ctrl-=` (remember to save the file first!) If you inject a subclass of `XCTest` InjectionIII will try running that individual test inside your application provided has been compiled at some time in the past and doesn't require test specific support code. When you run your application without rebuilding (^⌘R), recent injections will be re-applied. You can detect when a *class* has been injected in your code (to reload a view controller for example) by adding an `@objc func injected()` class or instance method. The instance `@objc func injected()` method relies on a "sweep" of all objects in your application to find those of the class you have just injected which can be unreliable when using `unowned` instance variables. If you encounter problems, remomve the injected() method and subscribe to the `"INJECTION_BUNDLE_NOTIFICATION"` instead along the lines of the following: ``` NotificationCenter.default.addObserver(self, selector: #selector(configureView), name: Notification.Name("INJECTION_BUNDLE_NOTIFICATION"), object: nil) ``` Included in this release is "Xprobe" which allows you to browse and inspect the objects in your application through a web-like interface and execute code against them. Enter text into the search textfield to locate objects quickly by class name. If you want to build this project from source (which you may need to do to use injection with macOS apps) you'll need to use: git clone https://github.com/johnno1962/InjectionIII --recurse-submodules ### Available downloads | Xcode 10.2+ | For Big Sur | AppCode Plugin | | ------------- | ------------- | ------------- | | [Mac app store](https://itunes.apple.com/app/injectioniii/id1380446739?mt=12) | [Github Releases](https://github.com/johnno1962/InjectionIII/releases) | [Install Injection.jar](https://github.com/johnno1962/InjectionIII/tree/master/AppCodePlugin) | ### Limitations/FAQ New releases of InjectionIII use a [different patching technique](http://johnholdsworth.com/dyld_dynamic_interpose.html) than previous versions in that you can now update the implementations of class, struct and enum methods (final or not) provided they have not been inlined which shouldn't be the case for a debug build. You can't however alter the layout of a class or struct in the course of an injection i.e. add or rearrange properties with storage or add or move methods of a non-final class or your app will likely crash. Also, see the notes below for injecting `SwiftUI` views and how they require type erasure. If you have a complex project including Objective-C or C dependancies, using the `-interposable` flag may provoke the following error on linking: ``` Can't find ordinal for imported symbol for architecture x86_64 ``` If this is the case, add the following additional "Other linker Flags" and it should go away. ``` -Xlinker -undefined -Xlinker dynamic_lookup ``` If you inject code which calls a function with default arguments you may get an error starting as follows reporting an undefined symbol: ``` 💉 *** dlopen() error: dlopen(/var/folders/nh/gqmp6jxn4tn2tyhwqdcwcpkc0000gn/T/com.johnholdsworth.InjectionIII/eval101.dylib, 2): Symbol not found: _$s13TestInjection15QTNavigationRowC4text10detailText4icon6object13customization6action21accessoryButtonActionACyxGSS_AA08QTDetailG0OAA6QTIconOSgypSgySo15UITableViewCellC_AA5QTRow_AA0T5StyleptcSgyAaT_pcSgAWtcfcfA1_ Referenced from: /var/folders/nh/gqmp6jxn4tn2tyhwqdcwcpkc0000gn/T/com.johnholdsworth.InjectionIII/eval101.dylib Expected in: flat namespace in /var/folders/nh/gqmp6jxn4tn2tyhwqdcwcpkc0000gn/T/com.johnholdsworth.InjectionIII/eval101.dylib *** ``` If you encounter this problem, download and build [the unhide project](https://github.com/johnno1962/unhide) then add the following as a "Run Script", "Build Phase" to your project after the linking phase: ``` UNHIDE=~/bin/unhide.sh if [ -f $UNHIDE ]; then $UNHIDE else echo "File $UNHIDE used for code Injection does not exist. Download and build the https://github.com/johnno1962/unhide project." fi ``` This changes the visibility of symbols for default argument generators and this issue should disappear. If you are using Code Coverage, you may need to disable it or you will receive a: > `Symbol not found: ___llvm_profile_runtime` error.` Go to `Edit Scheme -> Test -> Options -> Code Coverage` and (temporarily) disable. Keep in mind global state -- If the file you're injecting has top level variables e.g. singletons, static or global vars they will be reset when you inject the code as the new method implementations will refer to the newly loaded object file containing the type. As injection needs to know how to compile Swift files individually it is not compatible with building using `Whole Module Optimisation`. A workaround for this is to build with `WMO` switched off so there are logs of individual compiles available then switching `WMO` back on if it suits your workflow better. ### SwiftUI Injection It is possible to inject `SwiftUI` interfaces but it requires some minor code changes. This is because when you add elements to an interface or use modifiers that change their type, this changes the return type of the body properties' `Content` which causes a crash. To avoid this you need to erase the return type. The easiest way to do this is to add the code below to your source somewhere then add the modifier `.eraseToAnyView()` at the very end of any declaration of a view's body property that you want to inject: ```Swift #if DEBUG private var loadInjection: () = { #if os(macOS) let bundleName = "macOSInjection.bundle" #elseif os(tvOS) let bundleName = "tvOSInjection.bundle" #elseif targetEnvironment(simulator) let bundleName = "iOSInjection.bundle" #else let bundleName = "maciOSInjection.bundle" #endif Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/"+bundleName)!.load() }() import Combine public let injectionObserver = InjectionObserver() public class InjectionObserver: ObservableObject { @Published var injectionNumber = 0 var cancellable: AnyCancellable? = nil let publisher = PassthroughSubject() init() { cancellable = NotificationCenter.default.publisher(for: Notification.Name("INJECTION_BUNDLE_NOTIFICATION")) .sink { [weak self] change in self?.injectionNumber += 1 self?.publisher.send() } } } extension View { public func eraseToAnyView() -> some View { _ = loadInjection return AnyView(self) } public func onInjection(bumpState: @escaping () -> ()) -> some View { return self .onReceive(injectionObserver.publisher, perform: bumpState) .eraseToAnyView() } } #else extension View { public func eraseToAnyView() -> some View { return self } public func onInjection(bumpState: @escaping () -> ()) -> some View { return self } } #endif ``` To have the view you are working on redisplay automatically when it is injected it's sufficient to add an `@ObservedObject`, initialised to the `injectionObserver` instance as follows: ```Swift .eraseToAnyView() } #if DEBUG @ObservedObject var iO = injectionObserver #endif ``` You can make all these changes automatically once you've opened a project using the `"Prepare Project"` menu item. If you'd like to execute some code each time your interface is injected, use the `.onInjection { ... }` modifier instead of .`eraseToAnyView()`. ### macOS Injection It is possible to use injection with a macOS/Catalyst project but it is getting progressively more difficult with each release of the OS. You need to make sure to turn off the "App Sandbox" and also "Disable Library Validation" under the "Hardened Runtime" options for your project while you inject. With an Apple Silicon Mac it is possible to run your iOS application natively on macOS. You cuse injection with these apps but as you can't turn off library validation it's a little involved. You need re-codesign the maciOSInjection.bundle contained in the InjectionIII app package using the signing identity used by your target app which you can determine from the `Sign` phase in your app's build logs. You will also need to set a user default with the path to your project file as the name and the signing identity as the value to injected code changes can be signed properly. All this is best done by adding the following as a build phase to your target project: ``` # Type a script or drag a script file from your workspace to insert its path. export CODESIGN_ALLOCATE\=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate INJECTION_APP_RESOURCES=/Applications/InjectionIII.app/Contents/Resources /usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY $INJECTION_APP_RESOURCES/maciOSInjection.bundle/maciOSInjection /usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY $INJECTION_APP_RESOURCES/maciOSSwiftUISupport.bundle/maciOSSwiftUISupport /usr/bin/codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY $INJECTION_APP_RESOURCES/maciOSInjection.bundle/Frameworks/SwiftTrace.framework/SwiftTrace defaults write com.johnholdsworth.InjectionIII "$PROJECT_FILE_PATH" $EXPANDED_CODE_SIGN_IDENTITY ``` ### Storyboard injection Sometimes when you are iterating over a UI it is useful to be able to inject storyboards. This works slightly differently from code injection. To inject changes to a storyboard scene, make your changes then _build_ the project instead of saving the storyboard. The "nib" of the currently displayed view controlled should be reloaded and viewDidLoad etc. will be called. ### Vaccine Injection now includes the higher level `Vaccine` functionality, for more information consult the [project README](https://github.com/zenangst/Vaccine) or one of the [following](https://medium.com/itch-design-no/code-injection-in-swift-c49be095414c) [references](https://medium.com/@robnorback/the-secret-to-1-second-compile-times-in-xcode-9de4ec8345a1). ### Method Tracing menu item (SwiftTrace) It's possible to inject tracing aspects into your program that don't affect it's operation but log every method call. Where possible it will also decorate their arguments. You can add logging to all methods in your app's main bundle or the frameworks it uses or trace calls to system frameworks such as UIKit or SwiftUI. If you opt into "Type Lookup", custom types in your appliction can also be decorated using the CustomStringConvertable conformance or the default formatter for structs. These features are implemented by the package [SwiftTrace](https://github.com/johnno1962/SwiftTrace) which is built into the InjectionBundle. If you want finer grain control of what is being traced, include the following header file in your project's bridging header and a subset of the internal api will be available to Swift (after an injection bundle has been loaded): ```C++ #import "/Applications/InjectionIII.app/Contents/Resources/SwiftTrace.h" ``` The "Trace Main Bundle" menu item can be mimicked by using the following call: ```Swift NSObject.swiftTraceMainBundleMethods() ``` If you want instead to also trace all Swift calls your application makes to a system framework such as SwiftUI you can use the following: ```Swift NSObject.swiftTraceMethods(inFrameworkContaining:UIHostingController.self) ``` To include or exclude the methods to be traced use the `methodInclusionPattern` and `methodExclusionPattern` class properties of SwiftTrace. For more information consult the [SwiftTrace source repo](https://github.com/johnno1962/SwiftTrace). It's also possible to access the Swift API of SwiftTrace directly in your app. For example, to add a new handler to format a particular type by importing SwiftTrace and adding the following to your app's `"Framework Search Paths"` and `"Runpath Search Paths"` (for the Debug configuration): ``` /Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle/Frameworks ``` Then, you can use something like the following to register the type: ``` SwiftTrace.makeTraceable(types: [MovieSwift.MovieRow.Props.self]) ``` In this case however the `MovieSwift.MovieRow.Props` type from the excellent `MovieSwift` SwiftUI [example project](https://github.com/Dimillian/MovieSwiftUI) is too large to format but can be changed to be a class instead of a struct. Finally, if you'd like to go directly to the file that defines a logged method, select the fully qualified method and use the service `Injection Goto` to open the file declaring that function. (To have the `Injection Goto` item appear on your services context menu you need to select it in System Preferences/Keyboard, tab Shortcuts/Services, under the "Text" section.) There are other SwifTrace features that allow you to "profile" your application to optimise the order object files are linked into your application which could potentially minimise paging on startup. These are surfaced in the "Method Tracing" submenu but if I'm honest, these would only make a difference if you had a very, very large application binary. ### Remote Control Newer versions of InjectionIII contain a server that allows you to control your development device from your desktop once the service has been started. The UI allows you to record and replay macros of UI actions then verify the device screen against snapshots for end-to-end testing. To use, import the Swift Package `https://github.com/johnno1962/Remote.git` and call `RemoteCapture.start("hostname")` where hostname is a space separated list of hostnames or IP addreses. When InjectionIII is running, select the "Remote/Start Server" menu item to start the server and then run your app. It should connect to the server which will pop up a window showing the device display and accepting tap events. Events can be saved as `macros` and replayed. If you include a snapshot in a macro this will be compared against the device display (within a tolerance) when you replay the macro for automated testing. Remote can also be used to capture videos of your app in operation but, as it operates over the network, it isn't fast enough to capture animated transitions. ## SwiftEval - Yes, it's eval() for Swift ![Icon](https://courses.cs.washington.edu/courses/cse190m/10su/lectures/slides/images/drevil.png) InjectionIII started out as the SwiftEval class which is a [single Swift source](InjectionBundle/SwiftEval.swift) that can be added to your iOS simulator or macOS projects to implement an eval function inside classes that inherit from NSObject. There is a generic form which has the following signature: ```Swift extension NSObject { public func eval(_ expression: String, type: T.Type) -> T { ``` This takes a Swift expression as a String and returns an entity of the type specified. There is also a shorthand function for expressions of type String which accepts the contents of the String literal as it's argument: ```Swift public func swiftEvalString(contents: String) -> String { return eval("\"" + expression + "\"", String.self) } ``` An example of how it is used can be found in the EvalApp example. ```Swift @IBAction func performEval(_: Any) { textView.string = swiftEvalString(contents: textField.stringValue) } @IBAction func closureEval(_: Any) { _ = swiftEval(code: closureText.stringValue+"()") } ``` The code works by adding an extension to your class source containing the expression. It then compiles and loads this new version of the class "swizzling" this extension onto the original class. The expression can refer to instance members in the class containing the eval class and global variables & functions in other class sources. ### Acknowledgements: This project includes code from [rentzsch/mach_inject](https://github.com/rentzsch/mach_inject), [erwanb/MachInjectSample](https://github.com/erwanb/MachInjectSample), [davedelong/DDHotKey](https://github.com/davedelong/DDHotKey) and [acj/TimeLapseBuilder-Swift](https://github.com/acj/TimeLapseBuilder-Swift) under their respective licenses. 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). See the project source and header file included in the app bundle for licensing details. 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 using injection under an MIT license. $Date: 2021/02/18 $ ================================================ FILE: Contents/Resources/SwiftTrace.h ================================================ // // SwiftTrace.h // SwiftTrace // // Created by John Holdsworth on 10/06/2016. // Copyright © 2016 John Holdsworth. All rights reserved. // // Repo: https://github.com/johnno1962/SwiftTrace // $Id: //depot/SwiftTrace/SwiftTraceGuts/include/SwiftTrace.h#45 $ // #ifndef SWIFTTRACE_H #define SWIFTTRACE_H #import //! Project version number for SwiftTrace. FOUNDATION_EXPORT double SwiftTraceVersionNumber; //! Project version string for SwiftTrace. FOUNDATION_EXPORT const unsigned char SwiftTraceVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import /** Objective-C inteface to SwftTrace as a category on NSObject as a summary of the functionality available. Intended to be used from Swift where SwifTrace has been provided from a dynamically loaded bundle, for example, from InjectionIII. Each trace superceeds any previous traces when they where not explicit about the class or instance being traced (see swiftTraceIntances and swiftTraceInstance). For example, the following code: UIView.swiftTraceBundle() UITouch.traceInstances(withSubLevels: 3) Will put a trace on all of the UIKit frameowrk which is then refined by the specific trace for only instances of class UITouch to be printed and any calls to UIKit made by those methods up to three levels deep. */ @interface NSObject(SwiftTrace) /** The default regexp used to exclude certain methods from tracing. */ + (NSString * _Nonnull)swiftTraceDefaultMethodExclusions; /** Optional filter of methods to be included in subsequent traces. */ @property (nonatomic, class, copy) NSString *_Nullable swiftTraceMethodInclusionPattern; /** Provide a regular expression to exclude methods. */ @property (nonatomic, class, copy) NSString *_Nullable swiftTraceMethodExclusionPattern; /** Real time control over methods to be traced (regular expressions) */ @property (nonatomic, class, copy) NSString *_Nullable swiftTraceFilterInclude; @property (nonatomic, class, copy) NSString *_Nullable swiftTraceFilterExclude; /** Function type suffixes at end of mangled symbol name. */ @property (nonatomic, class, copy) NSArray * _Nonnull swiftTraceFunctionSuffixes; /** Are we tracing? */ @property (readonly, class) BOOL swiftTracing; /** Pointer to common interposed state dictionary */ @property (readonly, class) void * _Nonnull swiftTraceInterposed; /** lookup unknown types */ @property (class) BOOL swiftTraceTypeLookup; /** Class will be traced (as opposed to swiftTraceInstances which will trace methods declared in super classes as well and only for instances of that particular class not any subclasses.) */ + (void)swiftTrace; /** Trace all methods defined in classes contained in the main executable of the application. */ + (void)swiftTraceMainBundle; /** Trace all methods of classes in the main bundle but also up to subLevels of calls made by those methods if a more general trace has already been placed on them. */ + (void)swiftTraceMainBundleWithSubLevels:(int)subLevels; /** Add a trace to all methods of all classes defined in the bundle or framework that contains the receiving class. */ + (void)swiftTraceBundle; /** Add a trace to all methods of all classes defined in the all frameworks in the app bundle. */ + (void)swiftTraceFrameworkMethods; /** Output a trace of methods defined in the bundle containing the reciever and up to subLevels of calls made by them. */ + (void)swiftTraceBundleWithSubLevels:(int)subLevels; /** Trace classes in the application that have names matching the regular expression. */ + (void)swiftTraceClassesMatchingPattern:(NSString * _Nonnull)pattern; /** Trace classes in the application that have names matching the regular expression and subLevels of cals they make to classes that have already been traced. */ + (void)swiftTraceClassesMatchingPattern:(NSString * _Nonnull)pattern subLevels:(intptr_t)subLevels; /** Return an array of the demangled names of methods declared in the reciving Swift class that can be traced. */ + (NSArray * _Nonnull)swiftTraceMethodNames; /** Return an array of the demangled names of methods declared in the Swift class provided. */ + (NSArray * _Nonnull)switTraceMethodsNamesOfClass:(Class _Nonnull)aClass; /** Trace instances of the specific receiving class (including the methods of its superclasses.) */ + (void)swiftTraceInstances; /** Trace instances of the specific receiving class (including the methods of its superclasses and subLevels of previously traced methods called by those methods.) */ + (void)swiftTraceInstancesWithSubLevels:(int)subLevels; /** Trace a methods (including those of all superclasses) for a particular instance only. */ - (void)swiftTraceInstance; /** Trace methods including those of all superclasses for a particular instance only and subLevels of calls they make. */ - (void)swiftTraceInstanceWithSubLevels:(int)subLevels; /** Trace all protocols contained in the bundle declaring the receiver class */ + (void)swiftTraceProtocolsInBundle; /** Trace protocols in bundle with qualifications */ + (void)swiftTraceProtocolsInBundleWithMatchingPattern:(NSString * _Nullable)pattern; + (void)swiftTraceProtocolsInBundleWithSubLevels:(int)subLevels; + (void)swiftTraceProtocolsInBundleWithMatchingPattern:(NSString * _Nullable)pattern subLevels:(int)subLevels; /** Use interposing to trace all methods in main bundle Use swiftTraceInclusionPattern, swiftTraceExclusionPattern to filter */ + (void)swiftTraceMethodsInFrameworkContaining:(Class _Nonnull)aClass; + (void)swiftTraceMainBundleMethods; + (void)swiftTraceMethodsInBundle:(const char * _Nonnull)bundlePath packageName:(NSString * _Nullable)packageName; + (void)swiftTraceBundlePath:(const char * _Nonnull)bundlePath; /** Remove most recent trace */ + (BOOL)swiftTraceUndoLastTrace; /** Remove all tracing swizles. */ + (void)swiftTraceRemoveAllTraces; /** Remove all interposes from tracing. */ + (void)swiftTraceRevertAllInterposes; /** Total elapsed time by traced method. */ + (NSDictionary * _Nonnull)swiftTraceElapsedTimes; /** Invocation counts by traced method. */ + (NSDictionary * _Nonnull)swiftTraceInvocationCounts; @end #import #import #import #ifdef __cplusplus extern "C" { #endif IMP _Nonnull imp_implementationForwardingToTracer(void * _Nonnull patch, IMP _Nonnull onEntry, IMP _Nonnull onExit); NSArray * _Nonnull objc_classArray(void); NSMethodSignature * _Nullable method_getSignature(Method _Nonnull Method); const char * _Nonnull sig_argumentType(id _Nonnull signature, NSUInteger index); const char * _Nonnull sig_returnType(id _Nonnull signature); const char * _Nonnull classesIncludingObjc(); void findSwiftSymbols(const char * _Nullable path, const char * _Nonnull suffix, void (^ _Nonnull callback)(const void * _Nonnull address, const char * _Nonnull symname, void * _Nonnull typeref, void * _Nonnull typeend)); void appBundleImages(void (^ _Nonnull callback)(const char * _Nonnull imageName, const struct mach_header * _Nonnull header, intptr_t slide)); const char * _Nullable swiftUIBundlePath(); const char * _Nullable callerBundle(void); int fast_dladdr(const void * _Nonnull, Dl_info * _Nonnull); #ifdef __cplusplus } #endif struct dyld_interpose_tuple { const void * _Nonnull replacement; const void * _Nonnull replacee; }; #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED #import #define OSRect CGRect #define OSMakeRect CGRectMake #else #define OSRect NSRect #define OSMakeRect NSMakeRect #endif @interface ObjcTraceTester: NSObject - (OSRect)a:(float)a i:(int)i b:(double)b c:(NSString *_Nullable)c o:o s:(SEL _Nullable)s; @end #endif // Copy paste of fishhook.h follows... // =================================== // Copyright (c) 2013, Facebook, Inc. // All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name Facebook nor the names of its contributors may be used to // endorse or promote products derived from this software without specific // prior written permission. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef fishhook_h #define fishhook_h #include #include #if !defined(FISHHOOK_EXPORT) #define FISHHOOK_VISIBILITY __attribute__((visibility("hidden"))) #else #define FISHHOOK_VISIBILITY __attribute__((visibility("default"))) #endif #ifdef __cplusplus extern "C" { #endif //__cplusplus /* * A structure representing a particular intended rebinding from a symbol * name to its replacement */ struct rebinding { const char * _Nonnull name; void * _Nonnull replacement; void * _Nonnull * _Nullable replaced; }; /* * For each rebinding in rebindings, rebinds references to external, indirect * symbols with the specified name to instead point at replacement for each * image in the calling process as well as for all future images that are loaded * by the process. If rebind_functions is called more than once, the symbols to * rebind are added to the existing list of rebindings, and if a given symbol * is rebound more than once, the later rebinding will take precedence. */ FISHHOOK_VISIBILITY int rebind_symbols(struct rebinding rebindings[_Nonnull], size_t rebindings_nel); /* * Rebinds as above, but only in the specified image. The header should point * to the mach-o header, the slide should be the slide offset. Others as above. */ FISHHOOK_VISIBILITY int rebind_symbols_image(void * _Nonnull header, intptr_t slide, struct rebinding rebindings[_Nonnull], size_t rebindings_nel); #ifdef __cplusplus } #endif //__cplusplus #endif //fishhook_h ================================================ FILE: Contents/Resources/fishhook.h ================================================ // Copyright (c) 2013, Facebook, Inc. // All rights reserved. // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright notice, // this list of conditions and the following disclaimer in the documentation // and/or other materials provided with the distribution. // * Neither the name Facebook nor the names of its contributors may be used to // endorse or promote products derived from this software without specific // prior written permission. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifndef fishhook_h #define fishhook_h #include #include #if !defined(FISHHOOK_EXPORT) #define FISHHOOK_VISIBILITY __attribute__((visibility("hidden"))) #else #define FISHHOOK_VISIBILITY __attribute__((visibility("default"))) #endif #ifdef __cplusplus extern "C" { #endif //__cplusplus /* * A structure representing a particular intended rebinding from a symbol * name to its replacement */ struct rebinding { const char *name; void *replacement; void **replaced; }; /* * For each rebinding in rebindings, rebinds references to external, indirect * symbols with the specified name to instead point at replacement for each * image in the calling process as well as for all future images that are loaded * by the process. If rebind_functions is called more than once, the symbols to * rebind are added to the existing list of rebindings, and if a given symbol * is rebound more than once, the later rebinding will take precedence. */ FISHHOOK_VISIBILITY int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); /* * Rebinds as above, but only in the specified image. The header should point * to the mach-o header, the slide should be the slide offset. Others as above. */ FISHHOOK_VISIBILITY int rebind_symbols_image(void *header, intptr_t slide, struct rebinding rebindings[], size_t rebindings_nel); #ifdef __cplusplus } #endif //__cplusplus #endif //fishhook_h ================================================ FILE: Contents/Resources/graph.gv ================================================ digraph sweep { node [href="javascript:void(click_node('\N'))" id="\N" fontname="Arial"]; 0 [label="UIApplication" tooltip=" #0" color="#000000"]; 1 [label="ztruct.AppDelegate" tooltip=" #1" style="filled" fillcolor="#e0e0e0" color="#000000"]; 0 -> 1 [label="_delegate" color="#000000" eid="1"]; 6 [label="UIMotionEvent" tooltip=" #6" color="#000000"]; 7 [label="BKSAccelerometer" tooltip=" #7" style="filled" fillcolor="#e0e0e0" color="#000000"]; 6 -> 7 [label="_motionAccelerometer" color="#000000" eid="9"]; 7 -> 6 [label="_delegate" color="#000000" eid="10"]; 8 [label="NSLock" tooltip=" #8" color="#000000"]; 7 -> 8 [label="_lock" color="#000000" eid="11"]; 7 -> 6 [label="delegate" color="#000000" eid="12"]; 14 [label="BSSimpleAssertion" tooltip=" #14" style="filled" fillcolor="#e0e0e0" color="#000000"]; 0 -> 14 [label="_keyCommandToken" color="#000000" eid="25"]; 15 [label="BSAtomicSignal" tooltip=" #15" style="filled" fillcolor="#e0e0e0" color="#000000"]; 14 -> 15 [label="_invalidated" color="#000000" eid="26"]; 21 [label="BSServiceConnectionEndpointMonitor" tooltip=" #21" style="filled" fillcolor="#e0e0e0" color="#000000"]; 0 -> 21 [label="_endpointMonitor" color="#000000" eid="33"]; 22 [label="BSServiceManager" tooltip=" #22" style="filled" fillcolor="#e0e0e0" color="#000000"]; 21 -> 22 [label="_manager" color="#000000" eid="34"]; 23 [label="BSServicesConfiguration" tooltip=" #23" style="filled" fillcolor="#e0e0e0" color="#000000"]; 22 -> 23 [label="_configuration" color="#000000" eid="35"]; 24 [label="RBSService" tooltip=" #24" style="filled" fillcolor="#e0e0e0" color="#000000"]; 22 -> 24 [label="_RBSService" color="#000000" eid="36"]; 24 -> 22 [label="_delegate" color="#000000" eid="37"]; 25 [label="RBSConnection" tooltip=" #25" style="filled" fillcolor="#e0e0e0" color="#000000"]; 24 -> 25 [label="_connection" color="#000000" eid="38"]; 26 [label="OS_xpc_connection" tooltip=" #26" color="#000000"]; 25 -> 26 [label="_connection" color="#000000" eid="39"]; 27 [label="RBSProcessHandle" tooltip=" #27" style="filled" fillcolor="#e0e0e0" color="#000000"]; 25 -> 27 [label="_handle" color="#000000" eid="40"]; 28 [label="BSAuditToken" tooltip=" #28" style="filled" fillcolor="#e0e0e0" color="#000000"]; 27 -> 28 [label="_bsAuditToken" color="#000000" eid="41"]; 29 [label="RBSEmbeddedAppProcessIdentity" tooltip=" #29" style="filled" fillcolor="#e0e0e0" color="#000000"]; 27 -> 29 [label="_identity" color="#000000" eid="42"]; 30 [label="RBSProcessBundle" tooltip=" #30" style="filled" fillcolor="#e0e0e0" color="#000000"]; 27 -> 30 [label="_bundle" color="#000000" eid="43"]; 31 [label="RBSProcessInstance" tooltip=" #31" style="filled" fillcolor="#e0e0e0" color="#000000"]; 30 -> 31 [label="_instance" color="#000000" eid="44"]; 31 -> 29 [label="_identity" color="#000000" eid="45"]; 32 [label="RBSProcessIdentifier" tooltip=" #32" style="filled" fillcolor="#e0e0e0" color="#000000"]; 31 -> 32 [label="_identifier" color="#000000" eid="46"]; 25 -> 24 [label="_serviceDelegate" color="#000000" eid="47"]; 33 [label="OS_dispatch_queue_serial" tooltip=" #33" color="#000000"]; 25 -> 33 [label="_connectionQueue" color="#000000" eid="48"]; 34 [label="OS_dispatch_queue_serial" tooltip=" #34" color="#000000"]; 25 -> 34 [label="_handshakeQueue" color="#000000" eid="49"]; 35 [label="OS_dispatch_queue_serial" tooltip=" #35" color="#000000"]; 25 -> 35 [label="_monitorCalloutQueue" color="#000000" eid="50"]; 37 [label="OS_dispatch_queue_serial" tooltip=" #37" color="#000000"]; 24 -> 37 [label="_calloutQueue" color="#000000" eid="52"]; 38 [label="BSSimpleAssertion" tooltip=" #38" style="filled" fillcolor="#e0e0e0" color="#000000"]; 21 -> 38 [label="_registrationLock_assertion" color="#000000" eid="53"]; 39 [label="BSAtomicSignal" tooltip=" #39" style="filled" fillcolor="#e0e0e0" color="#000000"]; 38 -> 39 [label="_invalidated" color="#000000" eid="54"]; 0 -> 1 [label="delegate" color="#000000" eid="55"]; 42 [label="UISplitViewControllerPanelImpl" tooltip=" #42" color="#000000"]; 64 [label="ztruct.SceneDelegate" tooltip=" #64" style="filled" fillcolor="#e0e0e0" color="#000000"]; 42 -> 64 [label="_delegate" color="#000000" eid="100"]; 42 -> 64 [label="delegate" color="#000000" eid="122"]; 100 [label="UIButtonLabel" tooltip=" #100" shape=box color="#000000"]; 102 [label="CUIStyleEffectConfiguration" tooltip=" #102" style="filled" fillcolor="#e0e0e0" color="#000000"]; 100 -> 102 [label="_cuiStyleEffectConfiguration" color="#000000" eid="173"]; 117 [label="UILabel" tooltip=" #117" shape=box color="#000000"]; 119 [label="CUIStyleEffectConfiguration" tooltip=" #119" style="filled" fillcolor="#e0e0e0" color="#000000"]; 117 -> 119 [label="_cuiStyleEffectConfiguration" color="#000000" eid="206"]; 130 [label="UITableView" tooltip=" #130" shape=box color="#000000"]; 131 [label="ztruct.MasterViewController" tooltip=" #131" style="filled" fillcolor="#e0e0e0" color="#000000"]; 130 -> 131 [label="_dataSource" color="#000000" eid="237"]; 132 [label="UIAutoRespondingScrollViewControllerKeyboardSupport" tooltip=" #132" color="#000000"]; 131 -> 132 [label="_keyboardSupport" color="#000000" eid="238"]; 132 -> 131 [label="_viewController" color="#000000" eid="239"]; 131 -> 130 [label="_view" color="#000000" eid="240"]; 133 [label="UINavigationItem" tooltip=" #133" color="#000000"]; 131 -> 133 [label="_navigationItem" color="#000000" eid="241"]; 134 [label="NSBundle" tooltip=" #134" color="#000000"]; 131 -> 134 [label="_nibBundle" color="#000000" eid="243"]; 79 [label="UINavigationController" tooltip=" #79" color="#000000"]; 131 -> 79 [label="_parentViewController" color="#000000" eid="244"]; 135 [label="UIStoryboard" tooltip=" #135" color="#000000"]; 131 -> 135 [label="_storyboard" color="#000000" eid="245"]; 136 [label="UIBarButtonItem" tooltip=" #136" color="#000000"]; 131 -> 136 [label="_editButtonItem" color="#000000" eid="247"]; 136 -> 131 [label="_target" color="#000000" eid="248"]; 136 -> 131 [label="_toggleEditing:" color="#000000" eid="250"]; 138 [label="UITraitCollection" tooltip=" #138" color="#000000"]; 131 -> 138 [label="_lastNotifiedTraitCollection" color="#000000" eid="251"]; 139 [label="UINavigationContentAdjustments" tooltip=" #139" color="#000000"]; 131 -> 139 [label="_navigationInsetAdjustment" color="#000000" eid="252"]; 130 -> 131 [label="_delegate" color="#000000" eid="262"]; 130 -> 131 [label="_viewDelegate" color="#000000" eid="303"]; 130 -> 131 [label="delegate" color="#000000" eid="304"]; 41 [label="UISplitViewController" tooltip=" #41" color="#000000"]; 41 -> 64 [label="delegate" color="#000000" eid="354"]; 189 [label="UIWindowScene" tooltip=" #189" color="#000000"]; 189 -> 64 [label="_delegate" color="#000000" eid="375"]; 212 [label="FBSWorkspace" tooltip=" #212" color="#000000"]; 215 [label="BSAtomicSignal" tooltip=" #215" style="filled" fillcolor="#e0e0e0" color="#000000"]; 212 -> 215 [label="_activateSignal" color="#000000" eid="395"]; 216 [label="FBSWorkspaceFencingImpl" tooltip=" #216" color="#000000"]; 218 [label="BSMutableIntegerMap" tooltip=" #218" style="filled" fillcolor="#e0e0e0" color="#000000"]; 216 -> 218 [label="_triggerToFenceNameMap" color="#000000" eid="399"]; 219 [label="BSMutableIntegerSet" tooltip=" #219" style="filled" fillcolor="#e0e0e0" color="#000000"]; 216 -> 219 [label="_triggersToIgnore" color="#000000" eid="400"]; 220 [label="BSServiceConnectionEndpointMonitor" tooltip=" #220" style="filled" fillcolor="#e0e0e0" color="#000000"]; 212 -> 220 [label="_connectionEndpointMonitor" color="#000000" eid="402"]; 220 -> 22 [label="_manager" color="#000000" eid="403"]; 220 -> 212 [label="_lock_delegate" color="#000000" eid="404"]; 221 [label="BSSimpleAssertion" tooltip=" #221" style="filled" fillcolor="#e0e0e0" color="#000000"]; 220 -> 221 [label="_registrationLock_assertion" color="#000000" eid="405"]; 222 [label="BSAtomicSignal" tooltip=" #222" style="filled" fillcolor="#e0e0e0" color="#000000"]; 221 -> 222 [label="_invalidated" color="#000000" eid="406"]; 220 -> 212 [label="delegate" color="#000000" eid="407"]; 223 [label="BSServiceConnectionEndpoint" tooltip=" #223" style="filled" fillcolor="#e0e0e0" color="#000000"]; 212 -> 223 [label="_defaultShellEndpoint" color="#000000" eid="408"]; 224 [label="OS_xpc_endpoint" tooltip=" #224" color="#000000"]; 223 -> 224 [label="_endpoint" color="#000000" eid="409"]; 211 [label="FBSWorkspaceScenesClient" tooltip=" #211" color="#000000"]; 225 [label="BSServiceConnection" tooltip=" #225" style="filled" fillcolor="#e0e0e0" color="#000000"]; 211 -> 225 [label="_connection" color="#000000" eid="414"]; 226 [label="BSXPCServiceConnection" tooltip=" #226" style="filled" fillcolor="#e0e0e0" color="#000000"]; 225 -> 226 [label="_connection" color="#000000" eid="415"]; 227 [label="BSXPCServiceConnectionPeer" tooltip=" #227" style="filled" fillcolor="#e0e0e0" color="#000000"]; 226 -> 227 [label="_lock_peer" color="#000000" eid="416"]; 228 [label="BSProcessHandle" tooltip=" #228" style="filled" fillcolor="#e0e0e0" color="#000000"]; 227 -> 228 [label="_processHandle" color="#000000" eid="417"]; 229 [label="BSAuditToken" tooltip=" #229" style="filled" fillcolor="#e0e0e0" color="#000000"]; 228 -> 229 [label="_auditToken" color="#000000" eid="418"]; 230 [label="BSMachPortTaskNameRight" tooltip=" #230" style="filled" fillcolor="#e0e0e0" color="#000000"]; 228 -> 230 [label="_taskNameRight" color="#000000" eid="419"]; 231 [label="BSXPCServiceConnectionMessage" tooltip=" #231" style="filled" fillcolor="#e0e0e0" color="#000000"]; 226 -> 231 [label="_lock_invalidationMessage" color="#000000" eid="420"]; 232 [label="OS_dispatch_queue_serial" tooltip=" #232" color="#000000"]; 231 -> 232 [label="_targetQueue" color="#000000" eid="421"]; 233 [label="OS_xpc_dictionary" tooltip=" #233" color="#000000"]; 231 -> 233 [label="_message" color="#000000" eid="422"]; 234 [label="OS_xpc_connection" tooltip=" #234" color="#000000"]; 231 -> 234 [label="_xpcConnection" color="#000000" eid="423"]; 235 [label="BSXPCServiceConnectionEventHandler" tooltip=" #235" style="filled" fillcolor="#e0e0e0" color="#000000"]; 226 -> 235 [label="_lock_eventHandler" color="#000000" eid="424"]; 236 [label="BSXPCServiceConnectionProxy" tooltip=" 0x600002931200> #236" style="filled" fillcolor="#e0e0e0" color="#000000"]; 235 -> 236 [label="_lock_remoteTarget" color="#000000" eid="425"]; 237 [label="BSObjCProtocol" tooltip=" #237" style="filled" fillcolor="#e0e0e0" color="#000000"]; 236 -> 237 [label="_remoteProtocol" color="#000000" eid="426"]; 238 [label="Protocol" tooltip=" #238" style="filled" fillcolor="#e0e0e0" color="#000000"]; 237 -> 238 [label="_protocol" color="#000000" eid="427"]; 239 [label="BSObjCProtocol" tooltip=" #239" style="filled" fillcolor="#e0e0e0" color="#000000"]; 236 -> 239 [label="_localProtocol" color="#000000" eid="428"]; 240 [label="Protocol" tooltip=" #240" style="filled" fillcolor="#e0e0e0" color="#000000"]; 239 -> 240 [label="_protocol" color="#000000" eid="429"]; 236 -> 226 [label="_connection" color="#000000" eid="430"]; 236 -> 234 [label="_XPCConnection" color="#000000" eid="431"]; 236 -> 232 [label="_XPCConnectionTargetQueue" color="#000000" eid="432"]; 235 -> 211 [label="_interfaceTarget" color="#000000" eid="433"]; 241 [label="BSZeroingWeakReference" tooltip=" #241" style="filled" fillcolor="#e0e0e0" color="#000000"]; 235 -> 241 [label="_context" color="#000000" eid="434"]; 241 -> 225 [label="_object" color="#000000" eid="435"]; 242 [label="OS_dispatch_queue_serial" tooltip=" #242" color="#000000"]; 235 -> 242 [label="_targetQueue" color="#000000" eid="436"]; 243 [label="BSServiceQuality" tooltip=" #243" style="filled" fillcolor="#e0e0e0" color="#000000"]; 235 -> 243 [label="_serviceQuality" color="#000000" eid="437"]; 244 [label="BSServiceInterface" tooltip=" #244" style="filled" fillcolor="#e0e0e0" color="#000000"]; 235 -> 244 [label="_interface" color="#000000" eid="438"]; 244 -> 237 [label="_server" color="#000000" eid="439"]; 244 -> 239 [label="_client" color="#000000" eid="440"]; 245 [label="BSXPCCoder" tooltip=" #245" style="filled" fillcolor="#e0e0e0" color="#000000"]; 235 -> 245 [label="_initiatingContext" color="#000000" eid="441"]; 246 [label="OS_xpc_dictionary" tooltip=" #246" color="#000000"]; 245 -> 246 [label="_message" color="#000000" eid="442"]; 247 [label="BSXPCServiceConnection" tooltip=" #247" style="filled" fillcolor="#e0e0e0" color="#000000"]; 226 -> 247 [label="_lock_parent" color="#000000" eid="443"]; 247 -> 227 [label="_lock_peer" color="#000000" eid="444"]; 247 -> 234 [label="_lock_connection" color="#000000" eid="445"]; 248 [label="BSXPCServiceConnectionEventHandler" tooltip=" #248" style="filled" fillcolor="#e0e0e0" color="#000000"]; 247 -> 248 [label="_lock_eventHandler" color="#000000" eid="446"]; 249 [label="BSXPCServiceConnectionProxy" tooltip=" #249" style="filled" fillcolor="#e0e0e0" color="#000000"]; 248 -> 249 [label="_lock_remoteTarget" color="#000000" eid="447"]; 249 -> 247 [label="_connection" color="#000000" eid="448"]; 249 -> 234 [label="_XPCConnection" color="#000000" eid="449"]; 249 -> 232 [label="_XPCConnectionTargetQueue" color="#000000" eid="450"]; 248 -> 232 [label="_targetQueue" color="#000000" eid="451"]; 248 -> 243 [label="_serviceQuality" color="#000000" eid="452"]; 250 [label="BSXPCServiceConnectionRootClientEndpointContext" tooltip=" #250" style="filled" fillcolor="#e0e0e0" color="#000000"]; 247 -> 250 [label="_context" color="#000000" eid="453"]; 251 [label="OS_xpc_endpoint" tooltip=" #251" color="#000000"]; 250 -> 251 [label="_endpoint" color="#000000" eid="454"]; 252 [label="BSXPCServiceConnectionChildContext" tooltip=" #252" style="filled" fillcolor="#e0e0e0" color="#000000"]; 226 -> 252 [label="_context" color="#000000" eid="455"]; 252 -> 250 [label="_parent" color="#000000" eid="456"]; 211 -> 223 [label="_endpoint" color="#000000" eid="457"]; 253 [label="UIApplicationSceneSettings" tooltip=" #253" color="#000000"]; 257 [label="BSSettings" tooltip=" #257" style="filled" fillcolor="#e0e0e0" color="#000000"]; 253 -> 257 [label="_otherSettings" color="#000000" eid="462"]; 258 [label="BSMutableIntegerMap" tooltip=" #258" style="filled" fillcolor="#e0e0e0" color="#000000"]; 257 -> 258 [label="_settingToFlagMap" color="#000000" eid="463"]; 259 [label="BSMutableIntegerMap" tooltip=" #259" style="filled" fillcolor="#e0e0e0" color="#000000"]; 257 -> 259 [label="_settingToObjectMap" color="#000000" eid="464"]; 260 [label="BSCornerRadiusConfiguration" tooltip=" #260" style="filled" fillcolor="#e0e0e0" color="#000000"]; 259 -> 260 [label="_mapTable" color="#000000" eid="465"]; 257 -> 253 [label="_descriptionProvider" color="#000000" eid="466"]; 261 [label="BSSettings" tooltip=" #261" style="filled" fillcolor="#e0e0e0" color="#000000"]; 253 -> 261 [label="_transientLocalSettings" color="#000000" eid="467"]; 262 [label="UIApplicationSceneClientSettings" tooltip=" #262" color="#000000"]; 263 [label="BSSettings" tooltip=" #263" style="filled" fillcolor="#e0e0e0" color="#000000"]; 262 -> 263 [label="_otherSettings" color="#000000" eid="469"]; 264 [label="BSMutableIntegerMap" tooltip=" #264" style="filled" fillcolor="#e0e0e0" color="#000000"]; 263 -> 264 [label="_settingToFlagMap" color="#000000" eid="470"]; 265 [label="BSMutableIntegerMap" tooltip=" #265" style="filled" fillcolor="#e0e0e0" color="#000000"]; 263 -> 265 [label="_settingToObjectMap" color="#000000" eid="471"]; 263 -> 262 [label="_descriptionProvider" color="#000000" eid="472"]; 266 [label="FBSSceneIdentityToken" tooltip=" #266" color="#000000"]; 266 -> 223 [label="_endpoint" color="#000000" eid="475"]; 189 -> 64 [label="delegate" color="#000000" eid="480"]; 40 [label="UIWindow" tooltip=" #40" shape=box color="#000000"]; 269 [label="BSSimpleAssertion" tooltip=" #269" style="filled" fillcolor="#e0e0e0" color="#000000"]; 40 -> 269 [label="_eventFocusDeferralToken" color="#000000" eid="485"]; 270 [label="BSAtomicSignal" tooltip=" #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.: ![Icon](http://johnholdsworth.com/HotAdding.png) Then, you can inject function implementations without having to rebuild your app... ![Icon](http://johnholdsworth.com/HotReloading.png) 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 #import "InjectionClient.h" static std::map seen; static const char *strend(const char *str) { return str + strlen(str); } void unhide_reset(void) { seen.clear(); } int unhide_symbols(const char *framework, const char *linkFileList, FILE *log, time_t since) { FILE *linkFiles = fopen(linkFileList, "r"); if (!linkFiles) { fprintf(log, "unhide: Could not open link file list %s\n", linkFileList); return -1; } char buffer[PATH_MAX]; __block int totalExported = 0; while (fgets(buffer, sizeof buffer, linkFiles)) { buffer[strlen(buffer)-1] = '\000'; @autoreleasepool { totalExported += unhide_object(buffer, framework, log, nil, nil); } } fclose(linkFiles); return totalExported; } typedef BOOL (^ _Nonnull STSymbolFilter)(const char *_Nonnull symname); @interface NSObject(SwiftTrace) @property (nonatomic, class, copy) STSymbolFilter _Nonnull swiftTraceSymbolFilter; + (NSString *_Nullable)swiftTraceDemangle:(char const *_Nonnull)symbol; @end #define SWIFT_PRIVATE 0xe #define SWIFT_GLOBAL 0xf /// Unhiding is where symbols with "private extenal" visibility /// are exported so they will be available when a dynamic /// library is loaded. This was originally encountered for /// default argument generators but can also be useful /// for the addressors of private top level and static vars. /// @param object_file Path to object file /// @param framework Name of image containing object file /// @param log FILE * to log to /// @param class_references return references to objective-c classes so they can be fixed up. /// @param descriptor_refs return local varibles prefixed with l_got. that need to be fixed up. int unhide_object(const char *object_file, const char *framework, FILE *log, NSMutableArray *class_references, NSMutableArray *descriptor_refs) { // struct stat info; // if (stat(buffer, &info) || info.st_mtimespec.tv_sec < since) // continue; NSString *file = [NSString stringWithUTF8String:object_file]; NSData *patched = [[NSMutableData alloc] initWithContentsOfFile:file]; if (!patched) { fprintf(log, "unhide: Could not read %s\n", [file UTF8String]); return 0; } struct mach_header_64 *object = (struct mach_header_64 *)[patched bytes]; const char *filename = file.lastPathComponent.UTF8String; if (object->magic != MH_MAGIC_64) { fprintf(log, "unhide: Invalid magic 0x%x != 0x%x (bad arch?)\n", object->magic, MH_MAGIC_64); return 0; } struct symtab_command *symtab = NULL; struct dysymtab_command *dylib = NULL; for (struct load_command *cmd = (struct load_command *)((char *)object + sizeof *object) ; cmd < (struct load_command *)((char *)object + object->sizeofcmds) ; cmd = (struct load_command *)((char *)cmd + cmd->cmdsize)) { if (cmd->cmd == LC_SYMTAB) symtab = (struct symtab_command *)cmd; else if (cmd->cmd == LC_DYSYMTAB) dylib = (struct dysymtab_command *)cmd; } if (!symtab || !dylib) { fprintf(log, "unhide: Missing symtab or dylib cmd %s: %p & %p\n", filename, symtab, dylib); return 0; } struct nlist_64 *all_symbols64 = (struct nlist_64 *)((char *)object + symtab->symoff); #if 1 struct nlist_64 *end_symbols64 = all_symbols64 + symtab->nsyms; int exported = 0; // dylib->iextdefsym -= dylib->nlocalsym; // dylib->nextdefsym += dylib->nlocalsym; // dylib->nlocalsym = 0; #endif size_t isReverseInterpose = class_references ? strlen(framework) : 0; typedef std::pair class_pair; std::vector class_refs; for (int i=0 ; insyms ; i++) { struct nlist_64 &symbol = all_symbols64[i]; if (symbol.n_sect == NO_SECT) continue; // not definition const char *symname = (char *)object + symtab->stroff + symbol.n_un.n_strx; if (class_references) { static char classRef[] = {"l_OBJC_CLASS_REF_$_"}; int clasRefSize = sizeof classRef-1; if (strncmp(symname, classRef, clasRefSize) == 0) class_refs.push_back({symbol.n_value, symname + clasRefSize}); } if (descriptor_refs) { static char gotPrefix[] = {"l_got."}; int gotPrefixSize = sizeof gotPrefix-1; if (strncmp(symname, gotPrefix, gotPrefixSize) == 0) [descriptor_refs addObject:[NSString stringWithUTF8String:symname + gotPrefixSize]]; } if (strncmp(symname, "_$s", 3) != 0) continue; // not swift symbol // Default argument generators have a suffix ANN_ // Covers a few other cases encountred now as well. const char *symend = strend(symname) - 1; BOOL isMutableAddressor = strcmp(symend-2, "vau") == 0 || // witness table accessor functions... (strcmp(symend-1, "Wl") == 0 && strncmp(symname+1, framework, isReverseInterpose) == 0); BOOL isDefaultArgument = (*symend == '_' && (symend[-1] == 'A' || (isdigit(symend[-1]) && (symend[-2] == 'A' || (isdigit(symend[-2]) && symend[-3] == 'A'))))) ||// isMutableAddressor || strcmp(symend-1, "FZ") == 0 || (symend[-1] == 'M' && ( *symend == 'c' || *symend == 'g' || *symend == 'n')); #if DEBUG if (symbol.n_sect != NO_SECT && symbol.n_type == SWIFT_PRIVATE && [NSObject respondsToSelector:@selector(swiftTraceSymbolFilter)] && !isMutableAddressor && NSObject.swiftTraceSymbolFilter(symname)) { NSString *demangled = [NSObject swiftTraceDemangle:symname]; if (![demangled hasPrefix:@"reflection metadata "]) printf(APP_PREFIX"%s is private and may not inject\n", demangled.UTF8String); } #endif // fprintf(log, "symbol: #%d 0%lo 0x%x 0x%x %3d %s %d\n", // i, (char *)&symbol.n_type - (char *)object, // symbol.n_type, symbol.n_desc, // symbol.n_sect, symname, isDefaultArgument); // The following reads: If symbol is for a default argument // and it is the definition (not a reference) and we've not // seen it before and it hadsn't already been "unhidden"... if (isReverseInterpose ? isMutableAddressor : isDefaultArgument && !seen[symname]++ && symbol.n_type & N_PEXT) { symbol.n_type |= N_EXT; symbol.n_type &= ~N_PEXT; symbol.n_type = SWIFT_GLOBAL; symbol.n_desc = N_GSYM; if (!exported++) fprintf(log, "%s.%s: local: %d %d ext: %d %d undef: %d %d extref: %d %d indirect: %d %d extrel: %d %d localrel: %d %d symlen: 0%lo\n", framework, filename, dylib->ilocalsym, dylib->nlocalsym, dylib->iextdefsym, dylib->nextdefsym, dylib->iundefsym, dylib->nundefsym, dylib->extrefsymoff, dylib->nextrefsyms, dylib->indirectsymoff, dylib->nindirectsyms, dylib->extreloff, dylib->nextrel, dylib->locreloff, dylib->nlocrel, (char *)&end_symbols64->n_un - (char *)object); fprintf(log, "exported: #%d 0%lo 0x%x 0x%x %3d %s\n", i, (char *)&symbol.n_type - (char *)object, symbol.n_type, symbol.n_desc, symbol.n_sect, symname); } } if (class_references) { sort(class_refs.begin(), class_refs.end(), [&] (const class_pair &l, const class_pair &r) { return l.first < r.first; }); for (auto &cr : class_refs) [class_references addObject:[NSString stringWithUTF8String:cr.second]]; } if (exported && ![patched writeToFile:file atomically:YES]) fprintf(log, "unhide: Could not write %s\n", [file UTF8String]); return exported; } #if 0 && TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR // never used int unhide_framework(const char *framework, FILE *log) { int totalExported = 0; #if 0 // Not implemented @autoreleasepool { NSString *file = [NSString stringWithUTF8String:framework]; NSData *patched = [[NSMutableData alloc] initWithContentsOfFile:file]; if (!patched) { fprintf(log, "unhide: Could not read %s\n", [file UTF8String]); return -1; } struct mach_header_64 *object = (struct mach_header_64 *)[patched bytes]; const char *filename = file.lastPathComponent.UTF8String; if (object->magic != MH_MAGIC_64) { fprintf(log, "unhide: Invalid magic 0x%x != 0x%x (bad arch?)\n", object->magic, MH_MAGIC_64); return -1; } struct symtab_command *symtab = NULL; struct dysymtab_command *dylib = NULL; for (struct load_command *cmd = (struct load_command *)((char *)object + sizeof *object) ; cmd < (struct load_command *)((char *)object + object->sizeofcmds) ; cmd = (struct load_command *)((char *)cmd + cmd->cmdsize)) { if (cmd->cmd == LC_SYMTAB) symtab = (struct symtab_command *)cmd; else if (cmd->cmd == LC_DYSYMTAB) dylib = (struct dysymtab_command *)cmd; } if (!symtab || !dylib) { fprintf(log, "unhide: Missing symtab or dylib cmd %s: %p & %p\n", filename, symtab, dylib); return -1; } struct nlist_64 *all_symbols64 = (struct nlist_64 *)((char *)object + symtab->symoff); #if 1 struct nlist_64 *end_symbols64 = all_symbols64 + symtab->nsyms; int exported = 0; // dylib->iextdefsym -= dylib->nlocalsym; // dylib->nextdefsym += dylib->nlocalsym; // dylib->nlocalsym = 0; #endif for (int i=0 ; insyms ; i++) { struct nlist_64 &symbol = all_symbols64[i]; if (symbol.n_sect == NO_SECT) continue; // not definition const char *symname = (char *)object + symtab->stroff + symbol.n_un.n_strx; // printf("symbol: #%d 0%lo 0x%x 0x%x %3d %s\n", i, // (char *)&symbol.n_type - (char *)object, // symbol.n_type, symbol.n_desc, // symbol.n_sect, symname); if (strncmp(symname, "_$s", 3) != 0) continue; // not swift symbol // Default argument generators have a suffix ANN_ // Covers a few other cases encountred now as well. const char *symend = strend(symname) - 1; BOOL isDefaultArgument = (*symend == '_' && (symend[-1] == 'A' || (isdigit(symend[-1]) && (symend[-2] == 'A' || (isdigit(symend[-2]) && symend[-3] == 'A'))))) || strcmp(symend-2, "vau") == 0 || strcmp(symend-1, "FZ") == 0 || (symend[-1] == 'M' && ( *symend == 'c' || *symend == 'g' || *symend == 'n')); // The following reads: If symbol is for a default argument // and it is the definition (not a reference) and we've not // seen it before and it hadsn't already been "unhidden"... if (isDefaultArgument && !seen[symname]++ && symbol.n_type & N_PEXT) { symbol.n_type |= N_EXT; symbol.n_type &= ~N_PEXT; symbol.n_type = 0xf; symbol.n_desc = N_GSYM; if (!exported++) fprintf(log, "%s.%s: local: %d %d ext: %d %d undef: %d %d extref: %d %d indirect: %d %d extrel: %d %d localrel: %d %d symlen: 0%lo\n", framework, filename, dylib->ilocalsym, dylib->nlocalsym, dylib->iextdefsym, dylib->nextdefsym, dylib->iundefsym, dylib->nundefsym, dylib->extrefsymoff, dylib->nextrefsyms, dylib->indirectsymoff, dylib->nindirectsyms, dylib->extreloff, dylib->nextrel, dylib->locreloff, dylib->nlocrel, (char *)&end_symbols64->n_un - (char *)object); fprintf(log, "exported: #%d 0%lo 0x%x 0x%x %3d %s\n", i, (char *)&symbol.n_type - (char *)object, symbol.n_type, symbol.n_desc, symbol.n_sect, symname); } } if (exported && ![patched writeToFile:file atomically:YES]) fprintf(log, "unhide: Could not write %s\n", [file UTF8String]); totalExported += exported; } #endif return totalExported; } #endif #if !TARGET_IPHONE_SIMULATOR #import #import #import #import extern "C" { // Duplicated from SwiftTrace.h #define ST_LAST_IMAGE -1 #define ST_ANY_VISIBILITY 0 #define ST_GLOBAL_VISIBILITY 0xf #define ST_HIDDEN_VISIBILITY 0x1e #define ST_LOCAL_VISIBILITY 0xe typedef NS_ENUM(uint8_t, STVisibility) { STVisibilityAny = ST_ANY_VISIBILITY, STVisibilityGlobal = ST_GLOBAL_VISIBILITY, STVisibilityHidden = ST_HIDDEN_VISIBILITY, STVisibilityLocal = ST_LOCAL_VISIBILITY, }; typedef BOOL (^ _Nonnull STSymbolFilter)(const char *_Nonnull symname); /** Callback on selecting symbol. */ typedef void (^ _Nonnull STSymbolCallback)(const void *_Nonnull address, const char *_Nonnull symname, void *_Nonnull typeref, void *_Nonnull typeend); typedef void (*fast_dlscan_t)(const void *_Nonnull ptr, STVisibility visibility, STSymbolFilter filter, STSymbolCallback callback); typedef void *_Nullable (*fast_dlsym_t)(const void *_Nonnull ptr, const char *_Nonnull symname); typedef int (*fast_dladdr_t)(const void *_Nonnull, Dl_info *_Nonnull); typedef NSString *_Nonnull (*describeImageInfo_t)(const Dl_info *_Nonnull info); } /** The last piece of the injecting SwiftUI on a device puzzle. Symbolic references are a stream of bytes used to specify a complex type. They contain a relative pointer to the moninal type information of the component types which we need to switch from that newly injected to the original in the app executable. This is becuase when we don't use the dynamic linker it seems injected type information is not proberly initialised. @param image Pointer to pseudo image */ void reverse_symbolics(const void *image) { BOOL debug = FALSE; #define RSPREFIX "reverse_symbolics: ⚠️ " #define rsprintf if (debug) printf #define MAX_SYMBOLIC_REF 0x1f #define PAGE_ROUND(_sz) (((_sz) + PAGE_SIZE-1) & ~(PAGE_SIZE-1)) #define LATE_BIND(f) static f##_t f; if (!f) f = (f##_t)dlsym(RTLD_DEFAULT, #f) LATE_BIND(fast_dlscan); LATE_BIND(fast_dladdr); LATE_BIND(describeImageInfo); uint64_t typeref_size = 0; char *typeref_start = getsectdatafromheader_64((mach_header_64 *)image, SEG_TEXT, "__swift5_typeref", &typeref_size); if (mprotect((void *)((uintptr_t)typeref_start&~(PAGE_SIZE-1)), PAGE_ROUND(typeref_size), PROT_WRITE|PROT_READ) != KERN_SUCCESS) printf(RSPREFIX"Unable to make %d bytes writable %s\n", (int)typeref_size, strerror(errno)); static char symbolics[] = {"_symbolic _____"}; fast_dlscan(image, STVisibilityAny, ^BOOL(const char *symname) { return strncmp(symname, symbolics, sizeof symbolics-1) == 0; }, ^(const void * _Nonnull address, const char * _Nonnull symname, void * _Nonnull typeref, void * _Nonnull typeend) { // rsprintf("%s\n", symname); // char buffer[1000], first[100]; // const char *prefixPtr = symname + sizeof symbolics - 2; // const char *typesPtr = strchr(prefixPtr, ' ')+1; unsigned char *infoPtr = (unsigned char *)address; while (*infoPtr) { if (*infoPtr++ > MAX_SYMBOLIC_REF) { printf(RSPREFIX"Out of sync?\n"); break; } // const char *typeEnd = strchr(typesPtr, ' ') ?: // typesPtr + strlen(typesPtr); // snprintf(buffer, sizeof buffer, "$s%.*sMn", // (int)(typeEnd - typesPtr), typesPtr); int before = *(int *)infoPtr; const void *referenced = infoPtr + before, *value; Dl_info info; if (fast_dladdr(referenced, &info) && info.dli_fbase == image && strcmp(strend(info.dli_sname) - 2, "Mn") == 0 && (value = dlsym(RTLD_DEFAULT, info.dli_sname))) { ssize_t relative = (unsigned char *)value - infoPtr; *(int *)infoPtr = (int)relative; } if (before != *(int *)infoPtr) rsprintf("Reversed: %x -> %x %s\n", before, *(int *)infoPtr, describeImageInfo(&info).UTF8String); infoPtr += sizeof before; while (*infoPtr > MAX_SYMBOLIC_REF) infoPtr++; // static char delim[] = {"_____"}; // while (strncmp(prefixPtr, delim, sizeof delim-1) != 0) // prefixPtr++; // prefixPtr += sizeof delim-1; // // typesPtr = typeEnd; // if (*typesPtr == ' ') // typesPtr++; // else // break; } }); #if 000 static char associateds[] = {"_associated conformance "}; fast_dlscan(image, STVisibilityAny, ^(const char *symname) { return strncmp(symname, associateds, sizeof associateds-1) == 0; }, ^(const void * _Nonnull address, const char * _Nonnull symname, void * _Nonnull typeref, void * _Nonnull typeend) { unsigned char *infoPtr = (unsigned char *)address; infoPtr += 2; void *ptr = infoPtr + *(int *)infoPtr; Dl_info info; fast_dladdr(ptr, &info); printf("ASSOC %s\n", describeImageInfo(&info).UTF8String); int v0 = *(int *)infoPtr; if (image == info.dli_fbase) { extern int main(); void *value = fast_dlsym((void *)main, info.dli_sname); printf(">>>> %s %p %p %p\n", info.dli_sname, image, info.dli_fbase, value); size_t diff = (unsigned char *)value - infoPtr; *(int *)infoPtr = (int)diff; } printf("%p -> %p %s\n", v0, *(int *)infoPtr, describeImageInfo(&info).UTF8String); }); #endif if (mprotect((void *)((uintptr_t)typeref_start&~(PAGE_SIZE-1)), PAGE_ROUND(typeref_size), PROT_EXEC|PROT_READ) != KERN_SUCCESS) printf(RSPREFIX"Unable to make %d bytes executable %s\n", (int)typeref_size, strerror(errno)); } #endif #endif ================================================ FILE: Sources/HotReloadingGuts/include/InjectionClient.h ================================================ // // InjectionClient.h // InjectionBundle // // Created by John Holdsworth on 06/11/2017. // Copyright © 2017 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/HotReloadingGuts/include/InjectionClient.h#68 $ // // Shared definitions between server and client. // #import #import "UserDefaults.h" #import #ifndef __IPHONE_OS_VERSION_MIN_REQUIRED #import #import #import "../../injectiondGuts/include/Xcode.h" #endif #define HOTRELOADING_PORT ":8899" #define HOTRELOADING_SALT 2122172543 #define HOTRELOADING_MULTICAST "239.255.255.239" #ifdef INJECTION_III_APP #define INJECTION_ADDRESS ":8898" #import "../../../../InjectionIII/InjectionIIISalt.h" #define INJECTION_KEY @"bvijkijyhbtrbrebzjbbzcfbbvvq" #define APP_NAME "InjectionIII" #define APP_PREFIX "💉 " #else #define INJECTION_ADDRESS HOTRELOADING_PORT #define INJECTION_SALT HOTRELOADING_SALT extern NSString *INJECTION_KEY; #define APP_NAME "HotReloading" #define APP_PREFIX "🔥 " #if DEBUG @interface NSObject(InjectionTester) - (void)swiftTraceInjectionTest:(NSString *)sourceFile source:(NSString *)source; @end #endif #endif #define COMMANDS_PORT ":8896" #define DYLIB_PREFIX "/eval_injection_" // Was expected by DLKit #define VAPOR_SYMBOL "$s10RoutingKit10ParametersVN" #define FRAMEWORK_DELIMITER @"," #define CALLORDER_DELIMITER @"---" // The various environment variables #define INJECTION_HOST "INJECTION_HOST" #define INJECTION_DETAIL "INJECTION_DETAIL" #define INJECTION_PROJECT_ROOT "INJECTION_PROJECT_ROOT" #define INJECTION_DERIVED_DATA "INJECTION_DERIVED_DATA" #define INJECTION_DYNAMIC_CAST "INJECTION_DYNAMIC_CAST" #define INJECTION_PRESERVE_STATICS "INJECTION_PRESERVE_STATICS" #define INJECTION_SWEEP_DETAIL "INJECTION_SWEEP_DETAIL" #define INJECTION_SWEEP_EXCLUDE "INJECTION_SWEEP_EXCLUDE" #define INJECTION_OF_GENERICS "INJECTION_OF_GENERICS" #define INJECTION_NOGENERICS "INJECTION_NOGENERICS" #define INJECTION_USEINTESTS "INJECTION_USEINTESTS" #define INJECTION_UNHIDE "INJECTION_UNHIDE" #define INJECTION_QUICK_FILES "INJECTION_QUICK_FILES" #define INJECTION_DIRECTORIES "INJECTION_DIRECTORIES" #define INJECTION_STANDALONE "INJECTION_STANDALONE" #define INJECTION_NOKEYPATHS "INJECTION_NOKEYPATHS" #define INJECTION_KEYPATHS "INJECTION_KEYPATHS" #define INJECTION_DAEMON "INJECTION_DAEMON" #define INJECTION_LOOKUP "INJECTION_LOOKUP" #define INJECTION_REPLAY "INJECTION_REPLAY" #define INJECTION_TRACE "INJECTION_TRACE" #define INJECTION_BAZEL "INJECTION_BAZEL" #define INJECTION_DEBUG "INJECTION_DEBUG" #define INJECTION_BUNDLE_NOTIFICATION "INJECTION_BUNDLE_NOTIFICATION" #define INJECTION_METRICS_NOTIFICATION "INJECTION_METRICS_NOTIFICATION" @protocol InjectionReader - (BOOL)readBytes:(void *)buffer length:(size_t)length cmd:(SEL)cmd; @end @interface InjectionClientLegacy @property BOOL vaccineEnabled; + (InjectionClientLegacy *)sharedInstance; - (void)vaccine:object; + (void)flash:vc; - (void)rebuildWithStoryboard:(NSString *)changed error:(NSError **)err; @end @interface NSObject(HotReloading) + (void)runXCTestCase:(Class)aTestCase; #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + (BOOL)injectUI:(NSString *)changed; #endif @end @interface NSProcessInfo(iOSAppOnMac) @property BOOL isiOSAppOnMac; @end typedef NS_ENUM(int, InjectionCommand) { // commands to Bundle InjectionConnected, InjectionWatching, InjectionLog, InjectionSigned, InjectionLoad, InjectionInject, InjectionIdeProcPath, InjectionXprobe, InjectionEval, InjectionVaccineSettingChanged, InjectionTrace, InjectionUntrace, InjectionTraceUI, InjectionTraceUIKit, InjectionTraceSwiftUI, InjectionTraceFramework, InjectionQuietInclude, InjectionInclude, InjectionExclude, InjectionStats, InjectionCallOrder, InjectionFileOrder, InjectionFileReorder, InjectionUninterpose, InjectionFeedback, InjectionLookup, InjectionCounts, InjectionCopy, InjectionPseudoUnlock, InjectionPseudoInject, InjectionObjcClassRefs, InjectionDescriptorRefs, InjectionSetXcodeDev, InjectionAppVersion, InjectionProfileUI, InjectionInvalid = 1000, InjectionEOF = ~0 }; typedef NS_ENUM(int, InjectionResponse) { // responses from bundle InjectionComplete, InjectionPause, InjectionSign, InjectionError, InjectionFrameworkList, InjectionCallOrderList, InjectionScratchPointer, InjectionTestInjection, InjectionLegacyUnhide, InjectionForceUnhide, InjectionProjectRoot, InjectionGetXcodeDev, InjectionBuildCache, InjectionDerivedData, InjectionPlatform, InjectionExit = ~0 }; #ifdef __cplusplus extern "C" { #endif // defined in Unhide.mm extern int unhide_symbols(const char *framework, const char *linkFileList, FILE *log, time_t since); extern int unhide_object(const char *object_file, const char *framework, FILE *log, NSMutableArray *class_references, NSMutableArray *descriptor_refs); extern void unhide_reset(void); #if !TARGET_IPHONE_SIMULATOR extern void reverse_symbolics(const void *image); #endif // objc4-internal.h struct objc_image_info; OBJC_EXPORT Class objc_readClassPair(Class cls, const struct objc_image_info *info) OBJC_AVAILABLE(10.10, 8.0, 9.0, 1.0, 2.0); #ifdef __cplusplus } #endif ================================================ FILE: Sources/HotReloadingGuts/include/SimpleSocket.h ================================================ // // SimpleSocket.h // InjectionIII // // Created by John Holdsworth on 06/11/2017. // Copyright © 2017 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/HotReloadingGuts/include/SimpleSocket.h#16 $ // #import #import @interface SimpleSocket : NSObject { @protected int clientSocket; } @property BOOL isLocalClient; + (void)startServer:(NSString *_Nonnull)address; + (void)runServer:(NSString *_Nonnull)address; + (int)error:(NSString *_Nonnull)message; + (instancetype _Nullable)connectTo:(NSString *_Nonnull)address; + (BOOL)parseV4Address:(NSString *_Nonnull)address into:(struct sockaddr_storage *_Nonnull)serverAddr; + (void)multicastServe:(const char *_Nonnull)multicast port:(const char *_Nonnull)port; + (NSString *_Nonnull)getMulticastService:(const char *_Nonnull)multicast port:(const char *_Nonnull)port message:(const char *_Nonnull)format; - (instancetype _Nonnull)initSocket:(int)socket; - (void)run; - (void)runInBackground; - (int)readInt; - (void * _Nullable)readPointer; - (NSData *_Nullable)readData; - (NSString *_Nullable)readString; - (BOOL)readBytes:(void * _Nonnull)buffer length:(size_t)length cmd:(SEL _Nonnull)cmd; - (BOOL)writeInt:(int)length; - (BOOL)writePointer:(void * _Nullable)pointer; - (BOOL)writeData:(NSData *_Nonnull)data; - (BOOL)writeString:(NSString *_Nonnull)string; - (BOOL)writeCommand:(int)command withString:(NSString *_Nullable)string; @end ================================================ FILE: Sources/HotReloadingGuts/include/UserDefaults.h ================================================ // // UserDefaults.h // InjectionIII // // Created by Christoffer Winterkvist on 10/25/18. // Copyright © 2018 John Holdsworth. All rights reserved. // #import static NSString *const UserDefaultsTDDEnabled = @"Enabled TDD"; static NSString *const UserDefaultsVaccineEnabled = @"Enabled Vaccine"; static NSString *const UserDefaultsLastWatched = @"lastWatched"; static NSString *const UserDefaultsLastProject = @"lastProject"; static NSString *const UserDefaultsProjectFile = @"projectFile"; static NSString *const UserDefaultsBookmarks = @"persistentBookmarks"; static NSString *const UserDefaultsUpdateCheck = @"nextUpdateCheck"; static NSString *const UserDefaultsOrderFront = @"orderFront"; static NSString *const UserDefaultsFeedback = @"feedback"; static NSString *const UserDefaultsLookup = @"typeLookup"; static NSString *const UserDefaultsRemote = @"appRemote"; static NSString *const UserDefaultsReplay = @"replayInjections"; static NSString *const UserDefaultsUnlock = @"deviceUnlock"; static NSString *const UserDefaultsFeed = @"frontendFeed"; ================================================ FILE: Sources/injectiond/AppDelegate.swift ================================================ // // AppDelegate.swift // InjectionIII // // Created by John Holdsworth on 06/11/2017. // Copyright © 2017 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiond/AppDelegate.swift#82 $ // import Cocoa #if SWIFT_PACKAGE import injectiondGuts import RemoteUI // nib compatability... import WebKit @objc(WebView) class WebView : WKWebView {} #endif let XcodeBundleID = "com.apple.dt.Xcode" var appDelegate: AppDelegate! enum InjectionState: String { case ok = "OK" case idle = "Idle" case busy = "Busy" case error = "Error" case ready = "Ready" } @objc(AppDelegate) class AppDelegate : NSObject, NSApplicationDelegate { @IBOutlet var window: NSWindow! @IBOutlet weak var enableWatcher: NSMenuItem! @IBOutlet weak var traceItem: NSMenuItem! @IBOutlet weak var traceInclude: NSTextField! @IBOutlet weak var traceExclude: NSTextField! @IBOutlet weak var traceFilters: NSWindow! @IBOutlet weak var statusMenu: NSMenu! @IBOutlet weak var startItem: NSMenuItem! @IBOutlet weak var xprobeItem: NSMenuItem! @IBOutlet weak var enabledTDDItem: NSMenuItem! @IBOutlet weak var enableVaccineItem: NSMenuItem! @IBOutlet weak var windowItem: NSMenuItem! @IBOutlet weak var remoteItem: NSMenuItem! @IBOutlet weak var updateItem: NSMenuItem! @IBOutlet weak var frontItem: NSMenuItem! @IBOutlet weak var feedbackItem: NSMenuItem! @IBOutlet weak var lookupItem: NSMenuItem! @IBOutlet weak var sponsorItem: NSMenuItem! @IBOutlet var statusItem: NSStatusItem! var watchedDirectories = Set() weak var lastConnection: InjectionServer? var selectedProject: String? let openProject = NSLocalizedString("Select Project Directory", tableName: "Project Directory", comment: "Project Directory") @objc let defaults = UserDefaults.standard var defaultsMap: [NSMenuItem: String]! lazy var isSandboxed = ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil var runningXcodeDevURL: URL? = NSRunningApplication.runningApplications( withBundleIdentifier: XcodeBundleID).first? .bundleURL?.appendingPathComponent("Contents/Developer") var derivedLogs: String? /// Bringing in InjectionNext patching static var ui: AppDelegate { return appDelegate } static func alreadyWatching(_ projectRoot: String) -> String? { return appDelegate.watchedDirectories.first { projectRoot.hasPrefix($0) } } @IBOutlet weak var deviceTesting: NSMenuItem? @IBOutlet weak var selectXcodeItem: NSMenuItem? @IBOutlet weak var patchCompilerItem: NSMenuItem? @IBOutlet weak var librariesField: NSTextField! var codeSigningID: String { selectedProject.flatMap { defaults.string(forKey: $0) } ?? "-" } func watch(path: String) { watchedDirectories.insert(path) lastConnection?.watchDirectory(path) } #if !SWIFT_PACKAGE @IBAction func prepareXcode(_ sender: NSMenuItem) { let open = NSOpenPanel() open.prompt = "Select Xcode to Patch" open.directoryURL = URL(fileURLWithPath: Defaults.xcodePath) open.canChooseDirectories = false open.canChooseFiles = true if open.runModal() == .OK, let path = open.url?.path { selectXcodeItem?.toolTip = path Defaults.xcodeDefault = path patchCompiler(sender) } } #endif @objc func applicationDidFinishLaunching(_ aNotification: Notification) { // Insert code here to initialize your application appDelegate = self let statusBar = NSStatusBar.system statusItem = statusBar.statusItem(withLength: statusBar.thickness) statusItem.highlightMode = true statusItem.menu = statusMenu statusItem.isEnabled = true statusItem.title = "" if isSandboxed { // sponsorItem.isHidden = true updateItem.isHidden = true } else if let platform = getenv("PLATFORM_NAME"), strcmp(platform, "iphonesimulator") == 0 { DeviceServer.startServer(HOTRELOADING_PORT) } else if let unlock = defaults.string(forKey: UserDefaultsUnlock) { let deviceInform = "deviceInform" var openPort = "" if unlock == "any" { if defaults.string(forKey: deviceInform) == nil { let alert: NSAlert = NSAlert() alert.messageText = "Device Injection" alert.informativeText = """ This release supports injection on a real device \ as well as in the simulator. In order to do this it \ needs to open a port to receive socket connections \ from a device which will provoke an OS warning if \ your Mac's firewall is enabled. Decline the prompt \ if you don't intend to use this feature. """ alert.alertStyle = .critical alert.addButton(withTitle: "OK") _ = alert.runModal() defaults.set("Informed", forKey: deviceInform) } openPort = "*" setenv("XPROBE_ANY", "1", 1) DeviceServer.multicastServe(HOTRELOADING_MULTICAST, port: HOTRELOADING_PORT) } DeviceServer.startServer(openPort+HOTRELOADING_PORT) } #if !SWIFT_PACKAGE InjectionServer.startServer(INJECTION_ADDRESS) #endif defaultsMap = [ frontItem: UserDefaultsOrderFront, enabledTDDItem: UserDefaultsTDDEnabled, enableVaccineItem: UserDefaultsVaccineEnabled, feedbackItem: UserDefaultsFeedback, lookupItem: UserDefaultsLookup, remoteItem: UserDefaultsRemote ] for (menuItem, defaultsKey) in defaultsMap { menuItem.state = defaults.bool(forKey: defaultsKey) ? .on : .off } #if SWIFT_PACKAGE if remoteItem.state == .on { remoteItem.state = .off startRemote(remoteItem) } #else if !isSandboxed && defaults.value(forKey: UserDefaultsFeed) != nil { selectXcodeItem?.isHidden = false selectXcodeItem?.toolTip = Defaults.xcodePath selectXcodeItem?.state = updatePatchUnpatch() == .patched ? .on : .off } #endif setMenuIcon(.idle) versionSpecific() } func versionSpecific() { #if SWIFT_PACKAGE let appName = "Hot Reloading" statusMenu.item(withTitle: "Open Project")?.isHidden = true var arguments = CommandLine.arguments.dropFirst() let projectURL = URL(fileURLWithPath: arguments.removeFirst()) let projectRoot = projectURL.deletingLastPathComponent() AppDelegate.ensureInterposable(project: projectURL.path) NSDocumentController.shared.noteNewRecentDocumentURL(projectRoot) derivedLogs = arguments.removeFirst() selectedProject = projectURL.path appDelegate.watchedDirectories = [projectRoot.path] for dir in arguments where !dir.hasPrefix(projectRoot.path) { appDelegate.watchedDirectories.insert(dir) } #else let appName = "InjectionIII" DDHotKeyCenter.shared()? .registerHotKey(withKeyCode: UInt16(kVK_ANSI_Equal), modifierFlags: NSEvent.ModifierFlags.control.rawValue, target:self, action:#selector(autoInject(_:)), object:nil) NSApp.servicesProvider = self if let projectFile = defaults .string(forKey: UserDefaultsProjectFile) { // Received project file from command line option. _ = self.application(NSApp, openFile: URL(fileURLWithPath: projectFile).deletingLastPathComponent().path) } else if let lastWatched = defaults.string(forKey: UserDefaultsLastWatched) { _ = self.application(NSApp, openFile: lastWatched) } else { NSUpdateDynamicServices() } let nextUpdateCheck = defaults.double(forKey: UserDefaultsUpdateCheck) if !isSandboxed && nextUpdateCheck != 0.0 { updateItem.state = .on if Date.timeIntervalSinceReferenceDate > nextUpdateCheck { self.updateCheck(nil) } } #endif statusItem.title = appName if let quit = statusMenu.item(at: statusMenu.items.count-1) { quit.title = "Quit "+appName #if !SWIFT_PACKAGE if let build = Bundle.main .infoDictionary?[kCFBundleVersionKey as String] { quit.toolTip = "Quit (build #\(build))" } #endif } } func application(_ theApplication: NSApplication, openFile filename: String) -> Bool { #if SWIFT_PACKAGE return false #else guard filename != Bundle.main.bundlePath, let url = resolve(path: filename), let fileList = try? FileManager.default .contentsOfDirectory(atPath: url.path) else { return false } if url.pathExtension == "xcworkspace" || url.pathExtension == "xcodeproj" { let alert: NSAlert = NSAlert() alert.messageText = "InjectionIII" alert.informativeText = """ Please select the project directory to watch \ for file changes under, not the project file. """ alert.alertStyle = NSAlert.Style.warning alert.addButton(withTitle: "Sorry") _ = alert.runModal() return false } let projectFiles = SwiftEval.projects(in: fileList) selectedProject = nil if url.path.hasSuffix(".swiftpm") { selectedProject = url.path let pkg = url.appendingPathComponent("Package.swift") if let manifest = try? String(contentsOf: pkg), !manifest.contains("-interposable") { var modified = manifest modified[#""" ( \) ] \) )\Z """#] = """ , linkerSettings: [ .unsafeFlags(["-Xlinker", "-interposable"], .when(configuration: .debug)) ]$1 """ if modified != manifest { do { try modified.write(to: pkg, atomically: true, encoding: .utf8) let alert: NSAlert = NSAlert() alert.messageText = "InjectionIII" alert.informativeText = """ InjectionIII has patched Package.swift to include the -interposable linker flag. Use Menu item "Prepare Project" to complete conversion. """ alert.alertStyle = NSAlert.Style.warning alert.addButton(withTitle: "OK") _ = alert.runModal() } catch { } } } } else if projectFiles == nil || projectFiles!.count > 1 { for lastProjectFile in [UserDefaultsProjectFile, UserDefaultsLastProject] .compactMap({ defaults.string(forKey: $0) }) { for project in projectFiles ?? [] { if selectedProject == nil, url.appendingPathComponent(project) .path == lastProjectFile { selectedProject = lastProjectFile } } } if selectedProject == nil { let open = NSOpenPanel() open.prompt = "Select Project File" open.directoryURL = url open.canChooseDirectories = false open.canChooseFiles = true // open.showsHiddenFiles = TRUE; if open.runModal() == .OK, let url = open.url { selectedProject = url.path } } } else if projectFiles != nil { selectedProject = url .appendingPathComponent(projectFiles![0]).path } guard let projectFile = selectedProject else { let alert: NSAlert = NSAlert() alert.messageText = "InjectionIII" alert.informativeText = "Please select a directory with either a .xcworkspace or .xcodeproj file, below which, are the files you wish to inject." alert.alertStyle = NSAlert.Style.warning alert.addButton(withTitle: "OK") _ = alert.runModal() return false } watchedDirectories.removeAll() watchedDirectories.insert(url.path) if let alsoWatch = defaults.string(forKey: "addDirectory"), let resolved = resolve(path: alsoWatch) { watchedDirectories.insert(resolved.path) } lastConnection?.setProject(projectFile) // AppDelegate.ensureInterposable(project: selectedProject!) NSDocumentController.shared.noteNewRecentDocumentURL(url) statusItem.menu?.item(withTitle: "Open Recent")?.toolTip = url.path // let projectName = URL(fileURLWithPath: projectFile) // .deletingPathExtension().lastPathComponent // traceInclude.stringValue = projectName // updateTraceInclude(nil) defaults.set(projectFile, forKey: UserDefaultsLastProject) defaults.set(url.path, forKey: UserDefaultsLastWatched) return true #endif } func persist(url: URL) { if !isSandboxed { return } var bookmarks = defaults.value(forKey: UserDefaultsBookmarks) as? [String : Data] ?? [String: Data]() do { bookmarks[url.path] = try url.bookmarkData(options: [.withSecurityScope, .securityScopeAllowOnlyReadAccess], includingResourceValuesForKeys: [], relativeTo: nil) defaults.set(bookmarks, forKey: UserDefaultsBookmarks) } catch { _ = InjectionServer.error("Bookmarking failed for \(url), \(error)") } } func resolve(path: String) -> URL? { var isStale: Bool = false if !isSandboxed, FileManager.default.fileExists(atPath: path) { return URL(fileURLWithPath: path) } else if let bookmarks = defaults.value(forKey: UserDefaultsBookmarks) as? [String : Data], let bookmark = bookmarks[path], let resolved = try? URL(resolvingBookmarkData: bookmark, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale), !isStale { _ = resolved.startAccessingSecurityScopedResource() return resolved } else { let open = NSOpenPanel() open.prompt = openProject if path != "" { open.directoryURL = URL(fileURLWithPath: path) } open.canChooseDirectories = true open.canChooseFiles = true // open.showsHiddenFiles = TRUE; if open.runModal() == .OK, let url = open.url { persist(url: url) return url } } return nil } func setMenuIcon(_ state: InjectionState) { DispatchQueue.main.async { let tiffName = "Injection"+state.rawValue if let path = Bundle.main.path(forResource: tiffName, ofType: "tif"), let image = NSImage(contentsOfFile: path) { // image.template = TRUE; self.statusItem.image = image self.statusItem.alternateImage = self.statusItem.image let appRunning = tiffName != "InjectionIdle" self.startItem.isEnabled = appRunning self.xprobeItem.isEnabled = appRunning for item in self.traceItem.submenu!.items { if item.tag == 0 { item.isEnabled = appRunning if !appRunning { item.state = .off } } } } } } @IBAction func openProject(_ sender: Any) { _ = application(NSApp, openFile: "") } @IBAction func addDirectory(_ sender: Any) { let open = NSOpenPanel() open.prompt = openProject open.allowsMultipleSelection = true open.canChooseDirectories = true open.canChooseFiles = false if open.runModal() == .OK { for url in open.urls { watch(path: url.path) persist(url: url) } } } func setFrameworks(_ frameworks: String, menuTitle: String) { DispatchQueue.main.async { guard let frameworksMenu = self.traceItem.submenu? .item(withTitle: menuTitle)?.submenu else { return } frameworksMenu.removeAllItems() for framework in frameworks .components(separatedBy: FRAMEWORK_DELIMITER).sorted() where framework != "" { frameworksMenu.addItem(withTitle: framework, action: #selector(self.traceFramework(_:)), keyEquivalent: "") .target = self } } } @objc func traceFramework(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(.traceFramework, with: sender.title) } @IBAction func toggleTDD(_ sender: NSMenuItem) { toggleState(sender) } @IBAction func toggleVaccine(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(.vaccineSettingChanged, with:vaccineConfiguration()) } @IBAction func toggleFeedback(_ sender: NSMenuItem?) { sender.flatMap { toggleState($0) } lastConnection?.sendCommand(.feedback, with: feedbackItem.state == .on ? "1" : "0") } @IBAction func toggleLookup(_ sender: NSMenuItem?) { sender.flatMap { toggleState($0) } lastConnection?.sendCommand(.lookup, with: lookupItem.state == .on ? "1" : "0") } @IBAction func startRemote(_ sender: NSMenuItem) { #if SWIFT_PACKAGE remoteItem.state = .off toggleState(remoteItem) RMWindowController.startServer(sender) #endif } @IBAction func stopRemote(_ sender: NSMenuItem) { #if SWIFT_PACKAGE remoteItem.state = .on toggleState(remoteItem) RMWindowController.stopServer() #endif } @IBAction func traceApp(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(sender.state == .on ? .trace : .untrace, with: nil) } @IBAction func traceUIApp(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(.traceUI, with: nil) } @IBAction func traceUIKit(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(.traceUIKit, with: nil) } @IBAction func traceSwiftUI(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(.traceSwiftUI, with: nil) } @IBAction func profileSwiftUI(_ sender: NSMenuItem) { toggleState(sender) lastConnection?.sendCommand(.profileUI, with: nil) } @IBAction func traceStats(_ sender: NSMenuItem) { lastConnection?.sendCommand(.stats, with: nil) } @IBAction func remmoveTraces(_ sender: NSMenuItem?) { lastConnection?.sendCommand(.uninterpose, with: nil) } @IBAction func showTraceFilters(_ sender: NSMenuItem?) { NSApplication.shared.activate(ignoringOtherApps: true) traceFilters.makeKeyAndOrderFront(sender) } @IBAction func updateTraceInclude(_ sender: NSButton?) { guard traceInclude.stringValue != "" || sender != nil else { return } update(filter: sender == nil ? .quietInclude : .include, textField: traceInclude) } @IBAction func updateTraceExclude(_ sender: NSButton?) { guard traceExclude.stringValue != "" || sender != nil else { return } update(filter: .exclude, textField: traceExclude) } func update(filter: InjectionCommand, textField: NSTextField) { let regex = textField.stringValue do { if regex != "" { _ = try NSRegularExpression(pattern: regex, options: []) } lastConnection?.sendCommand(filter, with: regex) } catch { let alert = NSAlert(error: error) alert.informativeText = "Invalid regular expression syntax '\(regex)' for filter. Characters [](){}|?*+\\ and . have special meanings. Type: man re_syntax, in the terminal." alert.runModal() textField.becomeFirstResponder() showTraceFilters(nil) } } func vaccineConfiguration() -> String { let vaccineSetting = UserDefaults.standard.bool(forKey: UserDefaultsVaccineEnabled) let dictionary = [UserDefaultsVaccineEnabled: vaccineSetting] let jsonData = try! JSONSerialization .data(withJSONObject: dictionary, options:[]) let configuration = String(data: jsonData, encoding: .utf8)! return configuration } @IBAction func toggleState(_ sender: NSMenuItem) { sender.state = sender.state == .on ? .off : .on if let defaultsKey = defaultsMap[sender] { defaults.set(sender.state, forKey: defaultsKey) } } @IBAction func autoInject(_ sender: NSMenuItem) { lastConnection?.injectPending() // #if false // NSError *error = nil; // // Install helper tool // if ([HelperInstaller isInstalled] == NO) { // #pragma clang diagnostic push // #pragma clang diagnostic ignored "-Wdeprecated-declarations" // if ([[NSAlert alertWithMessageText:@"Injection Helper" // defaultButton:@"OK" alternateButton:@"Cancel" otherButton:nil // informativeTextWithFormat:@"InjectionIII needs to install a privileged helper to be able to inject code into " // "an app running in the iOS simulator. This is the standard macOS mechanism.\n" // "You can remove the helper at any time by deleting:\n" // "/Library/PrivilegedHelperTools/com.johnholdsworth.InjectorationIII.Helper.\n" // "If you'd rather not authorize, patch the app instead."] runModal] == NSAlertAlternateReturn) // return; // #pragma clang diagnostic pop // if ([HelperInstaller install:&error] == NO) { // NSLog(@"Couldn't install Smuggler Helper (domain: %@ code: %d)", error.domain, (int)error.code); // [[NSAlert alertWithError:error] runModal]; // return; // } // } // // // Inject Simulator process // NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"iOSInjection" ofType:@"bundle"]; // if ([HelperProxy inject:bundlePath error:&error] == FALSE) { // NSLog(@"Couldn't inject Simulator (domain: %@ code: %d)", error.domain, (int)error.code); // [[NSAlert alertWithError:error] runModal]; // } // #endif } @IBAction func help(_ sender: Any) { _ = NSWorkspace.shared.open(URL(string: "https://github.com/johnno1962/InjectionIII")!) } @IBAction func sponsor(_ sender: Any) { _ = NSWorkspace.shared.open(URL(string: "https://github.com/sponsors/johnno1962")!) } @IBAction func book(_ sender: Any) { _ = NSWorkspace.shared.open(URL(string: "https://books.apple.com/book/id1551005489")!) } @objc public func applicationWillTerminate(aNotification: NSNotification) { // Insert code here to tear down your application #if !SWIFT_PACKAGE DDHotKeyCenter.shared() .unregisterHotKey(withKeyCode: UInt16(kVK_ANSI_Equal), modifierFlags: NSEvent.ModifierFlags.control.rawValue) #endif } } ================================================ FILE: Sources/injectiond/DeviceServer.swift ================================================ // // DeviceServer.swift // InjectionIII // // Created by John Holdsworth on 13/01/2022. // Copyright © 2017 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiond/DeviceServer.swift#35 $ // import Foundation #if SWIFT_PACKAGE import HotReloadingGuts #endif class DeviceServer: InjectionServer { var scratchPointer: UnsafeMutableRawPointer? var lastSource: String? var loadFailed = false #if !SWIFT_PACKAGE override func validateConnection() -> Bool { switch readInt() { case INJECTION_SALT: return readString() == INJECTION_KEY case HOTRELOADING_SALT: return readString()?.hasPrefix(NSHomeDirectory()) == true default: return false } } #endif override func process(response: InjectionResponse, executable: String) { switch response { case .scratchPointer: scratchPointer = readPointer() builder.tmpDir = NSTemporaryDirectory() appDelegate.setMenuIcon(scratchPointer != nil ? .ok : .error) #if DEBUG case .testInjection: if let file = readString(), let source = readString() { do { if file.hasPrefix("/Users/johnholdsworth/Developer/") { try source.write(toFile: file, atomically: true, encoding: .utf8) } } catch { log("Error writing test source file: \(error)") } } #endif case .error: compileQueue.sync { if !loadFailed, let source = lastSource { loadFailed = true builder.updateLongTermCache(remove: source) recompileAndInject(source: source) } } fallthrough default: super.process(response: response, executable: executable) } } override func recompileAndInject(source: String) { appDelegate.setMenuIcon(.busy) lastSource = source if let slide = self.scratchPointer { if let unlock = UserDefaults.standard .string(forKey: UserDefaultsUnlock) { writeCommand(InjectionCommand.pseudoUnlock.rawValue, with: unlock) } compileQueue.async { self.builder.linkerOptions = " -fuse-ld=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld-classic" + " -Xlinker -image_base -Xlinker 0x" + String(Int(bitPattern: slide), radix: 16) do { let dylib = try self.prepare(source: source) if source[#"\.mm?$"#], // class references in Objective-C var sourceText = try? String(contentsOfFile: source) { sourceText[#"//.*|/\*[^*]+\*/"#] = "" // zap comments self.objcClassRefs.removeAllObjects() var seen = Set() for messagedClass: String in sourceText[#"\[([A-Z]\w+) "#] { if seen.insert(messagedClass).inserted { self.objcClassRefs.add(messagedClass) } } } if let objcClasses = self.objcClassRefs as? [String], let descriptors = self.descriptorRefs as? [String], let data = try? NSData(contentsOfFile: "\(dylib).dylib") as Data { commandQueue.async { self.writeCommand(InjectionCommand.objcClassRefs.rawValue, with: objcClasses.joined(separator: ",")) self.writeCommand(InjectionCommand.descriptorRefs.rawValue, with: descriptors.joined(separator: ",")) self.writeCommand(InjectionCommand.pseudoInject.rawValue, with: source) self.writePointer(slide) self.write(data as Data) } return } } catch { NSLog("\(error)") } } } else { // You can load a dylib on device after all... super.recompileAndInject(source: source) } } override func inject(dylib: String) { if isLocalClient { return super.inject(dylib: dylib) } if let data = NSData(contentsOfFile: "\(dylib).dylib") { commandQueue.sync { write(InjectionCommand.copy.rawValue) write(data as Data) appDelegate.setMenuIcon(.ok) } } else { sendCommand(.log, with: "\(APP_PREFIX)Error reading \(dylib).dylib") } } } ================================================ FILE: Sources/injectiond/Experimental.swift ================================================ // // Experimental.swift // InjectionIII // // Created by User on 20/10/2020. // Copyright © 2020 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiond/Experimental.swift#39 $ // import Cocoa #if SWIFT_PACKAGE import HotReloadingGuts import injectiondGuts import SwiftRegex import XprobeUI #endif extension AppDelegate { @IBAction func runXprobe(_ sender: NSMenuItem) { #if SWIFT_PACKAGE if xprobePlugin == nil { xprobePlugin = XprobePluginMenuController() xprobePlugin.applicationDidFinishLaunching( Notification(name: Notification.Name(rawValue: ""))) xprobePlugin.injectionPlugin = unsafeBitCast(self, to: AnyClass.self) } lastConnection?.sendCommand(.xprobe, with: "") windowItem.isHidden = false #endif } @objc func evalCode(_ swift: String) { lastConnection?.sendCommand(.eval, with:swift) } @IBAction func callOrder(_ sender: NSMenuItem) { lastConnection?.sendCommand(.callOrder, with: nil) } @IBAction func fileOrder(_ sender: NSMenuItem) { lastConnection?.sendCommand(.fileOrder, with: nil) } @IBAction func fileReorder(_ sender: NSMenuItem) { lastConnection?.sendCommand(.fileReorder, with: nil) } @IBAction func objectCounts(_ sender: NSMenuItem) { lastConnection?.sendCommand(.counts, with: nil) } func fileReorder(signatures: [String]) { var projectEncoding: String.Encoding = .utf8 let projectURL = selectedProject.flatMap { URL(fileURLWithPath: $0 .replacingOccurrences(of: ".xcworkspace", with: ".xcodeproj")) } guard let pbxprojURL = projectURL? .appendingPathComponent("project.pbxproj"), let projectSource = try? String(contentsOf: pbxprojURL, usedEncoding: &projectEncoding) else { lastConnection?.sendCommand(.log, with: "\(APP_PREFIX)Could not load project file \(projectURL?.path ?? "unknown").") return } var orders = ["AppDelegate.swift": 0] var order = 1 SwiftEval.uniqueTypeNames(signatures: signatures) { typeName in orders[typeName+".swift"] = order order += 1 } var newProjectSource = projectSource // For each PBXSourcesBuildPhase in project file newProjectSource[#""" ^\s+isa = PBXSourcesBuildPhase; \s+buildActionMask = \d+; \s+files = \( ((?:[^\n]+\n)*?)\# \s+\); """#.anchorsMatchLines, group: 1] = { (sources: String, stop) -> String in // reorder the lines for each file in the PBXSourcesBuildPhase // to bring those traced first to the front of the app binary. // This localises the startup code in as few pages as possible. return (sources[#"(\s+\S+ /\* (\S+) in Sources \*/,\n)"#] as [(line: String, file: String)]).sorted(by: { orders[$0.file] ?? order < orders[$1.file] ?? order }).map { $0.line }.joined() } DispatchQueue.main.sync { let project = projectURL!.lastPathComponent let backup = pbxprojURL.path+".preorder" let alert = NSAlert() alert.messageText = "About to reorder '\(project)'" alert.informativeText = "This experimental feature will modify the order of source files in memory to reduce paging on startup. There will be a backup of the project file before re-ordering at: \(backup)" alert.addButton(withTitle: "Cancel") alert.addButton(withTitle: "Go ahead") switch alert.runModal() { case .alertSecondButtonReturn: do { if !FileManager.default.fileExists(atPath: backup) { try projectSource.write(toFile: backup, atomically: true, encoding: projectEncoding) } try newProjectSource.write(to: pbxprojURL, atomically: true, encoding: projectEncoding) } catch { NSAlert(error: error).runModal() } default: break } } } /// Entry point for "Injection Goto" service /// - Parameters: /// - pboard: NSPasteboard containing selected type [+method) name /// - userData: N/A /// - errorPtr: NSString describing error on error @objc func injectionGoto(_ pboard: NSPasteboard, userData: NSString, error errorPtr: UnsafeMutablePointer) { guard pboard.canReadObject(forClasses: [NSString.self], options:nil), let target = pboard.string(forType: .string) else { return } let parts = target.components(separatedBy: ".") .filter { !$0.hasSuffix("init") } let builder = SwiftEval() builder.projectFile = selectedProject guard parts.count > 0, let (_, logsDir) = try? builder.determineEnvironment(classNameOrFile: "") else { errorPtr.pointee = "\(APP_PREFIX)Injection Goto service not availble." as NSString lastConnection?.sendCommand(.log, with: errorPtr.pointee as String) return } var className: String!, sourceFile: String? let tmpDir = NSTemporaryDirectory() for part in parts { let subParts = part.components(separatedBy: " ") className = subParts[0] if let (_, foundSourceFile) = try? builder.findCompileCommand(logsDir: logsDir, classNameOrFile: className, tmpfile: tmpDir+"/eval101") { sourceFile = foundSourceFile className = subParts.count > 1 ? subParts.last : parts.last break } } className = className.replacingOccurrences(of: #"\((\S+).*"#, with: "$1", options: .regularExpression) guard sourceFile != nil, let sourceText = try? NSString(contentsOfFile: sourceFile!, encoding: String.Encoding.utf8.rawValue), let finder = try? NSRegularExpression(pattern: #"(?:\b(?:var|func|struct|class|enum)\s+|^[+-]\s*(?:\([^)]*\)\s*)?)(\#(className!))\b"#, options: [.anchorsMatchLines]) else { errorPtr.pointee = """ \(APP_PREFIX)Unable to find source file for type '\(className!)' \ using build logs.\n\(APP_PREFIX)Do you have the right project selected? \ Try with a clean build. """ as NSString lastConnection?.sendCommand(.log, with: errorPtr.pointee as String) return } let match = finder.firstMatch(in: sourceText as String, options: [], range: NSMakeRange(0, sourceText.length)) DispatchQueue.main.async { if let xCode = SBApplication(bundleIdentifier: XcodeBundleID), // xCode.activeWorkspaceDocument.path != nil, let doc = xCode.open(sourceFile!) as? SBObject, doc.selectedCharacterRange != nil, let range = match?.range(at: 1) { doc.selectedCharacterRange = [NSNumber(value: range.location+1), NSNumber(value: range.location+range.length)] } else { var numberOfLine = 0, index = 0 if let range = match?.range(at: 1) { while index < range.location { index = NSMaxRange(sourceText .lineRange(for: NSMakeRange(index, 0))) numberOfLine += 1 } } guard numberOfLine != 0 else { return } var xed = "/usr/bin/xed" if let xcodeURL = self.runningXcodeDevURL { xed = xcodeURL .appendingPathComponent("usr/bin/xed").path } let script = tmpDir+"/injection_goto.sh" do { try "\"\(xed)\" --line \(numberOfLine) \"\(sourceFile!)\"" .write(toFile: script, atomically: false, encoding: .utf8) chmod(script, 0o700) let task = Process() task.launchPath = "/usr/bin/open" task.arguments = ["-b", "com.apple.Terminal", script] task.launch() task.waitUntilExit() } catch { errorPtr.pointee = "\(APP_PREFIX)Failed to write \(script): \(error)" as NSString NSLog("\(errorPtr.pointee)") } } } } static func ensureInterposable(project: String) { var projectEncoding: String.Encoding = .utf8 let projectURL = URL(fileURLWithPath: project) let pbxprojURL = projectURL.appendingPathComponent("project.pbxproj") if let projectSource = try? String(contentsOf: pbxprojURL, usedEncoding: &projectEncoding), !projectSource.contains("-interposable") { var newProjectSource = projectSource // For each PBXSourcesBuildPhase in project file... // Make sure "Other linker Flags" includes -interposable newProjectSource[#""" /\* Debug \*/ = \{ \s+isa = XCBuildConfiguration; (?:.*\n)*?(\s+)buildSettings = \{ ((?:.*\n)*?\1\};) """#, group: 2] = """ OTHER_LDFLAGS = ( "-Xlinker", "-interposable", ); ENABLE_BITCODE = NO; $2 """ if newProjectSource != projectSource { let backup = pbxprojURL.path+".prepatch" if !FileManager.default.fileExists(atPath: backup) { try? projectSource.write(toFile: backup, atomically: true, encoding: projectEncoding) } do { let alert = NSAlert() alert.messageText = "injectiond" alert.informativeText = """ \(APP_NAME) can patch your project slightly to add the \ required -Xlinker -interposable \"Other Linker Flags\". \ Restart the app to have these changes take effect. \ A backup has been saved at: \(backup) """ alert.addButton(withTitle: "Go ahead") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { try newProjectSource.write(to: pbxprojURL, atomically: true, encoding: projectEncoding) } } catch { NSLog("Could not patch project \(pbxprojURL): \(error)") let alert = NSAlert() alert.messageText = "Could not process project file \(projectURL): \(error)" _ = alert.runModal() } } } } @IBAction func prepareProject(_ sender: NSMenuItem) { guard let selectedProject = selectedProject else { let alert = NSAlert() alert.messageText = "Please select a project directory." _ = alert.runModal() return } Self.ensureInterposable(project: selectedProject) for directory in watchedDirectories { prepareSwiftUI(projectRoot: URL(fileURLWithPath: directory)) } } func prepareSwiftUI(projectRoot: URL) { do { guard let enumerator = FileManager.default .enumerator(atPath: projectRoot.path) else { return } let alert = NSAlert() alert.messageText = "About to patch SwiftUI files in the source directory: \(projectRoot.path) for injection." alert.addButton(withTitle: "Go ahead") alert.addButton(withTitle: "Cancel") switch alert.runModal() { case .alertSecondButtonReturn: return default: break } for file in enumerator { guard let file = file as? String, file.hasSuffix(".swift"), !file.hasPrefix("Packages") else { continue } let fileURL = projectRoot.appendingPathComponent(file) guard let original = try? String(contentsOf: fileURL) else { continue } var patched = original patched[#""" ^((\s+)(public )?(var body:|func body\([^)]*\) -\>) some View \{\n\# (\2(?! (if|switch|ForEach) )\s+(?!\.enableInjection)\S.*\n|\s*\n)+)(?() init() { _ = loadInjectionOnce // .enableInjection() optional Xcode 16+ cancellable = NotificationCenter.default.publisher(for: Notification.Name("\(INJECTION_BUNDLE_NOTIFICATION)")) .sink { [weak self] change in self?.injectionNumber += 1 self?.publisher.send() } } } extension SwiftUI.View { public func eraseToAnyView() -> some SwiftUI.View { _ = loadInjectionOnce return AnyView(self) } public func enableInjection() -> some SwiftUI.View { return eraseToAnyView() } public func loadInjection() -> some SwiftUI.View { return eraseToAnyView() } public func onInjection(bumpState: @escaping () -> ()) -> some SwiftUI.View { return self .onReceive(injectionObserver.publisher, perform: bumpState) .eraseToAnyView() } } @available(iOS 13.0, *) @propertyWrapper public struct ObserveInjection: DynamicProperty { @ObservedObject private var iO = injectionObserver public init() {} public private(set) var wrappedValue: Int { get {0} set {} } } #else extension SwiftUI.View { @inline(__always) public func eraseToAnyView() -> some SwiftUI.View { return self } @inline(__always) public func enableInjection() -> some SwiftUI.View { return self } @inline(__always) public func loadInjection() -> some SwiftUI.View { return self } @inline(__always) public func onInjection(bumpState: @escaping () -> ()) -> some SwiftUI.View { return self } } @available(iOS 13.0, *) @propertyWrapper public struct ObserveInjection { public init() {} public private(set) var wrappedValue: Int { get {0} set {} } } #endif #endif """ } if patched != original { try patched.write(to: fileURL, atomically: false, encoding: .utf8) } } } catch { print(error) } } } ================================================ FILE: Sources/injectiond/InjectionServer.swift ================================================ // // InjectionServer.swift // InjectionIII // // Created by John Holdsworth on 06/11/2017. // Copyright © 2017 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiond/InjectionServer.swift#73 $ // import Cocoa #if SWIFT_PACKAGE import HotReloadingGuts import injectiondGuts import XprobeUI #endif let commandQueue = DispatchQueue(label: "InjectionCommand") let compileQueue = DispatchQueue(label: "InjectionCompile") var projectInjected = [String: [String: TimeInterval]]() let MIN_INJECTION_INTERVAL = 1.0 public class InjectionServer: SimpleSocket { // InjectionNext integration static var clientQueue: DispatchQueue { commandQueue } static var currentClient: InjectionServer? { appDelegate.lastConnection } static var currentClients: [InjectionServer?] { [currentClient] } var injectionNumber = 100 var exports = [String: [String]]() var platform = "iPhoneSimulator" var tmpPath: String { builder.tmpDir } var arch: String { builder.arch } var fileChangeHandler: ((_ changed: NSArray, _ ideProcPath:String) -> Void)! var fileWatchers = [FileWatcher]() var pause: TimeInterval = 0.0 var pending = [String]() var builder = UnhidingEval() var lastIdeProcPath = "" let objcClassRefs = NSMutableArray() let descriptorRefs = NSMutableArray() open func log(_ msg: String) { NSLog("\(APP_PREFIX)\(APP_NAME) \(msg)") } @discardableResult override public class func error(_ message: String) -> Int32 { let saveno = errno let msg = String(format:message, strerror(saveno)) NSLog("\(APP_PREFIX)\(APP_NAME) \(msg)") DispatchQueue.main.async { let alert: NSAlert = NSAlert() alert.messageText = "\(self)" alert.informativeText = msg alert.alertStyle = .warning alert.addButton(withTitle: "OK") _ = alert.runModal() } return -1 } func sendCommand(_ command: InjectionCommand, with string: String?) { commandQueue.sync { _ = writeCommand(command.rawValue, with: string) } } func validateConnection() -> Bool { return readInt() == INJECTION_SALT && readString() == INJECTION_KEY } @objc override public func runInBackground() { var candiateProjectFile = appDelegate.selectedProject if candiateProjectFile == nil { DispatchQueue.main.sync { appDelegate.openProject(self) } candiateProjectFile = appDelegate.selectedProject } guard let projectFile = candiateProjectFile else { return } // tell client app the inferred project being watched log("Connection for project file: \(projectFile)") guard validateConnection() else { log("*** Error: SALT or KEY invalid. Are you running start_daemon.sh or InjectionIII.app from the right directory?") write("/tmp") write(InjectionCommand.invalid.rawValue) return } let ee = builder.evalError defer { builder.evalError = ee builder.signer = nil } // client specific data for building if let frameworks = readString() { builder.frameworks = frameworks } else { return } if let arch = readString() { builder.arch = arch } else { return } if appDelegate.isSandboxed { builder.tmpDir = NSTemporaryDirectory() } else { builder.tmpDir = builder.frameworks } write(builder.tmpDir) if !FileManager.default.fileExists(atPath: builder.tmpDir) { builder.tmpDir = NSTemporaryDirectory() } log("Using tmp dir: \(builder.tmpDir)") // log errors to client builder.evalError = { (message: String) in self.log("evalError: \(message)") self.sendCommand(.log, with: (message.hasPrefix("Compiling") ?"":"⚠️ ")+message) return NSError(domain:"SwiftEval", code:-1, userInfo:[NSLocalizedDescriptionKey: message]) } builder.signer = { let identity = appDelegate.defaults.string(forKey: projectFile) if identity != nil { self.log("Signing with identity: \(identity!)") } setenv("TOOLCHAIN_DIR", self.builder.xcodeDev + "/Toolchains/XcodeDefault.xctoolchain", 1) let dylib = self.builder.tmpDir+"/eval"+$0 var error = SignerService.codesignDylib(dylib, identity: identity) if error != nil && self.isLocalClient { error = SignerService.codesignDylib(dylib, identity: "-") } if let error = error { self.sendCommand(.log, with:"Codesigning failed with output: " + error.trimmingCharacters(in: .whitespacesAndNewlines)) return false } return true } // Xcode specific config if let xcodeDevURL = appDelegate.runningXcodeDevURL { builder.xcodeDev = xcodeDevURL.path } builder.projectFile = projectFile appDelegate.setMenuIcon(.ok) appDelegate.lastConnection = self pending = [] var lastInjected = projectInjected[projectFile] if lastInjected == nil { lastInjected = [String: Double]() projectInjected[projectFile] = lastInjected! } guard let executable = readString() else { return } if appDelegate.defaults.bool(forKey: UserDefaultsReplay) && appDelegate.enableWatcher.state == .on { let mtime = { (path: String) -> time_t in var info = stat() return stat(path, &info) == 0 ? info.st_mtimespec.tv_sec : 0 } let executableBuild = mtime(executable) for (source, _) in lastInjected! { if !source.hasSuffix("storyboard") && !source.hasSuffix("xib") && mtime(source) > executableBuild { recompileAndInject(source: source) } } } builder.createUnhider(executable: executable, objcClassRefs, descriptorRefs) var testCache = [String: [String]]() fileChangeHandler = { (changed: NSArray, ideProcPath: String) in var changed = changed as! [String] if UserDefaults.standard.bool(forKey: UserDefaultsTDDEnabled) { for injectedFile in changed { var matchedTests = testCache[injectedFile] if matchedTests == nil { matchedTests = Self.searchForTestWithFile(injectedFile, projectRoot: appDelegate .watchedDirectories.first ?? (projectFile as NSString) .deletingLastPathComponent, fileManager: FileManager.default) testCache[injectedFile] = matchedTests } changed += matchedTests! } } let now = NSDate.timeIntervalSinceReferenceDate let automatic = appDelegate.enableWatcher.state == .on for swiftSource in changed { if !self.pending.contains(swiftSource) { if (now > (lastInjected?[swiftSource] ?? 0.0) + MIN_INJECTION_INTERVAL && now > self.pause) { lastInjected![swiftSource] = now projectInjected[projectFile] = lastInjected! self.pending.append(swiftSource) if !automatic { let file = (swiftSource as NSString).lastPathComponent self.sendCommand(.log, with:"'\(file)' changed, type ctrl-= to inject") } } } } self.lastIdeProcPath = ideProcPath self.builder.lastIdeProcPath = ideProcPath if (automatic) { self.injectPending() } } defer { fileChangeHandler = nil } // start up file watchers to write generated tmpfile path to client app setProject(projectFile) if projectFile.contains("/Desktop/") || projectFile.contains("/Documents/") { sendCommand(.log, with: "\(APP_PREFIX)⚠️ Your project file seems to be in the Desktop or Documents folder and may prevent \(APP_NAME) working as it has special permissions.") } DispatchQueue.main.sync { appDelegate.updateTraceInclude(nil) appDelegate.updateTraceExclude(nil) appDelegate.toggleFeedback(nil) appDelegate.toggleLookup(nil) } if let appVersion = Bundle.main.infoDictionary?[ "CFBundleShortVersionString"] as? String { sendCommand(.appVersion, with: appVersion) } // read status responses from client app while true { let commandInt = readInt() guard let response = InjectionResponse(rawValue: commandInt) else { log("InjectionServer: Unexpected case \(commandInt)") break } if response == .exit { break } process(response: response, executable: executable) } // client app disconnected fileWatchers.removeAll() appDelegate.traceItem.state = .off appDelegate.setMenuIcon(.idle) } func process(response: InjectionResponse, executable: String) { switch response { case .frameworkList: appDelegate.setFrameworks(readString() ?? "", menuTitle: "Trace Framework") appDelegate.setFrameworks(readString() ?? "", menuTitle: "Trace SysInternal") appDelegate.setFrameworks(readString() ?? "", menuTitle: "Trace Package") case .complete: appDelegate.setMenuIcon(.ok) if appDelegate.frontItem.state == .on { print(executable) let appToOrderFront: URL if executable.contains("/MacOS/") { appToOrderFront = URL(fileURLWithPath: executable) .deletingLastPathComponent() .deletingLastPathComponent() .deletingLastPathComponent() } else if executable.contains("/Wrapper/") { appToOrderFront = URL(fileURLWithPath: executable) .deletingLastPathComponent() } else { appToOrderFront = URL(fileURLWithPath: builder.xcodeDev) .appendingPathComponent("Applications/Simulator.app") } NSWorkspace.shared.open(appToOrderFront) } break case .pause: pause = NSDate.timeIntervalSinceReferenceDate + Double(readString() ?? "0.0")! break case .getXcodeDev: if let xcodeDev = readString() { builder.xcodeDev = xcodeDev } case .sign: guard let signer = builder.signer, appDelegate.isSandboxed //|| xprobePlugin != nil else { sendCommand(.signed, with: "0") break } sendCommand(.signed, with: signer(readString() ?? "") ? "1": "0") case .callOrderList: if let calls = readString()? .components(separatedBy: CALLORDER_DELIMITER) { appDelegate.fileReorder(signatures: calls) } break case .error: appDelegate.setMenuIcon(.error) log("Injection error: \(readString() ?? "Uknown")") case .legacyUnhide: builder.legacyUnhide = readString() == "1" case .forceUnhide: builder.startUnhide() case .projectRoot: if let projectRoot = readString() { DispatchQueue.main.async { _ = appDelegate.application(NSApp, openFile: projectRoot) } } case .buildCache: if let buildCache = readString() { builder.buildCacheFile = buildCache } case .derivedData: if let derived = readString() { setenv(INJECTION_DERIVED_DATA, derived, 1) } case .platform: if let clientPlatform = readString() { platform = clientPlatform } default: break } } func recompileAndInject(source: String) { sendCommand(.ideProcPath, with: lastIdeProcPath) appDelegate.setMenuIcon(.busy) if appDelegate.isSandboxed || source.hasSuffix(".storyboard") || source.hasSuffix(".xib") { #if SWIFT_PACKAGE try? source.write(toFile: "/tmp/injecting_storyboard.txt", atomically: false, encoding: .utf8) #endif sendCommand(.inject, with: source) } else { compileQueue.async { do { let dylib = try self.prepare(source: source) self.sendCommand(.setXcodeDev, with: self.builder.xcodeDev) self.inject(dylib: dylib) return } catch { NSLog("\(APP_PREFIX)Build error: \(error)") } appDelegate.setMenuIcon(.error) self.builder.updateLongTermCache(remove: source) } } } public func prepare(source: String) throws -> String { #if INJECTION_III_APP if source.hasSuffix(".swift") && !appDelegate.isSandboxed && appDelegate.updatePatchUnpatch() == .patched, let prepared = NextCompiler.compileQueue.sync(execute: { try? FrontendServer.frontendRecompiler() .prepare(source: source, connected: InjectionServer.currentClient) }), builder.signer?(prepared.dylibName["/eval", ""]) == true { FrontendServer.writeCache(for: prepared.platform) return prepared.dylib[".dylib$", ""] } #endif return try builder.rebuildClass(oldClass: nil, classNameOrFile: source, extra: nil) } public func inject(dylib: String) { sendCommand(.load, with: dylib) } public func watchDirectory(_ directory: String) { fileWatchers.append(FileWatcher(roots: [directory], callback: fileChangeHandler)) sendCommand(.watching, with: directory) } @objc public func injectPending() { for swiftSource in pending { recompileAndInject(source: swiftSource) } pending.removeAll() } @objc public func setProject(_ projectFile: String) { guard fileChangeHandler != nil else { return } builder.projectFile = projectFile #if !SWIFT_PACKAGE let projectName = URL(fileURLWithPath: projectFile) .deletingPathExtension().lastPathComponent let derivedLogs = String(format: // legacy fallback of last resort removed "%@/NotLibrary/Developer/Xcode/DerivedData/%@-%@/Logs/Build", NSHomeDirectory(), projectName .replacingOccurrences(of: #"[\s]+"#, with:"_", options: .regularExpression), XcodeHash.hashString(forPath: projectFile)) #else let derivedLogs = appDelegate.derivedLogs ?? "No derived logs" #endif if FileManager.default.fileExists(atPath: derivedLogs) { builder.derivedLogs = derivedLogs } sendCommand(.vaccineSettingChanged, with:appDelegate.vaccineConfiguration()) fileWatchers.removeAll() sendCommand(.connected, with: projectFile) for directory in appDelegate.watchedDirectories { watchDirectory(directory) } } class func searchForTestWithFile(_ injectedFile: String, projectRoot: String, fileManager: FileManager) -> [String] { var matchedTests = [String]() let injectedFileName = URL(fileURLWithPath: injectedFile) .deletingPathExtension().lastPathComponent let projectUrl = URL(fileURLWithPath: projectRoot) if let enumerator = fileManager.enumerator(at: projectUrl, includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey], options: .skipsHiddenFiles, errorHandler: { (url: URL, error: Error) -> Bool in NSLog("[Error] \(error) (\(url))") return false }) { for fileURL in enumerator { var filename: AnyObject? var isDirectory: AnyObject? if let fileURL = fileURL as? NSURL { try! fileURL.getResourceValue(&filename, forKey:URLResourceKey.nameKey) try! fileURL.getResourceValue(&isDirectory, forKey:URLResourceKey.isDirectoryKey) if filename?.hasPrefix("_") == true && isDirectory?.boolValue == true { enumerator.skipDescendants() continue } if isDirectory?.boolValue == false && filename?.pathExtension == ".swift", let lastPathComponent = filename?.lastPathComponent, lastPathComponent != (injectedFile as NSString).lastPathComponent && filename?.lowercased .contains(injectedFileName.lowercased()) == true && (lastPathComponent.contains("Test") == true || lastPathComponent.contains("Spec.") == true) { matchedTests.append(fileURL.path!) } } } } return matchedTests } public class func urlEncode(string: String) -> String { let unreserved = "-._~/?" var allowed = CharacterSet.alphanumerics allowed.insert(charactersIn: unreserved) return string.addingPercentEncoding(withAllowedCharacters: allowed)! } deinit { log("\(self).deinit()") } } ================================================ FILE: Sources/injectiond/UpdateCheck.swift ================================================ // // UpdateCheck.swift // InjectionIII // // Created by John Holdsworth on 17/09/2020. // Copyright © 2020 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiond/UpdateCheck.swift#3 $ // import Cocoa #if SWIFT_PACKAGE import HotReloadingGuts #endif extension AppDelegate { @IBAction func updateCheck(_ sender: NSMenuItem?) { let userInitiated = sender != nil URLSession(configuration: .default).dataTask(with: URL(string: "https://api.github.com/repos/johnno1962/InjectionIII/releases")!) { data, response, error in do { if let data = data { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .iso8601 let releases = try decoder.decode([Release].self, from: data) DispatchQueue.main.async { guard let latest = releases .first(where: { !$0.prerelease }), let available = latest.tagName, let current = Bundle.main.object( forInfoDictionaryKey: "CFBundleShortVersionString") as? String, available.compare(current, options: .numeric) == .orderedDescending else { if userInitiated { let alert = NSAlert() alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Check Monthly") alert.messageText = "You are running the latest released version." switch alert.runModal() { case .alertSecondButtonReturn: self.updateItem.state = .on default: break } } self.setUpdateCheck() return } let fmt = DateFormatter() fmt.dateStyle = .medium let alert = NSAlert() alert.messageText = "New build \(available) (\(fmt.string(from: latest.publishedAt))) is available." alert.informativeText = latest.body alert.addButton(withTitle: "View") alert.addButton(withTitle: "Download") alert.addButton(withTitle: "Later") switch alert.runModal() { case .alertFirstButtonReturn: NSWorkspace.shared.open(latest.htmlUrl) case .alertSecondButtonReturn: NSWorkspace.shared.open(latest .assets[0].browserDownloadUrl) default: break } self.setUpdateCheck() } } else if let error = error { throw error } } catch { DispatchQueue.main.async { NSAlert(error: error).runModal() } } }.resume() } func setUpdateCheck() { if updateItem.state == .on { defaults.set(Date.timeIntervalSinceReferenceDate + 30 * 24 * 60 * 60, forKey: UserDefaultsUpdateCheck) } } struct Release: Decodable { let htmlUrl: URL let tagName: String? let prerelease: Bool let publishedAt: Date let assets: [Asset] let body: String } struct Asset: Decodable { let browserDownloadUrl: URL } } ================================================ FILE: Sources/injectiond/main.swift ================================================ // // main.swift // HotReloading // // Created by John Holdsworth on 02/24/2021. // Copyright © 2021 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiond/main.swift#19 $ // // Server daemon side of HotReloading simulating InjectionIII.app. // import Cocoa // Launch as a Cocoa app var argv = CommandLine.arguments.map { $0.withCString { strdup($0) } } argv.withUnsafeMutableBufferPointer { _ = NSApplicationMain(Int32($0.count), $0.baseAddress!) } ================================================ FILE: Sources/injectiondGuts/SignerService.m ================================================ // // SignerService.m // InjectionIII // // Created by John Holdsworth on 06/11/2017. // Copyright © 2017 John Holdsworth. All rights reserved. // // $Id: //depot/HotReloading/Sources/injectiondGuts/SignerService.m#18 $ // #import "SignerService.h" @implementation SignerService + (NSString *)codesignDylib:(NSString *)dylib identity:(NSString *)identity { static NSString *adhocSign = @"-"; const char *envIdentity = getenv("EXPANDED_CODE_SIGN_IDENTITY") ?: getenv("CODE_SIGN_IDENTITY"); const char *toolchainDir = getenv("TOOLCHAIN_DIR") ?: "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain"; if (envIdentity && strlen(envIdentity)) { identity = [NSString stringWithFormat:@"\"%s\"", envIdentity]; NSLog(@"Signing identity from environment: %@", identity); } NSString *command = [NSString stringWithFormat:@"" "(export CODESIGN_ALLOCATE=\"%s/usr/bin/codesign_allocate\"; " "if /usr/bin/file \"%@\" | /usr/bin/grep ' shared library ' >/dev/null;" "then /usr/bin/codesign --force -s %@ \"%@\";" "else echo BAD-FILE; exit 1; fi) 2>&1", toolchainDir, dylib, identity ?: adhocSign, dylib]; FILE *fp = popen(command.UTF8String, "r"); if (!fp) return @"Could not popen() for codesign"; NSMutableString *err = [NSMutableString new]; char buffer[1000]; while (fgets(buffer, sizeof buffer, fp)) [err appendFormat:@"%s", buffer]; if (pclose(fp) >> 8 == EXIT_SUCCESS) return nil; NSLog(@"*** Codesign failed with command: %@ Error: %@", command, err); return err; } #if 0 // no longer used - (void)runInBackground { char __unused skip, buffer[1000]; buffer[read(clientSocket, buffer, sizeof buffer-1)] = '\000'; NSString *path = [[NSString stringWithUTF8String:buffer] componentsSeparatedByString:@" "][1]; if ([[self class] codesignDylib:path identity:nil]) { snprintf(buffer, sizeof buffer, "HTTP/1.0 200 OK\r\n\r\n"); write(clientSocket, buffer, strlen(buffer)); } } #endif @end ================================================ FILE: Sources/injectiondGuts/include/SignerService.h ================================================ // // SignerService.h // InjectionIII // // Created by John Holdsworth on 06/11/2017. // Copyright © 2017 John Holdsworth. All rights reserved. // #import @interface SignerService: NSObject + (NSString * _Nullable)codesignDylib:(NSString * _Nonnull)dylib identity:(NSString * _Nullable)identity; @end ================================================ FILE: Sources/injectiondGuts/include/Xcode.h ================================================ /* * Xcode.h -- extracted using: sdef /Applications/Xcode.app | sdp -fh --basename Xcode */ #import #import @class XcodeApplication, XcodeDocument, XcodeWindow, XcodeFileDocument, XcodeTextDocument, XcodeSourceDocument, XcodeWorkspaceDocument, XcodeSchemeActionResult, XcodeSchemeActionIssue, XcodeBuildError, XcodeBuildWarning, XcodeAnalyzerIssue, XcodeTestFailure, XcodeScheme, XcodeRunDestination, XcodeDevice, XcodeBuildConfiguration, XcodeProject, XcodeBuildSetting, XcodeResolvedBuildSetting, XcodeTarget; // Category added for use with Swift @interface SBObject(InjectionIII) - (id) open:(id)x; // Open a document. @property (copy) NSString *path; // The document's path. @property (copy) NSArray *selectedCharacterRange; // The first and last character positions in the selection. @property (copy) XcodeWorkspaceDocument *activeWorkspaceDocument; // The active workspace document in Xcode. @end enum XcodeSaveOptions { XcodeSaveOptionsYes = 'yes ' /* Save the file. */, XcodeSaveOptionsNo = 'no ' /* Do not save the file. */, XcodeSaveOptionsAsk = 'ask ' /* Ask the user whether or not to save the file. */ }; typedef enum XcodeSaveOptions XcodeSaveOptions; // The status of a scheme action result object. enum XcodeSchemeActionResultStatus { XcodeSchemeActionResultStatusNotYetStarted = 'srsn' /* The action has not yet started. */, XcodeSchemeActionResultStatusRunning = 'srsr' /* The action is in progress. */, XcodeSchemeActionResultStatusCancelled = 'srsc' /* The action was cancelled. */, XcodeSchemeActionResultStatusFailed = 'srsf' /* The action ran but did not complete successfully. */, XcodeSchemeActionResultStatusErrorOccurred = 'srse' /* The action was not able to run due to an error. */, XcodeSchemeActionResultStatusSucceeded = 'srss' /* The action succeeded. */ }; typedef enum XcodeSchemeActionResultStatus XcodeSchemeActionResultStatus; @protocol XcodeGenericMethods - (void) closeSaving:(XcodeSaveOptions)saving savingIn:(NSURL *)savingIn; // Close a document. - (void) delete; // Delete an object. - (void) moveTo:(SBObject *)to; // Move an object to a new location. - (XcodeSchemeActionResult *) build; // Invoke the "build" scheme action. This command should be sent to a workspace document. The build will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result. - (XcodeSchemeActionResult *) clean; // Invoke the "clean" scheme action. This command should be sent to a workspace document. The clean will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result. - (void) stop; // Stop the active scheme action, if one is running. This command should be sent to a workspace document. This command does not wait for the action to stop. - (XcodeSchemeActionResult *) runWithCommandLineArguments:(id)withCommandLineArguments withEnvironmentVariables:(id)withEnvironmentVariables; // Invoke the "run" scheme action. This command should be sent to a workspace document. The run action will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result. - (XcodeSchemeActionResult *) testWithCommandLineArguments:(id)withCommandLineArguments withEnvironmentVariables:(id)withEnvironmentVariables; // Invoke the "test" scheme action. This command should be sent to a workspace document. The test action will be performed using the workspace document's current active scheme and active run destination. This command does not wait for the action to complete; its progress can be tracked with the returned scheme action result. @end /* * Standard Suite */ // The application's top-level scripting object. @interface XcodeApplication : SBApplication - (SBElementArray *) documents; - (SBElementArray *) windows; @property (copy, readonly) NSString *name; // The name of the application. @property (readonly) BOOL frontmost; // Is this the active application? @property (copy, readonly) NSString *version; // The version number of the application. - (id) open:(id)x; // Open a document. - (void) quitSaving:(XcodeSaveOptions)saving; // Quit the application. - (BOOL) exists:(id)x; // Verify that an object exists. @end // A document. @interface XcodeDocument : SBObject @property (copy, readonly) NSString *name; // Its name. @property (readonly) BOOL modified; // Has it been modified since the last save? @property (copy, readonly) NSURL *file; // Its location on disk, if it has one. @end // A window. @interface XcodeWindow : SBObject @property (copy, readonly) NSString *name; // The title of the window. - (NSInteger) id; // The unique identifier of the window. @property NSInteger index; // The index of the window, ordered front to back. @property NSRect bounds; // The bounding rectangle of the window. @property (readonly) BOOL closeable; // Does the window have a close button? @property (readonly) BOOL miniaturizable; // Does the window have a minimize button? @property BOOL miniaturized; // Is the window minimized right now? @property (readonly) BOOL resizable; // Can the window be resized? @property BOOL visible; // Is the window visible right now? @property (readonly) BOOL zoomable; // Does the window have a zoom button? @property BOOL zoomed; // Is the window zoomed right now? @property (copy, readonly) XcodeDocument *document; // The document whose contents are displayed in the window. @end /* * Xcode Application Suite */ // The Xcode application. @interface XcodeApplication (XcodeApplicationSuite) - (SBElementArray *) fileDocuments; - (SBElementArray *) sourceDocuments; - (SBElementArray *) workspaceDocuments; @property (copy) XcodeWorkspaceDocument *activeWorkspaceDocument; // The active workspace document in Xcode. @end /* * Xcode Document Suite */ // An Xcode-compatible document. @interface XcodeDocument (XcodeDocumentSuite) @property (copy) NSString *path; // The document's path. @end // A document that represents a file on disk. It also provides access to the window it appears in. @interface XcodeFileDocument : XcodeDocument @end // A document that represents a text file on disk. It also provides access to the window it appears in. @interface XcodeTextDocument : XcodeFileDocument @property (copy) NSArray *selectedCharacterRange; // The first and last character positions in the selection. @property (copy) NSArray *selectedParagraphRange; // The first and last paragraph positions that contain the selection. @property (copy) NSString *text; // The text of the text file referenced. @property BOOL notifiesWhenClosing; // Should Xcode notify other apps when this document is closed? @end // A document that represents a source file on disk. It also provides access to the window it appears in. @interface XcodeSourceDocument : XcodeTextDocument @end // A document that represents a workspace on disk. Workspaces are the top-level container for almost all objects and commands in Xcode. @interface XcodeWorkspaceDocument : XcodeDocument - (SBElementArray *) projects; - (SBElementArray *) schemes; - (SBElementArray *) runDestinations; @property BOOL loaded; // Whether the workspace document has finsished loading after being opened. Messages sent to a workspace document before it has loaded will result in errors. @property (copy) XcodeScheme *activeScheme; // The workspace's scheme that will be used for scheme actions. @property (copy) XcodeRunDestination *activeRunDestination; // The workspace's run destination that will be used for scheme actions. @property (copy) XcodeSchemeActionResult *lastSchemeActionResult; // The scheme action result for the last scheme action command issued to the workspace document. @property (copy, readonly) NSURL *file; // The workspace document's location on disk, if it has one. @end /* * Xcode Scheme Suite */ // An object describing the result of performing a scheme action command. @interface XcodeSchemeActionResult : SBObject - (SBElementArray *) buildErrors; - (SBElementArray *) buildWarnings; - (SBElementArray *) analyzerIssues; - (SBElementArray *) testFailures; - (NSString *) id; // The unique identifier for the scheme. @property (readonly) BOOL completed; // Whether this scheme action has completed (sucessfully or otherwise) or not. @property XcodeSchemeActionResultStatus status; // Indicates the status of the scheme action. @property (copy) NSString *errorMessage; // If the result's status is "error occurred", this will be the error message; otherwise, this will be "missing value". @property (copy) NSString *buildLog; // If this scheme action performed a build, this will be the text of the build log. @end // An issue (like an error or warning) generated by a scheme action. @interface XcodeSchemeActionIssue : SBObject @property (copy) NSString *message; // The text of the issue. @property (copy) NSString *filePath; // The file path where the issue occurred. This may be 'missing value' if the issue is not associated with a specific source file. @property NSInteger startingLineNumber; // The starting line number in the file where the issue occurred. This may be 'missing value' if the issue is not associated with a specific source file. @property NSInteger endingLineNumber; // The ending line number in the file where the issue occurred. This may be 'missing value' if the issue is not associated with a specific source file. @property NSInteger startingColumnNumber; // The starting column number in the file where the issue occurred. This may be 'missing value' if the issue is not associated with a specific source file. @property NSInteger endingColumnNumber; // The ending column number in the file where the issue occurred. This may be 'missing value' if the issue is not associated with a specific source file. @end // An error generated by a build. @interface XcodeBuildError : XcodeSchemeActionIssue @end // A warning generated by a build. @interface XcodeBuildWarning : XcodeSchemeActionIssue @end // A warning generated by the static analyzer. @interface XcodeAnalyzerIssue : XcodeSchemeActionIssue @end // A failure from a test. @interface XcodeTestFailure : XcodeSchemeActionIssue @end // A set of parameters for building, testing, launching or distributing the products of a workspace. @interface XcodeScheme : SBObject @property (copy, readonly) NSString *name; // The name of the scheme. - (NSString *) id; // The unique identifier for the scheme. @end // An object which specifies parameters such as the device and architecture for which to perform a scheme action. @interface XcodeRunDestination : SBObject @property (copy, readonly) NSString *name; // The name of the run destination, as displayed in Xcode's interface. @property (copy, readonly) NSString *architecture; // The architecture for which this run destination results in execution. @property (copy, readonly) NSString *platform; // The identifier of the platform which this run destination targets, such as "macosx", "iphoneos", "iphonesimulator", etc . @property (copy, readonly) XcodeDevice *device; // The physical or virtual device which this run destination targets. @property (copy, readonly) XcodeDevice *companionDevice; // If the run destination's device has a companion (e.g. a paired watch for a phone) which it will use, this is that device. @end // A device which can be used as the target for a scheme action, as part of a run destination. @interface XcodeDevice : SBObject @property (copy, readonly) NSString *name; // The name of the device. @property (copy, readonly) NSString *deviceIdentifier; // A stable identifier for the device, as shown in Xcode's "Devices" window. @property (copy, readonly) NSString *operatingSystemVersion; // The version of the operating system installed on the device which this run destination targets. @property (copy, readonly) NSString *deviceModel; // The model of device (e.g. "iPad Air") which this run destination targets. @property (readonly) BOOL generic; // Whether this run destination is generic instead of representing a specific device. Most destinations are not generic, but a generic destination (such as "Generic iOS Device") will be available for some platforms if no physical devices are connected. @end /* * Xcode Project Suite */ // A set of build settings for a target or project. Each target in a project has the same named build configurations as the project. @interface XcodeBuildConfiguration : SBObject - (SBElementArray *) buildSettings; - (SBElementArray *) resolvedBuildSettings; - (NSString *) id; // The unique identifier for the build configuration. @property (copy, readonly) NSString *name; // The name of the build configuration. @end // An Xcode project. Projects represent project files on disk and are always open in the context of a workspace document. @interface XcodeProject : SBObject - (SBElementArray *) buildConfigurations; - (SBElementArray *) targets; @property (copy, readonly) NSString *name; // The name of the project - (NSString *) id; // The unique identifier for the project. @end // A setting that controls how products are built. @interface XcodeBuildSetting : SBObject @property (copy) NSString *name; // The unlocalized build setting name (e.g. DSTROOT). @property (copy) NSString *value; // A string value for the build setting. @end // An object that represents a resolved value for a build setting. @interface XcodeResolvedBuildSetting : SBObject @property (copy) NSString *name; // The unlocalized build setting name (e.g. DSTROOT). @property (copy) NSString *value; // A string value for the build setting. @end // A target is a blueprint for building a product. Targets inherit build settings from their project if not overridden in the target. @interface XcodeTarget : SBObject - (SBElementArray *) buildConfigurations; @property (copy) NSString *name; // The name of this target. - (NSString *) id; // The unique identifier for the target. @property (copy, readonly) XcodeProject *project; // The project that contains this target @end ================================================ FILE: copy_bundle.sh ================================================ #!/bin/bash -x # # copy_bundle.sh # InjectionIII # # Copies injection bundle for on-device injection. # Thanks @oryonatan # # $Id: //depot/HotReloading/copy_bundle.sh#18 $ # if [[ "$CONFIGURATION" =~ Debug ]]; then if [ ! -w "$CODESIGNING_FOLDER_PATH" ]; then echo '*** copy_bundle.sh unable to write to file system. ***' 'Change build setting "User Script Sandboxing" to NO' exit 1; fi RESOURCES=${RESOURCES:-"$(dirname "$0")"} COPY="$CODESIGNING_FOLDER_PATH/iOSInjection.bundle" STRACE="$COPY/Frameworks/SwiftTrace.framework/SwiftTrace" PLIST="$COPY/Info.plist" if [ "$PLATFORM_NAME" == "macosx" ]; then BUNDLE=${1:-macOSInjection} COPY="$CODESIGNING_FOLDER_PATH/Contents/Resources/macOSInjection.bundle" STRACE="$COPY/Contents/Frameworks/SwiftTrace.framework/Versions/A/SwiftTrace" PLIST="$COPY/Contents/Info.plist" elif [ "$PLATFORM_NAME" == "appletvsimulator" ]; then BUNDLE=${1:-tvOSInjection} elif [ "$PLATFORM_NAME" == "appletvos" ]; then BUNDLE=${1:-tvdevOSInjection} elif [ "$PLATFORM_NAME" == "xrsimulator" ]; then BUNDLE=${1:-xrOSInjection} elif [ "$PLATFORM_NAME" == "watchsimulator" ]; then BUNDLE=${1:-watchOSInjection} elif [ "$PLATFORM_NAME" == "xros" ]; then BUNDLE=${1:-xrdevOSInjection} elif [ "$PLATFORM_NAME" == "iphoneos" ]; then BUNDLE=${1:-maciOSInjection} rsync -a "$PLATFORM_DEVELOPER_LIBRARY_DIR"/{Frameworks,PrivateFrameworks}/XC* "$PLATFORM_DEVELOPER_USR_DIR/lib"/*.dylib "$COPY/Frameworks/" && codesign -f --sign "$EXPANDED_CODE_SIGN_IDENTITY" --timestamp\=none --preserve-metadata\=identifier,entitlements,flags --generate-entitlement-der "$COPY/Frameworks"/{XC*,*.dylib}; else BUNDLE=${1:-iOSInjection} fi rsync -a "$RESOURCES/$BUNDLE.bundle"/* "$COPY/" && /usr/libexec/PlistBuddy -c "Add :UserHome string $HOME" "$PLIST" && codesign -f --sign "$EXPANDED_CODE_SIGN_IDENTITY" --timestamp\=none --preserve-metadata\=identifier,entitlements,flags --generate-entitlement-der "$STRACE" "$COPY" && defaults write com.johnholdsworth.InjectionIII "$PROJECT_FILE_PATH" $EXPANDED_CODE_SIGN_IDENTITY fi ================================================ FILE: fix_previews.sh ================================================ #!/bin/bash # # Workaround for limitations of Xcode Previews # for projects that have dynamic SPM libraries. # Running this script seemed to help Xcode not # leave out frameworks when running previews. # # $Id: //depot/HotReloading/fix_previews.sh#7 $ # echo "*** You no longer need to run fix_previews.sh" 1>&2 APP_ROOT="$CODESIGNING_FOLDER_PATH" if [ -d "$APP_ROOT"/Contents ]; then APP_ROOT="$APP_ROOT"/Contents fi mkdir "$APP_ROOT"/Frameworks for framework in "$CODESIGNING_FOLDER_PATH"/../PackageFrameworks/*.framework; do cp -r "$framework" "$APP_ROOT"/Frameworks >>/tmp/fix_previews.txt 2>&1 done exit 0 ================================================ FILE: start_daemon.sh ================================================ #!/bin/bash # # Start up daemon process to rebuild changed sources # # $Id: //depot/HotReloading/start_daemon.sh#42 $ # echo "*** You no longer need to run start_daemon.sh" 1>&2 # You used to use this script in a "Run Script/Build Phase" like this: #if [ -d $SYMROOT/../../SourcePackages ]; then # $SYMROOT/../../SourcePackages/checkouts/HotReloading/start_daemon.sh #fi export PROJECT_FILE_PATH="${PROJECT_FILE_PATH:-"$PWD/Package.swift"}" # Vapor cd "$(dirname "$0")" if [ "$CONFIGURATION" = "Release" ]; then echo "error: You shouldn't be shipping HotReloading in your app!" exit 1 fi if [ -f "/tmp/injecting_storyboard.txt" ]; then rm /tmp/injecting_storyboard.txt exit 0 fi export SYMROOT="${SYMROOT:-$(dirname "$PWD")}" # Vapor DERIVED_DATA="$(dirname $(dirname $SYMROOT))" export DERIVED_LOGS="$DERIVED_DATA/Logs/Build" LAST_LOG=`ls -t $DERIVED_LOGS/*.xcactivitylog | head -n 1` export NORMAL_ARCH_FILE="$OBJECT_FILE_DIR_normal/$ARCHS/$PRODUCT_NAME" export LINK_FILE_LIST="$NORMAL_ARCH_FILE.LinkFileList" # kill any existing daemon process kill -9 `ps auxww | grep .build/debug/injectiond | grep -v grep | awk '{ print $2 }'` # Avoid having to fetch dependancies again # mkdir -p .build; ln -s "$DERIVED_DATA"/SourcePackages/repositories .build # rebuild daemon /usr/bin/env -i PATH="$PATH" "$TOOLCHAIN_DIR"/usr/bin/swift build --product injectiond && # clone Contents directory for Cocoa rsync -at Contents .build/debug && # run in background passing project file, logs directory # followed by a list of additional directories to watch. # when working with a .xcworkspace set PROJECT_FILE_PATH # to the path to the workspace file in the build phase. (.build/debug/injectiond "$PROJECT_FILE_PATH" "$DERIVED_LOGS" `gunzip <$LAST_LOG | tr '\r' '\n' | grep -e ' cd ' | sort -u | grep -v DerivedData | awk '{ print $2 }'` >/tmp/hot_reloading.log 2>&1 &)