Skip to content

Commit a535f68

Browse files
committed
feat: display inproxy connection diagnostics in Logs tab
Parse 'inproxy-dial:' diagnostic notices from tunnel-core and display them as [Conduit] prefixed entries in the Logs tab. Includes: - parseInproxyDiagnostic(): extracts phase, duration, error from the diagnostic JSON (handles PsiphonTunnel.java's 'Type: {data}' format) - formatDuration(): rounds Go durations to human-friendly form (98.381771ms -> 98ms, 1.702052239s -> 1.7s) - Deduplication via m_lastInproxyDiagMsg to suppress duplicate entries caused by PsiphonTunnel.java's two notice dispatch paths - Suppresses raw diagnostic JSON for inproxy-dial messages Non-technical users can now screenshot the Logs tab to share diagnostic info about which inproxy connection phase is failing.
1 parent f84d598 commit a535f68

2 files changed

Lines changed: 148 additions & 0 deletions

File tree

app/src/main/java/com/psiphon3/psiphonlibrary/TunnelManager.java

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ boolean isConnected() {
210210
private PsiphonTunnel m_tunnel;
211211
private VpnManager m_vpnManager = VpnManager.getInstance();
212212
private String m_lastUpstreamProxyErrorMessage;
213+
private volatile String m_lastInproxyDiagMsg = "";
213214
private Handler m_Handler = new Handler();
214215

215216
private PendingIntent m_notificationPendingIntent;
@@ -1702,12 +1703,158 @@ else if (message.contains("tunnel connected (protocol:")) {
17021703
MyLog.i(R.string.tunnel_connected_protocol, MyLog.Sensitivity.NOT_SENSITIVE, protocol);
17031704
}
17041705
}
1706+
1707+
// Inproxy connection diagnostic messages
1708+
// These show the phase-by-phase progress of each relay attempt.
1709+
// Note: PsiphonTunnel.java dispatches diagnostic notices via two
1710+
// code paths (PsiphonProviderNoticeHandler and handleNotice),
1711+
// causing duplicate onDiagnosticMessage calls. We deduplicate
1712+
// by tracking the last displayed message.
1713+
if (message.contains("inproxy-dial:")) {
1714+
String diagMsg = parseInproxyDiagnostic(message);
1715+
if (diagMsg != null) {
1716+
// Deduplicate: suppress if identical to last message
1717+
if (!diagMsg.equals(m_lastInproxyDiagMsg)) {
1718+
m_lastInproxyDiagMsg = diagMsg;
1719+
MyLog.i(R.string.conduit_diag, MyLog.Sensitivity.NOT_SENSITIVE, diagMsg);
1720+
}
1721+
}
1722+
// Don't log the raw diagnostic JSON for inproxy-dial
1723+
return;
1724+
}
17051725

17061726
MyLog.i(now, message);
17071727
}
17081728
});
17091729
}
17101730

