120 lines
3.3 KiB
Go
120 lines
3.3 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//go:build windows
|
|
|
|
package busctl
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
|
|
)
|
|
|
|
// CREATE_NEW_PROCESS_GROUP (0x00000200) prevents the child from receiving
|
|
// the parent's Ctrl+C signal, similar in spirit to Setsid on Unix.
|
|
const createNewProcessGroup = 0x00000200
|
|
|
|
var (
|
|
spawnWindowsDial = transport.Dial
|
|
spawnWindowsPollInterval = 25 * time.Millisecond
|
|
)
|
|
|
|
// Spawn starts a detached Windows bus without cmd.ExtraFiles (unsupported by
|
|
// Go on Windows). Readiness is confirmed by dialing the bus's existing named
|
|
// pipe. The child is reaped in the background after readiness, while an early
|
|
// process exit is surfaced as ErrSpawnFailed.
|
|
func Spawn(cfg SpawnConfig) (pid int, err error) {
|
|
if strings.TrimSpace(cfg.ClientID) == "" {
|
|
return 0, fmt.Errorf("busctl: SpawnConfig.ClientID is required")
|
|
}
|
|
if strings.TrimSpace(cfg.IPCEndpoint) == "" {
|
|
return 0, fmt.Errorf("busctl: SpawnConfig.IPCEndpoint is required on Windows")
|
|
}
|
|
if cfg.ExecPath == "" {
|
|
execPath, err := spawnExecutable()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("busctl: locate executable: %w", err)
|
|
}
|
|
cfg.ExecPath = execPath
|
|
}
|
|
if cfg.Env == nil {
|
|
cfg.Env = os.Environ()
|
|
}
|
|
|
|
args := append([]string{"event", "_bus", "--client-id", cfg.ClientID}, cfg.ExtraArgs...)
|
|
cmd := exec.Command(cfg.ExecPath, args...)
|
|
cmd.Env = withoutReadyFDEnv(cfg.Env)
|
|
cmd.Stdin = nil
|
|
cmd.Stdout = nil
|
|
cmd.Stderr = nil
|
|
applyDetach(cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
return 0, fmt.Errorf("busctl: start %s: %w", cfg.ExecPath, err)
|
|
}
|
|
pid = cmd.Process.Pid
|
|
|
|
waitDone := make(chan error, 1)
|
|
go func() { waitDone <- cmd.Wait() }()
|
|
timer := time.NewTimer(ReadyTimeout)
|
|
defer timer.Stop()
|
|
ticker := time.NewTicker(spawnWindowsPollInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
if conn, dialErr := spawnWindowsDial(cfg.IPCEndpoint); dialErr == nil {
|
|
_ = conn.Close()
|
|
return pid, nil
|
|
}
|
|
select {
|
|
case waitErr := <-waitDone:
|
|
if waitErr == nil {
|
|
return pid, fmt.Errorf("%w: child exited before binding named pipe", ErrSpawnFailed)
|
|
}
|
|
return pid, fmt.Errorf("%w: %v", ErrSpawnFailed, waitErr)
|
|
case <-timer.C:
|
|
_ = cmd.Process.Kill()
|
|
select {
|
|
case <-waitDone:
|
|
case <-time.After(time.Second):
|
|
}
|
|
return pid, ErrSpawnTimeout
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func withoutReadyFDEnv(env []string) []string {
|
|
prefix := strings.ToUpper(ReadyFDEnv) + "="
|
|
out := make([]string, 0, len(env))
|
|
for _, entry := range env {
|
|
if strings.HasPrefix(strings.ToUpper(entry), prefix) {
|
|
continue
|
|
}
|
|
out = append(out, entry)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func applyDetach(cmd *exec.Cmd) {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
CreationFlags: createNewProcessGroup,
|
|
HideWindow: true,
|
|
}
|
|
}
|