A very experimental PLC implementation which uses BFT consensus for decentralization
19
fork

Configure Feed

Select the types of activity you want to include in your feed.

Continue implementing PLC HTTP server

gbl08ma f80970e7 8d399dc3

+235 -75
+3 -4
abciapp/app.go
··· 79 79 80 80 d.plc = plc.NewPLC(d) 81 81 82 - fmt.Println("TREE SIZE", tree.Size()) 82 + lastSnapshotVersion := tree.Version() 83 83 84 84 var wg sync.WaitGroup 85 85 closeCh := make(chan struct{}) 86 86 wg.Go(func() { 87 - lastSnapshotVersion := int64(0) 88 87 for { 89 88 select { 90 89 case <-closeCh: 91 90 return 92 - case <-time.After(1 * time.Minute): 91 + case <-time.After(5 * time.Minute): 93 92 } 94 93 treeVersion := tree.Version() 95 - if lastSnapshotVersion == 0 || treeVersion > int64(lastSnapshotVersion+1000) { 94 + if treeVersion > int64(lastSnapshotVersion+1000) { 96 95 err = d.createSnapshot(treeVersion, filepath.Join(snapshotDirectory, "snapshot.tmp")) 97 96 if err != nil { 98 97 fmt.Println("FAILED TO TAKE SNAPSHOT", stacktrace.Propagate(err, ""))
+27
config.go
··· 1 + package main 2 + 3 + import bftconfig "github.com/cometbft/cometbft/config" 4 + 5 + type UnifiedConfig struct { 6 + *bftconfig.Config `mapstructure:",squash"` 7 + 8 + PLC *PLCConfig `mapstructure:"plc"` 9 + } 10 + 11 + func DefaultConfig() *UnifiedConfig { 12 + return &UnifiedConfig{ 13 + Config: bftconfig.DefaultConfig(), 14 + PLC: DefaultPLCConfig(), 15 + } 16 + } 17 + 18 + type PLCConfig struct { 19 + // Address to listen for incoming connections 20 + ListenAddress string `mapstructure:"laddr"` 21 + } 22 + 23 + func DefaultPLCConfig() *PLCConfig { 24 + return &PLCConfig{ 25 + ListenAddress: "tcp://127.0.0.1:28080", 26 + } 27 + }
+119 -14
httpapi/server.go
··· 5 5 "encoding/json" 6 6 "errors" 7 7 "fmt" 8 + "log" 9 + "net" 8 10 "net/http" 11 + "slices" 12 + "strings" 13 + "sync" 14 + "sync/atomic" 9 15 "time" 10 16 17 + "github.com/bluesky-social/indigo/atproto/atcrypto" 11 18 "github.com/bluesky-social/indigo/atproto/syntax" 12 19 "github.com/cometbft/cometbft/node" 13 20 "github.com/did-method-plc/go-didplc" 14 21 "github.com/google/uuid" 15 22 cbornode "github.com/ipfs/go-ipld-cbor" 23 + "github.com/palantir/stacktrace" 16 24 "github.com/rs/cors" 17 25 18 26 "tangled.org/gbl08ma/didplcbft/abciapp" ··· 24 32 plc plc.ReadPLC 25 33 router *http.ServeMux 26 34 node *node.Node 35 + srv http.Server 27 36 handlerTimeout time.Duration 37 + proto string 38 + addr string 39 + 40 + started atomic.Bool 41 + exitDone sync.WaitGroup 28 42 } 29 43 30 44 // NewServer creates a new instance of the Server. 31 - func NewServer(plc plc.ReadPLC, node *node.Node, handlerTimeout time.Duration) *Server { 45 + func NewServer(plc plc.ReadPLC, node *node.Node, listenAddr string, handlerTimeout time.Duration) (*Server, error) { 32 46 s := &Server{ 33 47 plc: plc, 34 48 router: http.NewServeMux(), 35 49 node: node, 50 + srv: http.Server{Addr: listenAddr}, 36 51 handlerTimeout: handlerTimeout, 37 52 } 38 53 s.setupRoutes() 39 - return s 54 + 55 + handler := cors.Default().Handler(s.router) 56 + 57 + timeoutMsg, _ := json.Marshal(map[string]string{"message": "Internal server timeout"}) 58 + 59 + handler = http.TimeoutHandler(handler, s.handlerTimeout, string(timeoutMsg)) 60 + 61 + s.srv.Handler = handler 62 + 63 + parts := strings.SplitN(listenAddr, "://", 2) 64 + if len(parts) != 2 { 65 + return nil, stacktrace.NewError( 66 + "invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)", 67 + listenAddr, 68 + ) 69 + } 70 + s.proto, s.addr = parts[0], parts[1] 71 + 72 + return s, nil 40 73 } 41 74 42 75 // setupRoutes configures the routes for the server. ··· 57 90 } 58 91 } 59 92 60 - // Serve starts the HTTP server on the specified address. 61 - func (s *Server) Serve(addr string) error { 62 - handler := cors.Default().Handler(s.router) 93 + func (s *Server) Start() error { 94 + if !s.started.CompareAndSwap(false, true) { 95 + return stacktrace.NewError("server already started") 96 + } 97 + 98 + s.exitDone.Add(1) 99 + 100 + listener, err := net.Listen(s.proto, s.addr) 101 + if err != nil { 102 + return stacktrace.Propagate(err, "failed to listen on %v", s.addr) 103 + } 104 + 105 + go func() { 106 + defer s.exitDone.Done() 63 107 64 - timeoutMsg, _ := json.Marshal(map[string]string{"message": "Internal server timeout"}) 108 + if err := s.srv.Serve(listener); !errors.Is(err, http.ErrServerClosed) { 109 + log.Fatalf("ListenAndServe(): %v", err) 110 + } 111 + }() 112 + return nil 113 + } 114 + 115 + func (s *Server) Stop() error { 116 + ctx, cancel := context.WithTimeout(context.Background(), s.handlerTimeout) 117 + defer cancel() 118 + if err := s.srv.Shutdown(ctx); err != nil { 119 + return stacktrace.Propagate(err, "") 120 + } 121 + return nil 122 + } 65 123 66 - handler = http.TimeoutHandler(handler, s.handlerTimeout, string(timeoutMsg)) 67 - return http.ListenAndServe(addr, handler) 124 + func (s *Server) Wait() { 125 + s.exitDone.Wait() 68 126 } 69 127 70 128 // handleResolveDID handles the GET /{did} endpoint. 71 129 func (s *Server) handleResolveDID(w http.ResponseWriter, r *http.Request, did string) { 72 - 73 130 ctx := context.Background() 74 131 doc, err := s.plc.Resolve(ctx, plc.CommittedTreeVersion, did) 75 132 if handlePLCError(w, err, did) { 76 133 return 77 134 } 78 135 136 + wrapper := struct { 137 + Context []string `json:"@context"` 138 + didplc.Doc 139 + }{ 140 + Context: []string{ 141 + "https://www.w3.org/ns/did/v1", 142 + "https://w3id.org/security/multikey/v1", 143 + }, 144 + Doc: doc, 145 + } 146 + 147 + for _, method := range doc.VerificationMethod { 148 + pub, err := atcrypto.ParsePublicMultibase(method.PublicKeyMultibase) 149 + if err != nil { 150 + // not the time to be doubting what the PLC implementation is giving us, just ignore 151 + continue 152 + } 153 + jwk, err := pub.JWK() 154 + if err != nil { 155 + // not the time to be doubting what the PLC implementation is giving us, just ignore 156 + continue 157 + } 158 + addToContext := "" 159 + switch jwk.Curve { 160 + case "P-256": 161 + addToContext = "https://w3id.org/security/suites/ecdsa-2019/v1" 162 + case "secp256k1": 163 + addToContext = "https://w3id.org/security/suites/secp256k1-2019/v1" 164 + default: 165 + continue 166 + } 167 + if !slices.Contains(wrapper.Context, addToContext) { 168 + wrapper.Context = append(wrapper.Context, addToContext) 169 + } 170 + } 171 + 79 172 w.Header().Set("Content-Type", "application/did+ld+json") 80 - json.NewEncoder(w).Encode(doc) 173 + json.NewEncoder(w).Encode(wrapper) 81 174 } 82 175 83 176 // handleCreatePLC handles the POST /{did} endpoint. 84 177 func (s *Server) handleCreatePLC(w http.ResponseWriter, r *http.Request, did string) { 85 - 86 178 var op didplc.OpEnum 87 179 if err := json.NewDecoder(r.Body).Decode(&op); err != nil { 88 180 sendErrorResponse(w, http.StatusBadRequest, "Invalid operation") ··· 143 235 } 144 236 145 237 w.WriteHeader(http.StatusOK) 146 - 147 238 } 148 239 149 240 // handleGetPLCLog handles the GET /{did}/log endpoint. ··· 186 277 return 187 278 } 188 279 280 + resp := struct { 281 + DID string `json:"did"` 282 + RotationKeys []string `json:"rotationKeys"` 283 + VerificationMethods map[string]string `json:"verificationMethods"` 284 + AlsoKnownAs []string `json:"alsoKnownAs"` 285 + Services map[string]didplc.OpService `json:"services"` 286 + }{ 287 + DID: did, 288 + RotationKeys: data.RotationKeys, 289 + VerificationMethods: data.VerificationMethods, 290 + AlsoKnownAs: data.AlsoKnownAs, 291 + Services: data.Services, 292 + } 293 + 189 294 w.Header().Set("Content-Type", "application/json") 190 - json.NewEncoder(w).Encode(data) 295 + json.NewEncoder(w).Encode(resp) 191 296 } 192 297 193 298 // handleExport handles the GET /export endpoint. ··· 215 320 216 321 w.Header().Set("Content-Type", "application/jsonlines") 217 322 for _, entry := range entries { 218 - json.NewEncoder(w).Encode(entry) 323 + json.NewEncoder(w).Encode(&entry) 219 324 } 220 325 } 221 326
+65 -51
httpapi/server_test.go
··· 11 11 "time" 12 12 13 13 "github.com/did-method-plc/go-didplc" 14 - "github.com/stretchr/testify/assert" 14 + "github.com/stretchr/testify/require" 15 15 "tangled.org/gbl08ma/didplcbft/plc" 16 16 ) 17 17 ··· 103 103 mockPLC := &MockReadPLC{} 104 104 105 105 t.Run("Test Resolve DID", func(t *testing.T) { 106 - server := NewServer(mockPLC, nil, 15*time.Second) 106 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 107 + require.NoError(t, err) 107 108 108 109 req, err := http.NewRequest("GET", "/did:plc:test", nil) 109 - assert.NoError(t, err) 110 + require.NoError(t, err) 110 111 111 112 rr := httptest.NewRecorder() 112 113 server.router.ServeHTTP(rr, req) 113 114 114 - assert.Equal(t, http.StatusOK, rr.Code) 115 - assert.Contains(t, rr.Body.String(), "did:plc:test") 115 + require.Equal(t, http.StatusOK, rr.Code) 116 + require.Contains(t, rr.Body.String(), "did:plc:test") 116 117 }) 117 118 118 119 t.Run("Test Resolve DID Not Found", func(t *testing.T) { 119 120 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "notfound"} 120 - server := NewServer(mockPLC, nil, 15*time.Second) 121 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 122 + require.NoError(t, err) 121 123 122 124 req, err := http.NewRequest("GET", "/did:plc:test", nil) 123 - assert.NoError(t, err) 125 + require.NoError(t, err) 124 126 125 127 rr := httptest.NewRecorder() 126 128 server.router.ServeHTTP(rr, req) 127 129 128 - assert.Equal(t, http.StatusNotFound, rr.Code) 129 - assert.Contains(t, rr.Body.String(), "DID not registered: did:plc:test") 130 + require.Equal(t, http.StatusNotFound, rr.Code) 131 + require.Contains(t, rr.Body.String(), "DID not registered: did:plc:test") 130 132 }) 131 133 132 134 t.Run("Test Resolve DID Gone", func(t *testing.T) { 133 135 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "gone"} 134 - server := NewServer(mockPLC, nil, 15*time.Second) 136 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 137 + require.NoError(t, err) 135 138 136 139 req, err := http.NewRequest("GET", "/did:plc:test", nil) 137 - assert.NoError(t, err) 140 + require.NoError(t, err) 138 141 139 142 rr := httptest.NewRecorder() 140 143 server.router.ServeHTTP(rr, req) 141 144 142 - assert.Equal(t, http.StatusGone, rr.Code) 143 - assert.Contains(t, rr.Body.String(), "DID not available: did:plc:test") 145 + require.Equal(t, http.StatusGone, rr.Code) 146 + require.Contains(t, rr.Body.String(), "DID not available: did:plc:test") 144 147 }) 145 148 146 149 t.Run("Test Resolve DID Internal Error", func(t *testing.T) { 147 150 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "internal"} 148 - server := NewServer(mockPLC, nil, 15*time.Second) 151 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 152 + require.NoError(t, err) 149 153 150 154 req, err := http.NewRequest("GET", "/did:plc:test", nil) 151 - assert.NoError(t, err) 155 + require.NoError(t, err) 152 156 153 157 rr := httptest.NewRecorder() 154 158 server.router.ServeHTTP(rr, req) 155 159 156 - assert.Equal(t, http.StatusInternalServerError, rr.Code) 157 - assert.Contains(t, rr.Body.String(), "Internal server error") 160 + require.Equal(t, http.StatusInternalServerError, rr.Code) 161 + require.Contains(t, rr.Body.String(), "Internal server error") 158 162 }) 159 163 160 164 t.Run("Test Create PLC Operation", func(t *testing.T) { 161 - server := NewServer(mockPLC, nil, 15*time.Second) 165 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 166 + require.NoError(t, err) 162 167 163 168 op := map[string]interface{}{ 164 169 "type": "plc_operation", ··· 172 177 opBytes, _ := json.Marshal(op) 173 178 174 179 req, err := http.NewRequest("POST", "/did:plc:test", bytes.NewBuffer(opBytes)) 175 - assert.NoError(t, err) 180 + require.NoError(t, err) 176 181 177 182 rr := httptest.NewRecorder() 178 183 server.router.ServeHTTP(rr, req) 179 184 180 - assert.Equal(t, http.StatusOK, rr.Code) 185 + require.Equal(t, http.StatusOK, rr.Code) 181 186 }) 182 187 183 188 t.Run("Test Get PLC Log", func(t *testing.T) { 184 - server := NewServer(mockPLC, nil, 15*time.Second) 189 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 190 + require.NoError(t, err) 185 191 186 192 req, err := http.NewRequest("GET", "/did:plc:test/log", nil) 187 - assert.NoError(t, err) 193 + require.NoError(t, err) 188 194 189 195 rr := httptest.NewRecorder() 190 196 server.router.ServeHTTP(rr, req) 191 197 192 - assert.Equal(t, http.StatusOK, rr.Code) 198 + require.Equal(t, http.StatusOK, rr.Code) 193 199 }) 194 200 195 201 t.Run("Test Get PLC Log Not Found", func(t *testing.T) { 196 202 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "notfound"} 197 - server := NewServer(mockPLC, nil, 15*time.Second) 203 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 204 + require.NoError(t, err) 198 205 199 206 req, err := http.NewRequest("GET", "/did:plc:test/log", nil) 200 - assert.NoError(t, err) 207 + require.NoError(t, err) 201 208 202 209 rr := httptest.NewRecorder() 203 210 server.router.ServeHTTP(rr, req) 204 211 205 - assert.Equal(t, http.StatusNotFound, rr.Code) 206 - assert.Contains(t, rr.Body.String(), "DID not registered: did:plc:test") 212 + require.Equal(t, http.StatusNotFound, rr.Code) 213 + require.Contains(t, rr.Body.String(), "DID not registered: did:plc:test") 207 214 }) 208 215 209 216 t.Run("Test Get PLC Audit Log", func(t *testing.T) { 210 - server := NewServer(mockPLC, nil, 15*time.Second) 217 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 218 + require.NoError(t, err) 211 219 212 220 req, err := http.NewRequest("GET", "/did:plc:test/log/audit", nil) 213 - assert.NoError(t, err) 221 + require.NoError(t, err) 214 222 215 223 rr := httptest.NewRecorder() 216 224 server.router.ServeHTTP(rr, req) 217 225 218 - assert.Equal(t, http.StatusOK, rr.Code) 226 + require.Equal(t, http.StatusOK, rr.Code) 219 227 }) 220 228 221 229 t.Run("Test Get Last Operation", func(t *testing.T) { 222 - server := NewServer(mockPLC, nil, 15*time.Second) 230 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 231 + require.NoError(t, err) 223 232 224 233 req, err := http.NewRequest("GET", "/did:plc:test/log/last", nil) 225 - assert.NoError(t, err) 234 + require.NoError(t, err) 226 235 227 236 rr := httptest.NewRecorder() 228 237 server.router.ServeHTTP(rr, req) 229 238 230 - assert.Equal(t, http.StatusOK, rr.Code) 239 + require.Equal(t, http.StatusOK, rr.Code) 231 240 }) 232 241 233 242 t.Run("Test Get Last Operation Internal Error", func(t *testing.T) { 234 243 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "internal"} 235 - server := NewServer(mockPLC, nil, 15*time.Second) 244 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 245 + require.NoError(t, err) 236 246 237 247 req, err := http.NewRequest("GET", "/did:plc:test/log/last", nil) 238 - assert.NoError(t, err) 248 + require.NoError(t, err) 239 249 240 250 rr := httptest.NewRecorder() 241 251 server.router.ServeHTTP(rr, req) 242 252 243 - assert.Equal(t, http.StatusInternalServerError, rr.Code) 244 - assert.Contains(t, rr.Body.String(), "Internal server error") 253 + require.Equal(t, http.StatusInternalServerError, rr.Code) 254 + require.Contains(t, rr.Body.String(), "Internal server error") 245 255 }) 246 256 247 257 t.Run("Test Get PLC Data", func(t *testing.T) { 248 - server := NewServer(mockPLC, nil, 15*time.Second) 258 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 259 + require.NoError(t, err) 249 260 250 261 req, err := http.NewRequest("GET", "/did:plc:test/data", nil) 251 - assert.NoError(t, err) 262 + require.NoError(t, err) 252 263 253 264 rr := httptest.NewRecorder() 254 265 server.router.ServeHTTP(rr, req) 255 266 256 - assert.Equal(t, http.StatusOK, rr.Code) 267 + require.Equal(t, http.StatusOK, rr.Code) 257 268 }) 258 269 259 270 t.Run("Test Get PLC Data Not Found", func(t *testing.T) { 260 271 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "notfound"} 261 - server := NewServer(mockPLC, nil, 15*time.Second) 272 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 273 + require.NoError(t, err) 262 274 263 275 req, err := http.NewRequest("GET", "/did:plc:test/data", nil) 264 - assert.NoError(t, err) 276 + require.NoError(t, err) 265 277 266 278 rr := httptest.NewRecorder() 267 279 server.router.ServeHTTP(rr, req) 268 280 269 - assert.Equal(t, http.StatusNotFound, rr.Code) 270 - assert.Contains(t, rr.Body.String(), "DID not registered: did:plc:test") 281 + require.Equal(t, http.StatusNotFound, rr.Code) 282 + require.Contains(t, rr.Body.String(), "DID not registered: did:plc:test") 271 283 }) 272 284 273 285 t.Run("Test Export", func(t *testing.T) { 274 - server := NewServer(mockPLC, nil, 15*time.Second) 286 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 287 + require.NoError(t, err) 275 288 276 289 req, err := http.NewRequest("GET", "/export?count=10", nil) 277 - assert.NoError(t, err) 290 + require.NoError(t, err) 278 291 279 292 rr := httptest.NewRecorder() 280 293 server.router.ServeHTTP(rr, req) 281 294 282 - assert.Equal(t, http.StatusOK, rr.Code) 295 + require.Equal(t, http.StatusOK, rr.Code) 283 296 }) 284 297 285 298 t.Run("Test Export Internal Error", func(t *testing.T) { 286 299 mockPLC := &MockReadPLC{shouldReturnError: true, errorType: "internal"} 287 - server := NewServer(mockPLC, nil, 15*time.Second) 300 + server, err := NewServer(mockPLC, nil, "tcp://127.0.0.1:8080", 15*time.Second) 301 + require.NoError(t, err) 288 302 289 303 req, err := http.NewRequest("GET", "/export?count=10", nil) 290 - assert.NoError(t, err) 304 + require.NoError(t, err) 291 305 292 306 rr := httptest.NewRecorder() 293 307 server.router.ServeHTTP(rr, req) 294 308 295 - assert.Equal(t, http.StatusInternalServerError, rr.Code) 296 - assert.Contains(t, rr.Body.String(), "Internal server error") 309 + require.Equal(t, http.StatusInternalServerError, rr.Code) 310 + require.Contains(t, rr.Body.String(), "Internal server error") 297 311 }) 298 312 }
+21 -6
main.go
··· 16 16 "github.com/cometbft/cometbft/proxy" 17 17 "github.com/samber/lo" 18 18 "tangled.org/gbl08ma/didplcbft/abciapp" 19 + "tangled.org/gbl08ma/didplcbft/httpapi" 19 20 20 21 bftconfig "github.com/cometbft/cometbft/config" 21 22 cmtflags "github.com/cometbft/cometbft/libs/cli/flags" ··· 38 39 homeDir = filepath.Join(lo.Must(os.Getwd()), "didplcbft-data") 39 40 } 40 41 41 - config := bftconfig.DefaultConfig() 42 + config := DefaultConfig() 42 43 config.SetRoot(homeDir) 43 44 viper.SetConfigFile(fmt.Sprintf("%s/%s", homeDir, "config/config.toml")) 44 45 ··· 95 96 } 96 97 defer cleanup() 97 98 98 - _ = plc // TODO 99 - 100 99 pv := privval.LoadFilePV( 101 100 config.PrivValidatorKeyFile(), 102 101 config.PrivValidatorStateFile(), ··· 115 114 } 116 115 117 116 node, err := nm.NewNode( 118 - config, 117 + config.Config, 119 118 pv, 120 119 nodeKey, 121 120 proxy.NewLocalClientCreator(app), 122 - nm.DefaultGenesisDocProviderFunc(config), 121 + nm.DefaultGenesisDocProviderFunc(config.Config), 123 122 bftconfig.DefaultDBProvider, 124 - nm.DefaultMetricsProvider(config.Instrumentation), 123 + nm.DefaultMetricsProvider(config.Config.Instrumentation), 125 124 logger, 126 125 ) 127 126 ··· 137 136 node.Stop() 138 137 node.Wait() 139 138 }() 139 + 140 + if config.PLC.ListenAddress != "" { 141 + plcAPIServer, err := httpapi.NewServer(plc, node, config.PLC.ListenAddress, 15*time.Second) 142 + if err != nil { 143 + log.Fatalf("Creating PLC API server: %v", err) 144 + } 145 + 146 + err = plcAPIServer.Start() 147 + if err != nil { 148 + log.Fatalf("Starting PLC API server: %v", err) 149 + } 150 + defer func() { 151 + plcAPIServer.Stop() 152 + plcAPIServer.Wait() 153 + }() 154 + } 140 155 141 156 c := make(chan os.Signal, 1) 142 157 signal.Notify(c, os.Interrupt, syscall.SIGTERM)