Windows服务以SYSTEM运行调用子进程时管道断裂问题求助
问题:Windows服务以SYSTEM身份启动用户态Go子进程,无法读取stdout(管道断裂)
我正在尝试从以SYSTEM身份运行的Windows服务中,启动一个以登录用户身份运行的Go语言子可执行文件,使用CreateProcessAsUser方法启动进程。子进程确实能启动(可以看到它生成的C:\log.text里的PID信息),但父服务进程无法读取子进程的stdout,持续收到**管道断裂(ERROR_BROKEN_PIPE)**错误。
我已经按照Windows文档的步骤重定向了子进程的stdin/stdout,也为管道设置了Everyone权限,但问题依然存在。
父进程(C语言服务端)代码:
#include "test.h" #include <windows.h> #include <winbase.h> #include <userenv.h> #include <tchar.h> #include <wtsapi32.h> #include <strsafe.h> #include <processthreadsapi.h> #define BUFSIZE 4096 #define PROC_THREAD_ATTRIBUTE_HANDLE_LIST ( 2 | 0x00020000) typedef struct _STARTUPINFOEXA { STARTUPINFOA StartupInfo; LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; } STARTUPINFOEXA, *LPSTARTUPINFOEXA; void ReadAndHandleOutput(HANDLE hPipeRead, FILE* fp) { CHAR lpBuffer[BUFSIZE]; DWORD nBytesRead; DWORD bytesAvailable; DWORD bytesLeftInMsg; do { if (!PeekNamedPipe(hPipeRead, lpBuffer, sizeof(lpBuffer), &nBytesRead, &bytesAvailable, &bytesLeftInMsg)) { if (GetLastError() == ERROR_BROKEN_PIPE) { fprintf(fp, "Broken Pipe \n"); fflush(fp); break; // pipe done - normal exit path. } else { fprintf(fp, "ReadFileError %d\n", GetLastError()); // Something bad happened. fflush(fp); } } } while(bytesLeftInMsg > 0); fprintf(fp, "GOT OUTPUT, %s -- %d -- %d -- %d\n", lpBuffer, nBytesRead, bytesAvailable, bytesLeftInMsg); fflush(fp); } bool LaunchProcess(char *process_path) { FILE *fp = fopen("C:\\test.log", "a"); DWORD SessionId = WTSGetActiveConsoleSessionId(); if (SessionId == -1) { // no-one logged in fprintf(fp, "Session ID is -1\n"); fclose(fp); return false; } HANDLE hToken; BOOL ok = WTSQueryUserToken(SessionId, &hToken); if (!ok) { fprintf(fp, "Unable to get the token for session id %d\n", SessionId); fclose(fp); return false; } void *environment = NULL; ok = CreateEnvironmentBlock(&environment, hToken, TRUE); if (!ok) { fprintf(fp, "Unable to create environment with session ID %d\n", SessionId); CloseHandle(hToken); fclose(fp); return false; } HANDLE hServerPipe = CreateNamedPipe("\\\\.\\Pipe\\test-pipe.em", PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE, 1, 0, 0, 0, 0); HANDLE hClientPipe; if (hServerPipe != INVALID_HANDLE_VALUE) { static SECURITY_ATTRIBUTES sa = { sizeof(sa), 0, TRUE }; hClientPipe = CreateFile("\\\\.\\Pipe\\test-pipe.em", FILE_GENERIC_READ|FILE_GENERIC_WRITE, 0, &sa, OPEN_EXISTING, 0, 0); if (hClientPipe == INVALID_HANDLE_VALUE) { fprintf(fp, "Error creating client handle %d", GetLastError()); CloseHandle(hServerPipe); return false; } } else { fprintf(fp, "Error creating server handle %d", GetLastError()); return false; } STARTUPINFOEXA si = { { sizeof(si) } }; PROCESS_INFORMATION pi; si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; si.StartupInfo.hStdInput = si.StartupInfo.hStdOutput = si.StartupInfo.hStdError = hClientPipe; DWORD dwCreationFlags = CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT; BOOL fInit = FALSE; SIZE_T Size; if (!InitializeProcThreadAttributeList(0, 1, 0, &Size) && GetLastError() == ERROR_INSUFFICIENT_BUFFER && InitializeProcThreadAttributeList(si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)alloca(Size), 1, 0, &Size)) { fInit = TRUE; if (UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &si.StartupInfo.hStdError, sizeof(HANDLE), 0, 0)) { dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT; } } fprintf(fp, "calling the process %s\n", process_path); fflush(fp); ok = CreateProcessAsUser(hToken, NULL, process_path, NULL, NULL, TRUE, dwCreationFlags, environment, NULL, &si.StartupInfo, &pi); fprintf(fp, "Running process %s and %d with PID %d\n", process_path, ok, pi.dwProcessId); fflush(fp); if (!ok) { wchar_t buf[BUFSIZE]; FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buf, (sizeof(buf) / sizeof(wchar_t)), NULL); fprintf(fp, "Failed to create processes as user - %d, %d, %S, %s\n", SessionId, GetLastError(), buf, process_path); fclose(fp); return false; } else { CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } // Close pipe handles (do not continue to modify the parent). // You need to make sure that no handles to the write end of the // output pipe are maintained in this process or else the pipe will // not close when the child process exits and the ReadFile will hang. if (!CloseHandle(si.StartupInfo.hStdError)) { fprintf(fp, "CloseHandle - outputwrite Error %d\n", GetLastError()); fclose(fp); return false; } ReadAndHandleOutput(hServerPipe, fp); CloseHandle(hServerPipe); fclose(fp); return true; Cleanup: fprintf(fp, "Something went wrong"); fclose(fp); return false; }
子进程(Go语言)代码:
package main import ( "fmt" "os" "time" ) func main() { os.Stdout.Write([]byte("Hello from the GO side...")) f, err := os.OpenFile("C:\\log.text", os.O_APPEND|os.O_WRONLY, 0600) if err != nil { panic(err) } defer f.Close() var output = fmt.Sprintf("I'm running as %d launched by %d", os.Getpid(), os.Getppid()) if _, err = f.WriteString(output); err != nil { panic(err) } os.Stdout.Close() }
解决方案分析与修改建议
我帮你梳理了几个核心问题,逐个修复后应该能解决管道断裂和读取不到输出的问题:
1. 管道安全权限不支持跨会话访问
SYSTEM服务运行在会话0,用户进程在用户专属会话(比如会话1),默认的命名管道安全属性会阻止用户进程访问管道。你需要显式创建允许所有用户访问的安全描述符:
// 构建允许全局访问的管道安全描述符 PSECURITY_DESCRIPTOR pSD = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH); InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION); SetSecurityDescriptorDacl(pSD, TRUE, NULL, FALSE); // 允许所有用户访问 SECURITY_ATTRIBUTES saPipe = {0}; saPipe.nLength = sizeof(SECURITY_ATTRIBUTES); saPipe.lpSecurityDescriptor = pSD; saPipe.bInheritHandle = TRUE; // 创建管道时传入这个安全属性 HANDLE hServerPipe = CreateNamedPipe("\\\\.\\Pipe\\test-pipe.em", PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE|PIPE_READMODE_BYTE, 1, 0, 0, 0, &saPipe); // 用完后释放内存 LocalFree(pSD);
2. 管道句柄继承与关闭时机错误
你把同一个管道句柄同时用作stdin/stdout/stderr,但只将hStdError加入了继承列表,而且在读取输出前就关闭了该句柄,直接导致管道写端被切断。修改点:
- 将所有三个标准句柄加入继承列表:
HANDLE handlesToInherit[] = {si.StartupInfo.hStdInput, si.StartupInfo.hStdOutput, si.StartupInfo.hStdError}; if (UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handlesToInherit, sizeof(handlesToInherit), 0, 0)) { dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT; }
- 延迟关闭父进程中的管道句柄,直到读完输出后再关闭:
// 先读取输出,再关闭父端持有的客户端管道句柄 ReadAndHandleOutput(hServerPipe, fp); CloseHandle(si.StartupInfo.hStdInput); CloseHandle(si.StartupInfo.hStdOutput); CloseHandle(si.StartupInfo.hStdError); CloseHandle(hServerPipe);
3. Go进程的stdout缓冲未刷新
Go的标准输出默认是带缓冲的,直接调用os.Stdout.Write后如果不手动刷新,数据可能还在内存中就关闭了句柄,导致父进程读不到内容。修改子进程代码:
package main import ( "fmt" "os" ) func main() { // 写入后强制刷新stdout缓冲 _, err := os.Stdout.Write([]byte("Hello from the GO side...")) if err != nil { panic(err) } if err := os.Stdout.Sync(); err != nil { panic(err) } // 或者直接用fmt.Fprint,它会自动刷新缓冲 // fmt.Fprint(os.Stdout, "Hello from the GO side...") f, err := os.OpenFile("C:\\log.text", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) // 加上O_CREATE确保文件存在 if err != nil { panic(err) } defer f.Close() var output = fmt.Sprintf("I'm running as %d launched by %d\n", os.Getpid(), os.Getppid()) if _, err = f.WriteString(output); err != nil { panic(err) } // 不需要手动关闭os.Stdout,进程退出时会自动处理 }
4. PeekNamedPipe的读取逻辑缺陷
你的ReadAndHandleOutput函数仅调用一次PeekNamedPipe,且没有实际读取管道数据。改用ReadFile来循环读取,这才是正确的管道读取方式:
void ReadAndHandleOutput(HANDLE hPipeRead, FILE* fp) { CHAR lpBuffer[BUFSIZE]; DWORD nBytesRead; BOOL success; do { success = ReadFile(hPipeRead, lpBuffer, sizeof(lpBuffer) - 1, &nBytesRead, NULL); if (!success) { if (GetLastError() == ERROR_BROKEN_PIPE) { fprintf(fp, "Broken Pipe (child process closed pipe)\n"); fflush(fp); break; } else { fprintf(fp, "ReadFile failed with error %d\n", GetLastError()); fflush(fp); break; } } if (nBytesRead > 0) { lpBuffer[nBytesRead] = '\0'; // 确保字符串以null结尾 fprintf(fp, "GOT OUTPUT: %s (bytes read: %d)\n", lpBuffer, nBytesRead); fflush(fp); } } while (nBytesRead > 0); }
额外注意点
- 确保你的服务账号拥有
SE_TCB_NAME权限,否则WTSQueryUserToken会调用失败; CreateProcessAsUser的bInheritHandles参数你已经设置为TRUE,这部分是正确的。
内容的提问来源于stack exchange,提问作者Tchinmai
相关产品推荐
相关产品推荐

