{"record":{"id":"06d280df6a49493c","repo":"infiniflow/ragflow","slug":"process-finished-but-returncode-is-none","errorCode":null,"errorMessage":"Process finished but returncode is None","messagePattern":"Process finished but returncode is None","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"warning","filePath":"agent/sandbox/executor_manager/utils/common.py","lineNumber":27,"sourceCode":"#\n#  Unless required by applicable law or agreed to in writing, software\n#  distributed under the License is distributed on an \"AS IS\" BASIS,\n#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#  See the License for the specific language governing permissions and\n#  limitations under the License.\n#\nimport asyncio\nfrom typing import Tuple\n\n\nasync def async_run_command(*args, timeout: float = 5) -> Tuple[int, str, str]:\n    \"\"\"Safe asynchronous command execution tool\"\"\"\n    proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)\n\n    try:\n        stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)\n        if proc.returncode is None:\n            raise RuntimeError(\"Process finished but returncode is None\")\n        return proc.returncode, stdout.decode(), stderr.decode()\n    except asyncio.TimeoutError:\n        proc.kill()\n        await proc.wait()\n        raise RuntimeError(\"Command timed out\")\n    except Exception as e:\n        proc.kill()\n        await proc.wait()\n        raise e\n","sourceCodeStart":9,"sourceCodeEnd":37,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/agent/sandbox/executor_manager/utils/common.py#L9-L37","documentation":"Defensive check inside the shared `async_run_command` helper: after `proc.communicate()` resolves, the transport reports no exit code. In CPython's asyncio this is effectively unreachable for subprocesses (communicate sets returncode), so hitting it indicates a broken event loop, a patched/edge-case transport, or a library bug rather than a normal failure mode.","triggerScenarios":"Calling any helper-wrapped command (docker exec, mkdir, run args) while: the event loop is being closed mid-await, a custom transport or test double returns from communicate() without reaping the process, or exotic interaction with loop shutdown during interpreter exit.","commonSituations":"Almost never seen in production; occasionally surfaced in test suites that mock create_subprocess_exec, or when tasks are cancelled during loop teardown and the exception path masks the real cancellation error.","solutions":["Reproduce outside your harness: if it only occurs in tests, fix the mock so communicate() sets a returncode.","Audit for tasks cancelled during loop shutdown; ensure daemon-style subprocess tasks are awaited before loop.close().","If persistent, replace the check by calling `await proc.wait()` once before reading returncode to force reaping."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    rc, out, err = await async_run_command(*cmd, timeout=t)\nexcept RuntimeError as e:\n    if \"returncode is None\" in str(e):\n        logger.warning(\"Subprocess transport anomaly for %s; retrying once\", cmd)\n        rc, out, err = await async_run_command(*cmd, timeout=t)\n    else:\n        raise","preventionTips":["Do not close the event loop while subprocess tasks are still awaiting.","In tests, mock communicate() so it returns (stdout, stderr) with a set returncode.","Treat this message as a canary for loop-teardown or mocking bugs, not a runtime condition."],"tags":["asyncio","subprocess","defensive-check","edge-case"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}