Merge branch 'develop' version 1.2.2
This commit is contained in:
commit
ca10608d64
@ -1,8 +1,11 @@
|
||||
# top
|
||||
/woj
|
||||
|
||||
# runner
|
||||
# resource
|
||||
resource/deploy
|
||||
resource/frontend
|
||||
resource/runner/.mark.image
|
||||
resource/runner/problem/*
|
||||
resource/runner/tmp/*
|
||||
resource/runner/user/*
|
||||
|
||||
|
@ -33,4 +33,8 @@ COPY --from=builder /builder/config.docker.yaml /app
|
||||
COPY --from=builder /builder/docker-entrypoint.sh /app
|
||||
COPY --from=builder /builder/woj /app
|
||||
|
||||
# switch user
|
||||
RUN chown -R podman:podman /app
|
||||
USER podman
|
||||
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
|
@ -17,6 +17,7 @@ RUN --mount=type=cache,id=golang,target=/go/pkg make build
|
||||
|
||||
# UI Builder
|
||||
FROM git.0x7f.app/woj/woj-ui:1.0.0 AS ui-builder
|
||||
RUN find /app -type f -name "*.map" -delete
|
||||
|
||||
# main image
|
||||
FROM docker.io/library/alpine
|
||||
@ -26,7 +27,6 @@ RUN apk --no-cache add tzdata ca-certificates libc6-compat bash
|
||||
|
||||
COPY --from=go-builder /builder/config.docker.yaml /app
|
||||
COPY --from=go-builder /builder/docker-entrypoint.sh /app
|
||||
COPY --from=go-builder /builder/resource/frontend /app/resource/frontend
|
||||
COPY --from=go-builder /builder/woj /app
|
||||
|
||||
COPY --from=ui-builder /app /app/resource/frontend
|
||||
|
@ -76,8 +76,7 @@ func getBuildTime() time.Time {
|
||||
func setupSentry() {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: SentryDSN,
|
||||
EnableTracing: true,
|
||||
TracesSampleRate: 0.5,
|
||||
EnableTracing: false,
|
||||
SendDefaultPII: true,
|
||||
Release: GitCommit,
|
||||
})
|
||||
|
@ -105,6 +105,11 @@ func wrap(f func(i *do.Injector) error) func(*cli.Context) error {
|
||||
}()
|
||||
|
||||
injector := prepareServices(c)
|
||||
defer func() {
|
||||
if err := injector.Shutdown(); err != nil {
|
||||
slog.Printf("shutdown injector failed: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
logger := do.MustInvoke[log.Service](injector)
|
||||
defer func() { _ = logger.GetRawLogger().Sync() }()
|
||||
|
@ -1,8 +1,15 @@
|
||||
WebServer:
|
||||
Address: ${WEB_SERVER_ADDRESS}
|
||||
Port: ${WEB_SERVER_PORT}
|
||||
JwtSigningKey: ${WEB_SERVER_JWT_SIGNING_KEY}
|
||||
JwtExpireHour: ${WEB_SERVER_JWT_EXPIRE_HOUR}
|
||||
PublicBase: ${WEB_SERVER_PUBLIC_BASE}
|
||||
TrustedPlatform: ${WEB_SERVER_TRUSTED_PLATFORM}
|
||||
JWT:
|
||||
SigningKey: ${WEB_SERVER_JWT_SIGNING_KEY}
|
||||
ExpireHour: ${WEB_SERVER_JWT_EXPIRE_HOUR}
|
||||
OAuth:
|
||||
Domain: ${WEB_SERVER_OAUTH_DOMAIN}
|
||||
ClientID: ${WEB_SERVER_OAUTH_CLIENT_ID}
|
||||
ClientSecret: ${WEB_SERVER_OAUTH_CLIENT_SECRET}
|
||||
|
||||
Redis:
|
||||
Db: ${REDIS_DB}
|
||||
|
@ -1,6 +1,6 @@
|
||||
services:
|
||||
server:
|
||||
image: git.0x7f.app/woj/woj-server:1.2.1
|
||||
image: git.0x7f.app/woj/woj-server:1.2.2
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8000/health" ]
|
||||
@ -33,7 +33,7 @@ services:
|
||||
- "8000:8000"
|
||||
|
||||
runner:
|
||||
image: git.0x7f.app/woj/woj-runner:1.2.1
|
||||
image: git.0x7f.app/woj/woj-runner:1.2.2
|
||||
restart: unless-stopped
|
||||
command: runner
|
||||
security_opt:
|
||||
|
@ -36,8 +36,13 @@ function check_env() {
|
||||
|
||||
check_env "WEB_SERVER_ADDRESS" "0.0.0.0" true
|
||||
check_env "WEB_SERVER_PORT" 8000 false
|
||||
check_env "WEB_SERVER_PUBLIC_BASE" "http://127.0.0.1:8000" true
|
||||
check_env "WEB_SERVER_TRUSTED_PLATFORM" "" true
|
||||
check_env "WEB_SERVER_JWT_SIGNING_KEY" "$(head -n 10 /dev/urandom | md5sum | cut -c 1-32)" true
|
||||
check_env "WEB_SERVER_JWT_EXPIRE_HOUR" 12 false
|
||||
check_env "WEB_SERVER_OAUTH_DOMAIN" "" true
|
||||
check_env "WEB_SERVER_OAUTH_CLIENT_ID" "" true
|
||||
check_env "WEB_SERVER_OAUTH_CLIENT_SECRET" "" true
|
||||
|
||||
check_env "REDIS_DB" 0 false
|
||||
check_env "REDIS_QUEUE_DB" 1 false
|
||||
|
26
go.mod
26
go.mod
@ -1,9 +1,10 @@
|
||||
module git.0x7f.app/WOJ/woj-server
|
||||
|
||||
go 1.20
|
||||
go 1.21.5
|
||||
|
||||
require (
|
||||
github.com/TheZeroSlave/zapsentry v1.20.0
|
||||
github.com/TheZeroSlave/zapsentry v1.20.2
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/getsentry/sentry-go v0.25.0
|
||||
github.com/gin-contrib/cors v1.5.0
|
||||
github.com/gin-contrib/pprof v1.4.0
|
||||
@ -14,15 +15,16 @@ require (
|
||||
github.com/hibiken/asynq v0.24.1
|
||||
github.com/jackc/pgtype v1.14.0
|
||||
github.com/minio/minio-go/v7 v7.0.66
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/redis/go-redis/v9 v9.3.1
|
||||
github.com/samber/do v1.6.0
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.2
|
||||
github.com/urfave/cli/v2 v2.26.0
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.17.0
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
@ -42,10 +44,11 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.3 // indirect
|
||||
github.com/go-openapi/spec v0.20.12 // indirect
|
||||
github.com/go-openapi/swag v0.22.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||
github.com/go-openapi/spec v0.20.13 // indirect
|
||||
github.com/go-openapi/swag v0.22.7 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.16.0 // indirect
|
||||
@ -86,12 +89,13 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.6.0 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
63
go.sum
63
go.sum
@ -2,15 +2,17 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/TheZeroSlave/zapsentry v1.20.0 h1:PP7Qb2OPue+E87NqvEq4xcT50Y7BXKYHdGwb0GmsrpE=
|
||||
github.com/TheZeroSlave/zapsentry v1.20.0/go.mod h1:D1YMfSuu6xnkhwFXxrronesmsiyDhIqo+86I3Ok+r64=
|
||||
github.com/TheZeroSlave/zapsentry v1.20.2 h1:llgC91ZJdoU/OzGxYpUlEhKinf65mw9hJ2KkZ7+cGIk=
|
||||
github.com/TheZeroSlave/zapsentry v1.20.2/go.mod h1:D1YMfSuu6xnkhwFXxrronesmsiyDhIqo+86I3Ok+r64=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
@ -25,6 +27,8 @@ github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
@ -39,6 +43,7 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
|
||||
@ -46,6 +51,7 @@ github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w
|
||||
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
|
||||
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
|
||||
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
@ -58,18 +64,22 @@ github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
|
||||
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
|
||||
github.com/go-openapi/jsonreference v0.20.3 h1:EjGcjTW8pD1mRis6+w/gmoBdqv5+RbE9B85D1NgDOVQ=
|
||||
github.com/go-openapi/jsonreference v0.20.3/go.mod h1:FviDZ46i9ivh810gqzFLl5NttD5q3tSlMLqLr6okedM=
|
||||
github.com/go-openapi/spec v0.20.12 h1:cgSLbrsmziAP2iais+Vz7kSazwZ8rsUZd6TUzdDgkVI=
|
||||
github.com/go-openapi/spec v0.20.12/go.mod h1:iSCgnBcwbMW9SfzJb8iYynXvcY6C/QFrI7otzF7xGM4=
|
||||
github.com/go-openapi/swag v0.22.5 h1:fVS63IE3M0lsuWRzuom3RLwUMVI2peDH01s6M70ugys=
|
||||
github.com/go-openapi/swag v0.22.5/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
|
||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
||||
github.com/go-openapi/spec v0.20.13 h1:XJDIN+dLH6vqXgafnl5SUIMnzaChQ6QTo0/UPMbkIaE=
|
||||
github.com/go-openapi/spec v0.20.13/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
|
||||
github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
|
||||
github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
@ -90,6 +100,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
@ -178,6 +189,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
@ -218,13 +230,15 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
@ -240,6 +254,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
@ -288,8 +303,8 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/urfave/cli/v2 v2.26.0 h1:3f3AMg3HpThFNT4I++TKOejZO8yU55t3JnnSr4S4QEI=
|
||||
github.com/urfave/cli/v2 v2.26.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@ -303,6 +318,7 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
@ -318,12 +334,13 @@ go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.6.0 h1:S0JTfE48HbRj80+4tbvZDYsJ3tGv6BUU3XxyZ7CirAc=
|
||||
golang.org/x/arch v0.6.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
@ -339,6 +356,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@ -350,11 +368,13 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -377,8 +397,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@ -389,6 +409,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
@ -414,11 +435,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -19,7 +19,6 @@ func (h *handler) ProblemUpdate(_ context.Context, t *asynq.Task) error {
|
||||
|
||||
if p.Status != e.Success {
|
||||
h.log.Warn("RunnerError", zap.Any("payload", p))
|
||||
return nil
|
||||
}
|
||||
|
||||
status := h.problemService.UpdateVersion(
|
||||
@ -29,7 +28,7 @@ func (h *handler) ProblemUpdate(_ context.Context, t *asynq.Task) error {
|
||||
Bytes: []byte(p.Context),
|
||||
Status: pgtype.Present,
|
||||
},
|
||||
"IsEnabled": true,
|
||||
"IsEnabled": p.Status == e.Success,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -19,7 +19,6 @@ func (h *handler) SubmitUpdate(_ context.Context, t *asynq.Task) error {
|
||||
|
||||
if p.Status != e.Success {
|
||||
h.log.Warn("RunnerError", zap.Any("payload", p))
|
||||
return nil
|
||||
}
|
||||
|
||||
createData := &status.CreateData{
|
||||
|
116
internal/api/oauth/callback.go
Normal file
116
internal/api/oauth/callback.go
Normal file
@ -0,0 +1,116 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/service/user"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CallbackHandler
|
||||
// @Summary Callback with OAuth2
|
||||
// @Description Callback endpoint from OAuth2
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Router /oauth/callback [get]
|
||||
func (h *handler) CallbackHandler() gin.HandlerFunc {
|
||||
// TODO: we are returning e.Response directly here, we should redirect to a trampoline page, passing the response as query string
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Extract key from cookie
|
||||
key, err := c.Cookie(oauthStateCookieName)
|
||||
if err != nil {
|
||||
e.Pong[any](c, e.InvalidParameter, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get state from redis
|
||||
key = fmt.Sprintf(oauthStateKey, key)
|
||||
expected, err := h.cache.Get().Get(context.Background(), key).Result()
|
||||
if err != nil {
|
||||
e.Pong[any](c, e.RedisError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Whether state is valid, delete it
|
||||
h.cache.Get().Unlink(context.Background(), key)
|
||||
c.SetCookie(oauthStateCookieName, "", -1, "/", "", false, true)
|
||||
|
||||
// Verify state
|
||||
if c.Query("state") != expected {
|
||||
e.Pong[any](c, e.OAuthStateMismatch, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Exchange code for token
|
||||
token, err := h.conf.Exchange(context.Background(), c.Query("code"))
|
||||
if err != nil {
|
||||
e.Pong[any](c, e.OAuthExchangeFailed, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the ID Token from OAuth2 token.
|
||||
raw, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
e.Pong[any](c, e.OAuthExchangeFailed, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and verify ID Token payload.
|
||||
idToken, err := h.verifier.Verify(context.Background(), raw)
|
||||
if err != nil {
|
||||
e.Pong[any](c, e.OAuthVerifyFailed, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract custom claims
|
||||
// TODO: extract role from claims
|
||||
var claims struct {
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
Nickname string `json:"preferred_username"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
e.Pong[any](c, e.OAuthGetClaimsFailed, nil)
|
||||
return
|
||||
}
|
||||
if !claims.EmailVerified || claims.Email == "" || claims.Nickname == "" {
|
||||
e.Pong[any](c, e.UserInvalid, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Check user existence
|
||||
u, status := h.user.ProfileOrCreate(&user.CreateData{Email: claims.Email, NickName: claims.Nickname})
|
||||
if status != e.Success {
|
||||
e.Pong[any](c, status, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Increment user version
|
||||
version, status := h.user.IncrVersion(u.ID)
|
||||
if status != e.Success {
|
||||
e.Pong[any](c, status, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign JWT token
|
||||
claim := &model.Claim{
|
||||
UID: u.ID,
|
||||
Role: u.Role,
|
||||
Version: version,
|
||||
}
|
||||
jwt, status := h.jwt.SignClaim(claim)
|
||||
if status != e.Success {
|
||||
e.Pong[any](c, status, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Figure out a better way to cooperate with frontend
|
||||
c.Redirect(http.StatusFound, "/login?redirect_token="+jwt)
|
||||
// e.Pong(c, status, userApi.LoginResponse{Token: jwt, NickName: u.NickName})
|
||||
}
|
||||
}
|
71
internal/api/oauth/handler.go
Normal file
71
internal/api/oauth/handler.go
Normal file
@ -0,0 +1,71 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/repo/cache"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/service/user"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/web/jwt"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/samber/do"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/oauth2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
LoginHandler() gin.HandlerFunc
|
||||
CallbackHandler() gin.HandlerFunc
|
||||
}
|
||||
|
||||
const (
|
||||
oauthStateCookieName = "oauth_state"
|
||||
oauthStateKey = "OAuthState:%s"
|
||||
oauthStateLiveness = 15 * time.Minute
|
||||
)
|
||||
|
||||
func RouteRegister(rg *gin.RouterGroup, i *do.Injector) {
|
||||
conf := do.MustInvoke[config.Service](i).GetConfig()
|
||||
if conf.WebServer.OAuth.Domain == "" {
|
||||
return
|
||||
}
|
||||
|
||||
app := &handler{}
|
||||
app.log = do.MustInvoke[log.Service](i).GetLogger("oauth")
|
||||
app.jwt = do.MustInvoke[jwt.Service](i)
|
||||
app.user = do.MustInvoke[user.Service](i)
|
||||
app.cache = do.MustInvoke[cache.Service](i)
|
||||
|
||||
var err error
|
||||
app.provider, err = oidc.NewProvider(context.Background(), conf.WebServer.OAuth.Domain)
|
||||
if err != nil {
|
||||
app.log.Error("failed to create oauth provider", zap.Error(err), zap.String("domain", conf.WebServer.OAuth.Domain))
|
||||
return
|
||||
}
|
||||
|
||||
app.verifier = app.provider.Verifier(&oidc.Config{ClientID: conf.WebServer.OAuth.ClientID})
|
||||
|
||||
app.conf = oauth2.Config{
|
||||
ClientID: conf.WebServer.OAuth.ClientID,
|
||||
ClientSecret: conf.WebServer.OAuth.ClientSecret,
|
||||
RedirectURL: conf.WebServer.PublicBase + rg.BasePath() + "/callback",
|
||||
Endpoint: app.provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "roles"},
|
||||
}
|
||||
|
||||
rg.POST("/login", app.LoginHandler())
|
||||
rg.GET("/callback", app.CallbackHandler())
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
log *zap.Logger
|
||||
jwt jwt.Service
|
||||
user user.Service
|
||||
cache cache.Service
|
||||
|
||||
provider *oidc.Provider
|
||||
conf oauth2.Config
|
||||
verifier *oidc.IDTokenVerifier
|
||||
}
|
36
internal/api/oauth/login.go
Normal file
36
internal/api/oauth/login.go
Normal file
@ -0,0 +1,36 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// LoginHandler
|
||||
// @Summary Login with OAuth2
|
||||
// @Description Get OAuth2 Login URL
|
||||
// @Tags oauth
|
||||
// @Produce json
|
||||
// @Response 200 {object} e.Response[string] "random string"
|
||||
// @Router /oauth/login [post]
|
||||
func (h *handler) LoginHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
state := utils.RandomString(64)
|
||||
key := utils.RandomString(16)
|
||||
|
||||
err := h.cache.Get().Set(context.Background(), fmt.Sprintf(oauthStateKey, key), state, oauthStateLiveness).Err()
|
||||
if err != nil {
|
||||
e.Pong[any](c, e.RedisError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
c.SetSameSite(http.SameSiteStrictMode)
|
||||
c.SetCookie(oauthStateCookieName, key, int(oauthStateLiveness.Seconds()), "/", "", false, true)
|
||||
|
||||
url := h.conf.AuthCodeURL(state)
|
||||
e.Pong(c, e.Success, url)
|
||||
}
|
||||
}
|
@ -37,6 +37,8 @@ func (h *handler) Search(c *gin.Context) {
|
||||
param := problem.QueryData{
|
||||
Keyword: req.Keyword,
|
||||
Tag: req.Tag,
|
||||
Associations: true,
|
||||
ShouldEnable: true,
|
||||
Offset: req.Offset,
|
||||
Limit: req.Limit,
|
||||
Count: &count,
|
||||
|
@ -22,12 +22,12 @@ func (h *handler) Build(_ context.Context, t *asynq.Task) error {
|
||||
status, ctx := func() (e.Status, string) {
|
||||
url, status := h.storageService.Get(p.StorageKey, time.Second*60*5)
|
||||
if status != e.Success {
|
||||
return e.InternalError, "{}"
|
||||
return e.InternalError, "{\"Message\": \"storage error\"}"
|
||||
}
|
||||
|
||||
config, status := h.runnerService.NewProblem(p.ProblemVersionID, url, true)
|
||||
if status != e.Success {
|
||||
return e.InternalError, "{}"
|
||||
return e.InternalError, "{\"Message\": \"build error: " + status.String() + "\"}"
|
||||
}
|
||||
|
||||
for i := range config.Languages {
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/service/runner"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"github.com/hibiken/asynq"
|
||||
"go.uber.org/zap"
|
||||
@ -24,14 +25,17 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
|
||||
h.log.Info("judge", zap.Any("payload", p), zap.String("user", user))
|
||||
|
||||
status, point, ctx := func() (e.Status, int32, runner.JudgeStatus) {
|
||||
systemError := runner.JudgeStatus{Message: "System Error"}
|
||||
systemError := runner.JudgeStatus{
|
||||
Message: "System Error",
|
||||
Tasks: []runner.TaskStatus{{Verdict: runner.VerdictSystemError, Message: "API Error"}},
|
||||
}
|
||||
|
||||
// 1. write user code
|
||||
userCode := filepath.Join(runner.UserDir, user, fmt.Sprintf("%s.%s", user, p.Submission.Language))
|
||||
if !utils.FileTouch(userCode) {
|
||||
if !file.Touch(userCode) {
|
||||
return e.InternalError, 0, systemError
|
||||
}
|
||||
err := utils.FileWrite(userCode, []byte(p.Submission.Code))
|
||||
err := file.Write(userCode, []byte(p.Submission.Code))
|
||||
if err != nil {
|
||||
return e.InternalError, 0, systemError
|
||||
}
|
||||
@ -52,17 +56,11 @@ func (h *handler) Judge(_ context.Context, t *asynq.Task) error {
|
||||
// 3. compile
|
||||
compileResult, status := h.runnerService.Compile(p.ProblemVersionID, user, p.Submission.Language)
|
||||
if status != e.Success {
|
||||
return e.Success, 0, compileResult
|
||||
return e.InternalError, 0, compileResult
|
||||
}
|
||||
|
||||
// 4. config
|
||||
config, err := h.runnerService.ParseConfig(p.ProblemVersionID, true)
|
||||
if err != nil {
|
||||
return e.InternalError, 0, systemError
|
||||
}
|
||||
|
||||
// 5. run and judge
|
||||
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language, &config)
|
||||
// 4. run and judge
|
||||
result, point, status := h.runnerService.RunAndJudge(p.ProblemVersionID, user, p.Submission.Language)
|
||||
return utils.If(status != e.Success, e.InternalError, e.Success), point, result
|
||||
}()
|
||||
|
||||
|
@ -3,7 +3,6 @@ package status
|
||||
import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@ -58,7 +57,12 @@ func (h *handler) Query(c *gin.Context) {
|
||||
var response []*submissionWithScore
|
||||
for _, submission := range submissions {
|
||||
cur, _ := h.statusService.Query(submission.ID, false)
|
||||
point := utils.If(cur == nil, -1, cur.Point)
|
||||
|
||||
point := int32(-1)
|
||||
if cur != nil {
|
||||
point = cur.Point
|
||||
}
|
||||
|
||||
resp := &submissionWithScore{
|
||||
Submission: *submission,
|
||||
Point: point,
|
||||
|
@ -5,12 +5,13 @@ import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/service/user"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/mail"
|
||||
)
|
||||
|
||||
type createRequest struct {
|
||||
UserName string `form:"username" json:"username" binding:"required"`
|
||||
Password string `form:"password" json:"password" binding:"required"`
|
||||
Email string `form:"email" json:"email" binding:"required"`
|
||||
NickName string `form:"nickname" json:"nickname" binding:"required"`
|
||||
Password string `form:"password" json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// Create
|
||||
@ -19,7 +20,7 @@ type createRequest struct {
|
||||
// @Tags user
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Param username formData string true "username"
|
||||
// @Param email formData string true "email"
|
||||
// @Param nickname formData string true "nickname"
|
||||
// @Param password formData string true "password"
|
||||
// @Response 200 {object} e.Response[string] "jwt token"
|
||||
@ -31,11 +32,18 @@ func (h *handler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// verify email is valid
|
||||
_, err := mail.ParseAddress(req.Email)
|
||||
if err != nil {
|
||||
e.Pong[any](c, e.InvalidParameter, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// create user
|
||||
createData := &user.CreateData{
|
||||
UserName: req.UserName,
|
||||
Password: req.Password,
|
||||
Email: req.Email,
|
||||
NickName: req.NickName,
|
||||
Password: req.Password,
|
||||
}
|
||||
u, status := h.userService.Create(createData)
|
||||
if status != e.Success {
|
||||
|
@ -8,11 +8,11 @@ import (
|
||||
)
|
||||
|
||||
type loginRequest struct {
|
||||
UserName string `form:"username" json:"username" binding:"required"`
|
||||
Email string `form:"email" json:"email" binding:"required"`
|
||||
Password string `form:"password" json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
NickName string `json:"nickname"`
|
||||
}
|
||||
@ -23,9 +23,9 @@ type loginResponse struct {
|
||||
// @Tags user
|
||||
// @Accept application/x-www-form-urlencoded
|
||||
// @Produce json
|
||||
// @Param username formData string true "username"
|
||||
// @Param email formData string true "email"
|
||||
// @Param password formData string true "password"
|
||||
// @Response 200 {object} e.Response[loginResponse] "jwt token and user's nickname"
|
||||
// @Response 200 {object} e.Response[LoginResponse] "jwt token and user's nickname"
|
||||
// @Router /v1/user/login [post]
|
||||
func (h *handler) Login(c *gin.Context) {
|
||||
req := new(loginRequest)
|
||||
@ -36,7 +36,7 @@ func (h *handler) Login(c *gin.Context) {
|
||||
|
||||
// check password
|
||||
loginData := &user.LoginData{
|
||||
UserName: req.UserName,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
}
|
||||
u, status := h.userService.Login(loginData)
|
||||
@ -57,5 +57,5 @@ func (h *handler) Login(c *gin.Context) {
|
||||
Version: version,
|
||||
}
|
||||
token, status := h.jwtService.SignClaim(claim)
|
||||
e.Pong(c, status, loginResponse{Token: token, NickName: u.NickName})
|
||||
e.Pong(c, status, LoginResponse{Token: token, NickName: u.NickName})
|
||||
}
|
||||
|
@ -6,12 +6,10 @@ import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/zapasynq"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/samber/do"
|
||||
"go.uber.org/zap"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func RunRunner(i *do.Injector) error {
|
||||
@ -34,7 +32,7 @@ func RunRunner(i *do.Injector) error {
|
||||
DB: conf.Redis.QueueDb,
|
||||
},
|
||||
asynq.Config{
|
||||
Concurrency: utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1),
|
||||
Concurrency: 1, // there's a worker pool in runner service
|
||||
Logger: zapasynq.New(rlog),
|
||||
Queues: map[string]int{model.QueueRunner: 1},
|
||||
},
|
||||
|
@ -24,15 +24,8 @@ import (
|
||||
)
|
||||
|
||||
func RunServerMigrate(i *do.Injector) error {
|
||||
slog := do.MustInvoke[log.Service](i).GetLogger("app.server")
|
||||
|
||||
// Migrate and shutdown database
|
||||
err := do.MustInvoke[db.Service](i).Close()
|
||||
if err != nil {
|
||||
slog.Warn("Database Close Failed", zap.Error(err))
|
||||
}
|
||||
|
||||
return err
|
||||
do.MustInvoke[db.Service](i).Migrate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunServer(i *do.Injector) error {
|
||||
@ -98,11 +91,5 @@ func RunServer(i *do.Injector) error {
|
||||
// Graceful Shutdown Queue
|
||||
queueSrv.Shutdown()
|
||||
|
||||
// Graceful Shutdown Database
|
||||
err = do.MustInvoke[db.Service](i).Close()
|
||||
if err != nil {
|
||||
slog.Warn("Database Close Failed", zap.Error(err))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -21,6 +21,10 @@ const (
|
||||
TokenInvalid
|
||||
TokenSignError
|
||||
TokenRevoked
|
||||
OAuthStateMismatch
|
||||
OAuthExchangeFailed
|
||||
OAuthVerifyFailed
|
||||
OAuthGetClaimsFailed
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,6 +34,8 @@ const (
|
||||
UserUnauthenticated
|
||||
UserUnauthorized
|
||||
UserDisabled
|
||||
UserWithoutPassword
|
||||
UserInvalid
|
||||
)
|
||||
|
||||
const (
|
||||
@ -81,6 +87,10 @@ var msgText = map[Status]string{
|
||||
TokenInvalid: "Token Invalid",
|
||||
TokenSignError: "Token Sign Error",
|
||||
TokenRevoked: "Token Revoked",
|
||||
OAuthStateMismatch: "OAuth State Mismatch",
|
||||
OAuthExchangeFailed: "OAuth Exchange Failed",
|
||||
OAuthVerifyFailed: "OAuth Verify Failed",
|
||||
OAuthGetClaimsFailed: "OAuth Get Claims Failed",
|
||||
|
||||
UserNotFound: "User Not Found",
|
||||
UserWrongPassword: "User Wrong Password",
|
||||
@ -88,6 +98,8 @@ var msgText = map[Status]string{
|
||||
UserUnauthenticated: "User Unauthenticated",
|
||||
UserUnauthorized: "User Unauthorized",
|
||||
UserDisabled: "User Disabled",
|
||||
UserWithoutPassword: "User Without Password",
|
||||
UserInvalid: "User Invalid",
|
||||
|
||||
ProblemNotFound: "Problem Not Found",
|
||||
ProblemNotAvailable: "Problem Not Available",
|
||||
|
@ -1,5 +1,7 @@
|
||||
package e
|
||||
|
||||
import "errors"
|
||||
|
||||
type Status int
|
||||
|
||||
func (code Status) String() string {
|
||||
@ -9,3 +11,7 @@ func (code Status) String() string {
|
||||
}
|
||||
return msgText[InternalError]
|
||||
}
|
||||
|
||||
func (code Status) AsError() error {
|
||||
return errors.New(code.String())
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/model"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"github.com/samber/do"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -19,7 +19,7 @@ type Service interface {
|
||||
func NewService(i *do.Injector) (Service, error) {
|
||||
cliCtx := do.MustInvoke[*cli.Context](i)
|
||||
|
||||
data, err := utils.FileRead(cliCtx.String("config"))
|
||||
data, err := file.Read(cliCtx.String("config"))
|
||||
if err != nil {
|
||||
log.Printf("Failed to setup config: %s\n", err.Error())
|
||||
return nil, err
|
||||
|
@ -6,9 +6,9 @@ import (
|
||||
|
||||
type User struct {
|
||||
gorm.Model `json:"meta"`
|
||||
UserName string `json:"user_name" gorm:"not null;uniqueIndex"`
|
||||
NickName string `json:"nick_name" gorm:"not null"`
|
||||
Email string `json:"email" gorm:"not null;uniqueIndex"`
|
||||
NickName string `json:"nick_name" gorm:"not null;uniqueIndex"`
|
||||
Role Role `json:"role" gorm:"not null"`
|
||||
Password []byte `json:"-" gorm:"not null"`
|
||||
Password []byte `json:"-"`
|
||||
IsEnabled bool `json:"is_enabled" gorm:"not null;index"`
|
||||
}
|
||||
|
@ -3,8 +3,21 @@ package model
|
||||
type ConfigWebServer struct {
|
||||
Address string `yaml:"Address"`
|
||||
Port int `yaml:"Port"`
|
||||
JwtSigningKey string `yaml:"JwtSigningKey"`
|
||||
JwtExpireHour int `yaml:"JwtExpireHour"`
|
||||
PublicBase string `yaml:"PublicBase"`
|
||||
TrustedPlatform string `yaml:"TrustedPlatform"`
|
||||
JWT ConfigJWT `yaml:"JWT"`
|
||||
OAuth ConfigOAuth `yaml:"OAuth"`
|
||||
}
|
||||
|
||||
type ConfigJWT struct {
|
||||
SigningKey string `yaml:"SigningKey"`
|
||||
ExpireHour int `yaml:"ExpireHour"`
|
||||
}
|
||||
|
||||
type ConfigOAuth struct {
|
||||
Domain string `yaml:"Domain"`
|
||||
ClientID string `yaml:"ClientID"`
|
||||
ClientSecret string `yaml:"ClientSecret"`
|
||||
}
|
||||
|
||||
type ConfigRedis struct {
|
||||
|
@ -22,8 +22,9 @@ var _ Service = (*service)(nil)
|
||||
|
||||
type Service interface {
|
||||
Get() *gorm.DB
|
||||
Close() error
|
||||
Migrate()
|
||||
HealthCheck() error
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
func NewService(i *do.Injector) (Service, error) {
|
||||
@ -45,7 +46,44 @@ func (s *service) Get() *gorm.DB {
|
||||
return s.db
|
||||
}
|
||||
|
||||
func (s *service) Close() error {
|
||||
func (s *service) Migrate() {
|
||||
s.log.Info("Auto Migrating database...")
|
||||
|
||||
// Running AutoMigrate concurrently on the same model fails with various race conditions
|
||||
// https://github.com/go-gorm/gorm/pull/6680
|
||||
// https://github.com/go-gorm/postgres/pull/224
|
||||
|
||||
// Obtain a lock to prevent concurrent AutoMigrate
|
||||
|
||||
lockID := func(s string) int64 {
|
||||
h := fnv.New64a()
|
||||
_, err := h.Write([]byte(s))
|
||||
return utils.If(err != nil, int64(0x4242AA55), int64(h.Sum64()))
|
||||
}("gorm:migrator")
|
||||
|
||||
s.err = s.db.Exec("SELECT pg_advisory_lock(?)", lockID).Error
|
||||
if s.err != nil {
|
||||
s.log.Error("Failed to obtain lock", zap.Error(s.err))
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.db.AutoMigrate(&model.User{})
|
||||
_ = s.db.AutoMigrate(&model.Problem{})
|
||||
_ = s.db.AutoMigrate(&model.ProblemVersion{})
|
||||
_ = s.db.AutoMigrate(&model.Submission{})
|
||||
_ = s.db.AutoMigrate(&model.Status{})
|
||||
|
||||
s.err = s.db.Exec("SELECT pg_advisory_unlock(?)", lockID).Error
|
||||
if s.err != nil {
|
||||
s.log.Error("Failed to release lock", zap.Error(s.err))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) HealthCheck() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *service) Shutdown() error {
|
||||
var db *sql.DB
|
||||
db, s.err = s.db.DB()
|
||||
if s.err != nil {
|
||||
@ -56,10 +94,6 @@ func (s *service) Close() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *service) HealthCheck() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func (s *service) setup(conf *model.Config) {
|
||||
s.log.Info("Connecting to database...")
|
||||
|
||||
@ -104,41 +138,6 @@ func (s *service) setup(conf *model.Config) {
|
||||
db.SetMaxOpenConns(conf.Database.MaxOpenConns)
|
||||
db.SetMaxIdleConns(conf.Database.MaxIdleConns)
|
||||
db.SetConnMaxLifetime(time.Duration(conf.Database.ConnMaxLifetime) * time.Minute)
|
||||
|
||||
s.migrateDatabase()
|
||||
}
|
||||
|
||||
func (s *service) migrateDatabase() {
|
||||
s.log.Info("Auto Migrating database...")
|
||||
|
||||
// Running AutoMigrate concurrently on the same model fails with various race conditions
|
||||
// https://github.com/go-gorm/gorm/pull/6680
|
||||
// https://github.com/go-gorm/postgres/pull/224
|
||||
|
||||
// Obtain a lock to prevent concurrent AutoMigrate
|
||||
|
||||
lockID := func(s string) int64 {
|
||||
h := fnv.New64a()
|
||||
_, err := h.Write([]byte(s))
|
||||
return utils.If(err != nil, int64(0x4242AA55), int64(h.Sum64()))
|
||||
}("gorm:migrator")
|
||||
|
||||
s.err = s.db.Exec("SELECT pg_advisory_lock(?)", lockID).Error
|
||||
if s.err != nil {
|
||||
s.log.Error("Failed to obtain lock", zap.Error(s.err))
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.db.AutoMigrate(&model.User{})
|
||||
_ = s.db.AutoMigrate(&model.Problem{})
|
||||
_ = s.db.AutoMigrate(&model.ProblemVersion{})
|
||||
_ = s.db.AutoMigrate(&model.Submission{})
|
||||
_ = s.db.AutoMigrate(&model.Status{})
|
||||
|
||||
s.err = s.db.Exec("SELECT pg_advisory_unlock(?)", lockID).Error
|
||||
if s.err != nil {
|
||||
s.log.Error("Failed to release lock", zap.Error(s.err))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) checkAlive(retry int) (*sql.DB, error) {
|
||||
|
@ -3,10 +3,9 @@ package runner
|
||||
import (
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
@ -27,53 +26,43 @@ func init() {
|
||||
|
||||
Prefix = path.Join(wd, Prefix)
|
||||
ProblemDir = path.Join(Prefix, ProblemDir)
|
||||
ScriptsDir = path.Join(Prefix, ScriptsDir)
|
||||
UserDir = path.Join(Prefix, UserDir)
|
||||
TmpDir = path.Join(Prefix, TmpDir)
|
||||
}
|
||||
|
||||
func (s *service) execute(script string, args ...string) error {
|
||||
p := filepath.Join(ScriptsDir, script)
|
||||
cmd := exec.Command(p, args...)
|
||||
cmd.Dir = ScriptsDir
|
||||
if s.verbose {
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
return cmd.Run()
|
||||
func (s *service) ProblemExists(version uint) bool {
|
||||
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
|
||||
return file.Exist(problemPath)
|
||||
}
|
||||
|
||||
func (s *service) checkAndExecute(version uint, user string, lang string, script string, fail e.Status) e.Status {
|
||||
func (s *service) check(version uint, user string, lang string) e.Status {
|
||||
if !s.ProblemExists(version) {
|
||||
s.log.Info("problem not exists", zap.Uint("version", version))
|
||||
return e.RunnerProblemNotExist
|
||||
}
|
||||
|
||||
if !s.userExists(user, fmt.Sprintf("%s.%s", user, lang)) {
|
||||
userPath := filepath.Join(UserDir, user, fmt.Sprintf("%s.%s", user, lang))
|
||||
if !file.Exist(userPath) {
|
||||
s.log.Info("user program not exists", zap.String("user", user), zap.String("lang", lang))
|
||||
return e.RunnerUserNotExist
|
||||
}
|
||||
|
||||
err := s.execute(script, fmt.Sprintf("%d", version), user, lang)
|
||||
|
||||
if err != nil {
|
||||
s.log.Info("execute failed",
|
||||
zap.Error(err),
|
||||
zap.Uint("version", version),
|
||||
zap.String("user", user),
|
||||
zap.String("lang", lang))
|
||||
return fail
|
||||
}
|
||||
|
||||
return e.Success
|
||||
}
|
||||
|
||||
func (s *service) ProblemExists(version uint) bool {
|
||||
problemPath := filepath.Join(ProblemDir, fmt.Sprintf("%d", version))
|
||||
return utils.FileExist(problemPath)
|
||||
func (s *service) getLangInfo(config *Config, lang string) (configLanguage, bool) {
|
||||
for _, l := range config.Languages {
|
||||
if l.Lang == lang {
|
||||
return l, true
|
||||
}
|
||||
}
|
||||
return configLanguage{}, false
|
||||
}
|
||||
|
||||
func (s *service) userExists(user string, file string) bool {
|
||||
userPath := filepath.Join(UserDir, user, file)
|
||||
return utils.FileExist(userPath)
|
||||
func (s *service) getLangScript(l *configLanguage, lang string) string {
|
||||
if l.Type == "default" {
|
||||
return "/woj/framework/template/default/" + lang + ".Makefile"
|
||||
} else {
|
||||
return "/woj/problem/judge/" + l.Script
|
||||
}
|
||||
}
|
||||
|
@ -3,23 +3,102 @@ package runner
|
||||
import (
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *service) Compile(version uint, user string, lang string) (JudgeStatus, e.Status) {
|
||||
target := filepath.Join(UserDir, user, fmt.Sprintf("%s.out", user))
|
||||
// 1. ensure problem/user exists
|
||||
status := s.check(version, user, lang)
|
||||
if status != e.Success {
|
||||
return JudgeStatus{Message: "check failed"}, status
|
||||
}
|
||||
|
||||
_ = os.Remove(target)
|
||||
status := s.checkAndExecute(version, user, lang, "problem_compile.sh", e.RunnerUserCompileFailed)
|
||||
config, err := s.ParseConfig(version, true)
|
||||
if err != nil {
|
||||
s.log.Error("[compile] parse config failed", zap.Error(err))
|
||||
return JudgeStatus{
|
||||
Message: "parse config failed",
|
||||
Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "parse config failed"}},
|
||||
}, e.RunnerProblemParseFailed
|
||||
}
|
||||
|
||||
log := filepath.Join(UserDir, user, fmt.Sprintf("%s.compile.log", user))
|
||||
msg, err := utils.FileRead(log)
|
||||
// 2. prepare judge environment
|
||||
workDir := filepath.Join(UserDir, user)
|
||||
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
|
||||
|
||||
sourceFile := filepath.Join(workDir, fmt.Sprintf("%s.%s", user, lang))
|
||||
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user))
|
||||
|
||||
logFile := filepath.Join(workDir, fmt.Sprintf("%s.compile.log", user))
|
||||
log, err := os.Create(logFile)
|
||||
if err != nil {
|
||||
s.log.Error("[compile] create log file failed", zap.Error(err))
|
||||
return JudgeStatus{
|
||||
Message: "create log file failed",
|
||||
Tasks: []TaskStatus{{Verdict: VerdictSystemError, Message: "create log file failed"}},
|
||||
}, e.RunnerUserCompileFailed
|
||||
}
|
||||
defer func(log *os.File) {
|
||||
_ = log.Close()
|
||||
}(log)
|
||||
|
||||
// 3. compile
|
||||
err = utils.NewMust().
|
||||
DoAny(func() error { return os.Remove(targetFile) }).
|
||||
Do(func() error { return file.TouchErr(targetFile) }).
|
||||
Do(func() error {
|
||||
l, ok := s.getLangInfo(&config, lang)
|
||||
script := s.getLangScript(&l, lang)
|
||||
if !ok {
|
||||
return e.RunnerProblemParseFailed.AsError()
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-v", fmt.Sprintf("%s:/woj/problem/judge:ro", judgeDir),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%s.%s:ro", sourceFile, user, lang),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%s.out", targetFile, user),
|
||||
"-e", fmt.Sprintf("USER_PROG=%s", user),
|
||||
"-e", fmt.Sprintf("LANG=%s", lang),
|
||||
"git.0x7f.app/woj/ubuntu-full:latest",
|
||||
"sh", "-c", fmt.Sprintf("cd /woj/user && make -f %s compile", script),
|
||||
}
|
||||
|
||||
runArgs := &podmanArgs{
|
||||
executeArgs: executeArgs{
|
||||
args: args,
|
||||
timeout: 60 * time.Second,
|
||||
output: log,
|
||||
},
|
||||
memory: "256m",
|
||||
}
|
||||
|
||||
return s.podmanRun(runArgs)
|
||||
}).
|
||||
Done()
|
||||
|
||||
if err != nil {
|
||||
s.log.Info("[compile] compile failed",
|
||||
zap.Error(err),
|
||||
zap.Uint("version", version),
|
||||
zap.String("user", user),
|
||||
zap.String("lang", lang),
|
||||
)
|
||||
status = e.RunnerUserCompileFailed
|
||||
}
|
||||
|
||||
// 4. read log
|
||||
_, _ = log.Seek(0, io.SeekStart)
|
||||
msg, err := io.ReadAll(log)
|
||||
msg = utils.If(err == nil, msg, nil)
|
||||
msgText := string(msg)
|
||||
|
||||
if !utils.FileExist(target) || utils.FileEmpty(target) {
|
||||
if !file.Exist(targetFile) || file.Empty(targetFile) {
|
||||
return JudgeStatus{
|
||||
Message: "compile failed",
|
||||
Tasks: []TaskStatus{{Verdict: VerdictCompileError, Message: msgText}}},
|
||||
|
@ -8,18 +8,20 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type configLanguage struct {
|
||||
Lang string `json:"Lang"`
|
||||
Type string `json:"Type,omitempty"`
|
||||
Script string `json:"Script,omitempty"`
|
||||
Cmp string `json:"Cmp,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Runtime struct {
|
||||
TimeLimit int `json:"TimeLimit"`
|
||||
MemoryLimit int `json:"MemoryLimit"`
|
||||
NProcLimit int `json:"NProcLimit"`
|
||||
} `json:"Runtime"`
|
||||
Languages []struct {
|
||||
Lang string `json:"Lang"`
|
||||
Type string `json:"Type,omitempty"`
|
||||
Script string `json:"Script,omitempty"`
|
||||
Cmp string `json:"Cmp,omitempty"`
|
||||
} `json:"Languages"`
|
||||
Languages []configLanguage `json:"Languages"`
|
||||
Tasks []struct {
|
||||
Id int `json:"Id"`
|
||||
Points int32 `json:"Points"`
|
||||
|
@ -1,35 +1,84 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (s *service) EnsureDeps(force bool) e.Status {
|
||||
mark := filepath.Join(Prefix, ".mark.image")
|
||||
|
||||
if force {
|
||||
_ = os.Remove(mark)
|
||||
} else if utils.FileExist(mark) {
|
||||
return e.Success
|
||||
type depConfig struct {
|
||||
tarball string
|
||||
image string
|
||||
dockerfile string
|
||||
}
|
||||
|
||||
script := filepath.Join(ScriptsDir, "prepare_images.sh")
|
||||
cmd := exec.Command(script)
|
||||
cmd.Dir = ScriptsDir
|
||||
if s.verbose {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
func (s *service) loadImage(cfg *depConfig) e.Status {
|
||||
err := utils.NewTryErr().
|
||||
Try(func() error {
|
||||
// import from tarball
|
||||
if !file.Exist(cfg.tarball) {
|
||||
return errors.New("tarball not exists")
|
||||
}
|
||||
err := cmd.Run()
|
||||
return s.execute("bash", "-c", fmt.Sprintf("gzip -d -c %s | podman load", cfg.tarball))
|
||||
}).
|
||||
Or(func() error {
|
||||
// pull from docker hub
|
||||
return s.execute("podman", "pull", cfg.image)
|
||||
}).
|
||||
Or(func() error {
|
||||
// build from dockerfile
|
||||
if !file.Exist(cfg.dockerfile) {
|
||||
return errors.New("dockerfile not exists")
|
||||
}
|
||||
return s.execute("podman", "build", "-f", cfg.dockerfile, "-t", cfg.image, ".")
|
||||
}).
|
||||
Done()
|
||||
|
||||
if err != nil {
|
||||
s.log.Warn("prebuild docker images failed", zap.Error(err))
|
||||
s.log.Warn("[deps] load image failed", zap.Error(err))
|
||||
return e.RunnerDepsBuildFailed
|
||||
}
|
||||
|
||||
return e.Success
|
||||
}
|
||||
|
||||
func (s *service) EnsureDeps(force bool) e.Status {
|
||||
mark := filepath.Join(Prefix, ".mark.image")
|
||||
|
||||
// check mark
|
||||
if force {
|
||||
_ = os.Remove(mark)
|
||||
} else if file.Exist(mark) {
|
||||
return e.Success
|
||||
}
|
||||
|
||||
// full
|
||||
fullImage := &depConfig{
|
||||
tarball: filepath.Join(TmpDir, "ubuntu-full.tar.gz"),
|
||||
image: "git.0x7f.app/woj/ubuntu-full:latest",
|
||||
dockerfile: filepath.Join(ScriptsDir, "ubuntu-full.Dockerfile"),
|
||||
}
|
||||
if s.loadImage(fullImage) != e.Success {
|
||||
return e.RunnerDepsBuildFailed
|
||||
}
|
||||
|
||||
// tiny
|
||||
tinyImage := &depConfig{
|
||||
tarball: filepath.Join(TmpDir, "ubuntu-tiny.tar.gz"),
|
||||
image: "git.0x7f.app/woj/ubuntu-run:latest",
|
||||
dockerfile: filepath.Join(ScriptsDir, "ubuntu-run.Dockerfile"),
|
||||
}
|
||||
if s.loadImage(tinyImage) != e.Success {
|
||||
return e.RunnerDepsBuildFailed
|
||||
}
|
||||
|
||||
// mark
|
||||
_, _ = os.Create(mark)
|
||||
|
||||
return e.Success
|
||||
}
|
||||
|
109
internal/service/runner/exec.go
Normal file
109
internal/service/runner/exec.go
Normal file
@ -0,0 +1,109 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *service) execute(exe string, args ...string) error {
|
||||
cmd := exec.Command(exe, args...)
|
||||
cmd.Dir = Prefix
|
||||
if s.verbose {
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
type executeArgs struct {
|
||||
exe string
|
||||
args []string
|
||||
|
||||
timeout time.Duration
|
||||
kill func() error
|
||||
|
||||
output *os.File
|
||||
limit int64
|
||||
}
|
||||
|
||||
func (s *service) executeTimeout(arg *executeArgs) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), arg.timeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, arg.exe, arg.args...)
|
||||
cmd.Dir = Prefix
|
||||
if arg.kill != nil {
|
||||
cmd.Cancel = arg.kill
|
||||
}
|
||||
if s.verbose && arg.output == nil {
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
} else if arg.output != nil {
|
||||
if arg.limit == 0 {
|
||||
cmd.Stdout = arg.output
|
||||
cmd.Stderr = arg.output
|
||||
} else {
|
||||
lw := &file.LimitedWriter{
|
||||
File: arg.output,
|
||||
Limit: arg.limit,
|
||||
}
|
||||
cmd.Stdout = lw
|
||||
cmd.Stderr = lw
|
||||
}
|
||||
}
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
// make sure the process is killed
|
||||
_ = cmd.Process.Kill()
|
||||
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return fmt.Errorf("command timed out")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type podmanArgs struct {
|
||||
executeArgs
|
||||
memory string
|
||||
}
|
||||
|
||||
func (s *service) podmanRun(arg *podmanArgs) error {
|
||||
name := fmt.Sprintf("woj-%d-%s", time.Now().UnixNano(), utils.RandomString(8))
|
||||
execArgs := &executeArgs{
|
||||
exe: "podman",
|
||||
output: utils.If(arg.output == nil, os.Stderr, arg.output),
|
||||
limit: utils.If(arg.limit == 0, 4*1024, arg.limit),
|
||||
timeout: utils.If(arg.timeout == 0, 10*time.Second, arg.timeout),
|
||||
kill: func() error {
|
||||
if arg.kill != nil {
|
||||
_ = arg.kill()
|
||||
}
|
||||
return s.execute("podman", "kill", name)
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"run",
|
||||
"--rm",
|
||||
"--name", name,
|
||||
"--memory", utils.If(arg.memory == "", "256m", arg.memory),
|
||||
}
|
||||
args = append(args, arg.args...)
|
||||
|
||||
execArgs.args = args
|
||||
|
||||
return s.executeTimeout(execArgs)
|
||||
}
|
@ -4,11 +4,12 @@ import (
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/down"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/unzip"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *service) download(version uint, url string) e.Status {
|
||||
@ -17,13 +18,13 @@ func (s *service) download(version uint, url string) e.Status {
|
||||
|
||||
err := down.Down(zipPath, url)
|
||||
if err != nil {
|
||||
s.log.Error("download problem failed", zap.Error(err))
|
||||
s.log.Error("[new] download problem failed", zap.Error(err))
|
||||
return e.RunnerDownloadFailed
|
||||
}
|
||||
|
||||
err = unzip.Unzip(zipPath, problemPath)
|
||||
if err != nil {
|
||||
s.log.Warn("unzip problem failed", zap.Error(err))
|
||||
s.log.Warn("[new] unzip problem failed", zap.Error(err))
|
||||
return e.RunnerUnzipFailed
|
||||
}
|
||||
|
||||
@ -38,14 +39,37 @@ func (s *service) prebuild(version uint, force bool) e.Status {
|
||||
mark := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), ".mark.prebuild")
|
||||
if force {
|
||||
_ = os.Remove(mark)
|
||||
} else if utils.FileExist(mark) {
|
||||
} else if file.Exist(mark) {
|
||||
return e.Success
|
||||
}
|
||||
|
||||
err := s.execute("problem_prebuild.sh", fmt.Sprintf("%d", version))
|
||||
prebuildScript := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge", "prebuild.Makefile")
|
||||
if !file.Exist(prebuildScript) {
|
||||
s.log.Info("[new] prebuild script not found", zap.String("path", prebuildScript), zap.Uint("version", version))
|
||||
return e.Success
|
||||
}
|
||||
|
||||
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data")
|
||||
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
|
||||
|
||||
args := []string{
|
||||
"-v", fmt.Sprintf("%s:/woj/problem/data", dataDir),
|
||||
"-v", fmt.Sprintf("%s:/woj/problem/judge", judgeDir),
|
||||
"-e", "PREFIX=/woj/problem",
|
||||
"git.0x7f.app/woj/ubuntu-full:latest",
|
||||
"sh", "-c", "cd /woj/problem/judge && make -f prebuild.Makefile prebuild && touch .mark.prebuild",
|
||||
}
|
||||
runArgs := &podmanArgs{
|
||||
executeArgs: executeArgs{
|
||||
args: args,
|
||||
timeout: 300 * time.Second,
|
||||
},
|
||||
memory: "1g",
|
||||
}
|
||||
err := s.podmanRun(runArgs)
|
||||
|
||||
if err != nil {
|
||||
s.log.Warn("prebuild problem failed", zap.Error(err), zap.Uint("version", version))
|
||||
s.log.Warn("[new] prebuild problem failed", zap.Error(err), zap.Uint("version", version))
|
||||
return e.RunnerProblemPrebuildFailed
|
||||
}
|
||||
|
||||
@ -67,6 +91,7 @@ func (s *service) NewProblem(version uint, url string, force bool) (Config, e.St
|
||||
|
||||
cfg, err := s.ParseConfig(version, false)
|
||||
if err != nil {
|
||||
s.log.Info("[new] parse problem failed", zap.Error(err), zap.Uint("version", version))
|
||||
return Config{}, e.RunnerProblemParseFailed
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
package runner
|
||||
|
||||
import "git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
|
||||
func (s *service) RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status) {
|
||||
// run user program
|
||||
status := s.checkAndExecute(version, user, lang, "problem_run.sh", e.RunnerRunFailed)
|
||||
if status != e.Success {
|
||||
return JudgeStatus{Message: "run failed"}, 0, status
|
||||
}
|
||||
|
||||
// run judger
|
||||
status = s.checkAndExecute(version, user, lang, "problem_judge.sh", e.RunnerJudgeFailed)
|
||||
if status != e.Success {
|
||||
return JudgeStatus{Message: "judge failed"}, 0, status
|
||||
}
|
||||
|
||||
// check result
|
||||
result, pts := s.checkResults(user, config)
|
||||
|
||||
return result, pts, e.Success
|
||||
}
|
183
internal/service/runner/run_judge.go
Normal file
183
internal/service/runner/run_judge.go
Normal file
@ -0,0 +1,183 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *service) problemRun(version uint, user string, lang string, config *Config) {
|
||||
workDir := filepath.Join(UserDir, user)
|
||||
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data", "input")
|
||||
|
||||
// woj-sandbox killer will add 2 more seconds, here we add 2 more seconds
|
||||
timeout := time.Duration((config.Runtime.TimeLimit+1000)/1000+2+2) * time.Second
|
||||
|
||||
ids := make([]int, 0)
|
||||
for _, task := range config.Tasks {
|
||||
f := func(id int) func() {
|
||||
return func() {
|
||||
testCase := filepath.Join(dataDir, fmt.Sprintf("%d.input", id))
|
||||
targetFile := filepath.Join(workDir, fmt.Sprintf("%s.out", user))
|
||||
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
|
||||
ifoFile := filepath.Join(workDir, fmt.Sprintf("%d.info", id))
|
||||
|
||||
err := utils.NewMust().
|
||||
DoAny(func() error { return os.Remove(ansFile) }).
|
||||
DoAny(func() error { return os.Remove(ifoFile) }).
|
||||
Do(func() error { return file.TouchErr(ansFile) }).
|
||||
Do(func() error { return file.TouchErr(ifoFile) }).
|
||||
Do(func() error {
|
||||
args := []string{
|
||||
"--cpus", "1",
|
||||
"--network", "none",
|
||||
"-v", fmt.Sprintf("%s:/woj/problem/data/input/%d.input:ro", testCase, id),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%s.out:ro", targetFile, user),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%d.out.usr", ansFile, id),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%d.info", ifoFile, id),
|
||||
"git.0x7f.app/woj/ubuntu-run:latest",
|
||||
"sh", "-c",
|
||||
fmt.Sprintf("cd /woj/user && /woj/framework/scripts/woj_launcher "+
|
||||
"--memory_limit=%d "+
|
||||
"--nproc_limit=%d "+
|
||||
"--time_limit=%d "+
|
||||
"--sandbox_template=%s "+
|
||||
"--sandbox_action=ret "+
|
||||
"--uid=1000 "+
|
||||
"--gid=1000 "+
|
||||
"--file_input=/woj/problem/data/input/%d.input "+
|
||||
"--file_output=/woj/user/%d.out.usr "+
|
||||
"--file_info=/woj/user/%d.info "+
|
||||
"-program=/woj/user/%s.out",
|
||||
config.Runtime.MemoryLimit,
|
||||
config.Runtime.NProcLimit,
|
||||
config.Runtime.TimeLimit,
|
||||
lang,
|
||||
id, id, id,
|
||||
user,
|
||||
),
|
||||
}
|
||||
|
||||
runArgs := &podmanArgs{
|
||||
executeArgs: executeArgs{
|
||||
args: args,
|
||||
timeout: timeout,
|
||||
},
|
||||
}
|
||||
|
||||
return s.podmanRun(runArgs)
|
||||
}).
|
||||
Done()
|
||||
|
||||
if err != nil {
|
||||
s.log.Info("[run] run failed",
|
||||
zap.Error(err),
|
||||
zap.Uint("version", version),
|
||||
zap.String("user", user),
|
||||
zap.String("lang", lang),
|
||||
)
|
||||
}
|
||||
}
|
||||
}(task.Id)
|
||||
|
||||
id := s.pool.AddTask(f)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
s.pool.WaitForTask(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) problemJudge(version uint, user string, lang string, config *Config) {
|
||||
workDir := filepath.Join(UserDir, user)
|
||||
dataDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "data")
|
||||
judgeDir := filepath.Join(ProblemDir, fmt.Sprintf("%d", version), "judge")
|
||||
|
||||
ids := make([]int, 0)
|
||||
for _, task := range config.Tasks {
|
||||
f := func(id int) func() {
|
||||
return func() {
|
||||
ansFile := filepath.Join(workDir, fmt.Sprintf("%d.out.usr", id))
|
||||
jdgFile := filepath.Join(workDir, fmt.Sprintf("%d.judge", id))
|
||||
|
||||
c, ok := s.getLangInfo(config, lang)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err := utils.NewMust().
|
||||
DoAny(func() error { return os.Remove(jdgFile) }).
|
||||
Do(func() error { return file.TouchErr(jdgFile) }).
|
||||
Do(func() error {
|
||||
args := []string{
|
||||
"-v", fmt.Sprintf("%s:/woj/problem/judge:ro", judgeDir),
|
||||
"-v", fmt.Sprintf("%s:/woj/problem/data:ro", dataDir),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%d.out.usr", ansFile, id),
|
||||
"-v", fmt.Sprintf("%s:/woj/user/%d.judge", jdgFile, id),
|
||||
"-e", fmt.Sprintf("TEST_NUM=%d", id),
|
||||
"-e", fmt.Sprintf("CMP=%s", c.Cmp),
|
||||
"git.0x7f.app/woj/ubuntu-full:latest",
|
||||
"sh", "-c",
|
||||
fmt.Sprintf("cd /woj/user && make -f %s judge", s.getLangScript(&c, lang)),
|
||||
}
|
||||
|
||||
runArgs := &podmanArgs{
|
||||
executeArgs: executeArgs{
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
|
||||
return s.podmanRun(runArgs)
|
||||
}).
|
||||
Done()
|
||||
|
||||
if err != nil {
|
||||
s.log.Info("[judge] judge failed",
|
||||
zap.Error(err),
|
||||
zap.Uint("version", version),
|
||||
zap.String("user", user),
|
||||
zap.String("lang", lang),
|
||||
)
|
||||
}
|
||||
}
|
||||
}(task.Id)
|
||||
|
||||
id := s.pool.AddTask(f)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
s.pool.WaitForTask(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status) {
|
||||
// 1. ensure problem/user exists
|
||||
status := s.check(version, user, lang)
|
||||
if status != e.Success {
|
||||
return JudgeStatus{Message: "check failed"}, 0, status
|
||||
}
|
||||
|
||||
// 2. config
|
||||
config, err := s.ParseConfig(version, false)
|
||||
if err != nil {
|
||||
return JudgeStatus{Message: "parse config failed"}, 0, e.RunnerProblemParseFailed
|
||||
}
|
||||
|
||||
// 3. run user program
|
||||
s.problemRun(version, user, lang, &config)
|
||||
|
||||
// 4. run judger
|
||||
s.problemJudge(version, user, lang, &config)
|
||||
|
||||
// 5. check result
|
||||
result, pts := s.checkResults(user, &config)
|
||||
|
||||
return result, pts, e.Success
|
||||
}
|
@ -4,8 +4,11 @@ import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/log"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/pool"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"github.com/samber/do"
|
||||
"go.uber.org/zap"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
@ -19,7 +22,7 @@ type Service interface {
|
||||
// Compile compile user submission
|
||||
Compile(version uint, user string, lang string) (JudgeStatus, e.Status)
|
||||
// RunAndJudge execute user program
|
||||
RunAndJudge(version uint, user string, lang string, config *Config) (JudgeStatus, int32, e.Status)
|
||||
RunAndJudge(version uint, user string, lang string) (JudgeStatus, int32, e.Status)
|
||||
|
||||
// ParseConfig parse config file
|
||||
ParseConfig(version uint, skipCheck bool) (Config, error)
|
||||
@ -27,20 +30,33 @@ type Service interface {
|
||||
ProblemExists(version uint) bool
|
||||
|
||||
HealthCheck() error
|
||||
Shutdown() error
|
||||
}
|
||||
|
||||
func NewService(i *do.Injector) (Service, error) {
|
||||
return &service{
|
||||
concurrency := utils.If(runtime.NumCPU() > 1, runtime.NumCPU()-1, 1)
|
||||
|
||||
srv := &service{
|
||||
log: do.MustInvoke[log.Service](i).GetLogger("runner"),
|
||||
pool: pool.NewTaskPool(concurrency, concurrency),
|
||||
verbose: do.MustInvoke[config.Service](i).GetConfig().Development,
|
||||
}, nil
|
||||
}
|
||||
srv.pool.Start()
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
type service struct {
|
||||
log *zap.Logger
|
||||
pool *pool.TaskPool
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (s *service) HealthCheck() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) Shutdown() error {
|
||||
s.pool.Stop()
|
||||
return nil
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/file"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@ -57,7 +57,7 @@ func (t *TaskStatus) getInfoText(infoFile string) *TaskStatus {
|
||||
}
|
||||
|
||||
var err error
|
||||
t.infoText, err = utils.FileRead(infoFile)
|
||||
t.infoText, err = file.Read(infoFile)
|
||||
if err != nil {
|
||||
t.Verdict = VerdictSystemError
|
||||
t.Message = "cannot read info file"
|
||||
@ -128,7 +128,7 @@ func (t *TaskStatus) getJudgeText(judgeFile string) *TaskStatus {
|
||||
return t
|
||||
}
|
||||
|
||||
j, err := utils.FileRead(judgeFile)
|
||||
j, err := file.Read(judgeFile)
|
||||
if err != nil {
|
||||
t.Verdict = VerdictSystemError
|
||||
t.Message = "cannot read judge file"
|
||||
|
@ -9,9 +9,9 @@ import (
|
||||
)
|
||||
|
||||
type CreateData struct {
|
||||
UserName string
|
||||
Password string
|
||||
Email string
|
||||
NickName string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (s *service) Create(data *CreateData) (*model.User, e.Status) {
|
||||
@ -22,9 +22,9 @@ func (s *service) Create(data *CreateData) (*model.User, e.Status) {
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
UserName: data.UserName,
|
||||
Password: hashed,
|
||||
Email: data.Email,
|
||||
NickName: data.NickName,
|
||||
Password: hashed,
|
||||
Role: model.RoleGeneral,
|
||||
IsEnabled: true,
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ import (
|
||||
)
|
||||
|
||||
type LoginData struct {
|
||||
UserName string
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (s *service) Login(data *LoginData) (*model.User, e.Status) {
|
||||
user := &model.User{UserName: data.UserName}
|
||||
user := &model.User{Email: data.Email}
|
||||
|
||||
err := s.db.Get().Where(user).First(&user).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@ -29,6 +29,10 @@ func (s *service) Login(data *LoginData) (*model.User, e.Status) {
|
||||
if !user.IsEnabled {
|
||||
return nil, e.UserDisabled
|
||||
}
|
||||
if len(user.Password) == 0 {
|
||||
// created by oauth
|
||||
return nil, e.UserWithoutPassword
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(user.Password, []byte(data.Password))
|
||||
if err != nil {
|
||||
|
@ -22,3 +22,22 @@ func (s *service) Profile(uid uint) (*model.User, e.Status) {
|
||||
|
||||
return user, e.Success
|
||||
}
|
||||
|
||||
func (s *service) ProfileOrCreate(data *CreateData) (*model.User, e.Status) {
|
||||
user := &model.User{
|
||||
Email: data.Email,
|
||||
NickName: data.NickName,
|
||||
Role: model.RoleGeneral,
|
||||
IsEnabled: true,
|
||||
}
|
||||
|
||||
// Notice: FirstOrCreate will not update the record if it exists, and also we should not update the record
|
||||
// Notice: OAuth2 created user will not have password
|
||||
err := s.db.Get().Where(model.User{Email: data.Email}).FirstOrCreate(&user, data).Error
|
||||
if err != nil {
|
||||
s.log.Warn("DatabaseError", zap.Error(err), zap.Any("user", user))
|
||||
return nil, e.DatabaseError
|
||||
}
|
||||
|
||||
return user, e.Success
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ type Service interface {
|
||||
Login(data *LoginData) (*model.User, e.Status)
|
||||
IncrVersion(uid uint) (int64, e.Status)
|
||||
Profile(uid uint) (*model.User, e.Status)
|
||||
ProfileOrCreate(data *CreateData) (*model.User, e.Status)
|
||||
|
||||
HealthCheck() error
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ func NewService(i *do.Injector) (Service, error) {
|
||||
srv.cacheService = do.MustInvoke[cache.Service](i) // .Get().(*redis.Client)
|
||||
|
||||
conf := do.MustInvoke[config.Service](i).GetConfig()
|
||||
srv.SigningKey = []byte(conf.WebServer.JwtSigningKey)
|
||||
srv.ExpireHour = conf.WebServer.JwtExpireHour
|
||||
srv.SigningKey = []byte(conf.WebServer.JWT.SigningKey)
|
||||
srv.ExpireHour = conf.WebServer.JWT.ExpireHour
|
||||
|
||||
return srv, srv.err
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package metrics
|
||||
import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/e"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/misc/config"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/pkg/cast"
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/cast"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/samber/do"
|
||||
@ -61,7 +61,7 @@ func (s *service) setup(namespace string, subsystem string) {
|
||||
Subsystem: subsystem,
|
||||
Name: "requests_details",
|
||||
Help: "Details of each request",
|
||||
Buckets: []float64{0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 25, 50, 100, 250, 500, 1000},
|
||||
Buckets: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 1.5, 2, 2.5, 3, 5, 10, 15, 20, 25, 50, 75, 100, 150, 200, 500, 750, 1000, 1500, 2000},
|
||||
},
|
||||
[]string{"method", "url", "success", "http_code", "err_code"},
|
||||
)
|
||||
|
@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"git.0x7f.app/WOJ/woj-server/internal/api/debug"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/api/oauth"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/api/problem"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/api/status"
|
||||
"git.0x7f.app/WOJ/woj-server/internal/api/submission"
|
||||
@ -30,4 +31,5 @@ var endpoints = []model.EndpointInfo{
|
||||
{Version: "/v1", Path: "/problem", Register: problem.RouteRegister},
|
||||
{Version: "/v1", Path: "/submission", Register: submission.RouteRegister},
|
||||
{Version: "/v1", Path: "/status", Register: status.RouteRegister},
|
||||
{Version: "/v1", Path: "/oauth", Register: oauth.RouteRegister},
|
||||
}
|
||||
|
@ -40,9 +40,9 @@ func NewService(i *do.Injector) (Service, error) {
|
||||
}
|
||||
|
||||
type service struct {
|
||||
metric metrics.Service
|
||||
logger log.Service
|
||||
engine *gin.Engine
|
||||
metric metrics.Service
|
||||
err error
|
||||
}
|
||||
|
||||
@ -58,15 +58,20 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En
|
||||
gin.SetMode(utils.If[string](conf.Development, gin.DebugMode, gin.ReleaseMode))
|
||||
|
||||
r := gin.New()
|
||||
r.MaxMultipartMemory = 8 << 20 // 8MB
|
||||
|
||||
// +--------------+
|
||||
// |Configurations|
|
||||
// +--------------+
|
||||
|
||||
if conf.WebServer.TrustedPlatform != "" {
|
||||
// Extract Origin IP
|
||||
r.TrustedPlatform = conf.WebServer.TrustedPlatform
|
||||
}
|
||||
|
||||
// +-----------+
|
||||
// |Middlewares|
|
||||
// +-----------+
|
||||
|
||||
// static files - must before sentry
|
||||
r.Use(static.Serve("/", static.LocalFile("./resource/frontend", true)))
|
||||
|
||||
// Sentry middleware
|
||||
r.Use(sentrygin.New(sentrygin.Options{Repanic: true}))
|
||||
|
||||
@ -129,6 +134,9 @@ func (s *service) initRouters(conf *model.Config, injector *do.Injector) *gin.En
|
||||
api := r.Group("/api/")
|
||||
s.setupApi(api, injector)
|
||||
|
||||
// static files
|
||||
r.Use(static.Serve("/", static.LocalFile("./resource/frontend", true)))
|
||||
|
||||
// fallback to frontend
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
c.File("./resource/frontend/index.html")
|
||||
|
@ -1 +0,0 @@
|
||||
package web
|
45
pkg/file/file.go
Normal file
45
pkg/file/file.go
Normal file
@ -0,0 +1,45 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"git.0x7f.app/WOJ/woj-server/pkg/utils"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Read(filePath string) ([]byte, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func Write(filePath string, content []byte) error {
|
||||
return os.WriteFile(filePath, content, 0644)
|
||||
}
|
||||
|
||||
func Exist(filePath string) bool {
|
||||
_, err := os.Stat(filePath)
|
||||
return utils.If(err == nil || os.IsExist(err), true, false)
|
||||
}
|
||||
|
||||
func Empty(filePath string) bool {
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return stat.Size() == 0
|
||||
}
|
||||
|
||||
func Touch(filePath string) bool {
|
||||
err := TouchErr(filePath)
|
||||
return utils.If(err == nil, true, false)
|
||||
}
|
||||
|
||||
func TouchErr(filePath string) error {
|
||||
base := filepath.Dir(filePath)
|
||||
_ = os.MkdirAll(base, 0755)
|
||||
_, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||
return err
|
||||
}
|
21
pkg/file/writer.go
Normal file
21
pkg/file/writer.go
Normal file
@ -0,0 +1,21 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type LimitedWriter struct {
|
||||
File *os.File
|
||||
Limit int64
|
||||
n int64
|
||||
}
|
||||
|
||||
func (lw *LimitedWriter) Write(p []byte) (n int, err error) {
|
||||
if lw.n+int64(len(p)) > lw.Limit {
|
||||
return 0, fmt.Errorf("output limit exceeded")
|
||||
}
|
||||
n, err = lw.File.Write(p)
|
||||
lw.n += int64(n)
|
||||
return
|
||||
}
|
78
pkg/pool/pool.go
Normal file
78
pkg/pool/pool.go
Normal file
@ -0,0 +1,78 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type TaskPool struct {
|
||||
workers int
|
||||
queue chan Task
|
||||
wg sync.WaitGroup
|
||||
|
||||
lck sync.Mutex
|
||||
curTaskID int
|
||||
waitMap map[int]chan struct{}
|
||||
}
|
||||
|
||||
func NewTaskPool(maxWorkers, bufferSize int) *TaskPool {
|
||||
return &TaskPool{
|
||||
workers: maxWorkers,
|
||||
queue: make(chan Task, bufferSize),
|
||||
waitMap: make(map[int]chan struct{}),
|
||||
curTaskID: 1, // task id starts from 1
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *TaskPool) Start() {
|
||||
for i := 1; i <= tp.workers; i++ { // worker id starts from 1
|
||||
worker := NewWorker(i, tp.queue, tp)
|
||||
tp.wg.Add(1)
|
||||
go worker.Start(&tp.wg)
|
||||
}
|
||||
}
|
||||
|
||||
func (tp *TaskPool) AddTask(f func()) int {
|
||||
tp.lck.Lock()
|
||||
defer tp.lck.Unlock()
|
||||
|
||||
id := tp.curTaskID
|
||||
tp.curTaskID++
|
||||
|
||||
task := Task{id: id, f: f}
|
||||
tp.queue <- task
|
||||
|
||||
waitChan := make(chan struct{})
|
||||
tp.waitMap[id] = waitChan
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (tp *TaskPool) WaitForTask(taskID int) {
|
||||
tp.lck.Lock()
|
||||
waitChan, ok := tp.waitMap[taskID]
|
||||
if !ok {
|
||||
tp.lck.Unlock()
|
||||
return
|
||||
}
|
||||
tp.lck.Unlock()
|
||||
|
||||
<-waitChan
|
||||
}
|
||||
|
||||
func (tp *TaskPool) markTaskComplete(taskID int) {
|
||||
tp.lck.Lock()
|
||||
defer tp.lck.Unlock()
|
||||
|
||||
waitChan, ok := tp.waitMap[taskID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
close(waitChan)
|
||||
delete(tp.waitMap, taskID)
|
||||
}
|
||||
|
||||
func (tp *TaskPool) Stop() {
|
||||
close(tp.queue)
|
||||
tp.wg.Wait()
|
||||
}
|
62
pkg/pool/pool_test.go
Normal file
62
pkg/pool/pool_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTaskPool_Stop(t *testing.T) {
|
||||
pool := NewTaskPool(5, 10)
|
||||
pool.Start()
|
||||
|
||||
lck := sync.Mutex{}
|
||||
counter := 0
|
||||
|
||||
for i := 1; i <= 10; i++ {
|
||||
f := func(i int) func() {
|
||||
return func() {
|
||||
lck.Lock()
|
||||
t.Log("task", i, "locked")
|
||||
counter += i
|
||||
t.Log("task", i, "unlocked")
|
||||
lck.Unlock()
|
||||
|
||||
time.Sleep(time.Duration(i*100) * time.Millisecond)
|
||||
t.Log("task", i, "finished")
|
||||
}
|
||||
}(i)
|
||||
pool.AddTask(f)
|
||||
}
|
||||
|
||||
pool.Stop()
|
||||
|
||||
if counter != 55 {
|
||||
t.Error("some tasks were not executed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskPool_WaitForTask(t *testing.T) {
|
||||
pool := NewTaskPool(10, 10)
|
||||
pool.Start()
|
||||
|
||||
counter := 0
|
||||
|
||||
for i := 1; i <= 10; i++ {
|
||||
f := func(i int) func() {
|
||||
return func() {
|
||||
counter += 1
|
||||
t.Log("task", i, "finished")
|
||||
}
|
||||
}(i)
|
||||
id := pool.AddTask(f)
|
||||
|
||||
pool.WaitForTask(id)
|
||||
if counter != 1 {
|
||||
t.Errorf("Counter mismatch: expected %d, got %d, task %d", 1, counter, id)
|
||||
}
|
||||
counter -= 1
|
||||
}
|
||||
|
||||
pool.Stop()
|
||||
}
|
6
pkg/pool/task.go
Normal file
6
pkg/pool/task.go
Normal file
@ -0,0 +1,6 @@
|
||||
package pool
|
||||
|
||||
type Task struct {
|
||||
id int
|
||||
f func()
|
||||
}
|
24
pkg/pool/worker.go
Normal file
24
pkg/pool/worker.go
Normal file
@ -0,0 +1,24 @@
|
||||
package pool
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
id int
|
||||
queue chan Task
|
||||
pool *TaskPool // back reference to the pool
|
||||
}
|
||||
|
||||
func NewWorker(id int, queue chan Task, pool *TaskPool) *Worker {
|
||||
return &Worker{id: id, queue: queue, pool: pool}
|
||||
}
|
||||
|
||||
func (w *Worker) Start(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for task := range w.queue {
|
||||
task.f()
|
||||
w.pool.markTaskComplete(task.id)
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func FileRead(filePath string) ([]byte, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return io.ReadAll(f)
|
||||
}
|
||||
|
||||
func FileWrite(filePath string, content []byte) error {
|
||||
return os.WriteFile(filePath, content, 0644)
|
||||
}
|
||||
|
||||
func FileExist(filePath string) bool {
|
||||
_, err := os.Stat(filePath)
|
||||
return If(err == nil || os.IsExist(err), true, false)
|
||||
}
|
||||
|
||||
func FileEmpty(filePath string) bool {
|
||||
stat, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
return stat.Size() == 0
|
||||
}
|
||||
|
||||
func FileTouch(filePath string) bool {
|
||||
base := filepath.Dir(filePath)
|
||||
_ = os.MkdirAll(base, 0755)
|
||||
_, err := os.OpenFile(filePath, os.O_RDONLY|os.O_CREATE, 0644)
|
||||
return If(err == nil, true, false)
|
||||
}
|
25
pkg/utils/must.go
Normal file
25
pkg/utils/must.go
Normal file
@ -0,0 +1,25 @@
|
||||
package utils
|
||||
|
||||
type MustChain struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewMust() *MustChain {
|
||||
return &MustChain{}
|
||||
}
|
||||
|
||||
func (c *MustChain) Do(callback func() error) *MustChain {
|
||||
if c.err == nil {
|
||||
c.err = callback()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *MustChain) DoAny(callback func() error) *MustChain {
|
||||
_ = callback()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *MustChain) Done() error {
|
||||
return c.err
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
@ -14,3 +17,22 @@ func RandomString(n int) string {
|
||||
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func SignString(s string, key []byte) string {
|
||||
mac := hmac.New(sha512.New, key)
|
||||
mac.Write([]byte(s))
|
||||
|
||||
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func SignAndCompare(s string, exp string, key []byte) bool {
|
||||
mac := hmac.New(sha512.New, key)
|
||||
mac.Write([]byte(s))
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(exp)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return hmac.Equal(mac.Sum(nil), decoded)
|
||||
}
|
||||
|
51
pkg/utils/try.go
Normal file
51
pkg/utils/try.go
Normal file
@ -0,0 +1,51 @@
|
||||
package utils
|
||||
|
||||
type TryChain[T any, V comparable] struct {
|
||||
result T
|
||||
error V
|
||||
success V
|
||||
}
|
||||
|
||||
func NewTry[T any, V comparable](success V) *TryChain[T, V] {
|
||||
return &TryChain[T, V]{success: success}
|
||||
}
|
||||
|
||||
func (c *TryChain[T, V]) Try(callback func() (T, V)) *TryChain[T, V] {
|
||||
c.result, c.error = callback()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TryChain[T, V]) Or(callback func() (T, V)) *TryChain[T, V] {
|
||||
if c.error == c.success {
|
||||
return c
|
||||
}
|
||||
return c.Try(callback)
|
||||
}
|
||||
|
||||
func (c *TryChain[T, V]) Done() (T, V) {
|
||||
return c.result, c.error
|
||||
}
|
||||
|
||||
type TryChainErr struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func NewTryErr() *TryChainErr {
|
||||
return &TryChainErr{}
|
||||
}
|
||||
|
||||
func (c *TryChainErr) Try(callback func() error) *TryChainErr {
|
||||
c.err = callback()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TryChainErr) Or(callback func() error) *TryChainErr {
|
||||
if c.err == nil {
|
||||
return c
|
||||
}
|
||||
return c.Try(callback)
|
||||
}
|
||||
|
||||
func (c *TryChainErr) Done() error {
|
||||
return c.err
|
||||
}
|
57
pkg/utils/try_test.go
Normal file
57
pkg/utils/try_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package utils
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTry(t *testing.T) {
|
||||
t.Run("First Try", func(t *testing.T) {
|
||||
val, err := NewTry[int, bool](true).
|
||||
Try(func() (int, bool) { return 1, true }).
|
||||
Or(func() (int, bool) { return 2, false }).
|
||||
Or(func() (int, bool) { return 3, true }).
|
||||
Or(func() (int, bool) { return 4, false }).
|
||||
Done()
|
||||
|
||||
if val != 1 && err != true {
|
||||
t.Error("Try failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Middle Or", func(t *testing.T) {
|
||||
val, err := NewTry[int, bool](true).
|
||||
Try(func() (int, bool) { return 1, false }).
|
||||
Or(func() (int, bool) { return 2, false }).
|
||||
Or(func() (int, bool) { return 3, true }).
|
||||
Or(func() (int, bool) { return 4, false }).
|
||||
Done()
|
||||
|
||||
if val != 3 && err != true {
|
||||
t.Error("Try failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Last Or", func(t *testing.T) {
|
||||
val, err := NewTry[int, bool](true).
|
||||
Try(func() (int, bool) { return 1, false }).
|
||||
Or(func() (int, bool) { return 2, false }).
|
||||
Or(func() (int, bool) { return 3, false }).
|
||||
Or(func() (int, bool) { return 4, true }).
|
||||
Done()
|
||||
|
||||
if val != 4 && err != true {
|
||||
t.Error("Try failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Nothing", func(t *testing.T) {
|
||||
val, err := NewTry[int, bool](true).
|
||||
Try(func() (int, bool) { return 1, false }).
|
||||
Or(func() (int, bool) { return 2, false }).
|
||||
Or(func() (int, bool) { return 3, false }).
|
||||
Or(func() (int, bool) { return 4, false }).
|
||||
Done()
|
||||
|
||||
if val != 4 && err != false {
|
||||
t.Error("Try failed")
|
||||
}
|
||||
})
|
||||
}
|
@ -60,7 +60,8 @@ metadata:
|
||||
labels:
|
||||
app: cache
|
||||
spec:
|
||||
type: ClusterIP
|
||||
# for production use ClusterIP
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: cache
|
||||
ports:
|
||||
|
@ -70,7 +70,8 @@ metadata:
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
type: ClusterIP
|
||||
# for production use ClusterIP
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: db
|
||||
ports:
|
||||
|
@ -35,7 +35,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: runner
|
||||
image: git.0x7f.app/woj/woj-runner:1.2.1
|
||||
image: git.0x7f.app/woj/woj-runner:1.2.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- runner
|
||||
|
@ -20,7 +20,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: server
|
||||
image: git.0x7f.app/woj/woj-server:1.2.1
|
||||
image: git.0x7f.app/woj/woj-server:1.2.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- server
|
||||
|
@ -10,6 +10,5 @@ cmake .. -DCMAKE_BUILD_TYPE=Release || exit 1
|
||||
make -j || exit 1
|
||||
|
||||
cd ../..
|
||||
cp woj-sandbox/build/libwoj_sandbox.so . || exit 1
|
||||
cp woj-sandbox/build/woj_launcher . || exit 1
|
||||
rm -rf woj-sandbox || exit 1
|
||||
|
@ -4,4 +4,4 @@ compile:
|
||||
@$(CC) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
||||
|
||||
judge:
|
||||
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||
$($(CMP)) $(PREFIX)/problem/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/problem/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||
|
@ -4,4 +4,4 @@ compile:
|
||||
@$(CXX) $(CFLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
||||
|
||||
judge:
|
||||
$($(CMP)) $(PREFIX)/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||
$($(CMP)) $(PREFIX)/problem/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/problem/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
||||
|
7
resource/runner/framework/template/default/go.Makefile
Normal file
7
resource/runner/framework/template/default/go.Makefile
Normal file
@ -0,0 +1,7 @@
|
||||
include ${TEMPLATE}/go.mk ${TEMPLATE}/Judger.mk
|
||||
|
||||
compile:
|
||||
@$(GO) build $(GO_BUILD_FLAGS) -o $(PREFIX)/user/$(USER_PROG).out $(PREFIX)/user/$(USER_PROG).$(LANG)
|
||||
|
||||
judge:
|
||||
$($(CMP)) $(PREFIX)/problem/data/input/$(TEST_NUM).input $(PREFIX)/user/$(TEST_NUM).out.usr $(PREFIX)/problem/data/output/$(TEST_NUM).output $(PREFIX)/user/$(TEST_NUM).judge -appes
|
2
resource/runner/framework/template/go.mk
Normal file
2
resource/runner/framework/template/go.mk
Normal file
@ -0,0 +1,2 @@
|
||||
GO=/usr/local/go/bin/go
|
||||
GO_BUILD_FLAGS=-trimpath
|
@ -5,4 +5,4 @@ git clone --depth=1 https://github.com/MikeMirzayanov/testlib.git >/dev/null 2>&
|
||||
rm -rf testlib/.git
|
||||
rm -rf testlib/tests
|
||||
cd testlib/checkers || exit 1
|
||||
parallel clang++ -Ofast -march=native -Wall -pipe -I.. {}.cpp -o {} ::: fcmp hcmp lcmp ncmp nyesno rcmp4 rcmp6 rcmp9 wcmp yesno
|
||||
parallel clang++ -O2 -Wall -pipe -I.. {}.cpp -o {} ::: fcmp hcmp lcmp ncmp nyesno rcmp4 rcmp6 rcmp9 wcmp yesno
|
||||
|
2
resource/runner/problem/.gitignore
vendored
2
resource/runner/problem/.gitignore
vendored
@ -2,3 +2,5 @@
|
||||
!.gitignore
|
||||
!example
|
||||
!example/*
|
||||
!book
|
||||
!book/**/*
|
||||
|
43
resource/runner/problem/book/ch1/1. factorial/config.json
Normal file
43
resource/runner/problem/book/ch1/1. factorial/config.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"Runtime": {
|
||||
"TimeLimit": 1000,
|
||||
"MemoryLimit": 16,
|
||||
"NProcLimit": 1
|
||||
},
|
||||
"Languages": [
|
||||
{
|
||||
"Lang": "c",
|
||||
"Type": "default",
|
||||
"Script": "",
|
||||
"Cmp": "HCMP"
|
||||
},
|
||||
{
|
||||
"Lang": "cpp",
|
||||
"Type": "default",
|
||||
"Script": "",
|
||||
"Cmp": "HCMP"
|
||||
}
|
||||
],
|
||||
"Tasks": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 3,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 4,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 5,
|
||||
"Points": 20
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
20
|
@ -0,0 +1 @@
|
||||
55
|
@ -0,0 +1 @@
|
||||
88
|
@ -0,0 +1 @@
|
||||
133
|
@ -0,0 +1 @@
|
||||
345
|
@ -0,0 +1 @@
|
||||
2432902008176640000
|
@ -0,0 +1 @@
|
||||
12696403353658275925965100847566516959580321051449436762275840000000000000
|
@ -0,0 +1 @@
|
||||
185482642257398439114796845645546284380220968949399346684421580986889562184028199319100141244804501828416633516851200000000000000000000
|
@ -0,0 +1 @@
|
||||
14872707060906857289084508911813048098675809251055070300508818286592035566485075754388082124671571841702793317081960037166525246368924700537538282948117301741317436012998958826217903503076596121600000000000000000000000000000000
|
@ -0,0 +1 @@
|
||||
24215638650792346558700053691985855570120556040258652734839783267039961720178323593174739047913617079695531502689473012213820889134885853992818438056445080201482863675240494802269823110125881000284687377104376400792200165127855908498047507347955446603093964326987087311394274684237308398502911304969719715098068025497504900730580217016573270011698467378924291550780873605154736879542602554635558428265690302091342359471863508627516511203478353542187151045838267239168928747525890559708487655213488727530884968558716385000436989129479527833010340517760688345368715729020015336862534353876914871201776699205878662858555857265544230999178449256448000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
26
resource/runner/problem/book/ch1/1. factorial/description.md
Normal file
26
resource/runner/problem/book/ch1/1. factorial/description.md
Normal file
@ -0,0 +1,26 @@
|
||||
# 求 N! 的值
|
||||
|
||||
## Tags
|
||||
|
||||
- C++一本通
|
||||
- 高精度
|
||||
|
||||
## Description
|
||||
|
||||
用高精度方法,求$N!$的精确值($N$以一般整数输入)
|
||||
|
||||
## Example Cases
|
||||
|
||||
### Case 1
|
||||
|
||||
#### Input
|
||||
|
||||
```
|
||||
10
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
```
|
||||
3628800
|
||||
```
|
47
resource/runner/problem/book/ch1/2. divide/config.json
Normal file
47
resource/runner/problem/book/ch1/2. divide/config.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"Runtime": {
|
||||
"TimeLimit": 1000,
|
||||
"MemoryLimit": 16,
|
||||
"NProcLimit": 1
|
||||
},
|
||||
"Languages": [
|
||||
{
|
||||
"Lang": "c",
|
||||
"Type": "default",
|
||||
"Script": "",
|
||||
"Cmp": "FCMP"
|
||||
},
|
||||
{
|
||||
"Lang": "cpp",
|
||||
"Type": "default",
|
||||
"Script": "",
|
||||
"Cmp": "FCMP"
|
||||
}
|
||||
],
|
||||
"Tasks": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Points": 10
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"Points": 10
|
||||
},
|
||||
{
|
||||
"Id": 3,
|
||||
"Points": 10
|
||||
},
|
||||
{
|
||||
"Id": 4,
|
||||
"Points": 23
|
||||
},
|
||||
{
|
||||
"Id": 5,
|
||||
"Points": 22
|
||||
},
|
||||
{
|
||||
"Id": 6,
|
||||
"Points": 25
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
6 5
|
@ -0,0 +1 @@
|
||||
30 6
|
@ -0,0 +1 @@
|
||||
18 5
|
@ -0,0 +1 @@
|
||||
12 7
|
@ -0,0 +1 @@
|
||||
8 3
|
@ -0,0 +1 @@
|
||||
1034 1033
|
@ -0,0 +1 @@
|
||||
6/5=1.2
|
@ -0,0 +1 @@
|
||||
30/6=5.0
|
@ -0,0 +1 @@
|
||||
18/5=3.6
|
@ -0,0 +1 @@
|
||||
12/7=1.71428571428571428571
|
@ -0,0 +1 @@
|
||||
8/3=2.66666666666666666666
|
@ -0,0 +1 @@
|
||||
1034/1033=1.000968054211035818
|
42
resource/runner/problem/book/ch1/2. divide/description.md
Normal file
42
resource/runner/problem/book/ch1/2. divide/description.md
Normal file
@ -0,0 +1,42 @@
|
||||
# 求A/B高精度值
|
||||
|
||||
## Tags
|
||||
|
||||
- C++一本通
|
||||
- 高精度
|
||||
|
||||
## Description
|
||||
|
||||
计算$\frac AB$的精确值,设$A$,$B$是以一般整数输入,计算结果精确小数后$20$位
|
||||
|
||||
(若不足$20$位,末尾不用补$0$)
|
||||
|
||||
## Example Cases
|
||||
|
||||
### Case 1
|
||||
|
||||
#### Input
|
||||
|
||||
```
|
||||
4 3
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
```
|
||||
4/3=1.33333333333333333333
|
||||
```
|
||||
|
||||
### Case 2
|
||||
|
||||
#### Input
|
||||
|
||||
```
|
||||
6 5
|
||||
```
|
||||
|
||||
#### Output
|
||||
|
||||
```
|
||||
6/5=1.2
|
||||
```
|
43
resource/runner/problem/book/ch1/3. acc_sum/config.json
Normal file
43
resource/runner/problem/book/ch1/3. acc_sum/config.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"Runtime": {
|
||||
"TimeLimit": 1000,
|
||||
"MemoryLimit": 16,
|
||||
"NProcLimit": 1
|
||||
},
|
||||
"Languages": [
|
||||
{
|
||||
"Lang": "c",
|
||||
"Type": "default",
|
||||
"Script": "",
|
||||
"Cmp": "NCMP"
|
||||
},
|
||||
{
|
||||
"Lang": "cpp",
|
||||
"Type": "default",
|
||||
"Script": "",
|
||||
"Cmp": "NCMP"
|
||||
}
|
||||
],
|
||||
"Tasks": [
|
||||
{
|
||||
"Id": 1,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 2,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 3,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 4,
|
||||
"Points": 20
|
||||
},
|
||||
{
|
||||
"Id": 5,
|
||||
"Points": 20
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1 @@
|
||||
123
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user