{"record":{"id":"b78c62862035e4df","repo":"MuntashirAkon/AppManager","slug":"unable-to-add-commands-to-a-closed-shell","errorCode":null,"errorMessage":"Unable to add commands to a closed shell.","messagePattern":"Unable to add commands to a closed shell\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"libserver/src/main/java/io/github/muntashirakon/AppManager/server/common/Shell.java","lineNumber":186,"sourceCode":"     */\n    public boolean allCommandsOver() {\n        return mCommandQueue.isEmpty();\n    }\n\n\n    private int generateCommandID() {\n        int id = mNextCmdID.getAndIncrement();\n        if (id > 0x00FFFFFF) {\n            mNextCmdID.set(1);\n            id = generateCommandID();\n        }\n        return id;\n    }\n\n    @NonNull\n    private Command add(Command command) {\n        if (mClosed) {\n            throw new IllegalStateException(\"Unable to add commands to a closed shell.\");\n        }\n        command.setId(generateCommandID());\n        mCommandQueue.offer(command);\n        return command;\n    }\n\n    @NonNull\n    public Result exec(String cmd) {\n        Result result = new Result();\n        FLog.log(\"Command:  \" + cmd);\n        final StringBuilder outLine = new StringBuilder();\n        try {\n            result.mStatusCode = add(new Command(cmd) {\n                @Override\n                public void onUpdate(int id, String message) {\n                    outLine.append(message).append('\\n');\n                }\n","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/MuntashirAkon/AppManager/blob/0152f468fc9463ee02dc2ca83f6fe4989a2c4ca5/libserver/src/main/java/io/github/muntashirakon/AppManager/server/common/Shell.java#L168-L204","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nshell.exec(cmd); // throws if closed\n// after\nif (shell.isClosed()) {\n    shell = new Shell.Builder().build(); // or recreate via your shell provider\n}\nshell.exec(cmd);\n","handlingStrategy":"type-guard","validationCode":"if (shell == null || shell.isClosed()) {\n    shell = createNewShell(); // recreate before submitting commands\n}\n","typeGuard":"boolean usable(Shell s) {\n    return s != null && !s.isClosed();\n}\n","tryCatchPattern":"try {\n    Result r = shell.exec(cmd);\n} catch (IllegalStateException e) {\n    shell = createNewShell();\n    Result r = shell.exec(cmd); // single retry on fresh shell\n}\n","preventionTips":["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."],"tags":["illegal-state","shell","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"0152f468fc9463ee02dc2ca83f6fe4989a2c4ca5","analyzedAt":"2026-09-12T14:03:37.243Z","contentChangedAt":"2026-09-12T14:03:37.243Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}