Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions controller/app/src/main/java/org/iiab/controller/TarExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public void startExtraction(Context context, String archivePath, String destDir,
if (!destination.exists()) {
destination.mkdirs();
}
// ADFA-4544: record free space + archive size for failure diagnostics.
final long freeBefore = freeBytes(destination);
Log.d(TAG, "Extract start: freeBytes=" + freeBefore
+ ", archiveCompressed=" + new File(archivePath).length() + ", dest=" + destDir);
// 1. DYNAMIC BINARY SELECTION
File staticTar = new File(context.getApplicationInfo().nativeLibraryDir, "libtar.so");
String tarBinary = staticTar.exists() ? staticTar.getAbsolutePath() : "/system/bin/tar";
Expand Down Expand Up @@ -111,13 +115,17 @@ public void startExtraction(Context context, String archivePath, String destDir,
tarProcess = pb.start();

// 3. READ TAR OUTPUT (Prevents buffer blocking and logs errors)
// ADFA-4544: retain the last output lines (stderr is merged) for diagnostics.
final java.util.concurrent.ConcurrentLinkedDeque<String> tarTail = new java.util.concurrent.ConcurrentLinkedDeque<>();
final Handler uiHandler = new Handler(Looper.getMainLooper());
new Thread(() -> {
Thread readerThread = new Thread(() -> {
long[] lastEmit = {0L};
try (BufferedReader reader = new BufferedReader(new InputStreamReader(tarProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
Log.d(TAG, "Tar Output: " + line);
tarTail.addLast(line);
while (tarTail.size() > 20) tarTail.pollFirst();
long now = System.currentTimeMillis();
if (now - lastEmit[0] >= 50) {
lastEmit[0] = now;
Expand All @@ -127,40 +135,60 @@ public void startExtraction(Context context, String archivePath, String destDir,
}
} catch (Exception ignored) {
}
}).start();
});
readerThread.start();

// 4. THE JAVA DECOMPRESSION PIPE (If it's a .gz file)
boolean pipeBroke = false;
long totalWritten = 0;
if (isGzip) {
Log.d(TAG, "Starting Java GZIP Pipe stream to tar process...");
// Try-with-resources automatically safely closes the streams when done
try (GZIPInputStream gis = new GZIPInputStream(new FileInputStream(archivePath));
OutputStream tarInput = tarProcess.getOutputStream()) {

byte[] buffer = new byte[8192]; // 8KB RAM chunk
int bytesRead;
while ((bytesRead = gis.read(buffer)) != -1) {
tarInput.write(buffer, 0, bytesRead);
try {
tarInput.write(buffer, 0, bytesRead);
totalWritten += bytesRead;
} catch (java.io.IOException pipe) {
// ADFA-4544: tar (the pipe reader) closed its stdin early -> it
// failed or was killed. Don't report a generic decompression error;
// stop feeding and let waitFor() below surface tar's real exit code.
pipeBroke = true;
Log.e(TAG, "tar closed stdin early after " + totalWritten + " bytes", pipe);
break;
}
}
if (!pipeBroke) {
tarInput.flush();
Log.d(TAG, "Java GZIP Pipe finished pushing data.");
}
tarInput.flush();
Log.d(TAG, "Java GZIP Pipe finished pushing data.");
} catch (Exception streamError) {
Log.e(TAG, "Stream decompression error", streamError);
throw new Exception("Decompression failed: " + streamError.getMessage());
}
// A GZIPInputStream read error (corrupt archive) still propagates to the
// outer catch as a genuine decompression failure.
}

// 5. WAIT FOR COMPLETION
// By this point, the OutputStream is closed. Tar knows EOF is reached and will exit.
// 5. WAIT FOR COMPLETION + DIAGNOSE
int exitCode = tarProcess.waitFor();
try { readerThread.join(1500); } catch (InterruptedException ignored) { }
isExtracting = false;
final long freeAfter = freeBytes(destination);

final boolean broke = pipeBroke;
final long wrote = totalWritten;
new Handler(Looper.getMainLooper()).post(() -> {
if (exitCode == 0) {
if (exitCode == 0 && !broke) {
Log.d(TAG, "Extraction successful.");
listener.onComplete(destDir);
} else {
Log.e(TAG, "Extraction failed with code " + exitCode);
listener.onError("Tar process exited with error code: " + exitCode);
String diag = "tar exit=" + exitCode
+ (broke ? " (stdin pipe broke: tar died mid-stream; 137/killed => phantom-process or OOM)" : "")
+ ", wrote=" + wrote + "B, freeBefore=" + freeBefore + "B, freeAfter=" + freeAfter
+ "B, lastTarOutput=" + tarTail;
Log.e(TAG, "Extraction failed: " + diag);
listener.onError(diag);
}
});

Expand Down Expand Up @@ -226,6 +254,15 @@ private List<String> listEntries(String tarBinary, String archivePath, boolean i
return names;
}

/** Available bytes on the destination's filesystem, or -1 if unavailable (ADFA-4544). */
private static long freeBytes(File dir) {
try {
return new android.os.StatFs(dir.getAbsolutePath()).getAvailableBytes();
} catch (Exception e) {
return -1L;
}
}

public void stopExtraction() {
if (tarProcess != null) {
tarProcess.destroy();
Expand Down