You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:11:51