Description
THIS IS UNRELATED TO #66709
Go version
go version go1.22.5 darwin/arm64
Output of go env
in your module/workspace:
GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/nathanpierce/Library/Caches/go-build'
GOENV='/Users/nathanpierce/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/nathanpierce/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/nathanpierce/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/opt/homebrew/Cellar/go/1.22.5/libexec'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/opt/homebrew/Cellar/go/1.22.5/libexec/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.22.5'
GCCGO='gccgo'
AR='ar'
CC='cc'
CXX='c++'
CGO_ENABLED='1'
GOMOD='/Users/nathanpierce/test/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/vt/byhkgjmd4pq6v8j2f9tsghf40000gn/T/go-build1722732525=/tmp/go-build -gno-record-gcc-switches -fno-common'
What did you do?
I'm trying to create a child and then exit the parent so that I can create a daemon-like experience for my go application.
Here is the main.go you can reproduce with using go run main.go
:
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"syscall"
"time"
)
// Mark of daemon process - system environment variable _GO_DAEMON=1
const (
MARK_NAME = "_GO_DAEMON"
MARK_VALUE = "1"
)
// Default file permissions for log and pid files.
const FILE_PERM = os.FileMode(0640)
// WasReborn returns true in child process (daemon) and false in parent process.
func WasReborn() bool {
return os.Getenv(MARK_NAME) == MARK_VALUE
}
// A Context describes daemon context.
type Context struct {
// If PidFileName is non-empty, parent process will try to create and lock
// pid file with given name. Child process writes process id to file.
PidFileName string
// Permissions for new pid file.
PidFilePerm os.FileMode
// If LogFileName is non-empty, parent process will create file with given name
// and will link to fd 2 (stderr) for child process.
LogFileName string
// Permissions for new log file.
LogFilePerm os.FileMode
// If WorkDir is non-empty, the child changes into the directory before
// creating the process.
WorkDir string
// If Chroot is non-empty, the child changes root directory
Chroot string
// If Env is non-nil, it gives the environment variables for the
// daemon-process in the form returned by os.Environ.
// If it is nil, the result of os.Environ will be used.
Env []string
// If Args is non-nil, it gives the command-line args for the
// daemon-process. If it is nil, the result of os.Args will be used.
Args []string
// Credential holds user and group identities to be assumed by a daemon-process.
Credential *syscall.Credential
// If Umask is non-zero, the daemon-process call Umask() func with given value.
Umask int
// Struct contains only serializable public fields (!!!)
abspath string
logFile *os.File
nullFile *os.File
rpipe, wpipe *os.File
}
func (d *Context) SetLogFile(fd *os.File) {
d.logFile = fd
}
func (d *Context) openFiles() (err error) {
if d.PidFilePerm == 0 {
d.PidFilePerm = FILE_PERM
}
if d.LogFilePerm == 0 {
d.LogFilePerm = FILE_PERM
}
if d.nullFile, err = os.Open(os.DevNull); err != nil {
return
}
if len(d.LogFileName) > 0 {
if d.LogFileName == "/dev/stdout" {
d.logFile = os.Stdout
} else if d.LogFileName == "/dev/stderr" {
d.logFile = os.Stderr
} else if d.logFile, err = os.OpenFile(d.LogFileName,
os.O_WRONLY|os.O_CREATE|os.O_APPEND, d.LogFilePerm); err != nil {
return
}
}
d.rpipe, d.wpipe, err = os.Pipe()
return
}
func (d *Context) closeFiles() (err error) {
fmt.Println("closeFiles")
cl := func(file **os.File) {
if *file != nil {
(*file).Close()
*file = nil
}
}
cl(&d.rpipe)
cl(&d.wpipe)
cl(&d.logFile)
cl(&d.nullFile)
return
}
func (d *Context) prepareEnv() (err error) {
if d.abspath, err = os.Executable(); err != nil {
return
}
fmt.Println("abspath", d.abspath)
if len(d.Args) == 0 {
d.Args = os.Args
}
mark := fmt.Sprintf("%s=%s", MARK_NAME, MARK_VALUE)
if len(d.Env) == 0 {
d.Env = os.Environ()
}
d.Env = append(d.Env, mark)
return
}
func (d *Context) files() (f []*os.File) {
log := d.nullFile
if d.logFile != nil {
log = d.logFile
}
f = []*os.File{
d.rpipe, // (0) stdin
log, // (1) stdout
log, // (2) stderr
d.nullFile, // (3) dup on fd 0 after initialization
}
return
}
func (d *Context) reborn() (child *os.Process, err error) {
if !WasReborn() {
child, err = d.parent()
} else {
err = d.child()
}
return
}
func (d *Context) parent() (child *os.Process, err error) {
if err = d.prepareEnv(); err != nil {
return
}
defer d.closeFiles()
if err = d.openFiles(); err != nil {
return
}
attr := &os.ProcAttr{
Dir: d.WorkDir,
Env: d.Env,
Files: d.files(),
Sys: &syscall.SysProcAttr{},
}
fmt.Println("parent startProcess")
if child, err = os.StartProcess(d.abspath, d.Args, attr); err != nil {
return
}
d.rpipe.Close()
encoder := json.NewEncoder(d.wpipe)
err = encoder.Encode(d)
fmt.Println("parent done")
// if this sleep keeps the parent around, it will work.
// time.Sleep(3 * time.Second)
return
}
func (d *Context) child() (err error) {
decoder := json.NewDecoder(os.Stdin)
if err = decoder.Decode(d); err != nil {
return
}
return
}
func main() {
daemonContext := &Context{
PidFileName: "test.pid",
PidFilePerm: 0644,
LogFileName: "test.log",
LogFilePerm: 0640,
WorkDir: "/tmp",
Umask: 027,
Args: []string{"SecPolicyCreateSSL"},
}
d, err := daemonContext.reborn()
if err != nil {
log.Fatalln(err)
}
if d != nil { // return the parent process since it's now forked into a child
return
}
time.Sleep(2 * time.Second)
fmt.Println("doing get...")
resp, err := http.Get("https://google.com")
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println(err)
return
}
fmt.Println(string(body)[:100])
return
}
What did you see happen?
Once ran, you'll see in test.log:
doing get...
2024/07/23 13:07:21 Get "https://google.com": tls: failed to verify certificate: SecPolicyCreateSSL error: 0
Now under the parent
function, uncomment // time.Sleep(3 * time.Second)
and then run again.
You should then see it working:
doing get...
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en"><head><meta content
It seems as if the parent closing too soon is causing the child to not have, what I can only guess, is the proper environment available on linux and macOS.
Though, I'm not entirely sure why. I've tried digging into this as low level as I am aware of and nothing is clicking for me as to the cause. Does anyone know why this isn't possible?
What did you expect to see?
I would like the parent to be able to exit well before the child, so that I can use the golang binary as a controller for the daemon/service. But I also can't guarantee that https calls will happen before the parent exits.