From 61f9bef0ef9185a7146c04780f49d99d1759650f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 15 Apr 2022 14:19:24 +0200 Subject: [PATCH 01/15] go mod init; tidy --- go.mod | 14 +++++++ go.sum | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7817a2e --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/zimbatm/socketmaster + +go 1.17 + +require google.golang.org/grpc v1.45.0 + +require ( + github.com/golang/protobuf v1.5.2 // indirect + golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect + golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect + golang.org/x/text v0.3.0 // indirect + google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect + google.golang.org/protobuf v1.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..721297c --- /dev/null +++ b/go.sum @@ -0,0 +1,120 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 0c92bb32f2a9da8dc28be2cead84b38aad9716d0 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 15 Apr 2022 14:20:08 +0200 Subject: [PATCH 02/15] Add Nix boilerplate --- .gitignore | 4 ++++ flake.lock | 42 ++++++++++++++++++++++++++++++++++++++++ flake.nix | 46 ++++++++++++++++++++++++++++++++++++++++++++ nix/nixos/module.nix | 20 +++++++++++++++++++ nix/nixos/test.nix | 12 ++++++++++++ nix/package.nix | 15 +++++++++++++++ shell.nix | 2 ++ 7 files changed, 141 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 nix/nixos/module.nix create mode 100644 nix/nixos/test.nix create mode 100644 nix/package.nix create mode 100644 shell.nix diff --git a/.gitignore b/.gitignore index 8ff0afd..6579b63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /socketmaster /examples/childserver/childserver /.vagrant + +# Nix +result +result-* diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a316c1a --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1649676176, + "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1650022871, + "narHash": "sha256-0DhWgQN5v9TIKu+iVt5FH6V+gZvvp+UCX5cnQ2uiQ2g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a7cf9372e97725eaa6da1e72698af9d23a3ea083", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..5a15c8e --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + description = "A very basic flake"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, nixpkgs, flake-utils }: + let + inherit (flake-utils.lib) eachDefaultSystem; + inherit (nixpkgs) lib; + + flakeAttrs = { + nixosModules.default = { imports = [ ./nix/nixos/module.nix ]; }; + }; + + perSystem = system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShell = self.devShells.${system}.default; + devShells.default = pkgs.mkShell { + nativeBuildInputs = [ + pkgs.go + pkgs.gopls + pkgs.go-outline + pkgs.nixpkgs-fmt + ]; + }; + + defaultPackage = self.packages.${system}.default; + packages.default = pkgs.callPackage ./nix/package.nix { }; + checks = + lib.optionalAttrs pkgs.stdenv.isLinux { + nixos = pkgs.callPackage ./nix/nixos/test.nix { + socketmaster = self.packages.${system}.default; + }; + }; + }; + + systemAttrs = eachDefaultSystem perSystem; + + in + systemAttrs // flakeAttrs; + +} diff --git a/nix/nixos/module.nix b/nix/nixos/module.nix new file mode 100644 index 0000000..94c451b --- /dev/null +++ b/nix/nixos/module.nix @@ -0,0 +1,20 @@ +{ config, lib, ... }: +let + inherit (lib) + mkOption + mkIf + types + ; + + cfg = config.socketmaster; + +in +{ + options.socketmaster = { + # TODO + }; + config = mkIf (cfg != { }) { + # TODO + systemd.services = { }; + }; +} diff --git a/nix/nixos/test.nix b/nix/nixos/test.nix new file mode 100644 index 0000000..919574b --- /dev/null +++ b/nix/nixos/test.nix @@ -0,0 +1,12 @@ +{ nixosTest, socketmaster }: + +nixosTest { + nodes.machine = { ... }: { + imports = [ ./module.nix ]; + # TODO + # .package = socketmaster; + }; + testScript = '' + machine.succeed("${socketmaster}/bin/socketmaster -help") + ''; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..e114894 --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,15 @@ +{ buildGoModule, lib }: + +buildGoModule { + name = "socketmaster"; + src = ../.; # FIXME cleanSource + + vendorSha256 = "sha256-bPIzk+g8lxntEREAp1ZJiHTGxXFRybtZjhR23uGIFR4="; + + meta = with lib; { + description = "Restart services without losing connections"; + homepage = "https://github.com/zimbatm/socketmaster"; + license = licenses.mit; + maintainers = with maintainers; [ zimbatm ]; + }; +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..4ab395c --- /dev/null +++ b/shell.nix @@ -0,0 +1,2 @@ +# shell.nix is still required for vscode Nix Env Selector (2022-04) +(builtins.getFlake ("git+file://" + toString ./.)).devShells.${builtins.currentSystem}.default From ddbd228ea8c69fcdab9188685054d7b6effa833b Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 15 Apr 2022 14:21:20 +0200 Subject: [PATCH 03/15] Typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cecfcc..21550d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ =================== * Prefix output with the [pid] - * Set EINHORN_FDS to be eninhorn-compatible + * Set EINHORN_FDS to be einhorn-compatible * Adding license and changelog * Add a note about other related projects From 0f92304f765d953b3bb809218d5b9a9841de7d3f Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 15 Apr 2022 17:42:29 +0200 Subject: [PATCH 04/15] Support children that accept systemd socket activation --- process_group.go | 20 ++++++++++++++++---- socketmaster.go | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/process_group.go b/process_group.go index 3a41e83..634d472 100644 --- a/process_group.go +++ b/process_group.go @@ -42,7 +42,18 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { return nil, err } - env := append(os.Environ(), "EINHORN_FDS=3") + // Make sure parent values don't interfere with our child. + // All fds have already been consumed at this stage. + os.Unsetenv("EINHORN_FDS") + os.Unsetenv("LISTEN_FDS") + os.Unsetenv("LISTEN_FDNAMES") + os.Unsetenv("LISTEN_PID") + + env := append(os.Environ(), + "EINHORN_FDS=3", + "LISTEN_FDS=1", + "LISTEN_FDNAMES=socket", + ) procAttr := &os.ProcAttr{ Env: env, @@ -60,9 +71,10 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { } } - args := append([]string{self.commandPath}, flag.Args()...) - log.Println("Starting", self.commandPath, args) - process, err = os.StartProcess(self.commandPath, args, procAttr) + args := append([]string{LISTEN_PID_HELPER_ARGV0}, self.commandPath) + args = append(args, flag.Args()...) + log.Println("Starting", args[1:]) + process, err = os.StartProcess(os.Args[0], args, procAttr) if err != nil { return } diff --git a/socketmaster.go b/socketmaster.go index 9b91c80..cd10c1f 100644 --- a/socketmaster.go +++ b/socketmaster.go @@ -46,7 +46,31 @@ func handleSignals(processGroup *ProcessGroup, c <-chan os.Signal, startTime int } } +// Go won't let us set LISTEN_PID between fork, +// and exec because letting "language users" do +// that is not necessarily safe, because of rts +// issues. Very understandable, but a little annoying. +// +// So, instead we call ourselves with a special +// argv[0] that drops us into this little helper. +// That way we don't have to squeeze it between +// those syscalls, at the cost of an extra exec. +// These aren't hot execs, so performance is not +// really affected. +func setLISTEN_PIDHelper() { + os.Setenv("LISTEN_PID", fmt.Sprint(os.Getpid())) + args := os.Args[1:] + syscall.Exec(args[0], args, os.Environ()) +} + +// See setLISTEN_PIDHelper +var LISTEN_PID_HELPER_ARGV0 = "set-LISTEN_PID-helper" + func main() { + if os.Args[0] == LISTEN_PID_HELPER_ARGV0 { + setLISTEN_PIDHelper() + } + var ( addr string command string From 9b860ffd6a36241d399197c116f3fd3a8cdb2737 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 15 Apr 2022 17:44:04 +0200 Subject: [PATCH 05/15] Add basic NixOS module and test --- flake.nix | 14 +++- nix/nixos/module.nix | 39 ++++++++-- nix/nixos/socketmaster-service.nix | 47 ++++++++++++ nix/nixos/test.nix | 40 +++++++++-- nix/nixos/test/socket-server/.gitignore | 1 + nix/nixos/test/socket-server/default.nix | 8 +++ nix/nixos/test/socket-server/go.mod | 5 ++ nix/nixos/test/socket-server/go.sum | 2 + nix/nixos/test/socket-server/server.go | 91 ++++++++++++++++++++++++ 9 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 nix/nixos/socketmaster-service.nix create mode 100644 nix/nixos/test/socket-server/.gitignore create mode 100644 nix/nixos/test/socket-server/default.nix create mode 100644 nix/nixos/test/socket-server/go.mod create mode 100644 nix/nixos/test/socket-server/go.sum create mode 100644 nix/nixos/test/socket-server/server.go diff --git a/flake.nix b/flake.nix index 5a15c8e..fc9b2e8 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,17 @@ inherit (nixpkgs) lib; flakeAttrs = { - nixosModules.default = { imports = [ ./nix/nixos/module.nix ]; }; + nixosModules.default = { lib, pkgs, ... }: + let + inherit (pkgs.stdenv.hostPlatform) system; + in + { + imports = [ ./nix/nixos/module.nix ]; + config = { + socketmaster.package = + lib.mkDefault self.packages.${system}.default; + }; + }; }; perSystem = system: @@ -33,7 +43,7 @@ checks = lib.optionalAttrs pkgs.stdenv.isLinux { nixos = pkgs.callPackage ./nix/nixos/test.nix { - socketmaster = self.packages.${system}.default; + extraModules = [ self.nixosModules.default ]; }; }; }; diff --git a/nix/nixos/module.nix b/nix/nixos/module.nix index 94c451b..09b29fc 100644 --- a/nix/nixos/module.nix +++ b/nix/nixos/module.nix @@ -1,20 +1,47 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: let inherit (lib) - mkOption + mapAttrs mkIf + mkOption types ; + inherit (lib.types) + attrsOf + submoduleWith + ; cfg = config.socketmaster; in { options.socketmaster = { - # TODO + package = mkOption { + description = '' + The socketmaster package to use. + + Note that services are not automatically restarted + when this value changes. + ''; + type = types.package; + default = pkgs.socketmaster; + defaultText = "socketmaster"; + }; + services = mkOption { + description = '' + A collection of socketmaster-driven services. + + Each entry will be mapped to a systemd service + with the same name. + ''; + type = attrsOf (submoduleWith { + modules = [ ./socketmaster-service.nix ]; + specialArgs.systemConfig = config; + }); + default = { }; + }; }; - config = mkIf (cfg != { }) { - # TODO - systemd.services = { }; + config = mkIf (cfg.services != { }) { + systemd.services = mapAttrs (k: v: v.systemdServiceModule) cfg.services; }; } diff --git a/nix/nixos/socketmaster-service.nix b/nix/nixos/socketmaster-service.nix new file mode 100644 index 0000000..6435064 --- /dev/null +++ b/nix/nixos/socketmaster-service.nix @@ -0,0 +1,47 @@ +{ config, lib, systemConfig, ... }: +let + inherit (lib) + mkOption + types + ; + inherit (types) + str + ; +in +{ + options = { + + command = mkOption { + description = '' + Program to start + ''; + type = str; + # no default + }; + + startMillis = mkOption { + description = '' + How long the new process takes to boot in milliseconds. + ''; + default = 3000; + }; + + ### Internal ### + + systemdServiceModule = mkOption { + internal = true; + # https://github.com/NixOS/nixpkgs/pull/163617 + type = types.deferredModule or types.raw or types.unspecified; + }; + }; + + config = { + systemdServiceModule = { ... }: { + # Avoid automatic restarts. This will trigger a reload instead. + reloadIfChanged = true; + serviceConfig.ExecStart = [ + "${systemConfig.socketmaster.package}/bin/socketmaster -command ${lib.escapeShellArg config.command} -start ${toString config.startMillis} -listen fd://3" + ]; + }; + }; +} diff --git a/nix/nixos/test.nix b/nix/nixos/test.nix index 919574b..b1cf11b 100644 --- a/nix/nixos/test.nix +++ b/nix/nixos/test.nix @@ -1,12 +1,42 @@ -{ nixosTest, socketmaster }: +{ nixosTest, pkgs, extraModules ? [ ] }: + +let + socket-server = pkgs.callPackage ./test/socket-server { }; +in nixosTest { nodes.machine = { ... }: { - imports = [ ./module.nix ]; - # TODO - # .package = socketmaster; + imports = extraModules; + config = { + environment.systemPackages = [ + # Used by testScript + pkgs.socat + ]; + systemd.sockets.socket-server = { + wantedBy = [ "sockets.target" ]; + listenStreams = [ "0.0.0.0:2022" ]; + }; + systemd.services.socket-server = { + wantedBy = [ "multi-user.target" ]; + # TODO: this will have to move to the socketmaster config + environment.ECHO_VALUE = "v1"; + # serviceConfig.ExecStart = "${socket-server}/bin/socket-server"; + }; + + socketmaster.services.socket-server = { + command = "${socket-server}/bin/socket-server"; + }; + }; }; testScript = '' - machine.succeed("${socketmaster}/bin/socketmaster -help") + machine.wait_for_unit("sockets.target") + machine.succeed(""" + echo '{"cmd":"echo"}' \ + | socat - TCP4:localhost:2022 \ + | grep -E '^"v1"$' + """) + + machine.wait_for_unit("socket-server.service") + machine.succeed("sleep 3") ''; } diff --git a/nix/nixos/test/socket-server/.gitignore b/nix/nixos/test/socket-server/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/nix/nixos/test/socket-server/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/nix/nixos/test/socket-server/default.nix b/nix/nixos/test/socket-server/default.nix new file mode 100644 index 0000000..de7e5ac --- /dev/null +++ b/nix/nixos/test/socket-server/default.nix @@ -0,0 +1,8 @@ +{ buildGoModule }: + +buildGoModule { + pname = "socket-server"; + version = "1.0.0"; + src = ./.; + vendorSha256 = "sha256-wivLZzDVpLPi+RgVti03Se0TQ0kco7CBnPFOITLFwx8="; +} diff --git a/nix/nixos/test/socket-server/go.mod b/nix/nixos/test/socket-server/go.mod new file mode 100644 index 0000000..409e053 --- /dev/null +++ b/nix/nixos/test/socket-server/go.mod @@ -0,0 +1,5 @@ +module example.com/socket-server + +go 1.17 + +require github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf diff --git a/nix/nixos/test/socket-server/go.sum b/nix/nixos/test/socket-server/go.sum new file mode 100644 index 0000000..7c6c1bb --- /dev/null +++ b/nix/nixos/test/socket-server/go.sum @@ -0,0 +1,2 @@ +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= diff --git a/nix/nixos/test/socket-server/server.go b/nix/nixos/test/socket-server/server.go new file mode 100644 index 0000000..e2e649e --- /dev/null +++ b/nix/nixos/test/socket-server/server.go @@ -0,0 +1,91 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + + "github.com/coreos/go-systemd/activation" +) + +func main() { + echoValue := os.Getenv("ECHO_VALUE") + Log("Starting; ECHO_VALUE=", echoValue) + + Log("LISTEN_PID", os.Getenv("LISTEN_PID")) + Log("LISTEN_FDS", os.Getenv("LISTEN_FDS")) + Log("LISTEN_FDNAMES", os.Getenv("LISTEN_FDNAMES")) + + listeners, err := activation.Listeners() + if err != nil { + panic(err) + } + + if listeners == nil { + panic("listeners == nil") + } + if len(listeners) != 1 { + panic(fmt.Sprintf("Unexpected number of socket activation fds: %d", len(listeners))) + } + l := listeners[0] + + Log("Ready") + for { + conn, err := l.Accept() + if err != nil { + Log("Error accepting: ", err.Error()) + os.Exit(1) + } + go handleRequest(conn, echoValue) + } +} + +func handleRequest(conn net.Conn, echoValue string) { + Log("Handling connection") + r := bufio.NewReader(conn) + w := bufio.NewWriter(conn) + for { + Log("Reading command from connection") + ln, err := r.ReadBytes('\n') + if err != nil { + Log("Error reading connection: ", err.Error()) + break + } + var result map[string]interface{} + err = json.Unmarshal(ln, &result) + if err != nil { + Log("Error parsing message: ", err.Error()) + break + } + if result["cmd"] == "echo" { + bytes, err := json.Marshal(echoValue) + if err != nil { + Log("Error marshalling echo response: ", err.Error()) + break + } + n, err := w.Write(bytes) + if err != nil { + Log("Error writing to socket; wrote ", n, " bytes; error: ", err.Error()) + break + } + err = w.WriteByte('\n') + if err != nil { + Log("Error writing endline to socket: ", err.Error()) + break + } + err = w.Flush() + if err != nil { + Log("Error flushing socket: ", err.Error()) + break + } + } + } + Log("Closing connection") + conn.Close() // Ignoring Close() error +} + +func Log(args ...interface{}) { + fmt.Fprintln(os.Stderr, args...) +} From 525cda61f33fee1fa97a9bc31d6829cf985147c9 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Fri, 15 Apr 2022 17:44:17 +0200 Subject: [PATCH 06/15] nix/package.nix: Add source filtering Nixpkgs go stuff complained about the nested module in nix/nixos/test/socket-server, which is not a problem for go build. --- nix/package.nix | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/nix/package.nix b/nix/package.nix index e114894..e0d2ead 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -2,7 +2,15 @@ buildGoModule { name = "socketmaster"; - src = ../.; # FIXME cleanSource + src = lib.cleanSourceWith { + src = lib.cleanSource ../.; + filter = path: type: + baseNameOf path != "nix" && ( + lib.hasSuffix ".go" path + || lib.hasSuffix "/go.mod" path + || lib.hasSuffix "/go.sum" path + ); + }; vendorSha256 = "sha256-bPIzk+g8lxntEREAp1ZJiHTGxXFRybtZjhR23uGIFR4="; From 863e1ea80b1a4f20a2e08fac86ebbc91255e0ff1 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 14:44:37 +0200 Subject: [PATCH 07/15] Basic config file loading --- config.go | 63 +++++++++++++++++++++++++++++++++++++++++++ config_test.go | 32 ++++++++++++++++++++++ go.mod | 5 +++- go.sum | 3 +++ inputs.go | 60 +++++++++++++++++++++++++++++++++++++++++ inputs_test.go | 56 ++++++++++++++++++++++++++++++++++++++ nix/package.nix | 3 ++- socketmaster.go | 32 +++++++++++----------- testdata/command.yaml | 1 + 9 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 config.go create mode 100644 config_test.go create mode 100644 inputs.go create mode 100644 inputs_test.go create mode 100644 testdata/command.yaml diff --git a/config.go b/config.go new file mode 100644 index 0000000..144d970 --- /dev/null +++ b/config.go @@ -0,0 +1,63 @@ +package main + +import ( + "errors" + "flag" + "io/ioutil" + + yaml "gopkg.in/yaml.v3" +) + +/* + flag.StringVar(&command, "command", "", "Program to start") + flag.StringVar(&addr, "listen", "tcp://:8080", "Port on which to bind") + flag.IntVar(&startTime, "start", 3000, "How long the new process takes to boot in millis") + flag.BoolVar(&useSyslog, "syslog", false, "Log to syslog") + flag.StringVar(&username, "user", "", "run the command as this user") +*/ + +// The mutable configuration items +type Config struct { + Command string `yaml:"command"` +} + +func emptyConfig() Config { + return Config{ + Command: "", + } +} + +func (config *Config) LoadFile(path string) error { + yamlFile, err := ioutil.ReadFile(path) + if err != nil { + return err + } + return config.LoadBytes(yamlFile) +} + +func (config *Config) LoadBytes(yamlString []byte) error { + return yaml.Unmarshal(yamlString, &config) +} + +func (config *Config) LoadString(yamlString string) error { + return config.LoadBytes([]byte(yamlString)) +} + +// Some of the mutable flags can be set on the command line as well. +func (config *Config) LinkToFlagSet(flags *flag.FlagSet) { + flags.StringVar(&config.Command, "command", config.Command, "Program to start") +} + +func (config *Config) Merge(other Config) error { + empty := emptyConfig() + + if other.Command != empty.Command { + if config.Command == empty.Command { + config.Command = other.Command + } else { + return errors.New("command can only be set once.") + } + } + + return nil +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..066233f --- /dev/null +++ b/config_test.go @@ -0,0 +1,32 @@ +package main + +import ( + "testing" + + yaml "gopkg.in/yaml.v3" +) + +func Test_Config_Empty(t *testing.T) { + var config Config + err := config.LoadBytes( + []byte(""), + ) + if err != nil { + t.Fatal("unexpected error", err) + } + if config.Command != "" { + t.Fatalf("wrong Command value '%s'", config.Command) + } +} + +func Test_Config_Command(t *testing.T) { + var config Config + err := yaml.Unmarshal([]byte(`command: hi`), &config) + // err := config.LoadString(`command: hi`) + if err != nil { + t.Fatal("unexpected error", err) + } + if config.Command != "hi" { + t.Fatalf("wrong command value '%s'", config.Command) + } +} diff --git a/go.mod b/go.mod index 7817a2e..340f51a 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,10 @@ module github.com/zimbatm/socketmaster go 1.17 -require google.golang.org/grpc v1.45.0 +require ( + google.golang.org/grpc v1.45.0 + gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 +) require ( github.com/golang/protobuf v1.5.2 // indirect diff --git a/go.sum b/go.sum index 721297c..dcd2990 100644 --- a/go.sum +++ b/go.sum @@ -112,9 +112,12 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= +gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/inputs.go b/inputs.go new file mode 100644 index 0000000..66a2ee0 --- /dev/null +++ b/inputs.go @@ -0,0 +1,60 @@ +package main + +import ( + "errors" + "flag" + "fmt" +) + +type Inputs struct { + commandlineConfig Config + + configFile string + + addr string + startTime int + useSyslog bool + username string +} + +func ParseInputs(args []string) (*Inputs, error) { + var inputs Inputs + flags := flag.NewFlagSet("socketmaster", flag.ExitOnError) + + inputs.commandlineConfig.LinkToFlagSet(flags) + flags.StringVar(&inputs.configFile, "config-file", "", "Configuration file to load and watch") + flags.StringVar(&inputs.addr, "listen", "tcp://:8080", "Port on which to bind") + flags.IntVar(&inputs.startTime, "start", 3000, "How long the new process takes to boot in millis") + flags.BoolVar(&inputs.useSyslog, "syslog", false, "Log to syslog") + flags.StringVar(&inputs.username, "user", "", "run the command as this user") + + err := flags.Parse(args) + if err != nil { + return nil, err + } + + return &inputs, err +} + +func (inputs *Inputs) LoadConfig() (*Config, error) { + + if inputs.configFile == "" { + return &inputs.commandlineConfig, nil + } + + config := inputs.commandlineConfig + + var fileConfig Config + err := fileConfig.LoadFile(inputs.configFile) + if err != nil { + return nil, err + } + + err = config.Merge(fileConfig) + if err != nil { + return nil, errors.New(fmt.Sprintf( + "Between the command line and the config file '%s', %s", inputs.configFile, err.Error())) + } + + return &config, nil +} diff --git a/inputs_test.go b/inputs_test.go new file mode 100644 index 0000000..7f05ded --- /dev/null +++ b/inputs_test.go @@ -0,0 +1,56 @@ +package main + +import ( + "testing" +) + +func Test_ParseInputs_Empty(t *testing.T) { + inputs, err := ParseInputs([]string{}) + if err != nil { + t.Fatal("unexpected error", err) + } + if inputs.commandlineConfig.Command != "" { + t.Fatalf("wrong command value '%s'", inputs.commandlineConfig.Command) + } + if inputs.configFile != "" { + t.Fatalf("wrong configFile value '%s'", inputs.configFile) + } +} + +func Test_ParseInputs_Command(t *testing.T) { + inputs, err := ParseInputs([]string{"--command", "hello"}) + if err != nil { + t.Fatal("unexpected error", err) + } + if inputs.commandlineConfig.Command != "hello" { + t.Fatalf("wrong command value '%s'", inputs.commandlineConfig.Command) + } +} + +func Test_ParseInputs_ConfigFile(t *testing.T) { + inputs, err := ParseInputs([]string{"--config-file", "testdata/command.yaml"}) + if err != nil { + t.Fatal("unexpected error", err) + } + if inputs.configFile != "testdata/command.yaml" { + t.Fatalf("wrong command value '%s'", inputs.commandlineConfig.Command) + } +} + +func Test_ParseInputs_ConfigFile_Command_Error(t *testing.T) { + inputs, err := ParseInputs([]string{"--config-file", "testdata/command.yaml", "--command", "would-be-forgotten"}) + if err != nil { + t.Fatal("unexpected error", err) + } + + unused, err := inputs.LoadConfig() + + if unused != nil { + t.Fatal("LoadConfig must fail") + } + + if err.Error() != "Between the command line and the config file 'testdata/command.yaml', command can only be set once." { + t.Fatalf("Wrong error, was '%s'", err.Error()) + } + +} diff --git a/nix/package.nix b/nix/package.nix index e0d2ead..84fd3a5 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -9,10 +9,11 @@ buildGoModule { lib.hasSuffix ".go" path || lib.hasSuffix "/go.mod" path || lib.hasSuffix "/go.sum" path + || lib.hasPrefix (toString ../testdata) (toString path) ); }; - vendorSha256 = "sha256-bPIzk+g8lxntEREAp1ZJiHTGxXFRybtZjhR23uGIFR4="; + vendorSha256 = "sha256-dlDSa6UT3c/sLzPYgWnBt4PINk7JhTlC6ZFMKXBkUGw="; meta = with lib; { description = "Restart services without losing connections"; diff --git a/socketmaster.go b/socketmaster.go index cd10c1f..9498ce8 100644 --- a/socketmaster.go +++ b/socketmaster.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "log" "log/syslog" @@ -71,21 +70,22 @@ func main() { setLISTEN_PIDHelper() } - var ( - addr string - command string - err error - startTime int - useSyslog bool - username string - ) - - flag.StringVar(&command, "command", "", "Program to start") - flag.StringVar(&addr, "listen", "tcp://:8080", "Port on which to bind") - flag.IntVar(&startTime, "start", 3000, "How long the new process takes to boot in millis") - flag.BoolVar(&useSyslog, "syslog", false, "Log to syslog") - flag.StringVar(&username, "user", "", "run the command as this user") - flag.Parse() + inputs, err := ParseInputs(os.Args[1:]) + if err != nil { + log.Fatalf("Options not valid: %s\n", err) + } + + var config *Config + config, err = inputs.LoadConfig() + if err != nil { + log.Fatalf("Could not load config file: %s\n", err) + } + + useSyslog := inputs.useSyslog + command := config.Command + addr := inputs.addr + username := inputs.username + startTime := inputs.startTime if useSyslog { stream, err := syslog.New(syslog.LOG_INFO, PROGRAM_NAME) diff --git a/testdata/command.yaml b/testdata/command.yaml new file mode 100644 index 0000000..bcabfe8 --- /dev/null +++ b/testdata/command.yaml @@ -0,0 +1 @@ +command: the-command From 22e9a175ad5f467f57a98ca07676c64bde42e8a0 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 15:14:19 +0200 Subject: [PATCH 08/15] nixos: Use config file --- nix/nixos/module.nix | 4 +++ nix/nixos/socketmaster-service.nix | 45 ++++++++++++++++++++++++------ nix/nixos/test.nix | 2 +- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/nix/nixos/module.nix b/nix/nixos/module.nix index 09b29fc..45b9f8f 100644 --- a/nix/nixos/module.nix +++ b/nix/nixos/module.nix @@ -2,8 +2,10 @@ let inherit (lib) mapAttrs + attrValues mkIf mkOption + mkMerge types ; inherit (lib.types) @@ -37,11 +39,13 @@ in type = attrsOf (submoduleWith { modules = [ ./socketmaster-service.nix ]; specialArgs.systemConfig = config; + specialArgs.pkgs = pkgs; }); default = { }; }; }; config = mkIf (cfg.services != { }) { systemd.services = mapAttrs (k: v: v.systemdServiceModule) cfg.services; + environment = mkMerge (attrValues (mapAttrs (k: v: v.environmentConfig) cfg.services)); }; } diff --git a/nix/nixos/socketmaster-service.nix b/nix/nixos/socketmaster-service.nix index 6435064..18a96bf 100644 --- a/nix/nixos/socketmaster-service.nix +++ b/nix/nixos/socketmaster-service.nix @@ -1,4 +1,4 @@ -{ config, lib, systemConfig, ... }: +{ config, lib, name, pkgs, systemConfig, ... }: let inherit (lib) mkOption @@ -7,16 +7,34 @@ let inherit (types) str ; + + etcPath = "/etc/${etcSubPath}"; + etcSubPath = "socketmaster/services/${name}.yaml"; + + format = pkgs.formats.yaml { }; + + settingsModule = { + # Forward compatibility, supporting newer `package`'s config items. + freeformType = format.type; + + options = { + command = mkOption { + description = '' + Program to start + ''; + type = str; + # no default + }; + + }; + }; + in { options = { - command = mkOption { - description = '' - Program to start - ''; - type = str; - # no default + settings = mkOption { + type = types.submodule settingsModule; }; startMillis = mkOption { @@ -33,6 +51,12 @@ in # https://github.com/NixOS/nixpkgs/pull/163617 type = types.deferredModule or types.raw or types.unspecified; }; + + environmentConfig = mkOption { + internal = true; + # https://github.com/NixOS/nixpkgs/pull/163617 + type = types.deferredModule or types.raw or types.unspecified; + }; }; config = { @@ -40,8 +64,13 @@ in # Avoid automatic restarts. This will trigger a reload instead. reloadIfChanged = true; serviceConfig.ExecStart = [ - "${systemConfig.socketmaster.package}/bin/socketmaster -command ${lib.escapeShellArg config.command} -start ${toString config.startMillis} -listen fd://3" + "${systemConfig.socketmaster.package}/bin/socketmaster -config-file ${lib.escapeShellArg etcPath} -start ${toString config.startMillis} -listen fd://3" ]; }; + environmentConfig = { + etc."${etcSubPath}" = { + source = format.generate "socketmaster-service-${name}.yaml" config.settings; + }; + }; }; } diff --git a/nix/nixos/test.nix b/nix/nixos/test.nix index b1cf11b..2f94118 100644 --- a/nix/nixos/test.nix +++ b/nix/nixos/test.nix @@ -24,7 +24,7 @@ nixosTest { }; socketmaster.services.socket-server = { - command = "${socket-server}/bin/socket-server"; + settings.command = "${socket-server}/bin/socket-server"; }; }; }; From d75818b4dd16b05b04ab77839164ca3c9027ed58 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 15:19:02 +0200 Subject: [PATCH 09/15] -config-file help: No plan to watch it, but rely on signal --- inputs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inputs.go b/inputs.go index 66a2ee0..5dff18b 100644 --- a/inputs.go +++ b/inputs.go @@ -22,7 +22,7 @@ func ParseInputs(args []string) (*Inputs, error) { flags := flag.NewFlagSet("socketmaster", flag.ExitOnError) inputs.commandlineConfig.LinkToFlagSet(flags) - flags.StringVar(&inputs.configFile, "config-file", "", "Configuration file to load and watch") + flags.StringVar(&inputs.configFile, "config-file", "", "Configuration file to load on start and reload") flags.StringVar(&inputs.addr, "listen", "tcp://:8080", "Port on which to bind") flags.IntVar(&inputs.startTime, "start", 3000, "How long the new process takes to boot in millis") flags.BoolVar(&inputs.useSyslog, "syslog", false, "Log to syslog") From 5c887572769be3249dda9bda264632bf26d08d8a Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 15:47:04 +0200 Subject: [PATCH 10/15] Load config before each exec --- process_group.go | 32 +++++++++++++++++++++++--------- socketmaster.go | 8 +------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/process_group.go b/process_group.go index 634d472..b52a359 100644 --- a/process_group.go +++ b/process_group.go @@ -2,9 +2,12 @@ package main import ( "bufio" + "errors" "flag" + "fmt" "log" "os" + "os/exec" "os/user" "strconv" "sync" @@ -15,9 +18,9 @@ type ProcessGroup struct { set *processSet wg sync.WaitGroup - commandPath string - sockfile *os.File - user *user.User + inputs Inputs + sockfile *os.File + user *user.User } type processSet struct { @@ -25,12 +28,12 @@ type processSet struct { set map[*os.Process]bool } -func MakeProcessGroup(commandPath string, sockfile *os.File, u *user.User) *ProcessGroup { +func MakeProcessGroup(inputs Inputs, sockfile *os.File, u *user.User) *ProcessGroup { return &ProcessGroup{ - set: newProcessSet(), - commandPath: commandPath, - sockfile: sockfile, - user: u, + set: newProcessSet(), + inputs: inputs, + sockfile: sockfile, + user: u, } } @@ -71,7 +74,18 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { } } - args := append([]string{LISTEN_PID_HELPER_ARGV0}, self.commandPath) + var commandPath string + config, err := self.inputs.LoadConfig() + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not load config '%s'", err)) + } + + commandPath, err = exec.LookPath(config.Command) + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not find executable '%s'", err)) + } + + args := append([]string{LISTEN_PID_HELPER_ARGV0}, commandPath) args = append(args, flag.Args()...) log.Println("Starting", args[1:]) process, err = os.StartProcess(os.Args[0], args, procAttr) diff --git a/socketmaster.go b/socketmaster.go index 9498ce8..b87d49d 100644 --- a/socketmaster.go +++ b/socketmaster.go @@ -5,7 +5,6 @@ import ( "log" "log/syslog" "os" - "os/exec" "os/signal" "os/user" "syscall" @@ -105,11 +104,6 @@ func main() { log.Fatalln("Command path is mandatory") } - commandPath, err := exec.LookPath(command) - if err != nil { - log.Fatalln("Could not find executable", err) - } - log.Println("Listening on", addr) sockfile, err := ListenFile(addr) if err != nil { @@ -125,7 +119,7 @@ func main() { } // Run the first process - processGroup := MakeProcessGroup(commandPath, sockfile, targetUser) + processGroup := MakeProcessGroup(*inputs, sockfile, targetUser) _, err = processGroup.StartProcess() if err != nil { log.Fatalln("Could not start process", err) From 3a6a382c18b79ac1dd2e00ff48b3079c43cb5408 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 17:45:15 +0200 Subject: [PATCH 11/15] Manipulate environment with map --- environment.go | 33 +++++++++++++++++++++++++++++++++ environment_test.go | 23 +++++++++++++++++++++++ process_group.go | 15 +++++++-------- 3 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 environment.go create mode 100644 environment_test.go diff --git a/environment.go b/environment.go new file mode 100644 index 0000000..af92c9f --- /dev/null +++ b/environment.go @@ -0,0 +1,33 @@ +package main + +import "strings" + +func EnvironmentToMap(envStrings []string) map[string]string { + envMap := make(map[string]string) + for _, envLine := range envStrings { + + // Get the variable name + var name string + + var silly = strings.Split(envLine, "=") + if len(silly) == 0 { + // pathological, skip + continue + } + + name = silly[0] + + value := envLine[len(name)+1:] + + envMap[name] = value + } + return envMap +} + +func MapToEnvironment(envMap map[string]string) []string { + var envStrings []string + for k, v := range envMap { + envStrings = append(envStrings, k+"="+v) + } + return envStrings +} diff --git a/environment_test.go b/environment_test.go new file mode 100644 index 0000000..f39f75e --- /dev/null +++ b/environment_test.go @@ -0,0 +1,23 @@ +package main + +import "testing" + +func Test_EnvironmentToMap(t *testing.T) { + m := EnvironmentToMap([]string{"a=b=c"}) + if len(m) != 1 { + t.Fatal("len") + } + if m["a"] != "b=c" { + t.Fatal("a != b=c") + } +} + +func Test_MapToEnvironment(t *testing.T) { + m := MapToEnvironment(map[string]string{"a": "b=c"}) + if len(m) != 1 { + t.Fatal("len") + } + if m[0] != "a=b=c" { + t.Fatal("a=b=c") + } +} diff --git a/process_group.go b/process_group.go index b52a359..53298ce 100644 --- a/process_group.go +++ b/process_group.go @@ -47,16 +47,15 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { // Make sure parent values don't interfere with our child. // All fds have already been consumed at this stage. - os.Unsetenv("EINHORN_FDS") - os.Unsetenv("LISTEN_FDS") - os.Unsetenv("LISTEN_FDNAMES") os.Unsetenv("LISTEN_PID") - env := append(os.Environ(), - "EINHORN_FDS=3", - "LISTEN_FDS=1", - "LISTEN_FDNAMES=socket", - ) + envMap := EnvironmentToMap(os.Environ()) + + envMap["EINHORN_FDS"] = "3" + envMap["LISTEN_FDS"] = "1" + envMap["LISTEN_FDNAMES"] = "socket" + + env := MapToEnvironment(envMap) procAttr := &os.ProcAttr{ Env: env, From bbf94bac8817cd97b2bce94278ecdc80e5204969 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 19:09:35 +0200 Subject: [PATCH 12/15] Get environment from config file --- config.go | 15 +++++++++++++-- config_test.go | 24 ++++++++++++++++++++++++ process_group.go | 13 +++++++++---- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/config.go b/config.go index 144d970..7b51cfa 100644 --- a/config.go +++ b/config.go @@ -18,12 +18,14 @@ import ( // The mutable configuration items type Config struct { - Command string `yaml:"command"` + Command string `yaml:"command"` + Environment map[string]string `yaml:"environment"` } func emptyConfig() Config { return Config{ - Command: "", + Command: "", + Environment: make(map[string]string), } } @@ -59,5 +61,14 @@ func (config *Config) Merge(other Config) error { } } + if len(other.Environment) != 0 { + if len(config.Environment) == 0 { + config.Environment = other.Environment + } else { + // Can't be set via args, so impossible + return errors.New("environment can only be set once.") + } + } + return nil } diff --git a/config_test.go b/config_test.go index 066233f..2c5d7c9 100644 --- a/config_test.go +++ b/config_test.go @@ -30,3 +30,27 @@ func Test_Config_Command(t *testing.T) { t.Fatalf("wrong command value '%s'", config.Command) } } + +// environment +func Test_Config_Environment(t *testing.T) { + var config Config + err := yaml.Unmarshal([]byte("command: hi\nenvironment:\n hello: world"), &config) + // err := config.LoadString(`command: hi`) + if err != nil { + t.Fatal("unexpected error", err) + } + if config.Environment["hello"] != "world" { + t.Fatalf("wrong environment.hello value '%s'", config.Environment["hello"]) + } +} + +func Test_Config_Merge_Environment(t *testing.T) { + a := Config{Environment: map[string]string{}} + b := Config{Environment: map[string]string{"foo": "bar"}} + + a.Merge(b) + + if a.Environment["foo"] != "bar" { + t.Fatal("foo isn't bar") + } +} diff --git a/process_group.go b/process_group.go index 53298ce..1688848 100644 --- a/process_group.go +++ b/process_group.go @@ -45,6 +45,11 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { return nil, err } + config, err := self.inputs.LoadConfig() + if err != nil { + return nil, errors.New(fmt.Sprintf("Could not load config '%s'", err)) + } + // Make sure parent values don't interfere with our child. // All fds have already been consumed at this stage. os.Unsetenv("LISTEN_PID") @@ -55,6 +60,10 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { envMap["LISTEN_FDS"] = "1" envMap["LISTEN_FDNAMES"] = "socket" + for key, value := range config.Environment { + envMap[key] = value + } + env := MapToEnvironment(envMap) procAttr := &os.ProcAttr{ @@ -74,10 +83,6 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { } var commandPath string - config, err := self.inputs.LoadConfig() - if err != nil { - return nil, errors.New(fmt.Sprintf("Could not load config '%s'", err)) - } commandPath, err = exec.LookPath(config.Command) if err != nil { From f5eb94d4f9af855e2d5e9ee68d4d9d2566114384 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 19:10:00 +0200 Subject: [PATCH 13/15] Add config_test.go --- config_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/config_test.go b/config_test.go index 2c5d7c9..cc54b7f 100644 --- a/config_test.go +++ b/config_test.go @@ -19,6 +19,22 @@ func Test_Config_Empty(t *testing.T) { } } +// func Test_Config_Marshal(t *testing.T) { +// var config Config +// config.Command = "hi" +// config.Environment = make(map[string]string) +// config.Environment["greeting"] = "hello" + +// out, err := yaml.Marshal(config) + +// if err != nil { +// t.Fatal("unexpected error", err) +// } + +// fmt.Println(string(out)) +// t.Fatal("???") +// } + func Test_Config_Command(t *testing.T) { var config Config err := yaml.Unmarshal([]byte(`command: hi`), &config) From f38dfa450716cc21b38f79bdf510f52c40f00527 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 19 May 2022 19:12:47 +0200 Subject: [PATCH 14/15] Test command and environment update --- nix/nixos/socketmaster-service.nix | 12 +++- nix/nixos/test.nix | 104 ++++++++++++++++++++++++++--- 2 files changed, 103 insertions(+), 13 deletions(-) diff --git a/nix/nixos/socketmaster-service.nix b/nix/nixos/socketmaster-service.nix index 18a96bf..cfbcdba 100644 --- a/nix/nixos/socketmaster-service.nix +++ b/nix/nixos/socketmaster-service.nix @@ -29,6 +29,8 @@ let }; }; + configFile = format.generate "socketmaster-service-${name}.yaml" config.settings; + in { options = { @@ -61,15 +63,19 @@ in config = { systemdServiceModule = { ... }: { - # Avoid automatic restarts. This will trigger a reload instead. - reloadIfChanged = true; + # Avoid automatic restarts. + stopIfChanged = false; + restartIfChanged = false; + # Reload when the config changes. + reloadTriggers = [ configFile ]; + reload = "kill -HUP $MAINPID"; serviceConfig.ExecStart = [ "${systemConfig.socketmaster.package}/bin/socketmaster -config-file ${lib.escapeShellArg etcPath} -start ${toString config.startMillis} -listen fd://3" ]; }; environmentConfig = { etc."${etcSubPath}" = { - source = format.generate "socketmaster-service-${name}.yaml" config.settings; + source = configFile; }; }; }; diff --git a/nix/nixos/test.nix b/nix/nixos/test.nix index 2f94118..1f0a856 100644 --- a/nix/nixos/test.nix +++ b/nix/nixos/test.nix @@ -5,7 +5,7 @@ let in nixosTest { - nodes.machine = { ... }: { + nodes.machine = { config, lib, ... }: { imports = extraModules; config = { environment.systemPackages = [ @@ -18,25 +18,109 @@ nixosTest { }; systemd.services.socket-server = { wantedBy = [ "multi-user.target" ]; - # TODO: this will have to move to the socketmaster config - environment.ECHO_VALUE = "v1"; - # serviceConfig.ExecStart = "${socket-server}/bin/socket-server"; }; socketmaster.services.socket-server = { - settings.command = "${socket-server}/bin/socket-server"; + settings.command = lib.mkDefault "${socket-server}/bin/socket-server"; + settings.environment.ECHO_VALUE = lib.mkDefault "v1"; }; + + specialisation.system-v2 = { + inheritParentConfig = true; + configuration = { config, lib, pkgs, ... }: { + + socketmaster.services.socket-server = { + # Require both environment and command replacement to set it to "v2" + settings.environment.ECHO_VALUE = "v2-please"; + settings.command = "${pkgs.writeScript "socket-server-wrapped" '' + #!${pkgs.runtimeShell} + ECHO_VALUE=''${ECHO_VALUE/-please/} + exec ${socket-server}/bin/socket-server + ''}"; + }; + + }; + }; + + # The activation script runs after outdated services were stopped, but + # before the updated ones start. + system.activationScripts.etc.deps = [ "check-socket" ]; # run before etc + system.activationScripts.check-socket.text = '' + # # check that the service has been stopped + # ${config.systemd.package}/bin/systemctl status socket-server.service >/dev/console + # r=$? + # if [[ $r != 3 ]]; then + # echo >&2 systemctl returned unexpected exit code $r >/dev/console + # exit 1 + # fi + + # # The above trips the activation script error checker. Undo that. + # _localstatus=0 + # _status=0 + + # detach a client + ( (echo '{"cmd":"echo"}'; while ! [[ -e /tmp/client.stop ]]; do sleep 0.1; done; echo '{"cmd":"echo"}') \ + | ${pkgs.socat}/bin/socat - TCP4:localhost:2022 \ + | tee /tmp/client.out \ + | while read ln; do + echo "client got response: $ln" + done >/dev/console + ) /tmp/client.out 2>/tmp/client.err & + clientpid=$! + echo $clientpid >/tmp/client.pid + disown %% + + # delay to make sure client starts while service was down + sleep 1 + + # Fail fast if something is wrong with the client + if ! [[ -d /proc/$clientpid ]]; then + echo >/dev/console "client exited unexpectedly. It shouldn't be able to complete while the service is not running." + exit 1 + fi + ''; + }; }; testScript = '' machine.wait_for_unit("sockets.target") + + with subtest("Service v1 works"): + machine.succeed(""" + echo '{"cmd":"echo"}' \ + | socat - TCP4:localhost:2022 \ + | grep -E '^"v1"$' + """) + machine.succeed(""" - echo '{"cmd":"echo"}' \ - | socat - TCP4:localhost:2022 \ - | grep -E '^"v1"$' + /run/booted-system/specialisation/system-v2/bin/switch-to-configuration test """) - machine.wait_for_unit("socket-server.service") - machine.succeed("sleep 3") + # Wait for the configured startup time to elapse + machine.succeed("sleep 4") + + with subtest("Service v2 works"): + machine.succeed(""" + echo '{"cmd":"echo"}' \ + | socat - TCP4:localhost:2022 \ + | tee /dev/console \ + | grep -E '^"v2"$' + """) + + with subtest("Service v1 still serves a client that connected during NixOS switching"): + machine.succeed(""" + ( + touch /tmp/client.stop + while kill -0 "$(cat /tmp/client.pid)"; do + echo Waiting for client to exit + sleep 0.1; + done + echo out: + cat /tmp/client.out + echo err: + cat /tmp/client.err + ) >/dev/console + """) + ''; } From 21b0caca823a404bc516b2b2edbd63f00503e8a5 Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Tue, 31 May 2022 20:57:37 +0200 Subject: [PATCH 15/15] WIP process state --- process_group.go | 109 +++++++++++++++++++++++++++++++++++++++-------- socketmaster.go | 3 +- 2 files changed, 94 insertions(+), 18 deletions(-) diff --git a/process_group.go b/process_group.go index 1688848..b553380 100644 --- a/process_group.go +++ b/process_group.go @@ -18,22 +18,75 @@ type ProcessGroup struct { set *processSet wg sync.WaitGroup + // For tracking which processes run an up to date config. + // Incremented on SIGHUP. + generation int + inputs Inputs sockfile *os.File user *user.User } +// Below is the process life cycle state machine. +// +// It is not just descriptive, but prescriptive, as unexpected interleavings +// of events must not put socketmaster in an unexpected state. +// +// From \ To | Starting Operational Yielding Yielded Stopping Gone +// ------------+---------------------------------------------------------------------------------- +// Starting | id happy early stop startup-fail +// Operational | id happy shutdown operational-fail +// Yielding | id happy yield-timeout operational-fail +// Yielded | id last-call exit a or operational-fail +// Stopping | id exit b or killed or operational-fail +// Gone | id +// +// id: identity aka no-op +// happy: the usual path +// early stop: startup took too long and/or socketmaster wasn't notified that the process was ready +// shutdown: socketmaster is shutting down, not doing a hot reload +// yield-timeout: if the process doesn't respond to the request to yield, it does not know how to yield, or is defunct, so we stop and kill +// last-call: at some point we can't allow the old process to remain, so we stop and kill +// exit a: voluntary exit after being asked to yield +// exit b: graceful exit after being asked to yield and asked to stop +// *-fail: what it says on the tin +// +// Note that the lower triangle is empty, so the state machine has no cycles +// besides id. "Cyclical" behavior only manifests at a higher level, as new +// processes replace old ones. +type ProcessLifecycleState int + +const ( + Starting ProcessLifecycleState = iota + Operational // Accepting connections and handling existing connections + Yielding // Operational, should stop accepting connections + Yielded // Only handling existing connections + Stopping // releasing resources, cleaning up + Gone +) + +type ProcessState struct { + sync.Mutex + generation int + lifecycleState ProcessLifecycleState +} + +func (self *ProcessState) CanStop() bool { + return self.lifecycleState == Operational || self.lifecycleState == Stopping +} + type processSet struct { sync.Mutex - set map[*os.Process]bool + set map[*os.Process]ProcessState } func MakeProcessGroup(inputs Inputs, sockfile *os.File, u *user.User) *ProcessGroup { return &ProcessGroup{ - set: newProcessSet(), - inputs: inputs, - sockfile: sockfile, - user: u, + set: newProcessSet(), + inputs: inputs, + sockfile: sockfile, + user: u, + generation: 0, } } @@ -97,34 +150,56 @@ func (self *ProcessGroup) StartProcess() (process *os.Process, err error) { return } + state := ProcessState{ + generation: self.generation, + lifecycleState: Starting, + } + // Add to set - self.set.Add(process) + self.set.Add(process, state) // Prefix stdout and stderr lines with the [pid] and send it to the log logOutput(ioReader, process.Pid, &self.wg) // Handle the process death go func() { - state, err := process.Wait() + osProcState, err := process.Wait() - log.Println(process.Pid, state, err) + log.Println(process.Pid, osProcState, err) // Remove from set + self.set.Lock() self.set.Remove(process) + self.set.Unlock() // Process is gone ioReader.Close() self.wg.Done() + + state.Lock() + state.lifecycleState = Gone + state.Unlock() }() return } -func (self *ProcessGroup) SignalAll(signal os.Signal, except *os.Process) { - self.set.Each(func(process *os.Process) { - if process != except { +func (self *ProcessGroup) SignalAll(signal os.Signal, maxGeneration int) { + self.set.Each(func(process *os.Process, state ProcessState) { + if state.generation <= maxGeneration { + process.Signal(signal) + } + }) +} + +func (self *ProcessGroup) TerminateAll(signal os.Signal, maxGeneration int) { + self.set.Each(func(process *os.Process, state ProcessState) { + state.Lock() + if state.generation <= maxGeneration && state.CanStop() { process.Signal(signal) + state.lifecycleState = Stopping } + state.Unlock() }) } @@ -135,23 +210,23 @@ func (self *ProcessGroup) WaitAll() { // A thread-safe process set func newProcessSet() *processSet { set := new(processSet) - set.set = make(map[*os.Process]bool) + set.set = make(map[*os.Process]ProcessState) return set } -func (self *processSet) Add(process *os.Process) { +func (self *processSet) Add(process *os.Process, state ProcessState) { self.Lock() defer self.Unlock() - self.set[process] = true + self.set[process] = state } -func (self *processSet) Each(fn func(*os.Process)) { +func (self *processSet) Each(fn func(*os.Process, ProcessState)) { self.Lock() defer self.Unlock() - for process, _ := range self.set { - fn(process) + for process, state := range self.set { + fn(process, state) } } diff --git a/socketmaster.go b/socketmaster.go index b87d49d..bbf579e 100644 --- a/socketmaster.go +++ b/socketmaster.go @@ -23,6 +23,7 @@ func handleSignals(processGroup *ProcessGroup, c <-chan os.Signal, startTime int socketMasterFdEnvVar := fmt.Sprintf("SOCKETMASTER_FD=%d", sockfile.Fd()) syscall.Exec(os.Args[0], os.Args, append(os.Environ(), socketMasterFdEnvVar)) case syscall.SIGHUP: + oldGeneration := processGroup.generation process, err := processGroup.StartProcess() if err != nil { log.Printf("Could not start new process: %v\n", err) @@ -32,7 +33,7 @@ func handleSignals(processGroup *ProcessGroup, c <-chan os.Signal, startTime int } if processGroup.set.Len() > 1 { - processGroup.SignalAll(syscall.SIGTERM, process) + processGroup.SignalAll(syscall.SIGTERM, oldGeneration) } else { log.Println("Failed to kill old process, because there's no one left in the group") }