Update ShellImpl task scheduling

- Prevent execTask starvation: tasks scheduled through execTask are
  now queued along with submitted tasks, executing in order of
  submission
- waitAndClose now properly waits for all tasks to complete, including
  both synchronous and asynchronous tasks
This commit is contained in:
topjohnwu 2024-06-26 19:01:04 -07:00
parent 990a60377f
commit 9d245f0587
3 changed files with 122 additions and 41 deletions

View File

@ -21,6 +21,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils; import com.topjohnwu.superuser.ShellUtils;
@ -38,16 +39,48 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class ShellImpl extends Shell { class ShellImpl extends Shell {
private volatile int status; private volatile int status;
private final Process proc; private final Process process;
private final NoCloseOutputStream STDIN; private final NoCloseOutputStream STDIN;
private final NoCloseInputStream STDOUT; private final NoCloseInputStream STDOUT;
private final NoCloseInputStream STDERR; private final NoCloseInputStream STDERR;
// Guarded by scheduleLock
private final ReentrantLock scheduleLock = new ReentrantLock();
private final Condition idle = scheduleLock.newCondition();
private final ArrayDeque<Task> tasks = new ArrayDeque<>(); private final ArrayDeque<Task> tasks = new ArrayDeque<>();
private boolean runningTasks = false; private boolean isRunningTask = false;
private static final class SyncTask implements Task {
private final Condition condition;
private boolean set = false;
SyncTask(Condition c) {
condition = c;
}
void signal() {
set = true;
condition.signal();
}
void await() {
while (!set) {
try {
condition.await();
} catch (InterruptedException ignored) {}
}
}
@Override
public void run(OutputStream stdin, InputStream stdout, InputStream stderr) {}
}
private static class NoCloseInputStream extends FilterInputStream { private static class NoCloseInputStream extends FilterInputStream {
@ -84,12 +117,12 @@ class ShellImpl extends Shell {
} }
} }
ShellImpl(BuilderImpl builder, Process process) throws IOException { ShellImpl(BuilderImpl builder, Process proc) throws IOException {
status = UNKNOWN; status = UNKNOWN;
proc = process; process = proc;
STDIN = new NoCloseOutputStream(process.getOutputStream()); STDIN = new NoCloseOutputStream(proc.getOutputStream());
STDOUT = new NoCloseInputStream(process.getInputStream()); STDOUT = new NoCloseInputStream(proc.getInputStream());
STDERR = new NoCloseInputStream(process.getErrorStream()); STDERR = new NoCloseInputStream(proc.getErrorStream());
// Shell checks might get stuck indefinitely // Shell checks might get stuck indefinitely
FutureTask<Integer> check = new FutureTask<>(this::shellCheck); FutureTask<Integer> check = new FutureTask<>(this::shellCheck);
@ -117,7 +150,7 @@ class ShellImpl extends Shell {
private Integer shellCheck() throws IOException { private Integer shellCheck() throws IOException {
try { try {
proc.exitValue(); process.exitValue();
throw new IOException("Created process has terminated"); throw new IOException("Created process has terminated");
} catch (IllegalThreadStateException ignored) { } catch (IllegalThreadStateException ignored) {
// Process is alive // Process is alive
@ -156,7 +189,7 @@ class ShellImpl extends Shell {
try { STDIN.close0(); } catch (IOException ignored) {} try { STDIN.close0(); } catch (IOException ignored) {}
try { STDERR.close0(); } catch (IOException ignored) {} try { STDERR.close0(); } catch (IOException ignored) {}
try { STDOUT.close0(); } catch (IOException ignored) {} try { STDOUT.close0(); } catch (IOException ignored) {}
proc.destroy(); process.destroy();
} }
@Override @Override
@ -164,19 +197,16 @@ class ShellImpl extends Shell {
if (status < 0) if (status < 0)
return true; return true;
synchronized (tasks) { scheduleLock.lock();
if (runningTasks) { try {
tasks.clear(); if (isRunningTask && !idle.await(timeout, unit))
tasks.wait(unit.toMillis(timeout)); return false;
} close();
if (!runningTasks) { } finally {
release(); scheduleLock.unlock();
return true;
}
} }
status = UNKNOWN; return true;
return false;
} }
@Override @Override
@ -198,8 +228,9 @@ class ShellImpl extends Shell {
return false; return false;
try { try {
proc.exitValue(); process.exitValue();
// Process is dead, shell is not alive // Process is dead, shell is not alive
release();
return false; return false;
} catch (IllegalThreadStateException e) { } catch (IllegalThreadStateException e) {
// Process is still running // Process is still running
@ -228,43 +259,71 @@ class ShellImpl extends Shell {
} }
private void processTasks() { private void processTasks() {
for (;;) { Task task;
Task task; while ((task = processNextTask(false)) != null) {
synchronized (tasks) {
if ((task = tasks.poll()) == null) {
runningTasks = false;
tasks.notifyAll();
return;
}
}
try { try {
exec0(task); exec0(task);
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
@Nullable
private Task processNextTask(boolean fromExec) {
scheduleLock.lock();
try {
final Task task = tasks.poll();
if (task == null) {
isRunningTask = false;
idle.signalAll();
return null;
}
if (task instanceof SyncTask) {
((SyncTask) task).signal();
return null;
}
if (fromExec) {
// Put the task back in front of the queue
tasks.offerFirst(task);
} else {
return task;
}
} finally {
scheduleLock.unlock();
}
EXECUTOR.execute(this::processTasks);
return null;
}
@Override @Override
public void submitTask(@NonNull Task task) { public void submitTask(@NonNull Task task) {
synchronized (tasks) { scheduleLock.lock();
try {
tasks.offer(task); tasks.offer(task);
if (!runningTasks) { if (!isRunningTask) {
runningTasks = true; isRunningTask = true;
EXECUTOR.execute(this::processTasks); EXECUTOR.execute(this::processTasks);
} }
} finally {
scheduleLock.unlock();
} }
} }
@Override @Override
public void execTask(@NonNull Task task) throws IOException { public void execTask(@NonNull Task task) throws IOException {
synchronized (tasks) { scheduleLock.lock();
while (runningTasks) { try {
// Wait until all existing tasks are done if (isRunningTask) {
try { SyncTask sync = new SyncTask(scheduleLock.newCondition());
tasks.wait(); tasks.offer(sync);
} catch (InterruptedException ignored) {} // Wait until it's our turn
sync.await();
} }
isRunningTask = true;
} finally {
scheduleLock.unlock();
} }
exec0(task); exec0(task);
processNextTask(true);
} }
@NonNull @NonNull

View File

@ -260,6 +260,20 @@ public class MainActivity extends Activity implements Handler.Callback {
binding.testAsync.setOnClickListener(v -> binding.testAsync.setOnClickListener(v ->
Shell.cmd("test_async").to(consoleList).submit()); Shell.cmd("test_async").to(consoleList).submit());
binding.testQueue.setOnClickListener(v -> {
Shell.getShell(Shell.EXECUTOR, s -> {
Log.i(TAG, "Queue: 1");
s.newJob().to(consoleList).add("sleep 1", "echo 1").submit();
Log.i(TAG, "Queue: 2");
s.newJob().to(consoleList).add("echo 2").exec();
Log.i(TAG, "Queue: 3");
s.newJob().to(consoleList).add("sleep 1", "echo 3").submit();
Log.i(TAG, "Queue: 4");
s.newJob().to(consoleList).add("echo 4").submit();
Log.i(TAG, "Queue: done");
});
});
binding.clear.setOnClickListener(v -> binding.console.setText("")); binding.clear.setOnClickListener(v -> binding.console.setText(""));
binding.stressTest.setOnClickListener(v -> StressTest.perform(remoteFS)); binding.stressTest.setOnClickListener(v -> StressTest.perform(remoteFS));

View File

@ -44,7 +44,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="Shell Sync" /> android:text="Sync CMD" />
<Button <Button
android:id="@+id/test_async" android:id="@+id/test_async"
@ -52,7 +52,15 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="Shell Async" /> android:text="Async CMD" />
<Button
android:id="@+id/test_queue"
style="?android:borderlessButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Test Queue" />
<Button <Button
android:id="@+id/close_shell" android:id="@+id/close_shell"