-
-
Notifications
You must be signed in to change notification settings - Fork 286
Description
Summary
The DTX connection terminates prematurely on iOS 14+ devices when using custom MessageDispatcher implementations. After successfully receiving the first 3-4 messages, the connection closes with a plist parsing error, preventing further data collection.
Problem Description
When implementing FPS monitoring or other continuous data streaming services on iOS 14+ devices, the reader() goroutine in connection.go encounters messages that cannot be parsed as valid plist format. The current implementation treats all ReadMessage() errors as fatal and closes the entire connection, even though these parse errors are non-fatal and the underlying network connection is still healthy.
Error logs:
DTX Connection error: plist: error parsing text property list: missing = in dictionary at line 0 character 2
error reading dtx connection plist: error parsing text property list: missing = in dictionary at line 0 character 2After this error, the reader() goroutine exits, the connection closes, and no further messages can be received despite the service still running on the device.
Impact
Severity: High - Makes continuous monitoring impossible on iOS 14+
Affected iOS versions: iOS 14.0 and later
Unaffected: iOS 13 and earlier work perfectly
Affected services: Primarily com.apple.instruments.server.services.coreprofilesessiontap and other streaming data services
Reproduction Steps
Setup: Connect to an iOS 14+ device with developer mode enabled
Create a custom dispatcher for handling kperf data:
type customDispatcher struct {
conn *dtx.Connection
}
func (d *customDispatcher) Dispatch(m dtx.Message) {
// Send ACK if needed
dtx.SendAckIfNeeded(d.conn, m)
// Process message data
if m.Auxiliary.GetArguments() != nil {
// Handle kperf data from auxiliary
}
}Connect to instruments service:
conn, err := dtx.NewUsbmuxdConnection(device, "com.apple.instruments.remoteserver.DVTSecureSocketProxy")
if err != nil {
log.Fatal(err)
}
dispatcher := &customDispatcher{conn: conn}
conn.MessageDispatcher = dispatcherRequest CoreProfileSessionTap channel and start streaming:
channel := conn.RequestChannelIdentifier(
"com.apple.instruments.server.services.coreprofilesessiontap",
dispatcher,
)
// Configure and start
config := map[string]interface{}{
"ur": 500,
"rp": 10,
"tc": []interface{}{
map[string]interface{}{
"kdf2": []interface{}{630784000, 833617920, 830472456},
"tk": 3,
"uuid": uuid.New().String(),
},
},
}
channel.MethodCall("setConfig:", config)
channel.MethodCall("start")Observe the failure:
First 3-4 messages arrive successfully with kperf data
Connection then breaks with plist parse error
No further messages are received
Auto-reconnect attempts show the same pattern
Root Cause Analysis
In ios/dtx_codec/connection.go, the reader() function (around line 120-150):
func reader(dtxConn *Connection) {
reader := bufio.NewReader(dtxConn.deviceConnection.Reader())
for {
msg, err := ReadMessage(reader)
if err != nil {
defer dtxConn.close(err) // ← Closes on ANY error
errText := err.Error()
log.Infof("DTX Connection error: %s", errText)
if err == io.EOF || strings.Contains(errText, "use of closed network") {
log.Debug("DTX Connection with EOF")
return
}
log.Errorf("error reading dtx connection %+v", err)
return // ← Exits reader goroutine, connection is dead
}
// ... dispatch message
}
}The problem: iOS 14+ occasionally sends messages that aren't valid plist format. These could be:
Protocol-level control messages
Binary data with different encoding
Device-specific communication that varies by iOS version
The current code treats these as fatal errors and terminates the connection, when it should skip the malformed message and continue reading.
Working Solution
I've verified this fix works perfectly
func reader(dtxConn *Connection) {
reader := bufio.NewReader(dtxConn.deviceConnection.Reader())
consecutiveErrors := 0
const maxConsecutiveErrors = 5
for {
msg, err := ReadMessage(reader)
if err != nil {
errText := err.Error()
// Fatal: actual connection failure
if err == io.EOF || strings.Contains(errText, "use of closed network") {
log.Debug("DTX Connection with EOF")
defer dtxConn.close(err)
return
}
// Non-fatal: plist parse error - skip and continue
if strings.Contains(errText, "plist") ||
strings.Contains(errText, "parsing text property list") {
consecutiveErrors++
log.Warnf("Skipping malformed message (plist parse error), consecutive errors: %d", consecutiveErrors)
// Safety: if too many consecutive errors, connection might be truly broken
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Too many consecutive parse errors, closing connection")
defer dtxConn.close(err)
return
}
// Give buffer time to resync
time.Sleep(10 * time.Millisecond)
continue // Skip this message, read next one
}
// Unknown error: treat as fatal
log.Errorf("error reading dtx connection %+v", err)
defer dtxConn.close(err)
return
}
// Success: reset error counter
consecutiveErrors = 0
// Dispatch as normal
if _channel, ok := dtxConn.activeChannels.Load(msg.ChannelCode); ok {
channel := _channel.(*Channel)
channel.Dispatch(msg)
} else {
dtxConn.globalChannel.Dispatch(msg)
}
}
}