diff --git a/.github/workflows/publish - zen.yml b/.github/workflows/publish - zen.yml index 885a55788..1c57a8562 100644 --- a/.github/workflows/publish - zen.yml +++ b/.github/workflows/publish - zen.yml @@ -58,9 +58,11 @@ jobs: - uses: actions/setup-go@v3 with: go-version: 'stable' - - name: Install dependencies + - name: Setup run: | - sudo apt update && sudo apt install -y gcc-aarch64-linux-gnu + sudo apt update + sudo apt install -y gcc-aarch64-linux-gnu + go generate ./... - name: Build amd64 env: CGO_ENABLED: 1 @@ -125,6 +127,9 @@ jobs: security import $APPLE_CERT_PATH -P $APPLE_CERT_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security find-identity -v $KEYCHAIN_PATH -p codesigning security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $APPLE_KEYCHAIN_PASSWORD $KEYCHAIN_PATH + + # generate + go generate ./... - name: Build amd64 env: APPLE_CERT_ID: ${{ secrets.APPLE_CERT_ID }} @@ -176,6 +181,9 @@ jobs: - uses: actions/setup-go@v3 with: go-version: 'stable' + - name: Setup + shell: bash + run: go generate ./... - name: Build amd64 env: CGO_ENABLED: 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 43407d727..58bdbb5a4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -56,9 +56,11 @@ jobs: - uses: actions/setup-go@v3 with: go-version: 'stable' - - name: Install dependencies + - name: Setup run: | - sudo apt update && sudo apt install -y gcc-aarch64-linux-gnu + sudo apt update + sudo apt install -y gcc-aarch64-linux-gnu + go generate ./... - name: Build amd64 env: CGO_ENABLED: 1 @@ -123,6 +125,9 @@ jobs: security import $APPLE_CERT_PATH -P $APPLE_CERT_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN_PATH security find-identity -v $KEYCHAIN_PATH -p codesigning security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $APPLE_KEYCHAIN_PASSWORD $KEYCHAIN_PATH + + # generate + go generate ./... - name: Build amd64 env: APPLE_CERT_ID: ${{ secrets.APPLE_CERT_ID }} @@ -174,6 +179,10 @@ jobs: - uses: actions/setup-go@v3 with: go-version: 'stable' + - name: Setup + shell: bash + run: | + go generate ./... - name: Build amd64 env: CGO_ENABLED: 1 diff --git a/api/autopilot.go b/api/autopilot.go index 4dc0aecbc..2b8b4a0d1 100644 --- a/api/autopilot.go +++ b/api/autopilot.go @@ -99,6 +99,15 @@ type ( UptimeMS ParamDuration `json:"uptimeMS"` } + // AutopilotStateResponse is the response type for the /autopilot/state + // endpoint. + AutopilotStateResponse struct { + AutopilotStatusResponse // TODO: deprecate /autopilot/status + + StartTime time.Time `json:"startTime"` + BuildState + } + HostHandlerResponseChecks struct { Gouging bool `json:"gouging"` GougingBreakdown HostGougingBreakdown `json:"gougingBreakdown"` diff --git a/api/bus.go b/api/bus.go index f48fe7a79..f2ae9e299 100644 --- a/api/bus.go +++ b/api/bus.go @@ -79,6 +79,12 @@ type AccountHandlerPOST struct { HostKey types.PublicKey `json:"hostKey"` } +// BusStateResponse is the response type for the /bus/state endpoint. +type BusStateResponse struct { + StartTime time.Time `json:"startTime"` + BuildState +} + // ConsensusState holds the current blockheight and whether we are synced or not. type ConsensusState struct { BlockHeight uint64 `json:"blockHeight"` diff --git a/api/state.go b/api/state.go new file mode 100644 index 000000000..6bb58250c --- /dev/null +++ b/api/state.go @@ -0,0 +1,14 @@ +package api + +import "time" + +type ( + // BuildState contains static information about the build. + BuildState struct { + Network string `json:"network"` + Version string `json:"version"` + Commit string `json:"commit"` + OS string `json:"OS"` + BuildTime time.Time `json:"buildTime"` + } +) diff --git a/api/worker.go b/api/worker.go index c67a33c5a..aeddf7d38 100644 --- a/api/worker.go +++ b/api/worker.go @@ -172,6 +172,13 @@ type UploaderStats struct { AvgSectorUploadSpeedMBPS float64 `json:"avgSectorUploadSpeedMBPS"` } +// WorkerStateResponse is the response type for the /worker/state endpoint. +type WorkerStateResponse struct { + ID string `json:"id"` + StartTime time.Time `json:"startTime"` + BuildState +} + // An UploadOption overrides an option on the upload and migrate endpoints in // the worker. type UploadOption func(url.Values) diff --git a/autopilot/autopilot.go b/autopilot/autopilot.go index 748e6bc37..12c2ef5b4 100644 --- a/autopilot/autopilot.go +++ b/autopilot/autopilot.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "runtime" "strings" "sync" "time" @@ -16,6 +17,7 @@ import ( "go.sia.tech/jape" "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" + "go.sia.tech/renterd/build" "go.sia.tech/renterd/hostdb" "go.sia.tech/renterd/object" "go.sia.tech/renterd/tracing" @@ -110,11 +112,11 @@ type Autopilot struct { tickerDuration time.Duration wg sync.WaitGroup - startStopMu sync.Mutex - runningSince time.Time - stopChan chan struct{} - ticker *time.Ticker - triggerChan chan bool + startStopMu sync.Mutex + startTime time.Time + stopChan chan struct{} + ticker *time.Ticker + triggerChan chan bool } // state holds a bunch of variables that are used by the autopilot and updated @@ -171,6 +173,7 @@ func (ap *Autopilot) Handler() http.Handler { "POST /hosts": ap.hostsHandlerPOST, "GET /host/:hostKey": ap.hostHandlerGET, "GET /status": ap.statusHandlerGET, + "GET /state": ap.stateHandlerGET, })) } @@ -180,7 +183,7 @@ func (ap *Autopilot) Run() error { ap.startStopMu.Unlock() return errors.New("already running") } - ap.runningSince = time.Now() + ap.startTime = time.Now() ap.stopChan = make(chan struct{}) ap.triggerChan = make(chan bool) ap.ticker = time.NewTicker(ap.tickerDuration) @@ -296,7 +299,7 @@ func (ap *Autopilot) Shutdown(_ context.Context) error { close(ap.stopChan) close(ap.triggerChan) ap.wg.Wait() - ap.runningSince = time.Time{} + ap.startTime = time.Time{} } return nil } @@ -319,11 +322,17 @@ func (ap *Autopilot) Trigger(forceScan bool) bool { } } +func (ap *Autopilot) StartTime() time.Time { + ap.startStopMu.Lock() + defer ap.startStopMu.Unlock() + return ap.startTime +} + func (ap *Autopilot) Uptime() (dur time.Duration) { ap.startStopMu.Lock() defer ap.startStopMu.Unlock() if ap.isRunning() { - dur = time.Since(ap.runningSince) + dur = time.Since(ap.startTime) } return } @@ -389,7 +398,7 @@ func (ap *Autopilot) blockUntilSynced(interrupt <-chan time.Time) bool { } func (ap *Autopilot) isRunning() bool { - return !ap.runningSince.IsZero() + return !ap.startTime.IsZero() } func (ap *Autopilot) updateState(ctx context.Context) error { @@ -601,6 +610,35 @@ func (ap *Autopilot) statusHandlerGET(jc jape.Context) { }) } +func (ap *Autopilot) stateHandlerGET(jc jape.Context) { + migrating, mLastStart := ap.m.Status() + scanning, sLastStart := ap.s.Status() + _, err := ap.bus.Autopilot(jc.Request.Context(), ap.id) + if err != nil && !strings.Contains(err.Error(), api.ErrAutopilotNotFound.Error()) { + jc.Error(err, http.StatusInternalServerError) + return + } + + jc.Encode(api.AutopilotStateResponse{ + AutopilotStatusResponse: api.AutopilotStatusResponse{ + Configured: err == nil, + Migrating: migrating, + MigratingLastStart: api.ParamTime(mLastStart), + Scanning: scanning, + ScanningLastStart: api.ParamTime(sLastStart), + UptimeMS: api.ParamDuration(ap.Uptime()), + }, + StartTime: ap.StartTime(), + BuildState: api.BuildState{ + Network: build.NetworkName(), + Version: build.Version(), + Commit: build.Commit(), + OS: runtime.GOOS, + BuildTime: build.BuildTime(), + }, + }) +} + func (ap *Autopilot) hostsHandlerPOST(jc jape.Context) { var req api.SearchHostsRequest if jc.Decode(&req) != nil { diff --git a/autopilot/client.go b/autopilot/client.go index b47d3d836..b190c21a1 100644 --- a/autopilot/client.go +++ b/autopilot/client.go @@ -61,6 +61,12 @@ func (c *Client) HostInfos(ctx context.Context, filterMode, usabilityMode string return } +// State returns the current state of the autopilot. +func (c *Client) State() (state api.AutopilotStateResponse, err error) { + err = c.c.GET("/state", &state) + return +} + func (c *Client) Status() (resp api.AutopilotStatusResponse, err error) { err = c.c.GET("/status", &resp) return diff --git a/build/env.go b/build/env_default.go similarity index 95% rename from build/env.go rename to build/env_default.go index 494cd56d3..6ce7d919a 100644 --- a/build/env.go +++ b/build/env_default.go @@ -5,20 +5,16 @@ package build import ( "time" - "go.sia.tech/core/chain" "go.sia.tech/core/types" "go.sia.tech/renterd/api" ) const ( - ConsensusNetworkName = "Mainnet" DefaultAPIAddress = "localhost:9980" DefaultGatewayAddress = ":9981" ) var ( - ConsensusNetwork, _ = chain.Mainnet() - // DefaultGougingSettings define the default gouging settings the bus is // configured with on startup. These values can be adjusted using the // settings API. diff --git a/build/env_testnet.go b/build/env_testnet.go index 0588c2ee8..acd9e86bc 100644 --- a/build/env_testnet.go +++ b/build/env_testnet.go @@ -5,20 +5,16 @@ package build import ( "time" - "go.sia.tech/core/chain" "go.sia.tech/core/types" "go.sia.tech/renterd/api" ) const ( - ConsensusNetworkName = "Testnet-Zen" DefaultAPIAddress = "localhost:9880" DefaultGatewayAddress = ":9881" ) var ( - ConsensusNetwork, _ = chain.TestnetZen() - // DefaultGougingSettings define the default gouging settings the bus is // configured with on startup. These values can be adjusted using the // settings API. diff --git a/build/gen.go b/build/gen.go new file mode 100644 index 000000000..efeeb5c53 --- /dev/null +++ b/build/gen.go @@ -0,0 +1,132 @@ +//go:build ignore + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "os/exec" + "strings" + "text/template" + "time" +) + +const logFormat = `{%n "commit": "%H",%n "shortCommit": "%h",%n "timestamp": "%cD",%n "tag": "%(describe:tags=true)"%n}` + +type ( + gitTime time.Time + + gitMeta struct { + Commit string `json:"commit"` + ShortCommit string `json:"shortCommit"` + Timestamp gitTime `json:"timestamp"` + Tag string `json:"tag"` + } +) + +var buildTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. +// This file was generated by go generate at {{ .RunTime }}. +package build + +import ( + "time" +) + +const ( + commit = "{{ .Commit }}" + version = "{{ .Version }}" + buildTime = {{ .UnixTimestamp }} +) + +// Commit returns the commit hash of hostd +func Commit() string { + return commit +} + +// Version returns the version of hostd +func Version() string { + return version +} + +// BuildTime returns the time at which the binary was built. +func BuildTime() time.Time { + return time.Unix(buildTime, 0) +} +`)) + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (t *gitTime) UnmarshalJSON(buf []byte) error { + timeFormats := []string{ + time.RFC1123Z, + "Mon, 2 Jan 2006 15:04:05 -0700", + "2006-01-02 15:04:05 -0700", + time.UnixDate, + time.ANSIC, + time.RFC3339, + time.RFC1123, + } + + for _, format := range timeFormats { + parsed, err := time.Parse(format, strings.Trim(string(buf), `"`)) + if err == nil { + *t = gitTime(parsed) + return nil + } + } + return errors.New("failed to parse time") +} + +func getGitMeta() (meta gitMeta, _ error) { + cmd := exec.Command("git", "log", "-1", "--pretty=format:"+logFormat+"") + buf, err := cmd.Output() + if err != nil { + if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 { + return gitMeta{}, fmt.Errorf("command failed: %w", errors.New(string(err.Stderr))) + } + return gitMeta{}, fmt.Errorf("failed to execute command: %w", err) + } else if err := json.Unmarshal(buf, &meta); err != nil { + return gitMeta{}, fmt.Errorf("failed to unmarshal json: %w", err) + } + return +} + +func main() { + meta, err := getGitMeta() + if err != nil { + log.Fatalln(err) + } + + commit := meta.ShortCommit + version := meta.Tag + if len(version) == 0 { + // no version, use commit and current time for development + version = commit + meta.Timestamp = gitTime(time.Now()) + } + + f, err := os.Create("meta.go") + if err != nil { + log.Fatalln(err) + } + defer f.Close() + + err = buildTemplate.Execute(f, struct { + Commit string + Version string + UnixTimestamp int64 + + RunTime string + }{ + Commit: commit, + Version: version, + UnixTimestamp: time.Time(meta.Timestamp).Unix(), + + RunTime: time.Now().Format(time.RFC3339), + }) + if err != nil { + log.Fatalln(err) + } +} diff --git a/build/meta.go b/build/meta.go new file mode 100644 index 000000000..690bbd231 --- /dev/null +++ b/build/meta.go @@ -0,0 +1,28 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by go generate at 2023-08-11T11:44:04+02:00. +package build + +import ( + "time" +) + +const ( + commit = "?" + version = "?" + buildTime = 0 +) + +// Commit returns the commit hash of hostd +func Commit() string { + return commit +} + +// Version returns the version of hostd +func Version() string { + return version +} + +// BuildTime returns the time at which the binary was built. +func BuildTime() time.Time { + return time.Unix(buildTime, 0) +} diff --git a/build/network.go b/build/network.go new file mode 100644 index 000000000..0db23554d --- /dev/null +++ b/build/network.go @@ -0,0 +1,26 @@ +package build + +//go:generate go run gen.go + +import ( + "go.sia.tech/core/chain" + "go.sia.tech/core/consensus" + "go.sia.tech/core/types" +) + +// Network returns the Sia network consts and genesis block for the current build. +func Network() (*consensus.Network, types.Block) { + return chain.Mainnet() +} + +func NetworkName() string { + n, _ := Network() + switch n.Name { + case "mainnet": + return "Mainnet" + case "zen": + return "Zen Testnet" + default: + return n.Name + } +} diff --git a/bus/bus.go b/bus/bus.go index e02cdcd73..c087e8aad 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -7,6 +7,7 @@ import ( "fmt" "math" "net/http" + "runtime" "strings" "time" @@ -162,6 +163,8 @@ type bus struct { accounts *accounts contractLocks *contractLocks uploadingSectors *uploadingSectorsCache + + startTime time.Time } func (b *bus) consensusAcceptBlock(jc jape.Context) { @@ -1372,6 +1375,19 @@ func (b *bus) contractTaxHandlerGET(jc jape.Context) { jc.Encode(cs.FileContractTax(types.FileContract{Payout: payout})) } +func (b *bus) stateHandlerGET(jc jape.Context) { + jc.Encode(api.BusStateResponse{ + StartTime: b.startTime, + BuildState: api.BuildState{ + Network: build.NetworkName(), + Version: build.Version(), + Commit: build.Commit(), + OS: runtime.GOOS, + BuildTime: build.BuildTime(), + }, + }) +} + func (b *bus) uploadTrackHandlerPOST(jc jape.Context) { var id api.UploadID if jc.DecodeParam("id", &id) == nil { @@ -1414,6 +1430,8 @@ func New(s Syncer, cm ChainManager, tp TransactionPool, w Wallet, hdb HostDB, as contractLocks: newContractLocks(), uploadingSectors: newUploadingSectorsCache(), logger: l.Sugar().Named("bus"), + + startTime: time.Now(), } ctx, span := tracing.Tracer.Start(context.Background(), "bus.New") defer span.End() @@ -1595,6 +1613,8 @@ func (b *bus) Handler() http.Handler { "PUT /setting/:key": b.settingKeyHandlerPUT, "DELETE /setting/:key": b.settingKeyHandlerDELETE, + "GET /state": b.stateHandlerGET, + "POST /upload/:id": b.uploadTrackHandlerPOST, "POST /upload/:id/sector": b.uploadAddSectorHandlerPOST, "DELETE /upload/:id": b.uploadFinishedHandlerDELETE, diff --git a/bus/client.go b/bus/client.go index 705eb8aa0..9390357f9 100644 --- a/bus/client.go +++ b/bus/client.go @@ -769,6 +769,12 @@ func (c *Client) SlabBuffers() (buffers []api.SlabBuffer, err error) { return } +// State returns the current state of the bus. +func (c *Client) State() (state api.BusStateResponse, err error) { + err = c.c.GET("/state", &state) + return +} + // RenameObject renames a single object. func (c *Client) RenameObject(ctx context.Context, from, to string) (err error) { return c.renameObjects(ctx, from, to, api.ObjectsRenameModeSingle) diff --git a/cmd/renterd/main.go b/cmd/renterd/main.go index cf4415f26..2f129f6f8 100644 --- a/cmd/renterd/main.go +++ b/cmd/renterd/main.go @@ -120,7 +120,7 @@ func main() { apiPassword string node.BusConfig } - busCfg.Network = build.ConsensusNetwork + busCfg.Network, _ = build.Network() var dbCfg struct { uri string @@ -231,7 +231,7 @@ func main() { flag.Parse() log.Println("renterd v0.4.0-beta") - log.Println("Network", build.ConsensusNetworkName) + log.Println("Network", build.NetworkName()) if flag.Arg(0) == "version" { log.Println("Commit:", githash) log.Println("Build Date:", builddate) @@ -420,7 +420,7 @@ func main() { log.Println("Shutting down...") shutdownFns = append(shutdownFns, srv.Shutdown) case err := <-autopilotErr: - log.Fatalln("Fatal autopilot error:", err) + log.Fatal("Fatal autopilot error:", err) } // Shut down the autopilot first, then the rest of the services in reverse order. diff --git a/docker/Dockerfile b/docker/Dockerfile index d4eab6705..242d3d64b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,6 +6,7 @@ WORKDIR /renterd # Copy and build binary. COPY . . +RUN go generate ./... RUN go build ./cmd/renterd # Build image that will be used to run renterd. diff --git a/docker/Dockerfile.testnet b/docker/Dockerfile.testnet index c889fbadc..c75e96e3a 100644 --- a/docker/Dockerfile.testnet +++ b/docker/Dockerfile.testnet @@ -6,6 +6,7 @@ WORKDIR /renterd # Copy and build binary. COPY . . +RUN go generate ./... RUN go build -tags=testnet ./cmd/renterd # Build image that will be used to run renterd. diff --git a/internal/node/node.go b/internal/node/node.go index 2b7af24e4..c058379b0 100644 --- a/internal/node/node.go +++ b/internal/node/node.go @@ -70,7 +70,10 @@ type AutopilotConfig struct { MigratorParallelSlabsPerWorker uint64 } -type ShutdownFn = func(context.Context) error +type ( + RunFn = func() error + ShutdownFn = func(context.Context) error +) func convertToSiad(core types.EncoderTo, siad encoding.SiaUnmarshaler) { var buf bytes.Buffer @@ -315,7 +318,7 @@ func NewWorker(cfg WorkerConfig, b worker.Bus, seed types.PrivateKey, l *zap.Log return w.Handler(), w.Shutdown, nil } -func NewAutopilot(cfg AutopilotConfig, b autopilot.Bus, workers []autopilot.Worker, l *zap.Logger) (http.Handler, func() error, ShutdownFn, error) { +func NewAutopilot(cfg AutopilotConfig, b autopilot.Bus, workers []autopilot.Worker, l *zap.Logger) (http.Handler, RunFn, ShutdownFn, error) { ap, err := autopilot.New(cfg.ID, b, workers, l, cfg.Heartbeat, cfg.ScannerInterval, cfg.ScannerBatchSize, cfg.ScannerMinRecentFailures, cfg.ScannerNumThreads, cfg.MigrationHealthCutoff, cfg.AccountsRefillInterval, cfg.RevisionSubmissionBuffer, cfg.MigratorParallelSlabsPerWorker, cfg.RevisionBroadcastInterval) if err != nil { return nil, nil, nil, err diff --git a/internal/testing/cluster.go b/internal/testing/cluster.go index 95e6eac5f..50a938de1 100644 --- a/internal/testing/cluster.go +++ b/internal/testing/cluster.go @@ -268,7 +268,7 @@ func newTestClusterCustom(dir, dbName string, funding bool, wk types.PrivateKey, busShutdownFns = append(busShutdownFns, bStopFn) // Create worker. - w, wStopFn, err := node.NewWorker(workerCfg, busClient, wk, logger) + w, wShutdownFn, err := node.NewWorker(workerCfg, busClient, wk, logger) if err != nil { return nil, err } @@ -279,7 +279,7 @@ func newTestClusterCustom(dir, dbName string, funding bool, wk types.PrivateKey, var workerShutdownFns []func(context.Context) error workerShutdownFns = append(workerShutdownFns, workerServer.Shutdown) - workerShutdownFns = append(workerShutdownFns, wStopFn) + workerShutdownFns = append(workerShutdownFns, wShutdownFn) // Create autopilot. ap, aStartFn, aStopFn, err := node.NewAutopilot(apCfg, busClient, []autopilot.Worker{workerClient}, logger) diff --git a/worker/client.go b/worker/client.go index 6b466e3d2..f893aa1d0 100644 --- a/worker/client.go +++ b/worker/client.go @@ -155,6 +155,12 @@ func (c *Client) RHPUpdateRegistry(ctx context.Context, hostKey types.PublicKey, return } +// State returns the current state of the worker. +func (c *Client) State() (state api.WorkerStateResponse, err error) { + err = c.c.GET("/state", &state) + return +} + // MigrateSlab migrates the specified slab. func (c *Client) MigrateSlab(ctx context.Context, slab object.Slab, set string) error { values := make(url.Values) diff --git a/worker/worker.go b/worker/worker.go index 4ae8eeb9c..c8b5d5eb1 100644 --- a/worker/worker.go +++ b/worker/worker.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -25,6 +26,7 @@ import ( "go.sia.tech/jape" "go.sia.tech/renterd/alerts" "go.sia.tech/renterd/api" + "go.sia.tech/renterd/build" "go.sia.tech/renterd/hostdb" "go.sia.tech/renterd/metrics" "go.sia.tech/renterd/object" @@ -233,6 +235,7 @@ type worker struct { id string bus Bus masterKey [32]byte + startTime time.Time downloadManager *downloadManager uploadManager *uploadManager @@ -1169,6 +1172,20 @@ func (w *worker) accountHandlerGET(jc jape.Context) { jc.Encode(account) } +func (w *worker) stateHandlerGET(jc jape.Context) { + jc.Encode(api.WorkerStateResponse{ + ID: w.id, + StartTime: w.startTime, + BuildState: api.BuildState{ + Network: build.NetworkName(), + Version: build.Version(), + Commit: build.Commit(), + OS: runtime.GOOS, + BuildTime: build.BuildTime(), + }, + }) +} + // New returns an HTTP handler that serves the worker API. func New(masterKey [32]byte, id string, b Bus, contractLockingDuration, busFlushInterval, downloadOverdriveTimeout, uploadOverdriveTimeout time.Duration, downloadMaxOverdrive, uploadMaxOverdrive uint64, allowPrivateIPs bool, l *zap.Logger) (*worker, error) { if contractLockingDuration == 0 { @@ -1193,6 +1210,7 @@ func New(masterKey [32]byte, id string, b Bus, contractLockingDuration, busFlush masterKey: masterKey, busFlushInterval: busFlushInterval, logger: l.Sugar().Named("worker").Named(id), + startTime: time.Now(), } w.initTransportPool() w.initAccounts(b) @@ -1230,6 +1248,8 @@ func (w *worker) Handler() http.Handler { "GET /objects/*path": w.objectsHandlerGET, "PUT /objects/*path": w.objectsHandlerPUT, "DELETE /objects/*path": w.objectsHandlerDELETE, + + "GET /state": w.stateHandlerGET, })) }