{"record":{"id":"7dde1563e3388fc2","repo":"skylot/jadx","slug":"found-classes-single-class-output-can-t-be-use","errorCode":null,"errorMessage":"Found {} classes, single class output can't be used","messagePattern":"Found (.+?) classes, single class output can't be used","errorType":"validation","errorClass":"JadxArgsValidateException","httpStatus":null,"severity":"error","filePath":"jadx-cli/src/main/java/jadx/cli/SingleClassMode.java","lineNumber":56,"sourceCode":"\t\t\t}\n\t\t\tif (clsForProcess.contains(AFlag.DONT_GENERATE)) {\n\t\t\t\tthrow new JadxArgsValidateException(\"Input class can't be saved by current jadx settings (marked as DONT_GENERATE)\");\n\t\t\t}\n\t\t\tif (clsForProcess.isInner()) {\n\t\t\t\tclsForProcess = clsForProcess.getTopParentClass();\n\t\t\t\tLOG.warn(\"Input class is inner, parent class will be saved: {}\", clsForProcess.getFullName());\n\t\t\t}\n\t\t} else {\n\t\t\t// singleClassOutput is set\n\t\t\t// expect only one class to be loaded\n\t\t\tList<ClassNode> classes = jadx.getRoot().getClasses().stream()\n\t\t\t\t\t.filter(c -> !c.isInner() && !c.contains(AFlag.DONT_GENERATE))\n\t\t\t\t\t.collect(Collectors.toList());\n\t\t\tint size = classes.size();\n\t\t\tif (size == 1) {\n\t\t\t\tclsForProcess = classes.get(0);\n\t\t\t} else {\n\t\t\t\tthrow new JadxArgsValidateException(\"Found \" + size + \" classes, single class output can't be used\");\n\t\t\t}\n\t\t}\n\t\tICodeInfo codeInfo;\n\t\ttry {\n\t\t\tcodeInfo = clsForProcess.decompile();\n\t\t} catch (Exception e) {\n\t\t\tthrow new JadxRuntimeException(\"Class decompilation failed\", e);\n\t\t}\n\t\tString fileExt = SaveCode.getFileExtension(jadx.getRoot());\n\t\tFile out;\n\t\tif (singleClassOutput == null) {\n\t\t\tout = new File(jadx.getArgs().getOutDirSrc(), clsForProcess.getClassInfo().getAliasFullPath() + fileExt);\n\t\t} else {\n\t\t\tif (singleClassOutput.endsWith(fileExt)) {\n\t\t\t\t// treat as file name\n\t\t\t\tout = new File(singleClassOutput);\n\t\t\t} else {\n\t\t\t\t// treat as directory","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/skylot/jadx/blob/e738a26571d02919f01df40de93bc9a44dee4e18/jadx-cli/src/main/java/jadx/cli/SingleClassMode.java#L38-L74","documentation":"Thrown by SmaliDebugger.attach when an IOException occurs during the whole attach sequence: opening the TCP socket to host:port, running initJDWP (handshake + suspend + id-sizes), or constructing/starting the debugger. It is the top-level wrapper that says the JDWP connection to the debugged app could not be established, with the original IOException chained as the cause.","triggerScenarios":"Calling SmaliDebugger.attach(host, port, suspendListener) when the socket cannot connect or the JDWP setup I/O fails: connection refused (nothing listening on the ADB-forwarded port), connect timeout (device unreachable), stream read/write error, or initJDWP throwing. The 5-second socket timeout (setSoTimeout(5000)) makes slow/unresponsive targets surface here quickly.","commonSituations":"Wrong ADB jdwp port or forgot `adb forward tcp:port jdwp:pid`; the target app is not debuggable (android:debuggable=false) so no jdwp thread exists; device disconnected/offline; app already terminated before attach; another debugger already attached to the same process; firewall/USB transport issue.","solutions":["Re-run the ADB jdwp port forward for the target process id (`adb forward tcp:<port> jdwp:<pid>`) and retry attach.","Confirm the app is installed as a debuggable build and is still running before attaching.","Verify the device is online (`adb devices`) and only one debugger attaches at a time.","Inspect the chained IOException cause to distinguish connection-refused vs timeout vs handshake failure, then address that specific layer."],"exampleFix":"// before\nSmaliDebugger dbg = SmaliDebugger.attach(host, port, listener);\n\n// after\nSmaliDebugger dbg;\ntry {\n    dbg = SmaliDebugger.attach(host, port, listener);\n} catch (SmaliDebuggerException e) {\n    // cause reveals refused vs timeout vs handshake; re-establish the jdwp forward then retry once\n    ensureJdwpForward(packageName, port);\n    dbg = SmaliDebugger.attach(host, port, listener);\n}","handlingStrategy":"retry","validationCode":"// Verify the jdwp forward and target liveness before calling attach:\nboolean ok = ensureJdwpForward(deviceSerial, packageName, port) && isProcessAlive(packageName);\nif (!ok) {\n    throw new IllegalStateException(\"jdwp forward/process not ready on port \" + port);\n}\nreturn SmaliDebugger.attach(host, port, listener);","typeGuard":null,"tryCatchPattern":"SmaliDebugger dbg = null;\nfor (int attempt = 0; attempt < 2 && dbg == null; attempt++) {\n    try {\n        dbg = SmaliDebugger.attach(host, port, listener);\n    } catch (SmaliDebuggerException e) {\n        Throwable cause = e.getCause();\n        if (cause instanceof java.net.SocketTimeoutException || cause instanceof java.net.ConnectException) {\n            ensureJdwpForward(packageName, port); // re-forward then retry once\n            continue;\n        }\n        throw e; // handshake/protocol errors are not transient\n    }\n}\nif (dbg == null) throw new SmaliDebuggerException(\"Attach failed after retry\");","preventionTips":["Always set up `adb forward tcp:<port> jdwp:<pid>` immediately before attach.","Attach to a debuggable build only, while the process is alive.","Ensure no second debugger grabs the single jdwp session.","Read the chained IOException cause to pick the right remedy (refused vs timeout vs handshake)."],"tags":["debugger","jdwp","network","adb","attach"],"backgroundTag":null,"analyzedSha":"e738a26571d02919f01df40de93bc9a44dee4e18","analyzedAt":"2026-08-14T00:10:24.238Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}