1731+
/**
1732+
* Format a Go duration string (e.g. "98.381771ms", "1.702052239s",
1733+
* "3m2.5s") into a human-friendly rounded form ("98ms", "1.7s", "3m2s").
1734+
*/
1735+
private static String formatDuration(String goDuration) {
1736+
if (goDuration == null) return null;
1737+
try {
1738+
// Handle seconds: "1.702052239s" -> "1.7s"
1739+
if (goDuration.endsWith("s") && !goDuration.endsWith("ms")) {
1740+
// Could be "3m2.5s" — find the last non-digit-dot prefix
1741+
int sIdx = goDuration.lastIndexOf('s');
1742+
// Find where the seconds number starts (after 'm' if present)
1743+
int mIdx = goDuration.indexOf('m');
1744+
String secPart;
1745+
String prefix = "";
1746+
if (mIdx >= 0 && mIdx < sIdx) {
1747+
prefix = goDuration.substring(0, mIdx + 1);
1748+
secPart = goDuration.substring(mIdx + 1, sIdx);
1749+
} else {
1750+
secPart = goDuration.substring(0, sIdx);
1751+
}
1752+
double secs = Double.parseDouble(secPart);
1753+
if (secs < 10) {
1754+
return prefix + String.format("%.1fs", secs);
1755+
} else {
1756+
return prefix + String.format("%.0fs", secs);
1757+
}
1758+
}
1759+
// Handle milliseconds: "98.381771ms" -> "98ms"
1760+
if (goDuration.endsWith("ms")) {
1761+
String numPart = goDuration.substring(0, goDuration.length() - 2);
1762+
double ms = Double.parseDouble(numPart);
1763+
return String.format("%.0fms", ms);
1764+
}
1765+
// Handle microseconds: "532.1µs" -> "<1ms"
1766+
if (goDuration.contains("µs") || goDuration.contains("us")) {
1767+
return "<1ms";
1768+
}
1769+
} catch (NumberFormatException e) {
1770+
// Fall through to return original
1771+
}
1772+
return goDuration;
1773+
}
1774+
1775+
/**
1776+
* Parse inproxy diagnostic messages from tunnel-core and return a
1777+
* human-readable string for display in the logs tab.
1778+
*
1779+
* Messages arrive from PsiphonTunnel.java formatted as:
1780+
* "Info: {"message":"inproxy-dial: ICE gathering OK","duration":"320ms","trace":"..."}"
1781+
*
1782+
* i.e. noticeType + ": " + data.toString()
1783+
*
1784+
* We strip the noticeType prefix, parse the JSON data object, extract the
1785+
* phase from "message", and append key diagnostic fields.
1786+
*/
1787+
private static String parseInproxyDiagnostic(String diagnosticMessage) {
1788+
try {
1789+
// The message format is "NoticeType: {json data object}"
1790+
// Strip the prefix to get the JSON data object
1791+
String jsonStr = diagnosticMessage;
1792+
int colonSpace = diagnosticMessage.indexOf(": {");
1793+
if (colonSpace >= 0) {
1794+
jsonStr = diagnosticMessage.substring(colonSpace + 2);
1795+
}
1796+
1797+
org.json.JSONObject data = new org.json.JSONObject(jsonStr);
1798+
1799+
String msg = data.optString("message", "");
1800+
if (!msg.startsWith("inproxy-dial:")) return null;
1801+
1802+
// Extract the phase description after "inproxy-dial: "
1803+
String phase = msg.substring("inproxy-dial: ".length());
1804+
1805+
StringBuilder sb = new StringBuilder(phase);
1806+
1807+
// Append diagnostic fields if present
1808+
String duration = formatDuration(data.optString("duration", null));
1809+
if (duration != null) {
1810+
sb.append(" (").append(duration).append(")");
1811+
}
1812+
1813+
String timeout = formatDuration(data.optString("timeout", null));
1814+
if (timeout != null) {
1815+
sb.append(" [timeout=").append(timeout).append("]");
1816+
}
1817+
1818+
String natType = data.optString("natType", null);
1819+
if (natType != null) {
1820+
sb.append(" [NAT=").append(natType).append("]");
1821+
}
1822+
1823+
String attempt = data.optString("attempt", null);
1824+
if (attempt != null) {
1825+
sb.insert(0, "#" + attempt + " ");
1826+
}
1827+
1828+
String error = data.optString("error", null);
1829+
if (error != null) {
1830+
// Truncate long error messages for readability
1831+
if (error.length() > 120) {
1832+
error = error.substring(0, 120) + "...";
1833+
}
1834+
sb.append(" | ").append(error);
1835+
}
1836+
1837+
return sb.toString();
1838+
} catch (org.json.JSONException e) {
1839+
// Fallback: extract just the phase text after "inproxy-dial: "
1840+
// and before any JSON artifacts (comma or closing brace)
1841+
if (diagnosticMessage.contains("inproxy-dial:")) {
1842+
int idx = diagnosticMessage.indexOf("inproxy-dial: ");
1843+
if (idx >= 0) {
1844+
String remainder = diagnosticMessage.substring(idx + "inproxy-dial: ".length());
1845+
// Stop at first JSON artifact
1846+
int end = remainder.indexOf("\",");
1847+
if (end < 0) end = remainder.indexOf("\"}");
1848+
if (end > 0) {
1849+
return remainder.substring(0, end);
1850+
}
1851+
return remainder;
1852+
}
1853+
}
1854+
return null;
1855+
}
1856+
}
1857+
17111858
@Override
17121859
public void onAvailableEgressRegions(final List<String> regions) {
17131860
m_Handler.post(new Runnable() {

app/src/main/res/values/psiphon_android_library_strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
<string name="conduit_proxy_trying">Trying Conduit relay (country: %s)</string>
105105
<string name="conduit_proxy_connected">Tunnel connected via Conduit (protocol: %1$s, country: %2$s)</string>
106106
<string name="conduit_proxy_connected_no_country">Tunnel connected via Conduit (protocol: %s)</string>
107+
<string name="conduit_diag">[Conduit] %s</string>
107108
<string name="tunnel_connected_protocol">Tunnel connected (protocol: %s)</string>
108109
<string name="region_selector">Select server region</string>
109110
<string name="network_proxy_connect_invalid_values">Invalid HTTP Proxy settings</string>

0 commit comments

Comments
 (0)