MuntashirAkon/AppManager · error · IllegalStateException
Unable to add commands to a closed shell.
Error message
Unable to add commands to a closed shell.
What it means
Shell is a queue-based command shell. add() is the internal path exec() uses to enqueue a Command; if the shell has been closed (mClosed == true, e.g. after exit/finish or the underlying process died) it throws IllegalStateException instead of queueing into a dead shell. It is an invalid-state error: the caller must not submit commands to a terminated shell.
Source
Thrown at libserver/src/main/java/io/github/muntashirakon/AppManager/server/common/Shell.java:186
*/
public boolean allCommandsOver() {
return mCommandQueue.isEmpty();
}
private int generateCommandID() {
int id = mNextCmdID.getAndIncrement();
if (id > 0x00FFFFFF) {
mNextCmdID.set(1);
id = generateCommandID();
}
return id;
}
@NonNull
private Command add(Command command) {
if (mClosed) {
throw new IllegalStateException("Unable to add commands to a closed shell.");
}
command.setId(generateCommandID());
mCommandQueue.offer(command);
return command;
}
@NonNull
public Result exec(String cmd) {
Result result = new Result();
FLog.log("Command: " + cmd);
final StringBuilder outLine = new StringBuilder();
try {
result.mStatusCode = add(new Command(cmd) {
@Override
public void onUpdate(int id, String message) {
outLine.append(message).append('\n');
}
View on GitHub (pinned to 0152f468fc)
Solutions
- Create a new Shell instance instead of reusing the closed one (check shell.isClosed() before exec if available).
- Serialize access: ensure no code path calls exit()/close() while commands are still being submitted (guard with lifecycle checks).
- If the shell closed because the su process died, re-request root and reopen the shell before running further commands.
- Wrap exec() in try-catch for IllegalStateException and recreate the shell as recovery.
Example fix
// before
shell.exec(cmd); // throws if closed
// after
if (shell.isClosed()) {
shell = new Shell.Builder().build(); // or recreate via your shell provider
}
shell.exec(cmd);
Defensive patterns
Strategy: type-guard
Validate before calling
if (shell == null || shell.isClosed()) {
shell = createNewShell(); // recreate before submitting commands
}
Type guard
boolean usable(Shell s) {
return s != null && !s.isClosed();
}
Try / catch
try {
Result r = shell.exec(cmd);
} catch (IllegalStateException e) {
shell = createNewShell();
Result r = shell.exec(cmd); // single retry on fresh shell
}
Prevention
- Own the shell lifecycle in one place; close and null it out so stale references aren't reused.
- Never send an 'exit' command through exec(); use the shell's own exit()/close() API.
- Check isClosed() before every batch of commands, especially from worker threads.
- Avoid sharing one Shell across threads without synchronization.
When it happens
Trigger: Calling shell.exec(cmd) — or any code path that reaches add() — after shell.exit()/close() has been invoked, or after the shell's process has terminated and marked itself closed; notably calling exec() on a Shell instance that a previous exec() already terminated (e.g. an 'exit' command).
Common situations: Reusing a long-lived Shell object after a script ran 'exit'; executing a command after the su session was killed by the system or root manager; running exec() from another thread concurrently with shell shutdown; holding a Shell across a configuration change/restart in the app.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Could not ${enable ? "enable" : "disable"} sensor.
- Could not reset sensor.
- FS Root not found.
- Not mounted
- Stream closed
AI-assisted analysis of MuntashirAkon/AppManager@0152f468fc (2026-09-12).
Data as JSON: /api/errors/b78c62862035e4df.
Report an issue: GitHub.