2023-09-07 18:00:53 -05:00
//
// E n a b l e J I T . s w i f t
// A l t P a c k a g e
//
// C r e a t e d b y R i l e y T e s t u t o n 8 / 2 9 / 2 3 .
// C o p y r i g h t © 2 0 2 3 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import Foundation
import OSLog
import RegexBuilder
import ArgumentParser
struct EnableJIT : PythonCommand
{
static let configuration = CommandConfiguration ( commandName : " enable " , abstract : " Enable JIT for a specific app on your device. " )
@ Argument ( help : " The name or PID of the app to enable JIT for. " , transform : AppProcess . init )
var process : AppProcess
@ Option ( help : " Your iOS device's UDID. " )
var udid : String
2023-11-29 13:40:51 -06:00
@ Option ( name : . shortAndLong , help : " Number of seconds to wait when connecting to an iOS device before operation is cancelled. " )
var timeout : TimeInterval = 90.0
2023-09-07 18:00:53 -05:00
// P y t h o n C o m m a n d
var pythonPath : String ?
mutating func run ( ) async throws
{
// U s e l o c a l v a r i a b l e s t o f i x " e s c a p i n g a u t o c l o s u r e c a p t u r e s m u t a t i n g s e l f p a r a m e t e r " c o m p i l e r e r r o r .
let process = self . process
let udid = self . udid
do
{
do
{
Logger . main . info ( " Enabling JIT for \( process , privacy : . private ( mask : . hash ) ) on device \( udid , privacy : . private ( mask : . hash ) ) ... " )
try await self . prepare ( )
let rsdTunnel = try await self . startRSDTunnel ( )
defer { rsdTunnel . process . terminate ( ) }
print ( " Connected to device \( self . udid ) ! " , rsdTunnel )
let port = try await self . startDebugServer ( rsdTunnel : rsdTunnel )
print ( " Started debugserver on port \( port ) . " )
print ( " Attaching debugger... " )
let lldb = try await self . attachDebugger ( ipAddress : rsdTunnel . ipAddress , port : port )
defer { lldb . terminate ( ) }
print ( " Attached debugger to \( process ) . " )
try await self . detachDebugger ( lldb )
print ( " Detached debugger from \( process ) . " )
print ( " ✅ Successfully enabled JIT for \( process ) on device \( udid ) ! " )
}
catch let error as ProcessError
{
if let output = error . output
{
print ( output )
}
throw error
}
}
catch
{
print ( " ❌ Unable to enable JIT for \( process ) on device \( udid ) . " )
print ( error . localizedDescription )
Logger . main . error ( " Failed to enable JIT for \( process , privacy : . private ( mask : . hash ) ) on device \( udid , privacy : . private ( mask : . hash ) ) . \( error , privacy : . public ) " )
throw ExitCode . failure
}
}
}
private extension EnableJIT
{
func startRSDTunnel ( ) async throws -> RemoteServiceDiscoveryTunnel
{
do
{
2023-11-29 13:40:51 -06:00
Logger . main . info ( " Starting RSD tunnel with timeout: \( self . timeout ) " )
2023-09-07 18:00:53 -05:00
let process = try Process . launch ( . python3 , arguments : [ " -u " , " -m " , " pymobiledevice3 " , " remote " , " start-quic-tunnel " , " --udid " , self . udid ] , environment : self . processEnvironment )
do
{
2023-11-29 13:40:51 -06:00
let rsdTunnel = try await withTimeout ( seconds : self . timeout ) {
2023-09-07 18:00:53 -05:00
let regex = Regex {
" --rsd "
OneOrMore ( . whitespace )
Capture {
OneOrMore ( . anyGraphemeCluster )
}
OneOrMore ( . whitespace )
TryCapture {
OneOrMore ( . digit )
} transform : { match in
Int ( match )
}
}
for try await line in process . outputLines
{
if let match = line . firstMatch ( of : regex )
{
let rsdTunnel = RemoteServiceDiscoveryTunnel ( ipAddress : String ( match . 1 ) , port : match . 2 , process : process )
return rsdTunnel
}
}
throw ProcessError . unexpectedOutput ( executableURL : . python3 , output : process . output )
}
// M U S T c l o s e s t a n d a r d O u t p u t i n o r d e r t o s t r e a m o u t p u t l a t e r .
process . stopOutput ( )
return rsdTunnel
}
catch is TimedOutError
{
process . terminate ( )
let error = ProcessError . timedOut ( executableURL : . python3 , output : process . output )
throw error
}
catch
{
process . terminate ( )
throw error
}
}
catch let error as NSError
{
let localizedFailure = NSLocalizedString ( " Could not connect to device \( self . udid ) . " , comment : " " )
throw error . withLocalizedFailure ( localizedFailure )
}
}
func startDebugServer ( rsdTunnel : RemoteServiceDiscoveryTunnel ) async throws -> Int
{
do
{
2023-11-29 13:40:51 -06:00
Logger . main . info ( " Starting debugserver with timeout: \( self . timeout ) " )
2023-09-07 18:00:53 -05:00
2023-11-29 13:40:51 -06:00
return try await withTimeout ( seconds : self . timeout ) {
2023-09-07 18:00:53 -05:00
let arguments = [ " -u " , " -m " , " pymobiledevice3 " , " developer " , " debugserver " , " start-server " ] + rsdTunnel . commandArguments
let output = try await Process . launchAndWait ( . python3 , arguments : arguments , environment : self . processEnvironment )
let port = Reference ( Int . self )
let regex = Regex {
" connect:// "
OneOrMore ( . anyGraphemeCluster , . eager )
" : "
TryCapture ( as : port ) {
OneOrMore ( . digit )
} transform : { match in
Int ( match )
}
}
if let match = output . firstMatch ( of : regex )
{
return match [ port ]
}
throw ProcessError . unexpectedOutput ( executableURL : . python3 , output : output )
}
}
catch let error as NSError
{
let localizedFailure = NSLocalizedString ( " Could not start debugserver on device \( self . udid ) . " , comment : " " )
throw error . withLocalizedFailure ( localizedFailure )
}
}
func attachDebugger ( ipAddress : String , port : Int ) async throws -> Process
{
do
{
Logger . main . info ( " Attaching debugger... " )
let processID : Int
switch self . process
{
case . pid ( let pid ) : processID = pid
case . name ( let name ) :
guard let pid = try await self . getPID ( for : name ) else { throw JITError . processNotRunning ( self . process ) }
processID = pid
}
let process = try Process . launch ( . lldb , environment : self . processEnvironment )
do
{
try await withThrowingTaskGroup ( of : Void . self ) { taskGroup in
// / / T h r o w e r r o r i f p r o g r a m t e r m i n a t e s .
// t a s k G r o u p . a d d T a s k {
// t r y a w a i t w i t h C h e c k e d T h r o w i n g C o n t i n u a t i o n { c o n t i n u a t i o n i n
// p r o c e s s . t e r m i n a t i o n H a n d l e r = { p r o c e s s i n
// T a s k {
// / / S h o u l d N E V E R b e c a l l e d u n l e s s a n e r r o r o c c u r s .
// c o n t i n u a t i o n . r e s u m e ( t h r o w i n g : P r o c e s s E r r o r . t e r m i n a t e d ( e x e c u t a b l e U R L : . l l d b , e x i t C o d e : p r o c e s s . t e r m i n a t i o n S t a t u s , o u t p u t : p r o c e s s . o u t p u t ) )
// }
// }
// }
// }
taskGroup . addTask {
do
{
try await self . sendDebuggerCommand ( " platform select remote-ios " , to : process , timeout : 5 ) {
ChoiceOf {
" SDK Roots: "
" unable to locate SDK "
}
}
let ipAddress = " [ \( ipAddress ) ] "
let connectCommand = " process connect connect:// \( ipAddress ) : \( port ) "
try await self . sendDebuggerCommand ( connectCommand , to : process , timeout : 10 )
try await self . sendDebuggerCommand ( " settings set target.memory-module-load-level minimal " , to : process , timeout : 5 )
let attachCommand = " attach -p \( processID ) "
let failureMessage = " attach failed "
let output = try await self . sendDebuggerCommand ( attachCommand , to : process , timeout : 120 ) {
ChoiceOf {
failureMessage
Regex {
" Process "
OneOrMore ( . digit )
" stopped "
}
}
}
if output . contains ( failureMessage )
{
throw ProcessError . failed ( executableURL : . lldb , exitCode : - 1 , output : process . output )
}
}
catch is TimedOutError
{
let error = ProcessError . timedOut ( executableURL : . lldb , output : process . output )
throw error
}
}
// W a i t u n t i l f i r s t c h i l d t a s k r e t u r n s
_ = try await taskGroup . next ( ) !
// C a n c e l r e m a i n i n g t a s k s
taskGroup . cancelAll ( )
}
return process
}
catch
{
process . terminate ( )
throw error
}
}
catch let error as NSError
{
let localizedFailure = String ( format : NSLocalizedString ( " Could not attach debugger to %@. " , comment : " " ) , self . process . description )
throw error . withLocalizedFailure ( localizedFailure )
}
}
func detachDebugger ( _ process : Process ) async throws
{
do
{
Logger . main . info ( " Detaching debugger... " )
try await withThrowingTaskGroup ( of : Void . self ) { taskGroup in
// / / T h r o w e r r o r i f p r o g r a m t e r m i n a t e s .
// t a s k G r o u p . a d d T a s k {
// t r y a w a i t w i t h C h e c k e d T h r o w i n g C o n t i n u a t i o n { c o n t i n u a t i o n i n
// p r o c e s s . t e r m i n a t i o n H a n d l e r = { p r o c e s s i n
// i f p r o c e s s . t e r m i n a t i o n S t a t u s = = 0
// {
// c o n t i n u a t i o n . r e s u m e ( )
// }
// e l s e
// {
// c o n t i n u a t i o n . r e s u m e ( t h r o w i n g : P r o c e s s E r r o r . t e r m i n a t e d ( e x e c u t a b l e U R L : . l l d b , e x i t C o d e : p r o c e s s . t e r m i n a t i o n S t a t u s , o u t p u t : p r o c e s s . o u t p u t ) )
// }
// }
// }
// }
taskGroup . addTask {
do
{
try await self . sendDebuggerCommand ( " c " , to : process , timeout : 10 ) {
" Process "
OneOrMore ( . digit )
" resuming "
}
try await self . sendDebuggerCommand ( " detach " , to : process , timeout : 10 ) {
" Process "
OneOrMore ( . digit )
" detached "
}
}
catch is TimedOutError
{
let error = ProcessError . timedOut ( executableURL : . lldb , output : process . output )
throw error
}
}
// W a i t u n t i l f i r s t c h i l d t a s k r e t u r n s
_ = try await taskGroup . next ( ) !
// C a n c e l r e m a i n i n g t a s k s
taskGroup . cancelAll ( )
}
}
catch let error as NSError
{
let localizedFailure = NSLocalizedString ( " Could not detach debugger from \( self . process ) . " , comment : " " )
throw error . withLocalizedFailure ( localizedFailure )
}
}
}
private extension EnableJIT
{
func getPID ( for name : String ) async throws -> Int ?
{
Logger . main . info ( " Retrieving PID for \( name , privacy : . private ( mask : . hash ) ) ... " )
let arguments = [ " -m " , " pymobiledevice3 " , " processes " , " pgrep " , name , " --udid " , self . udid ]
let output = try await Process . launchAndWait ( . python3 , arguments : arguments , environment : self . processEnvironment )
let regex = Regex {
" INFO "
OneOrMore ( . whitespace )
TryCapture {
OneOrMore ( . digit )
} transform : { match in
Int ( match )
}
OneOrMore ( . whitespace )
name
}
if let match = output . firstMatch ( of : regex )
{
Logger . main . info ( " \( name , privacy : . private ( mask : . hash ) ) PID is \( match . 1 ) " )
return match . 1
}
return nil
}
@ discardableResult
func sendDebuggerCommand ( _ command : String , to process : Process , timeout : TimeInterval ,
@ RegexComponentBuilder regex : @ escaping ( ) -> ( some RegexComponent < Substring > ) ? = { Optional < Regex < Substring > > . none } ) async throws -> String
{
guard let inputPipe = process . standardInput as ? Pipe else { preconditionFailure ( " `process` must have a Pipe as its standardInput " ) }
defer {
inputPipe . fileHandleForWriting . writeabilityHandler = nil
}
let initialOutput = process . output
let data = ( command + " \n " ) . data ( using : . utf8 ) ! // W i l l a l w a y s s u c c e e d .
Logger . main . info ( " Sending lldb command: \( command , privacy : . public ) " )
let output = try await withTimeout ( seconds : timeout ) {
// W a i t u n t i l p r o c e s s i s r e a d y t o r e c e i v e i n p u t .
try await withCheckedThrowingContinuation { continuation in
inputPipe . fileHandleForWriting . writeabilityHandler = { fileHandle in
inputPipe . fileHandleForWriting . writeabilityHandler = nil
let result = Result { try fileHandle . write ( contentsOf : data ) }
continuation . resume ( with : result )
}
}
// W a i t u n t i l w e r e c e i v e a t l e a s t o n e l i n e o f o u t p u t .
for try await _ in process . outputLines
{
break
}
// K e e p w a i t i n g u n t i l o u t p u t d o e s n ' t c h a n g e .
// I f r e g e x i s p r o v i d e d , w e k e e p w a i t i n g u n t i l a m a t c h i s f o u n d .
var previousOutput = process . output
while true
{
try await Task . sleep ( for : . seconds ( 0.2 ) )
let output = process . output
if output = = previousOutput
{
guard let regex = regex ( ) else {
// N o r e g e x , s o b r e a k a s s o o n a s o u t p u t s t o p s c h a n g i n g .
break
}
if output . contains ( regex )
{
// F o u n d a m a t c h , s o e x i t w h i l e l o o p .
break
}
else
{
// O u t p u t h a s n ' t c h a n g e d , b u t r e g e x d o e s n o t m a t c h ( y e t ) .
continue
}
}
previousOutput = output
}
return previousOutput
}
// S u b t r a c t i n i t i a l O u t p u t f r o m o u t p u t t o g e t j u s t t h i s c o m m a n d ' s o u t p u t .
let commandOutput = output . replacingOccurrences ( of : initialOutput , with : " " )
return commandOutput
}
}