Compare commits
484 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e6efc2319 | ||
|
|
1d5bae0c62 | ||
|
|
368bae8c07 | ||
|
|
58a19affcb | ||
|
|
8754a5edfa | ||
|
|
aeb41008c9 | ||
|
|
059e19d790 | ||
|
|
baa9bad6f8 | ||
|
|
94e13cb93b | ||
|
|
c800dd4bf9 | ||
|
|
7c91d200e4 | ||
|
|
d45daeab7a | ||
|
|
b3ccea5f0e | ||
|
|
3178086dd7 | ||
|
|
1e4e5b3c65 | ||
|
|
7c8e74716d | ||
|
|
46a5799079 | ||
|
|
9feb788108 | ||
|
|
e2f3e005ef | ||
|
|
9d54ea4c49 | ||
|
|
671e875f12 | ||
|
|
f708fce112 | ||
|
|
460c466053 | ||
|
|
84ad0a4cac | ||
|
|
129690e901 | ||
|
|
9191861cab | ||
|
|
c05e42f597 | ||
|
|
09a0c9c3f2 | ||
|
|
859d0c8f1a | ||
|
|
baaeaf43b1 | ||
|
|
76b376aef7 | ||
|
|
3df6a24a0a | ||
|
|
efa7a1d7b5 | ||
|
|
5c6406c1e7 | ||
|
|
3392251dda | ||
|
|
bbad4539b1 | ||
|
|
6b0d7c0ce1 | ||
|
|
e03fbadcaa | ||
|
|
42b003db62 | ||
|
|
d8eed66a1d | ||
|
|
8083ba086b | ||
|
|
02210f1f8d | ||
|
|
817a19c757 | ||
|
|
36da07f277 | ||
|
|
83056bbf4f | ||
|
|
37a4a80996 | ||
|
|
9d4256306e | ||
|
|
ddfc9249ec | ||
|
|
a431b7050f | ||
|
|
debd4b23df | ||
|
|
410610349b | ||
|
|
3ac7a559df | ||
|
|
7c878b1ee3 | ||
|
|
a1506d0f31 | ||
|
|
15ff2f3282 | ||
|
|
ffb0abe4da | ||
|
|
3dbf4f9b19 | ||
|
|
b02cd5c4f0 | ||
|
|
0c768bb3d6 | ||
|
|
f78bcf3e4b | ||
|
|
535e725618 | ||
|
|
4aaeda3fbb | ||
|
|
600243c87d | ||
|
|
82aebf6647 | ||
|
|
89ae98b035 | ||
|
|
ed3b195b26 | ||
|
|
f54089e072 | ||
|
|
6bb82b2204 | ||
|
|
d77d69a331 | ||
|
|
297ec9005c | ||
|
|
5649d4410a | ||
|
|
db32479c4e | ||
|
|
8fa53f4e32 | ||
|
|
a7acebc5d0 | ||
|
|
954bb23bb8 | ||
|
|
7134782245 | ||
|
|
5a42dc9857 | ||
|
|
9caf45af78 | ||
|
|
56e3ce328e | ||
|
|
6de1828ffe | ||
|
|
b2e275a6cd | ||
|
|
45dba68b15 | ||
|
|
31c670252c | ||
|
|
1831e44eef | ||
|
|
71f5de3bf9 | ||
|
|
88a0911f93 | ||
|
|
8686c4a6e5 | ||
|
|
68acb5d790 | ||
|
|
2c50ae2290 | ||
|
|
f09500f289 | ||
|
|
1147aeda14 | ||
|
|
7b57f73b26 | ||
|
|
45cc9e8245 | ||
|
|
795771e3d6 | ||
|
|
2ef1a93eaf | ||
|
|
6ccbea8bd9 | ||
|
|
003eb5b995 | ||
|
|
0ab6f58d36 | ||
|
|
763f1e5d11 | ||
|
|
16cf91f669 | ||
|
|
0d8db25c3c | ||
|
|
2084d0958b | ||
|
|
a67a077024 | ||
|
|
f1ece58c7b | ||
|
|
fd34d5a972 | ||
|
|
bdf3598367 | ||
|
|
b9d35fa626 | ||
|
|
7da993a4c7 | ||
|
|
ddb16f899f | ||
|
|
dea9926e4f | ||
|
|
61eb55f736 | ||
|
|
e2521eea94 | ||
|
|
8f859cd600 | ||
|
|
0232384fe2 | ||
|
|
7873631613 | ||
|
|
466257d3be | ||
|
|
1ff7be38eb | ||
|
|
c87b5cec65 | ||
|
|
80afc18892 | ||
|
|
34a947ac6d | ||
|
|
d855e56a2f | ||
|
|
7ee98c1b03 | ||
|
|
af8569a605 | ||
|
|
d6199c9598 | ||
|
|
ba7db72093 | ||
|
|
83b040b39f | ||
|
|
d8a3e82f1e | ||
|
|
c40f9c9127 | ||
|
|
7e612458a0 | ||
|
|
72b791cc5e | ||
|
|
e46b739311 | ||
|
|
daf6a20e1d | ||
|
|
ba45936d77 | ||
|
|
ca64bd127e | ||
|
|
b93eb4911c | ||
|
|
20e9449bb1 | ||
|
|
c2e9d67bcb | ||
|
|
3989a735ac | ||
|
|
ede15c4fe5 | ||
|
|
99976dd560 | ||
|
|
0500550fc0 | ||
|
|
c5c2578d0f | ||
|
|
b843538fea | ||
|
|
9b56ffc87c | ||
|
|
a4f0dbbca8 | ||
|
|
b530f7a6cf | ||
|
|
421e9115cc | ||
|
|
b812887f6e | ||
|
|
b3f677d32f | ||
|
|
b755bcd1bf | ||
|
|
8d79680b81 | ||
|
|
a013192bf8 | ||
|
|
dceb6f9993 | ||
|
|
d922e58278 | ||
|
|
8caad44ade | ||
|
|
22d1bd7620 | ||
|
|
effa5c46c5 | ||
|
|
cebbcdf256 | ||
|
|
f557d16997 | ||
|
|
3e0fcfbfcf | ||
|
|
7377b4f15b | ||
|
|
ecbe7f6ad9 | ||
|
|
084c14175e | ||
|
|
5da77a6b4f | ||
|
|
84f55691eb | ||
|
|
4d5033f03c | ||
|
|
ff6c344a15 | ||
|
|
104498dec0 | ||
|
|
8654604b9b | ||
|
|
2bc64a3be8 | ||
|
|
2aaf37b766 | ||
|
|
47bd72e1cd | ||
|
|
29eedee3ec | ||
|
|
a349fdd0cf | ||
|
|
cf5248ec54 | ||
|
|
4fc53e7220 | ||
|
|
f8392ab0be | ||
|
|
43d3f84993 | ||
|
|
72e846894e | ||
|
|
84d2261391 | ||
|
|
608da9f64b | ||
|
|
91ace06f8b | ||
|
|
cdbb5e98f5 | ||
|
|
92e59672f9 | ||
|
|
3568b7a12a | ||
|
|
4195b7f2dc | ||
|
|
17fe1a938a | ||
|
|
c699455b58 | ||
|
|
000bc4f661 | ||
|
|
9308e1b388 | ||
|
|
1452317d45 | ||
|
|
41bfe4f4d3 | ||
|
|
b79b4c405a | ||
|
|
e7e281083f | ||
|
|
3459c7e8d6 | ||
|
|
6258f9a5e4 | ||
|
|
61b4108dce | ||
|
|
ce7aa1be62 | ||
|
|
e2226e7492 | ||
|
|
b903700077 | ||
|
|
46067dec8f | ||
|
|
11295a28f6 | ||
|
|
4e241d0448 | ||
|
|
a6e56a208c | ||
|
|
ccb0f410ab | ||
|
|
1a44b94c85 | ||
|
|
53e55728db | ||
|
|
53a3624aef | ||
|
|
d6ceaad220 | ||
|
|
e67b98d6fe | ||
|
|
a3f07057f4 | ||
|
|
610e087bcd | ||
|
|
fca46f1a32 | ||
|
|
f7d12e65e5 | ||
|
|
27c9ce681b | ||
|
|
1993950cd7 | ||
|
|
f475dbabb7 | ||
|
|
b7b530fe60 | ||
|
|
4fbfa1d4e3 | ||
|
|
a77069a634 | ||
|
|
aa10008d1b | ||
|
|
e6e537d799 | ||
|
|
2e91feb861 | ||
|
|
d805ba8eb8 | ||
|
|
ad07ad2014 | ||
|
|
5d65ff14d5 | ||
|
|
99ceb5d72c | ||
|
|
7f2f1d70b1 | ||
|
|
07f40351fa | ||
|
|
6525106116 | ||
|
|
3e116fcc52 | ||
|
|
cb968d782a | ||
|
|
10507d9c53 | ||
|
|
10c4cd9b7f | ||
|
|
38dc69e4a2 | ||
|
|
377306dc80 | ||
|
|
da7059dc79 | ||
|
|
9ec6541322 | ||
|
|
e80e31da42 | ||
|
|
a69a934029 | ||
|
|
e4aab1019c | ||
|
|
fb5d8c216b | ||
|
|
7977bce8a3 | ||
|
|
a8d47fd1bf | ||
|
|
1b82c48dc1 | ||
|
|
f7c2524098 | ||
|
|
61c70d3289 | ||
|
|
494ac0ed3b | ||
|
|
9c768277c7 | ||
|
|
e8cec39476 | ||
|
|
afb76aa970 | ||
|
|
d698660c19 | ||
|
|
f8fb128e97 | ||
|
|
f11cad157b | ||
|
|
b1b33713d6 | ||
|
|
072b674fb1 | ||
|
|
989d3b1c85 | ||
|
|
92aea18a8c | ||
|
|
f875d3fc42 | ||
|
|
cddf081b8f | ||
|
|
c1c4731640 | ||
|
|
a901c84d99 | ||
|
|
226bb0384e | ||
|
|
8703309090 | ||
|
|
da51639625 | ||
|
|
e83614068f | ||
|
|
56d8c4a928 | ||
|
|
85cef0dfec | ||
|
|
8dd9e9c9be | ||
|
|
196930281b | ||
|
|
878d7fe298 | ||
|
|
cccb3dce84 | ||
|
|
992b6e06da | ||
|
|
d7e7112e25 | ||
|
|
594b02d37e | ||
|
|
3b06905e15 | ||
|
|
f2d21c67dc | ||
|
|
ae3f91dfc6 | ||
|
|
882aae9daf | ||
|
|
c27c88c0fa | ||
|
|
240a1f92ce | ||
|
|
8e92f6822e | ||
|
|
b2e4848ed3 | ||
|
|
ddc841e8c6 | ||
|
|
49cb752b5c | ||
|
|
7fe3a893f6 | ||
|
|
7c0a2051d4 | ||
|
|
b7fb079243 | ||
|
|
f9a5ce0e68 | ||
|
|
d278ab6e3d | ||
|
|
4a627a30f2 | ||
|
|
75bb90c90d | ||
|
|
a0e549a6b9 | ||
|
|
d7e8ca5ace | ||
|
|
be83b1c287 | ||
|
|
8fef57aa8d | ||
|
|
cc414881ea | ||
|
|
65b6e3208f | ||
|
|
d5bb271469 | ||
|
|
c063dd92c6 | ||
|
|
42b82a368c | ||
|
|
6106eee97e | ||
|
|
2e1c837643 | ||
|
|
860c8b9d45 | ||
|
|
1f7b5139c6 | ||
|
|
e5ac43a59e | ||
|
|
a6bcf5cd94 | ||
|
|
7668ef7bd8 | ||
|
|
2ed6e8724f | ||
|
|
a4f47396e5 | ||
|
|
d237a5b6ea | ||
|
|
5b4f74e2b7 | ||
|
|
624eecd4e8 | ||
|
|
851d9a6fc1 | ||
|
|
167071af6f | ||
|
|
75e41e4fd0 | ||
|
|
0aeaa5580c | ||
|
|
70d9ff32b7 | ||
|
|
d985440930 | ||
|
|
39c63ec44a | ||
|
|
1dc0cabc2b | ||
|
|
b227260060 | ||
|
|
9231120a55 | ||
|
|
63e3a7f6bd | ||
|
|
3815e18027 | ||
|
|
73639eeb64 | ||
|
|
3175747ee5 | ||
|
|
2add701c39 | ||
|
|
f7d8a5dc50 | ||
|
|
ed21e32b1f | ||
|
|
bee6192894 | ||
|
|
e50dc102bb | ||
|
|
30584b3faa | ||
|
|
dc48c93804 | ||
|
|
0c532a7f93 | ||
|
|
c4895fae84 | ||
|
|
e17a358ade | ||
|
|
bb9f904104 | ||
|
|
40bd57c9a0 | ||
|
|
ea3263ca6c | ||
|
|
7455ad3f47 | ||
|
|
317079ddd3 | ||
|
|
d9def73096 | ||
|
|
e485cc8862 | ||
|
|
b6d6e464ed | ||
|
|
494979cf38 | ||
|
|
229cd4044c | ||
|
|
62fc68e88b | ||
|
|
ece1efad16 | ||
|
|
9bb0c151a9 | ||
|
|
3bd32715cb | ||
|
|
e10490adda | ||
|
|
8893e1e5bf | ||
|
|
c13099c56a | ||
|
|
eda90d74e6 | ||
|
|
ee58af488d | ||
|
|
c8eb672ecd | ||
|
|
b65e8dc3f3 | ||
|
|
137d1b63d8 | ||
|
|
31638b0805 | ||
|
|
df980c5cf4 | ||
|
|
f00c27a226 | ||
|
|
853c7a6f81 | ||
|
|
fbdb517d24 | ||
|
|
b48a6c92f6 | ||
|
|
5034f86667 | ||
|
|
d830821ceb | ||
|
|
137397dbc1 | ||
|
|
66dbdba863 | ||
|
|
33099d098f | ||
|
|
43cb05e088 | ||
|
|
a1b24b0a85 | ||
|
|
7c2b88716b | ||
|
|
9325fddc1f | ||
|
|
13e49a8c3f | ||
|
|
5920af5a36 | ||
|
|
256896df52 | ||
|
|
f6e86a465f | ||
|
|
7b0d212fc0 | ||
|
|
6539ddd09c | ||
|
|
e3ecd8fef0 | ||
|
|
b0b7966b47 | ||
|
|
ec5bcbc9c7 | ||
|
|
686f5e7e87 | ||
|
|
cfd5b1ae8d | ||
|
|
7667948bf5 | ||
|
|
b5cb33812b | ||
|
|
9178dab7dd | ||
|
|
583e4659ad | ||
|
|
c797365641 | ||
|
|
99687a1c28 | ||
|
|
0f66f35673 | ||
|
|
40c8867e44 | ||
|
|
d7f61c4b30 | ||
|
|
c6d35cba5e | ||
|
|
361c3a67ee | ||
|
|
83b40e10e5 | ||
|
|
3c2c24038d | ||
|
|
4a686e9fce | ||
|
|
9c65d28933 | ||
|
|
b24e82c145 | ||
|
|
f30e017101 | ||
|
|
b6366e7b88 | ||
|
|
3bb54fdad4 | ||
|
|
c8f5bdbcdb | ||
|
|
cd056ee976 | ||
|
|
faf347a44a | ||
|
|
c237ee1a47 | ||
|
|
04d9817b94 | ||
|
|
207d514e8a | ||
|
|
d27d819821 | ||
|
|
beaf862dec | ||
|
|
8b4b238cf9 | ||
|
|
3076be593e | ||
|
|
21a2680027 | ||
|
|
8006939d69 | ||
|
|
c7b5d1afc3 | ||
|
|
652ffe37cc | ||
|
|
0d7467048a | ||
|
|
c1c88cd2ad | ||
|
|
46259e924d | ||
|
|
0c3b8ab8d7 | ||
|
|
2b81c3fb87 | ||
|
|
c641065564 | ||
|
|
2df0135604 | ||
|
|
cd90bfe58d | ||
|
|
d12218e9db | ||
|
|
07bc3d3a39 | ||
|
|
3bd959b4ef | ||
|
|
14b589477f | ||
|
|
e3b2175420 | ||
|
|
6b6c391f81 | ||
|
|
7d355611eb | ||
|
|
be445d281a | ||
|
|
d79e772471 | ||
|
|
8e49b05226 | ||
|
|
54ee9a7af3 | ||
|
|
6da953ffc6 | ||
|
|
e214025bef | ||
|
|
1750d48b89 | ||
|
|
8025cc1d87 | ||
|
|
e53cf45fa7 | ||
|
|
af98bddb14 | ||
|
|
b31d8a7029 | ||
|
|
b7c437e5c0 | ||
|
|
be0e00c7bf | ||
|
|
ed60ccf41e | ||
|
|
9b2e41ff86 | ||
|
|
89b1683438 | ||
|
|
4baccc4701 | ||
|
|
7a0bf5864e | ||
|
|
c8accaeb62 | ||
|
|
0665c3e607 | ||
|
|
3bbf075f92 | ||
|
|
63d8a0354c | ||
|
|
046e272c5f | ||
|
|
fac6cc4495 | ||
|
|
59c3d4a6c7 | ||
|
|
c190177955 | ||
|
|
76c66339aa | ||
|
|
9ad3c98a7d | ||
|
|
12352d5677 | ||
|
|
242b8f0a1f | ||
|
|
3292964f01 | ||
|
|
2a283e24db | ||
|
|
02a061a580 | ||
|
|
2f841f8946 | ||
|
|
f688f3c5f4 | ||
|
|
589cda91f4 | ||
|
|
24ff0f2ea2 | ||
|
|
ad822a624d | ||
|
|
535177bd46 | ||
|
|
1271aa8438 | ||
|
|
17bec2e987 | ||
|
|
339750a978 | ||
|
|
9acbecb813 | ||
|
|
db52ca3f2f | ||
|
|
dfe5622920 | ||
|
|
a73c0e8c5f | ||
|
|
c27e1fe2ab | ||
|
|
be52ee18c5 | ||
|
|
891e397104 | ||
|
|
03107ba902 | ||
|
|
69ff8ae896 |
|
|
@ -1,18 +1,9 @@
|
|||
.git/
|
||||
.github/
|
||||
.idea/
|
||||
.vscode/
|
||||
.gitignore
|
||||
|
||||
.editorconfig
|
||||
.env
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
|
||||
LICENSE
|
||||
README.md
|
||||
bin/
|
||||
docs/
|
||||
release/
|
||||
|
||||
assets/games/
|
||||
/**
|
||||
!cmd/
|
||||
!pkg/
|
||||
!scripts/
|
||||
!web/
|
||||
!go.mod
|
||||
!go.sum
|
||||
!LICENSE
|
||||
!Makefile
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
1
.env
|
|
@ -1 +0,0 @@
|
|||
CLOUD_GAME_GAMES_PATH=./assets/games
|
||||
16
.gitattributes
vendored
|
|
@ -1,16 +0,0 @@
|
|||
* linguist-vendored
|
||||
*.go linguist-vendored=false
|
||||
* text=auto eol=lf
|
||||
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have CRLF line endings on checkout.
|
||||
*.sln text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
120
.github/workflows/build.yml
vendored
|
|
@ -1,5 +1,5 @@
|
|||
# ------------------------------------------------------------
|
||||
# Build workflow (Linux x64, macOS x64, Windows x64)
|
||||
# Build and test workflow (Linux x64, macOS x64, Windows x64)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
name: build
|
||||
|
|
@ -16,101 +16,73 @@ on:
|
|||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest, windows-latest ]
|
||||
step: [ build, check ]
|
||||
os: [ ubuntu-latest, windows-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.17
|
||||
go-version: 'stable'
|
||||
|
||||
- name: Get Linux dev libraries and tools
|
||||
- name: Linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
env:
|
||||
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
|
||||
run: |
|
||||
sudo apt-get -qq update
|
||||
sudo apt-get -qq install -y make pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libgl1-mesa-glx
|
||||
sudo apt-get -qq install -y \
|
||||
make pkg-config \
|
||||
libvpx-dev libx264-dev libopus-dev libyuv-dev libjpeg-turbo8-dev \
|
||||
libsdl2-dev libgl1 libglx-mesa0 libspeexdsp-dev
|
||||
|
||||
make build
|
||||
xvfb-run --auto-servernum make test verify-cores
|
||||
|
||||
- name: Get MacOS dev libraries and tools
|
||||
if: matrix.os == 'macos-latest'
|
||||
- name: macOS
|
||||
if: matrix.os == 'macos-12'
|
||||
run: |
|
||||
brew install pkg-config libvpx x264 opus sdl2
|
||||
brew install libvpx x264 sdl2 speexdsp
|
||||
make build test verify-cores
|
||||
|
||||
- name: Get Windows dev libraries and tools
|
||||
- uses: msys2/setup-msys2@v2
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
msystem: MINGW64
|
||||
msystem: ucrt64
|
||||
path-type: inherit
|
||||
release: false
|
||||
install: >
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-pkgconf
|
||||
mingw-w64-x86_64-dlfcn
|
||||
mingw-w64-x86_64-libvpx
|
||||
mingw-w64-x86_64-opus
|
||||
mingw-w64-x86_64-x264-git
|
||||
mingw-w64-x86_64-SDL2
|
||||
mingw-w64-ucrt-x86_64-gcc
|
||||
mingw-w64-ucrt-x86_64-pkgconf
|
||||
mingw-w64-ucrt-x86_64-dlfcn
|
||||
mingw-w64-ucrt-x86_64-libvpx
|
||||
mingw-w64-ucrt-x86_64-opus
|
||||
mingw-w64-ucrt-x86_64-libx264
|
||||
mingw-w64-ucrt-x86_64-SDL2
|
||||
mingw-w64-ucrt-x86_64-libyuv
|
||||
mingw-w64-ucrt-x86_64-libjpeg-turbo
|
||||
mingw-w64-ucrt-x86_64-speexdsp
|
||||
|
||||
- name: Get Windows OpenGL drivers
|
||||
if: matrix.step == 'check' && matrix.os == 'windows-latest'
|
||||
- name: Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
env:
|
||||
MESA_VERSION: '24.0.7'
|
||||
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
wget -q https://github.com/pal1000/mesa-dist-win/releases/download/20.2.1/mesa3d-20.2.1-release-mingw.7z
|
||||
"/c/Program Files/7-Zip/7z.exe" x mesa3d-20.2.1-release-mingw.7z -omesa
|
||||
echo -e " 2\r\n 8\r\n " >> commands
|
||||
set MSYSTEM=UCRT64
|
||||
|
||||
wget -q https://github.com/pal1000/mesa-dist-win/releases/download/$MESA_VERSION/mesa3d-$MESA_VERSION-release-msvc.7z
|
||||
"/c/Program Files/7-Zip/7z.exe" x mesa3d-$MESA_VERSION-release-msvc.7z -omesa
|
||||
echo -e " 1\r\n 9\r\n " >> commands
|
||||
./mesa/systemwidedeploy.cmd < ./commands
|
||||
|
||||
make build test verify-cores
|
||||
|
||||
- name: Build Windows app
|
||||
if: matrix.step == 'build' && matrix.os == 'windows-latest'
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: Build Linux app
|
||||
if: matrix.step == 'build' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: Build macOS app
|
||||
if: matrix.step == 'build' && matrix.os == 'macos-latest'
|
||||
run: |
|
||||
make build
|
||||
|
||||
- name: Verify core rendering (windows-latest)
|
||||
if: matrix.step == 'check' && matrix.os == 'windows-latest' && always()
|
||||
shell: msys2 {0}
|
||||
env:
|
||||
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
|
||||
run: |
|
||||
GL_CTX=-autoGlContext make verify-cores
|
||||
|
||||
- name: Verify core rendering (ubuntu-latest)
|
||||
if: matrix.step == 'check' && matrix.os == 'ubuntu-latest' && always()
|
||||
env:
|
||||
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
|
||||
run: |
|
||||
GL_CTX=-autoGlContext xvfb-run --auto-servernum make verify-cores
|
||||
|
||||
- name: Verify core rendering (macos-latest)
|
||||
if: matrix.step == 'check' && matrix.os == 'macos-latest' && always()
|
||||
run: |
|
||||
make verify-cores
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: matrix.step == 'check' && always()
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: emulator-test-frames
|
||||
name: emulator-test-frames-${{ matrix.os }}
|
||||
path: _rendered/*.png
|
||||
|
||||
build_docker:
|
||||
name: Build (docker)
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: docker build --build-arg VERSION=$(./scripts/version.sh) .
|
||||
|
|
|
|||
40
.github/workflows/cd/cloudretro.io/config.yaml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
version: 4
|
||||
|
||||
coordinator:
|
||||
debug: true
|
||||
server:
|
||||
address:
|
||||
frameOptions: SAMEORIGIN
|
||||
https: true
|
||||
tls:
|
||||
domain: cloudretro.io
|
||||
analytics:
|
||||
inject: true
|
||||
gtag: UA-145078282-1
|
||||
|
||||
worker:
|
||||
debug: true
|
||||
network:
|
||||
coordinatorAddress: cloudretro.io
|
||||
publicAddress: cloudretro.io
|
||||
secure: true
|
||||
server:
|
||||
https: true
|
||||
tls:
|
||||
address: :444
|
||||
# domain: cloudretro.io
|
||||
|
||||
emulator:
|
||||
libretro:
|
||||
logLevel: 1
|
||||
cores:
|
||||
list:
|
||||
dos:
|
||||
uniqueSaveDir: true
|
||||
mame:
|
||||
options:
|
||||
"fbneo-diagnostic-input": "Hold Start"
|
||||
nes:
|
||||
scale: 2
|
||||
snes:
|
||||
scale: 2
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
CLOUD_GAME_COORDINATOR_ANALYTICS_GTAG=UA-145078282-1
|
||||
CLOUD_GAME_COORDINATOR_ANALYTICS_INJECT=true
|
||||
CLOUD_GAME_COORDINATOR_SERVER_ADDRESS=
|
||||
CLOUD_GAME_COORDINATOR_SERVER_HTTPS=true
|
||||
CLOUD_GAME_COORDINATOR_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
CLOUD_GAME_ENVIRONMENT=prod
|
||||
10
.github/workflows/cd/cloudretro.io/script.env
vendored
|
|
@ -1,6 +1,6 @@
|
|||
COORDINATORS="167.172.70.98 cloudretro.io"
|
||||
DOCKER_IMAGE_TAG=dev
|
||||
DO_ADDRESS_LIST="cloud-gaming cloud-gaming-eu cloud-gaming-usw"
|
||||
SPLIT_HOSTS=1
|
||||
COORDINATORS="138.68.48.200"
|
||||
DOCKER_IMAGE_TAG=master
|
||||
#DO_ADDRESS_LIST="cloud-gaming cloud-gaming-eu cloud-gaming-usw"
|
||||
#SPLIT_HOSTS=1
|
||||
USER=root
|
||||
WORKERS=${WORKERS:-5}
|
||||
WORKERS=${WORKERS:-4}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
CLOUD_GAME_WORKER_NETWORK_COORDINATORADDRESS=cloudretro.io
|
||||
CLOUD_GAME_WORKER_NETWORK_PUBLICADDRESS=cloudretro.io
|
||||
CLOUD_GAME_WORKER_NETWORK_SECURE=true
|
||||
CLOUD_GAME_WORKER_SERVER_ADDRESS=:80
|
||||
CLOUD_GAME_WORKER_SERVER_HTTPS=true
|
||||
CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:443
|
||||
CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
39
.github/workflows/cd/deploy-app.sh
vendored
|
|
@ -54,6 +54,10 @@ IP_LIST=${IP_LIST:-}
|
|||
# a list of machines mark some addresses to deploy only a coordinator there
|
||||
COORDINATORS=${COORDINATORS:-}
|
||||
|
||||
if [ -z "$SPLIT_HOSTS" ]; then
|
||||
IP_LIST+=$COORDINATORS
|
||||
fi
|
||||
|
||||
# Digital Ocean operations
|
||||
#DO_TOKEN
|
||||
DO_ADDRESS_LIST=${DO_ADDRESS_LIST:-}
|
||||
|
|
@ -64,7 +68,7 @@ REMOTE_WORK_DIR=${REMOTE_WORK_DIR:-"/cloud-game"}
|
|||
DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG:-latest}
|
||||
echo "Docker tag:$DOCKER_IMAGE_TAG"
|
||||
# the total number of worker replicas to deploy
|
||||
WORKERS=${WORKERS:-5}
|
||||
WORKERS=${WORKERS:-4}
|
||||
USER=${USER:-root}
|
||||
|
||||
compose_src=$(cat $LOCAL_WORK_DIR/docker-compose.yml)
|
||||
|
|
@ -124,7 +128,7 @@ echo "IPs:" $IP_LIST
|
|||
|
||||
# Run command builder
|
||||
#
|
||||
# By default it will run docker-compose with both coordinator and worker apps.
|
||||
# By default it will run docker compose with both coordinator and worker apps.
|
||||
# With the SPLIT_HOSTS parameter specified, it will run either coordinator app
|
||||
# if the current server address is found in the IP_LIST variable, otherwise it
|
||||
# will run just the worker app.
|
||||
|
|
@ -142,28 +146,39 @@ for ip in $IP_LIST; do
|
|||
fi
|
||||
|
||||
# build run command
|
||||
cmd="ZONE=\$zone docker-compose up -d --remove-orphans --scale worker=\${workers:-$WORKERS}"
|
||||
cmd="ZONE=\$zone docker compose up -d --remove-orphans"
|
||||
if [ ! -z "$SPLIT_HOSTS" ]; then
|
||||
cmd+=" worker"
|
||||
deploy_coordinator=0
|
||||
deploy_worker=1
|
||||
else
|
||||
cmd+=" worker"
|
||||
fi
|
||||
|
||||
# override run command
|
||||
if [ ! -z "$SPLIT_HOSTS" ]; then
|
||||
for addr in $COORDINATORS; do
|
||||
if [ "$ip" == $addr ]; then
|
||||
cmd="docker-compose up -d --remove-orphans coordinator"
|
||||
cmd="docker compose up -d --remove-orphans coordinator"
|
||||
deploy_coordinator=1
|
||||
deploy_worker=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
else
|
||||
cmd+=" coordinator"
|
||||
fi
|
||||
|
||||
# build Docker container env file
|
||||
run_env=""
|
||||
custom_config=""
|
||||
if [[ ! -z "${ENV_DIR}" ]]; then
|
||||
env_f=$ENV_DIR/config.yaml
|
||||
if [[ -e "$env_f" ]]; then
|
||||
echo "config.yaml found"
|
||||
custom_config=$(cat $env_f)
|
||||
fi
|
||||
|
||||
if [ $deploy_coordinator == 1 ]; then
|
||||
env_f=$ENV_DIR/coordinator.env
|
||||
if [[ -e "$env_f" ]]; then
|
||||
|
|
@ -190,13 +205,13 @@ for ip in $IP_LIST; do
|
|||
run="#!/bin/bash"$'\n'
|
||||
run+=$(remote_run_commands "$ENV_DIR")$'\n'
|
||||
run+=$(remote_run_commands "$PROVIDER_DIR")$'\n'
|
||||
run+="IMAGE_TAG=$DOCKER_IMAGE_TAG APP_DIR=$REMOTE_WORK_DIR $cmd"
|
||||
run+="IMAGE_TAG=$DOCKER_IMAGE_TAG APP_DIR=$REMOTE_WORK_DIR WORKER_REPLICAS=$WORKERS $cmd"
|
||||
|
||||
echo ""
|
||||
echo "run.sh:"$'\n'"$run"
|
||||
echo ""
|
||||
|
||||
# !to add docker-compose install / warning
|
||||
# !to add docker compose install / warning
|
||||
|
||||
# custom scripts
|
||||
remote_sudo_run_once $ip "$PROVIDER_DIR" "$ssh_i"
|
||||
|
|
@ -205,13 +220,13 @@ for ip in $IP_LIST; do
|
|||
echo "Update the remote host"
|
||||
|
||||
ssh -o ConnectTimeout=10 $USER@$ip ${ssh_i:-} "\
|
||||
docker-compose -v; \
|
||||
docker compose version; \
|
||||
mkdir -p $REMOTE_WORK_DIR; \
|
||||
cd $REMOTE_WORK_DIR; \
|
||||
mkdir -p $REMOTE_WORK_DIR/home; \
|
||||
echo \"$custom_config\" > $REMOTE_WORK_DIR/home/config.yaml; \
|
||||
echo '$compose_src' > ./docker-compose.yml; \
|
||||
echo '$run_env' > ./run.env; \
|
||||
IMAGE_TAG=$DOCKER_IMAGE_TAG docker-compose pull coordinator; \
|
||||
echo '$run' > ./run.sh; \
|
||||
chmod +x ./run.sh; \
|
||||
./run.sh"
|
||||
docker compose down; \
|
||||
IMAGE_TAG=$DOCKER_IMAGE_TAG docker compose pull; \
|
||||
docker compose up -d;"
|
||||
done
|
||||
|
|
|
|||
101
.github/workflows/cd/docker-compose.yml
vendored
|
|
@ -1,36 +1,93 @@
|
|||
version: "3.4"
|
||||
|
||||
x-params:
|
||||
&default-params
|
||||
env_file: run.env
|
||||
image: ghcr.io/giongto35/cloud-game/cloud-game:${IMAGE_TAG:-latest}
|
||||
x-params: &default-params
|
||||
image: ghcr.io/giongto35/cloud-game/cloud-game:${IMAGE_TAG:-master}
|
||||
network_mode: "host"
|
||||
privileged: true
|
||||
restart: always
|
||||
security_opt:
|
||||
- seccomp=unconfined
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "64m"
|
||||
max-file: "5"
|
||||
compress: "true"
|
||||
driver: "journald"
|
||||
x-worker: &worker
|
||||
depends_on:
|
||||
- coordinator
|
||||
command: ./worker
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
- ${APP_DIR:-/cloud-game}/libretro:/usr/local/share/cloud-game/libretro
|
||||
- ${APP_DIR:-/cloud-game}/home:/root/.cr
|
||||
- x11:/tmp/.X11-unix
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io/echo || exit 1
|
||||
interval: 1m
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
start_interval: 5s
|
||||
|
||||
services:
|
||||
|
||||
coordinator:
|
||||
<<: *default-params
|
||||
command: coordinator --v=5
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
|
||||
worker:
|
||||
<<: *default-params
|
||||
command: ./coordinator
|
||||
environment:
|
||||
- MESA_GL_VERSION_OVERRIDE=3.3
|
||||
entrypoint: [ "/bin/sh", "-c", "xvfb-run -a $$@", "" ]
|
||||
command: worker --v=5 --zone=${ZONE:-}
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
- ${APP_DIR:-/cloud-game}/home:/root/.cr
|
||||
|
||||
worker01:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:444
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:444/echo || exit 1
|
||||
worker02:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:445
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:445/echo || exit 1
|
||||
worker03:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:446
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:446/echo || exit 1
|
||||
worker04:
|
||||
<<: [ *default-params, *worker ]
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
|
||||
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
|
||||
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:447
|
||||
healthcheck:
|
||||
test: curl -f https://cloudretro.io:447/echo || exit 1
|
||||
|
||||
xvfb:
|
||||
image: kcollins/xvfb:latest
|
||||
volumes:
|
||||
- x11:/tmp/.X11-unix
|
||||
command: [ ":99", "-screen", "0", "320x240x16" ]
|
||||
|
||||
volumes:
|
||||
x11:
|
||||
|
|
|
|||
2
.github/workflows/deploy.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
known_hosts: 'PLACEHOLDER'
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Deploy to all servers
|
||||
env:
|
||||
|
|
|
|||
12
.github/workflows/docker_build.yml
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
name: docker_build
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: DOCKER_BUILDKIT=1 docker build --build-arg VERSION=$(./scripts/version.sh) .
|
||||
48
.github/workflows/docker_publish.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# ----------------------------------------------------------------------------------
|
||||
# Publish Docker image from the current master branch or v* into Github repository
|
||||
# ----------------------------------------------------------------------------------
|
||||
|
||||
name: docker-publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
docker-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/cloud-game
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: VERSION=${{ env.V }}
|
||||
context: .
|
||||
push: true
|
||||
provenance: false
|
||||
sbom: false
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tags }}
|
||||
labels: |
|
||||
${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
31
.github/workflows/docker_publish_stable.yml
vendored
|
|
@ -1,31 +0,0 @@
|
|||
# ------------------------------------------------------------------------
|
||||
# Publish Docker image from the stable snapshot into Github repository
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
name: publish-stable
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
docker-publish-stable:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
|
||||
- run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
|
||||
|
||||
- uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
add_git_labels: true
|
||||
tags: latest,${{ env.TAG }}
|
||||
build_args: VERSION=${{ env.V }}
|
||||
|
||||
registry: docker.pkg.github.com
|
||||
repository: ${{ github.REPOSITORY }}/cloud-game
|
||||
29
.github/workflows/docker_publish_unstable.yml
vendored
|
|
@ -1,29 +0,0 @@
|
|||
# ----------------------------------------------------------------------------
|
||||
# Publish Docker image from the current master branch into Github repository
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
name: publish-unstable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
docker-publish-unstable:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
|
||||
|
||||
- uses: docker/build-push-action@v1
|
||||
with:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
add_git_labels: true
|
||||
tags: dev
|
||||
build_args: VERSION=${{ env.V }}
|
||||
|
||||
registry: docker.pkg.github.com
|
||||
repository: ${{ github.REPOSITORY }}/cloud-game
|
||||
|
|
@ -37,11 +37,11 @@ jobs:
|
|||
env:
|
||||
release-dir: _release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ^1.17
|
||||
go-version: ^1.20
|
||||
|
||||
- name: Get Linux dev libraries and tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
15
.gitignore
vendored
|
|
@ -45,6 +45,11 @@ Network Trash Folder
|
|||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### SSL
|
||||
*.crt
|
||||
*.csr
|
||||
*.key
|
||||
|
||||
### Production
|
||||
DockerfileProd
|
||||
key.json
|
||||
|
|
@ -62,5 +67,15 @@ _output/
|
|||
./build
|
||||
release/
|
||||
vendor/
|
||||
tests/
|
||||
!tests/e2e/
|
||||
*.exe
|
||||
|
||||
.dockerignore
|
||||
|
||||
### Libretro
|
||||
fbneo/
|
||||
hi/
|
||||
nvram/
|
||||
*.mcd
|
||||
|
||||
|
|
|
|||
79
DESIGNv2.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Cloud Gaming Service Design Document
|
||||
|
||||
Cloud Gaming Service contains multiple workers for gaming streams and a coordinator for distributing traffic and pairing
|
||||
up connections.
|
||||
|
||||
## Coordinator
|
||||
|
||||
Coordinator is a web-frontend, load balancer and signalling server for WebRTC.
|
||||
|
||||
```
|
||||
WORKERS
|
||||
┌──────────────────────────────────┐
|
||||
│ │
|
||||
│ REGION 1 REGION 2 REGION N │
|
||||
│ (US) (DE) (XX) │
|
||||
│ ┌──────┐ ┌──────┐ ┌──────┐ |
|
||||
COORDINATOR │ │WORKER│ │WORKER│ │WORKER│ |
|
||||
┌───────────┐ │ └──────┘ └──────┘ └──────┘ |
|
||||
│ │ ───────────────────────HEALTH────────────────────► │ • • • |
|
||||
│ HTTP/WS │ ◄─────────────────────REG/DEREG─────────────────── │ • • • |
|
||||
│┌─────────┐│ │ • • • |
|
||||
│| |│ USER │ ┌──────┐* ┌──────┐ ┌──────┐ |
|
||||
│└─────────┘│ ┌──────┐ │ │WORKER│ │WORKER│ │WORKER│ |
|
||||
│ │ ◄──(1)CONNECT───────── │ │ ────(3)SELECT────► │ └──────┘ └──────┘ └──────┘ |
|
||||
│ │ ───(2)LIST WORKERS───► │ │ ◄───(4)STREAM───── │ │
|
||||
└───────────┘ └──────┘ │ * MULTIPLAYER │
|
||||
│ ┌──────┐────► ONE GAME │
|
||||
│ ┌───►│WORKER│◄──┐ │
|
||||
│ │ └──────┘ │ │
|
||||
│ │ ▲ ▲ │ │
|
||||
│ ┌┴─┐ │ │ ┌┴─┐ |
|
||||
│ │U1│ ┌─┴┐ ┌┴─┐ │U4│ |
|
||||
│ └──┘ │U2│ │U3│ └──┘ |
|
||||
│ └──┘ └──┘ |
|
||||
│ |
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
- (1) A user opens the main page of the app in the browser, i.e. connects to the coordinator.
|
||||
- (2) The coordinator searches and serves a list of most suitable workers to the user.
|
||||
- (3) The user proceeds with latency check of each worker from the list, then coordinator collects user-to-worker
|
||||
latency data and picks the best candidate.
|
||||
- (4) The coordinator sets up peer-to-peer connection between a worker and the user based on the WebRTC protocol and a
|
||||
game hosted on the worker is streamed to the user.
|
||||
|
||||
## Worker
|
||||
|
||||
Worker is responsible for running and streaming games to users.
|
||||
|
||||
```
|
||||
WORKER
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ EMULATOR WEBRTC │ BROWSER
|
||||
│ ┌─────────────────┐ ENCODER ┌────────┐ │ ┌──────────┐
|
||||
│ │ │ ┌─────────┐ | DMUX | | ───RTP──► | WEBRTC |
|
||||
│ │ AUDIO SAMPLES │ ──PCM──► │ │ ──OPUS──► │ ┌──► │ │ ◄──SCTP── | |
|
||||
│ │ VIDEO FRAMES │ ──RGB──► │ │ ──H264──► │ └──► | | └──────────┘ COORDINATOR
|
||||
│ │ │ └─────────┘ │ │ │ • ┌─────────────┐
|
||||
│ │ │ | MUX | | ───TCP──────── • ───────► | WEBSOCKET |
|
||||
│ │ │ │ ┌── │ │ • └─────────────┘
|
||||
| | | BINARY | ▼ | | BROWSER
|
||||
│ │ INPUT STATE │ ◄───────────────────────────── │ • │ │ ┌──────────┐
|
||||
│ │ │ │ ▲ │ │ ───RTP──► | WEBRTC |
|
||||
│ └─────────────────┘ HTTP/WS | └── | │ ◄──SCTP── │ │
|
||||
│ ┌─────────┐ └────────┘ │ └──────────┘
|
||||
| | | |
|
||||
| └─────────┘ |
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- After coordinator matches the most appropriate server (peer 1) to the user (peer 2), a WebRTC peer-to-peer handshake
|
||||
will be conducted. The coordinator will help initiate the session between the two peers over a WebSocket connection.
|
||||
- The worker either spawns new rooms running game emulators or connects users to existing rooms.
|
||||
- Raw image and audio streams from the emulator are captured and encoded to a WebRTC-supported streaming format. Next,
|
||||
these stream are piped out (dmux) to all users in the room.
|
||||
- On the other hand, input from players is sent to the worker over WebRTC DataChannel. The game logic on the emulator
|
||||
will be updated based on the input stream of all players, for that each stream is multiplexed (mux) into one.
|
||||
- Game states (saves) are stored in cloud storage, so all distributed workers can keep game states in sync and players
|
||||
can continue their games where they left off.
|
||||
132
Dockerfile
|
|
@ -1,61 +1,101 @@
|
|||
# The base cloud-game image
|
||||
ARG BUILD_PATH=/go/src/github.com/giongto35/cloud-game
|
||||
ARG BUILD_PATH=/tmp/cloud-game
|
||||
ARG VERSION=master
|
||||
|
||||
# build image
|
||||
FROM debian:bullseye-slim AS build
|
||||
# base build stage
|
||||
FROM ubuntu:plucky AS build0
|
||||
ARG GO=1.26rc1
|
||||
ARG GO_DIST=go${GO}.linux-amd64.tar.gz
|
||||
|
||||
ADD https://go.dev/dl/$GO_DIST ./
|
||||
RUN tar -C /usr/local -xzf $GO_DIST && \
|
||||
rm $GO_DIST
|
||||
ENV PATH="${PATH}:/usr/local/go/bin"
|
||||
|
||||
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
|
||||
ca-certificates \
|
||||
make \
|
||||
upx \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# next conditional build stage
|
||||
FROM build0 AS build_coordinator
|
||||
ARG BUILD_PATH
|
||||
ARG VERSION
|
||||
ENV GIT_VERSION=${VERSION}
|
||||
|
||||
WORKDIR ${BUILD_PATH}
|
||||
|
||||
# system libs layer
|
||||
RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y \
|
||||
gcc \
|
||||
ca-certificates \
|
||||
# by default we ignore all except some folders and files, see .dockerignore
|
||||
COPY . ./
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build make build.coordinator
|
||||
RUN find ./bin/* | xargs upx --best --lzma
|
||||
|
||||
WORKDIR /usr/local/share/cloud-game
|
||||
RUN mv ${BUILD_PATH}/bin/* ./ && \
|
||||
mv ${BUILD_PATH}/web ./web && \
|
||||
mv ${BUILD_PATH}/LICENSE ./
|
||||
RUN ${BUILD_PATH}/scripts/version.sh ./web/index.html ${VERSION} && \
|
||||
${BUILD_PATH}/scripts/mkdirs.sh
|
||||
|
||||
# next worker build stage
|
||||
FROM build0 AS build_worker
|
||||
ARG BUILD_PATH
|
||||
ARG VERSION
|
||||
ENV GIT_VERSION=${VERSION}
|
||||
|
||||
WORKDIR ${BUILD_PATH}
|
||||
|
||||
# install deps
|
||||
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
|
||||
build-essential \
|
||||
libopus-dev \
|
||||
libsdl2-dev \
|
||||
libvpx-dev \
|
||||
libyuv-dev \
|
||||
libjpeg-turbo8-dev \
|
||||
libx264-dev \
|
||||
make \
|
||||
libspeexdsp-dev \
|
||||
pkg-config \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# go setup layer
|
||||
ARG GO=go1.17.linux-amd64.tar.gz
|
||||
RUN wget -q https://golang.org/dl/$GO \
|
||||
&& rm -rf /usr/local/go \
|
||||
&& tar -C /usr/local -xzf $GO \
|
||||
&& rm $GO
|
||||
ENV PATH="${PATH}:/usr/local/go/bin"
|
||||
# by default we ignore all except some folders and files, see .dockerignore
|
||||
COPY . ./
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build make GO_TAGS=static,st build.worker
|
||||
RUN find ./bin/* | xargs upx --best --lzma
|
||||
|
||||
# go deps layer
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
WORKDIR /usr/local/share/cloud-game
|
||||
RUN mv ${BUILD_PATH}/bin/* ./ && \
|
||||
mv ${BUILD_PATH}/LICENSE ./
|
||||
RUN ${BUILD_PATH}/scripts/mkdirs.sh worker
|
||||
|
||||
# app build layer
|
||||
COPY pkg ./pkg
|
||||
COPY cmd ./cmd
|
||||
COPY Makefile .
|
||||
COPY scripts/version.sh scripts/version.sh
|
||||
ARG VERSION
|
||||
RUN GIT_VERSION=${VERSION} make build
|
||||
FROM scratch AS coordinator
|
||||
|
||||
COPY --from=build_coordinator /usr/local/share/cloud-game /cloud-game
|
||||
# autocertbot (SSL) requires these on the first run
|
||||
COPY --from=build_coordinator /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
FROM ubuntu:plucky AS worker
|
||||
|
||||
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
|
||||
curl \
|
||||
libx11-6 \
|
||||
libxext6 \
|
||||
&& apt-get autoremove \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/log/* /usr/share/bug /usr/share/doc /usr/share/doc-base \
|
||||
/usr/share/X11/locale/*
|
||||
|
||||
COPY --from=build_worker /usr/local/share/cloud-game /cloud-game
|
||||
COPY --from=build_worker /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
ADD https://github.com/sergystepanov/mesa-llvmpipe/releases/download/v1.0.0/libGL.so.1.5.0 \
|
||||
/usr/lib/x86_64-linux-gnu/
|
||||
RUN cd /usr/lib/x86_64-linux-gnu && \
|
||||
ln -s libGL.so.1.5.0 libGL.so.1 && \
|
||||
ln -s libGL.so.1 libGL.so
|
||||
|
||||
FROM worker AS cloud-game
|
||||
|
||||
# base image
|
||||
FROM debian:bullseye-slim
|
||||
ARG BUILD_PATH
|
||||
WORKDIR /usr/local/share/cloud-game
|
||||
|
||||
COPY scripts/install.sh install.sh
|
||||
RUN bash install.sh && \
|
||||
rm -rf /var/lib/apt/lists/* install.sh
|
||||
|
||||
COPY --from=build ${BUILD_PATH}/bin/ ./
|
||||
RUN cp -s $(pwd)/* /usr/local/bin
|
||||
COPY assets/cores ./assets/cores
|
||||
COPY configs ./configs
|
||||
COPY web ./web
|
||||
ARG VERSION
|
||||
COPY scripts/version.sh version.sh
|
||||
RUN bash ./version.sh ./web/index.html ${VERSION} && \
|
||||
rm -rf version.sh
|
||||
|
||||
EXPOSE 8000 9000
|
||||
COPY --from=coordinator /cloud-game ./
|
||||
COPY --from=worker /cloud-game ./
|
||||
|
|
|
|||
94
Makefile
|
|
@ -1,11 +1,13 @@
|
|||
# Makefile includes some useful commands to build or format incentives
|
||||
# More commands could be added
|
||||
|
||||
# Variables
|
||||
PROJECT = cloud-game
|
||||
REPO_ROOT = github.com/giongto35
|
||||
ROOT = ${REPO_ROOT}/${PROJECT}
|
||||
|
||||
CGO_CFLAGS='-g -O3'
|
||||
CGO_LDFLAGS='-g -O3'
|
||||
GO_TAGS=
|
||||
|
||||
.PHONY: clean test
|
||||
|
||||
fmt:
|
||||
@goimports -w cmd pkg tests
|
||||
@gofmt -s -w cmd pkg tests
|
||||
|
|
@ -13,74 +15,54 @@ fmt:
|
|||
compile: fmt
|
||||
@go install ./cmd/...
|
||||
|
||||
check: fmt
|
||||
@golangci-lint run cmd/... pkg/...
|
||||
# @staticcheck -checks="all,-S1*" ./cmd/... ./pkg/... ./tests/...
|
||||
|
||||
dep:
|
||||
go mod download
|
||||
# go mod tidy
|
||||
|
||||
# NOTE: there is problem with go mod vendor when it delete github.com/gen2brain/x264-go/x264c causing unable to build. https://github.com/golang/go/issues/26366
|
||||
#build.cross: build
|
||||
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-darwin ./cmd/coordinator
|
||||
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-darwin ./cmd/worker
|
||||
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-linu ./cmd/coordinator
|
||||
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-linux ./cmd/worker
|
||||
|
||||
# A user can invoke tests in different ways:
|
||||
# - make test runs all tests;
|
||||
# - make test TEST_TIMEOUT=10 runs all tests with a timeout of 10 seconds;
|
||||
# - make test TEST_PKG=./model/... only runs tests for the model package;
|
||||
# - make test TEST_ARGS="-v -short" runs tests with the specified arguments;
|
||||
# - make test-race runs tests with race detector enabled.
|
||||
TEST_TIMEOUT = 60
|
||||
TEST_PKGS ?= ./cmd/... ./pkg/...
|
||||
TEST_TARGETS := test-short test-verbose test-race test-cover
|
||||
.PHONY: $(TEST_TARGETS) test tests
|
||||
test-short: TEST_ARGS=-short
|
||||
test-verbose: TEST_ARGS=-v
|
||||
test-race: TEST_ARGS=-race
|
||||
test-cover: TEST_ARGS=-cover
|
||||
$(TEST_TARGETS): test
|
||||
|
||||
test: compile
|
||||
@go test -timeout $(TEST_TIMEOUT)s $(TEST_ARGS) $(TEST_PKGS)
|
||||
|
||||
test-e2e: compile
|
||||
@go test ./tests/e2e/...
|
||||
|
||||
cover:
|
||||
@go test -v -covermode=count -coverprofile=coverage.out $(TEST_PKGS)
|
||||
# @$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $(COVERALLS_TOKEN)
|
||||
|
||||
clean:
|
||||
@rm -rf bin
|
||||
@rm -rf build
|
||||
@go clean ./cmd/*
|
||||
|
||||
build:
|
||||
|
||||
build.coordinator:
|
||||
mkdir -p bin/
|
||||
CGO_ENABLED=0 go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
|
||||
go build -buildmode=exe -tags static -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) -o bin/ ./cmd/worker
|
||||
go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
|
||||
|
||||
build.worker:
|
||||
mkdir -p bin/
|
||||
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
|
||||
go build -pgo=auto -buildmode=exe $(if $(GO_TAGS),-tags $(GO_TAGS),) \
|
||||
-ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) \
|
||||
-o bin/ ./cmd/worker
|
||||
|
||||
build: build.coordinator build.worker
|
||||
|
||||
test:
|
||||
go test -v ./pkg/...
|
||||
|
||||
verify-cores:
|
||||
go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "../../../_rendered"
|
||||
go test -run TestAll ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "./_rendered"
|
||||
|
||||
dev.build: compile build
|
||||
|
||||
dev.build-local:
|
||||
mkdir -p bin/
|
||||
CGO_ENABLED=0 go build -o bin/ ./cmd/coordinator
|
||||
go build -buildmode=exe -o bin/ ./cmd/worker
|
||||
go build -o bin/ ./cmd/coordinator
|
||||
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} go build -pgo=auto -o bin/ ./cmd/worker
|
||||
|
||||
dev.run: dev.build-local
|
||||
./bin/coordinator --v=5 &
|
||||
./bin/worker --v=5
|
||||
ifeq ($(OS),Windows_NT)
|
||||
./bin/coordinator.exe & ./bin/worker.exe
|
||||
else
|
||||
./bin/coordinator & ./bin/worker
|
||||
endif
|
||||
|
||||
dev.run.debug:
|
||||
go build -race -o bin/ ./cmd/coordinator
|
||||
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
|
||||
go build -race -gcflags=all=-d=checkptr -o bin/ ./cmd/worker
|
||||
./bin/coordinator & ./bin/worker
|
||||
|
||||
dev.run-docker:
|
||||
docker rm cloud-game-local -f || true
|
||||
CLOUD_GAME_GAMES_PATH=$(PWD)/assets/games docker-compose up --build
|
||||
docker compose up --build
|
||||
|
||||
# RELEASE
|
||||
# Builds the app for new release.
|
||||
|
|
@ -97,8 +79,8 @@ dev.run-docker:
|
|||
# Config params:
|
||||
# - RELEASE_DIR: the name of the output folder (default: release).
|
||||
# - CONFIG_DIR: search dir for core config files.
|
||||
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; defalut: ldd).
|
||||
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., mylib.so; default: .*so).
|
||||
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; default: ldd).
|
||||
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., my_lib.so; default: .*so).
|
||||
# Be aware that this search pattern will return only matched regular expression part and not the whole line.
|
||||
# de. -> abc def ghj -> def
|
||||
# Makefile special symbols should be escaped with \.
|
||||
|
|
|
|||
83
README.md
|
|
@ -11,37 +11,27 @@ on generic solution for cloudgaming
|
|||
|
||||
Discord: [Join Us](https://discord.gg/sXRQZa2zeP)
|
||||
|
||||
## Announcement
|
||||

|
||||
|
||||
**(Currently, I'm working on [CloudMorph](https://github.com/giongto35/cloud-morph): It offers more generic solution to
|
||||
run any offline games/application on browser in Cloud Gaming
|
||||
approach: [https://github.com/giongto35/cloud-morph](https://github.com/giongto35/cloud-morph))**
|
||||
## Try it at **[cloudretro.io](https://cloudretro.io)**
|
||||
|
||||
Direct play an existing game: **[Pokemon Emerald](https://cloudretro.io/?id=1bd37d4b5dfda87c___Pokemon%20-%20Emerald%20Version%20(U))**
|
||||
|
||||
## Introduction
|
||||
|
||||
CloudRetro provides an open-source cloud gaming platform for retro games. It started as an experiment for testing cloud
|
||||
gaming performance with [WebRTC](https://github.com/pion/webrtc/) and [libretro](https://www.libretro.com/), and now it
|
||||
gaming performance with [WebRTC](https://github.com/pion/webrtc/) and [Libretro](https://www.libretro.com/), and now it
|
||||
aims to deliver the most modern and convenient gaming experience through the technology.
|
||||
|
||||
Theoretically, in cloud gaming, games are run on remote servers and media are streamed to the player optimally to ensure
|
||||
the most comfortable user interaction. It opens the ability to play any retro games on web-browser directly, which are
|
||||
fully compatible with multi-platform like Desktop, Android, ~~IOS~~.
|
||||
|
||||
## Try the service at **[cloudretro.io](https://cloudretro.io)**
|
||||
Direct play an existing
|
||||
game: **[Pokemon Emerald](https://cloudretro.io/?id=4a5073a4b05ad0fe___Pokemon%20-%20Emerald%20Version%20(U))**
|
||||
|
||||
In ideal network condition and less resource contention on servers, the game will run smoothly as in the video demo.
|
||||
Because I only hosted the platform on limited servers in US East, US West, Eu, Singapore, you may experience some
|
||||
latency issues + connection problem. You can try hosting the service following the instruction the next section to have
|
||||
a better sense of performance.
|
||||
|
||||
| Screenshot | Screenshot |
|
||||
| :--------------------------------------------: | :--------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Feature
|
||||
|
||||
1. **Cloud gaming**: Game logic and storage is hosted on cloud service. It reduces the cumbersome of game
|
||||
|
|
@ -63,23 +53,27 @@ a better sense of performance.
|
|||
|
||||
## Development environment
|
||||
|
||||
* Install Golang https://golang.org/doc/install.
|
||||
|
||||
* Install [Go](https://golang.org/doc/install)
|
||||
* Install [libvpx](https://www.webmproject.org/code/), [libx264](https://www.videolan.org/developers/x264.html)
|
||||
, [libopus](http://opus-codec.org/), [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/)
|
||||
, [sdl2](https://wiki.libsdl.org/Installation)
|
||||
, [sdl2](https://wiki.libsdl.org/Installation), [libyuv](https://chromium.googlesource.com/libyuv/libyuv/)+[libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo)
|
||||
|
||||
```
|
||||
# Ubuntu / Windows (WSL2)
|
||||
apt-get install -y make gcc pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev
|
||||
apt-get install -y make gcc pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libyuv-dev libjpeg-turbo8-dev libspeexdsp-dev
|
||||
|
||||
# MacOS
|
||||
brew install pkg-config libvpx x264 opus sdl2
|
||||
brew install pkg-config libvpx x264 opus sdl2 jpeg-turbo speexdsp
|
||||
|
||||
# Windows (MSYS2)
|
||||
pacman -Sy --noconfirm --needed git make mingw-w64-x86_64-{gcc,pkgconf,dlfcn,libvpx,opus,x264-git,SDL2}
|
||||
pacman -Sy --noconfirm --needed git make mingw-w64-ucrt-x86_64-{gcc,pkgconf,dlfcn,libvpx,opus,libx264,SDL2,libyuv,libjpeg-turbo,speexdsp}
|
||||
```
|
||||
|
||||
(You don't need to download libyuv on macOS)
|
||||
|
||||
(If you need to use the app on an older version of Ubuntu that does not have libyuv (when it says: unable to locate package libyuv-dev), you can add a custom apt repository:
|
||||
`add sudo add-apt-repository ppa:savoury1/graphics`)
|
||||
|
||||
Because the coordinator and workers need to run simultaneously. Workers connect to the coordinator.
|
||||
|
||||
1. Script
|
||||
|
|
@ -96,38 +90,33 @@ Because the coordinator and workers need to run simultaneously. Workers connect
|
|||
__Additionally, you may install and configure an `X Server` display in order to be able to run OpenGL cores.__
|
||||
__See the `docker-compose.yml` file for Xvfb example config.__
|
||||
|
||||
__Minimum supported libx264 (x264 codec) version is v160!__
|
||||
|
||||
## Run with Docker
|
||||
|
||||
Use makefile script: `make dev.run-docker` or Docker Compose directly: `docker-compose up --build`
|
||||
(`CLOUD_GAME_GAMES_PATH` is env variable for games on your host). It will spawn a docker environment and you can access
|
||||
the service on `localhost:8000`.
|
||||
Use makefile script: `make dev.run-docker` or Docker Compose directly: `docker compose up --build`.
|
||||
It will spawn a docker environment and you can access the service on `localhost:8000`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The configuration parameters are stored in the [`configs/config.yaml`](configs/config.yaml) file which is shared for all
|
||||
application instances on the same host system. It is possible to specify individual configuration files for each
|
||||
instance as well as override some parameters, for that purpose, please refer to the list of command-line options of the
|
||||
apps.
|
||||
The default configuration file is stored in the [`pkg/configs/config.yaml`](pkg/config/config.yaml) file.
|
||||
This configuration file will be embedded into the applications and loaded automatically during startup.
|
||||
In order to change the default parameters you can specify environment variables with the `CLOUD_GAME_` prefix, or place
|
||||
a custom `config.yaml` file into one of these places: just near the application, `.cr` folder in user's home, or
|
||||
specify own directory with `-w-conf` application param (`worker -w-conf /usr/conf`).
|
||||
|
||||
## Deployment
|
||||
|
||||
See an example of [deployment scripts](.github/workflows/cd) if you want to try to host your own cloud-retro copy in the cloud.
|
||||
This script (deploy-app.sh) allows pushing configured application to the group of servers automatically.
|
||||
The cloud server should be any Debian-based system with the docker-compose application [installed](https://docs.docker.com/compose/install/).
|
||||
See an example of [deployment scripts](.github/workflows/cd) if you want to try to host your own cloud-retro copy in the
|
||||
cloud. This script (deploy-app.sh) allows pushing configured application to the group of servers automatically. The
|
||||
cloud server should be any Debian-based system with the docker-compose
|
||||
application [installed](https://docs.docker.com/compose/install/).
|
||||
|
||||
## Technical documents
|
||||
|
||||
- [Design document v2](DESIGNv2.md)
|
||||
- [webrtchacks Blog: Open Source Cloud Gaming with WebRTC](https://webrtchacks.com/open-source-cloud-gaming-with-webrtc/)
|
||||
- [Wiki (outdated)](https://github.com/giongto35/cloud-game/wiki)
|
||||
|
||||
- [Code Pointer Wiki](https://github.com/giongto35/cloud-game/wiki/Code-Deep-Dive)
|
||||
|
||||
| High level | Worker internal |
|
||||
| :----------------------------------: | :-----------------------------------------: |
|
||||
|  |  |
|
||||
|
||||
## FAQ
|
||||
|
||||
- [FAQ](https://github.com/giongto35/cloud-game/wiki/FAQ)
|
||||
|
|
@ -136,7 +125,7 @@ The cloud server should be any Debian-based system with the docker-compose appli
|
|||
|
||||
By clicking these deep link, you can join the game directly and play it together with other people.
|
||||
|
||||
- [Play Pokemon Emerald](https://cloudretro.io/?id=652e45d78d2b91cd%7CPokemon%20-%20Emerald%20Version%20%28U%29)
|
||||
- [Play Pokemon Emerald](https://cloudretro.io/?id=652e45d78d2b91cd___Pokemon%20-%20Emerald%20Version%20(U))
|
||||
- [Fire Emblem](https://cloudretro.io/?id=314ea4d7f9c94d25___Fire%20Emblem%20%28U%29%20%5B%21%5D)
|
||||
- [Samurai Showdown 4](https://cloudretro.io/?id=733c73064c368832___samsho4)
|
||||
- [Metal Slug X](https://cloudretro.io/?id=2a9c4b3f1c872d28___mslugx)
|
||||
|
|
@ -144,11 +133,6 @@ By clicking these deep link, you can join the game directly and play it together
|
|||
And you can host the new game by yourself by accessing [cloudretro.io](https://cloudretro.io) and click "share" button
|
||||
to generate a permanent link to your game.
|
||||
|
||||
<p align="center">
|
||||
<img width="420" height="300" src="docs/img/multiplatform.png"> <br>
|
||||
Synchronize a game session on multiple devices
|
||||
</p>
|
||||
|
||||
## Credits
|
||||
|
||||
We are very much thankful to [everyone](https://github.com/giongto35/cloud-game/graphs/contributors) we've been lucky to
|
||||
|
|
@ -158,7 +142,8 @@ Thanks:
|
|||
|
||||
* [Pion](https://github.com/pion) team for the incredible Golang WebRTC library and their support.
|
||||
* [Libretro](https://www.libretro.com) team for the greatest emulation lib.
|
||||
* [kivutar](https://github.com/kivutar) for [go-nanoarch](https://github.com/libretro/go-nanoarch), [ludo](https://github.com/libretro/ludo), and all.
|
||||
* [kivutar](https://github.com/kivutar) for [go-nanoarch](https://github.com/libretro/go-nanoarch)
|
||||
and [ludo](https://github.com/libretro/ludo).
|
||||
* [gen2brain](https://github.com/gen2brain) for the [h264](https://github.com/gen2brain/x264-go) and VPX encoder.
|
||||
* [poi5305](https://github.com/poi5305) for the [YUV video encoding](https://github.com/poi5305/go-yuv2webRTC).
|
||||
* [fogleman](https://github.com/fogleman) for the [NES emulator](https://github.com/fogleman/nes).
|
||||
|
|
@ -171,13 +156,19 @@ Thanks:
|
|||
* [Linear Video game controller background Gadgets seamless pattern](https://stock.adobe.com/ru/images/linear-video-game-controller-background-gadgets-seamless-pattern/241143639)
|
||||
by [Anna](https://stock.adobe.com/contributor/208277224/anna)
|
||||
|
||||
# Announcement
|
||||
|
||||
**[CloudMorph](https://github.com/giongto35/cloud-morph) is a sibling project that offers a more generic to
|
||||
run any offline games/application on browser in Cloud Gaming
|
||||
approach: [https://github.com/giongto35/cloud-morph](https://github.com/giongto35/cloud-morph))**
|
||||
|
||||
## Team
|
||||
|
||||
Authors:
|
||||
|
||||
- Nguyen Huu Thanh (https://www.linkedin.com/in/huuthanhnguyen)
|
||||
- Tri Dang Minh (https://trich.im)
|
||||
|
||||
Maintainers:
|
||||
|
||||
- Sergey Stepanov (https://github.com/sergystepanov)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
mupen64plus-169screensize = 480x270
|
||||
mupen64plus-43screensize = 320x240
|
||||
mupen64plus-alt-map = False
|
||||
mupen64plus-aspect = 4:3
|
||||
mupen64plus-astick-deadzone = 15
|
||||
mupen64plus-astick-sensitivity = 100
|
||||
mupen64plus-BackgroundMode = OnePiece
|
||||
mupen64plus-BilinearMode = standard
|
||||
mupen64plus-CorrectTexrectCoords = Off
|
||||
mupen64plus-CountPerOp = 0
|
||||
mupen64plus-cpucore = dynamic_recompiler
|
||||
mupen64plus-CropMode = Auto
|
||||
mupen64plus-d-cbutton = C3
|
||||
mupen64plus-EnableCopyColorToRDRAM = Off
|
||||
mupen64plus-EnableCopyDepthToRDRAM = Software
|
||||
mupen64plus-EnableEnhancedHighResStorage = False
|
||||
mupen64plus-EnableEnhancedTextureStorage = False
|
||||
mupen64plus-EnableFBEmulation = True
|
||||
mupen64plus-EnableFragmentDepthWrite = False
|
||||
mupen64plus-EnableHWLighting = False
|
||||
mupen64plus-EnableLegacyBlending = True
|
||||
mupen64plus-EnableLODEmulation = True
|
||||
mupen64plus-EnableNativeResTexrects = Disabled
|
||||
mupen64plus-EnableOverscan = Enabled
|
||||
mupen64plus-EnableShadersStorage = True
|
||||
mupen64plus-EnableTextureCache = True
|
||||
mupen64plus-ForceDisableExtraMem = False
|
||||
mupen64plus-FrameDuping = False
|
||||
mupen64plus-Framerate = Original
|
||||
mupen64plus-FXAA = 0
|
||||
mupen64plus-l-cbutton = C2
|
||||
mupen64plus-MaxTxCacheSize = 8000
|
||||
mupen64plus-NoiseEmulation = True
|
||||
mupen64plus-OverscanBottom = 0
|
||||
mupen64plus-OverscanLeft = 0
|
||||
mupen64plus-OverscanRight = 0
|
||||
mupen64plus-OverscanTop = 0
|
||||
mupen64plus-pak1 = memory
|
||||
mupen64plus-pak2 = none
|
||||
mupen64plus-pak3 = none
|
||||
mupen64plus-pak4 = none
|
||||
mupen64plus-r-cbutton = C1
|
||||
mupen64plus-rdp-plugin = gliden64
|
||||
mupen64plus-rsp-plugin = hle
|
||||
mupen64plus-rspmode = HLE
|
||||
mupen64plus-txCacheCompression = True
|
||||
mupen64plus-txEnhancementMode = None
|
||||
mupen64plus-txFilterIgnoreBG = True
|
||||
mupen64plus-txFilterMode = None
|
||||
mupen64plus-txHiresEnable = False
|
||||
mupen64plus-txHiresFullAlphaChannel = False
|
||||
mupen64plus-u-cbutton = C4
|
||||
mupen64plus-virefresh = Auto
|
||||
2
assets/games/dos/rogue.conf
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[autoexec]
|
||||
ROGUE.EXE
|
||||
BIN
assets/games/dos/rogue.zip
Normal file
BIN
assets/games/nes/Alwa's Awakening (Demo).nes
Normal file
|
|
@ -1,40 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
goflag "flag"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
config "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
|
||||
"github.com/golang/glog"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/coordinator"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
)
|
||||
|
||||
var Version = ""
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
var Version = "?"
|
||||
|
||||
func main() {
|
||||
conf := config.NewConfig()
|
||||
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
|
||||
conf, paths := config.NewCoordinatorConfig()
|
||||
conf.ParseFlags()
|
||||
|
||||
logging.Init()
|
||||
defer logging.Flush()
|
||||
|
||||
glog.Infof("[coordinator] version: %v", Version)
|
||||
glog.V(4).Infof("Coordinator configs %v", conf)
|
||||
c := coordinator.New(conf)
|
||||
log := logger.NewConsole(conf.Coordinator.Debug, "c", false)
|
||||
log.Info().Msgf("version %s", Version)
|
||||
log.Info().Msgf("conf: v%v, loaded: %v", conf.Version, paths)
|
||||
if log.GetLevel() < logger.InfoLevel {
|
||||
log.Debug().Msgf("conf: %+v", conf)
|
||||
}
|
||||
c, err := coordinator.New(conf, log)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("init fail")
|
||||
return
|
||||
}
|
||||
c.Start()
|
||||
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
defer c.Shutdown(ctx)
|
||||
<-os.ExpectTermination()
|
||||
cancelCtx()
|
||||
if err := c.Stop(); err != nil {
|
||||
log.Error().Err(err).Msg("shutdown fail")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
cmd/worker/default.pgo
Normal file
|
|
@ -1,45 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
goflag "flag"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
config "github.com/giongto35/cloud-game/v2/pkg/config/worker"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/thread"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/worker"
|
||||
"github.com/golang/glog"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/worker/thread"
|
||||
)
|
||||
|
||||
var Version = ""
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
var Version = "?"
|
||||
|
||||
func run() {
|
||||
conf := config.NewConfig()
|
||||
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
|
||||
conf, paths := config.NewWorkerConfig()
|
||||
conf.ParseFlags()
|
||||
|
||||
logging.Init()
|
||||
defer logging.Flush()
|
||||
log := logger.NewConsole(conf.Worker.Debug, "w", false)
|
||||
log.Info().Msgf("version %s", Version)
|
||||
log.Info().Msgf("conf: v%v, loaded: %v", conf.Version, paths)
|
||||
if log.GetLevel() < logger.InfoLevel {
|
||||
log.Debug().Msgf("conf: %+v", conf)
|
||||
}
|
||||
|
||||
glog.Infof("[worker] version: %v", Version)
|
||||
glog.V(4).Infof("[worker] Local configuration %+v", conf)
|
||||
wrk := worker.New(conf)
|
||||
wrk.Start()
|
||||
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
defer wrk.Shutdown(ctx)
|
||||
<-os.ExpectTermination()
|
||||
cancelCtx()
|
||||
done := os.ExpectTermination()
|
||||
w, err := worker.New(conf, log)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("init fail")
|
||||
return
|
||||
}
|
||||
w.Start(done)
|
||||
<-done
|
||||
time.Sleep(100 * time.Millisecond) // hack
|
||||
if err := w.Stop(); err != nil {
|
||||
log.Error().Err(err).Msg("shutdown fail")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
thread.MainWrapMaybe(run)
|
||||
}
|
||||
func main() { thread.Wrap(run) }
|
||||
|
|
|
|||
|
|
@ -1,221 +0,0 @@
|
|||
#
|
||||
# Application configuration file
|
||||
#
|
||||
|
||||
# application environment (dev, staging, prod)
|
||||
# deprecated
|
||||
environment: dev
|
||||
|
||||
coordinator:
|
||||
# address if the server want to connect directly to debug
|
||||
debugHost:
|
||||
# games library
|
||||
library:
|
||||
# some directory which is gonna be the root folder for the library
|
||||
# where games are stored
|
||||
basePath: assets/games
|
||||
# an explicit list of supported file extensions
|
||||
# which overrides Libretro emulator ROMs configs
|
||||
supported:
|
||||
# a list of ignored words in the ROM filenames
|
||||
ignored:
|
||||
- neogeo
|
||||
- pgm
|
||||
# print some additional info
|
||||
verbose: true
|
||||
# enable library directory live reload
|
||||
# (experimental)
|
||||
watchMode: false
|
||||
monitoring:
|
||||
port: 6601
|
||||
# enable Go profiler HTTP server
|
||||
profilingEnabled: false
|
||||
metricEnabled: false
|
||||
urlPrefix: /coordinator
|
||||
# HTTP(S) server config
|
||||
server:
|
||||
address: :8000
|
||||
https: false
|
||||
# Letsencrypt or self cert config
|
||||
tls:
|
||||
address: :443
|
||||
# allowed host name
|
||||
domain:
|
||||
# if both are set then will use certs
|
||||
# and Letsencryt instead
|
||||
httpsCert:
|
||||
httpsKey:
|
||||
analytics:
|
||||
inject: false
|
||||
gtag:
|
||||
|
||||
worker:
|
||||
network:
|
||||
# a coordinator address to connect to
|
||||
coordinatorAddress: localhost:8000
|
||||
# where to connect
|
||||
endpoint: /wso
|
||||
# ping endpoint
|
||||
pingEndpoint: /echo
|
||||
# set public ping address (IP or hostname)
|
||||
publicAddress:
|
||||
# make coordinator connection secure (wss)
|
||||
secure: false
|
||||
# ISO Alpha-2 country code to group workers by zones
|
||||
zone:
|
||||
monitoring:
|
||||
# monitoring server port
|
||||
port: 6602
|
||||
profilingEnabled: false
|
||||
# monitoring server URL prefix
|
||||
metricEnabled: false
|
||||
urlPrefix: /worker
|
||||
server:
|
||||
address: :9000
|
||||
https: false
|
||||
tls:
|
||||
address: :444
|
||||
# LetsEncrypt config
|
||||
# allowed host name
|
||||
domain:
|
||||
# Own certs config
|
||||
httpsCert:
|
||||
httpsKey:
|
||||
|
||||
emulator:
|
||||
# set output viewport scale factor
|
||||
scale: 1
|
||||
|
||||
aspectRatio:
|
||||
# enable aspect ratio changing
|
||||
# (experimental)
|
||||
keep: false
|
||||
# recalculate emulator game frame size to the given WxH
|
||||
width: 320
|
||||
height: 240
|
||||
|
||||
# save directory for emulator states
|
||||
# special tag {user} will be replaced with current user's home dir
|
||||
storage: "{user}/.cr/save"
|
||||
|
||||
libretro:
|
||||
cores:
|
||||
paths:
|
||||
libs: assets/cores
|
||||
configs: assets/cores
|
||||
# Config params for Libretro cores repository,
|
||||
# available types are:
|
||||
# - buildbot (the default Libretro nightly repository)
|
||||
# - github (GitHub raw repository with a similar structure to buildbot)
|
||||
# - raw (just a link to a zip file extracted as is)
|
||||
repo:
|
||||
# enable auto-download for the list of cores (list->lib)
|
||||
sync: true
|
||||
# external cross-process mutex lock
|
||||
extLock: "{user}/.cr/cloud-game.lock"
|
||||
main:
|
||||
type: buildbot
|
||||
url: https://buildbot.libretro.com/nightly
|
||||
# if repo has file compression
|
||||
compression: zip
|
||||
# a secondary repo to use i.e. for not found in the main cores
|
||||
secondary:
|
||||
type: github
|
||||
url: https://github.com/sergystepanov/libretro-spiegel/blob/main
|
||||
compression: zip
|
||||
# Libretro core configuration
|
||||
#
|
||||
# The emulator selection will happen in this order:
|
||||
# - based on the folder name in the folder param
|
||||
# - based on the folder name (core name) in the list (i.e. nes, snes)
|
||||
# - based on the rom names in the roms param
|
||||
#
|
||||
# Available config params:
|
||||
# - lib (string)
|
||||
# - config (string)
|
||||
# - roms ([]string)
|
||||
# - folder (string)
|
||||
# By default emulator selection is based on the folder named as cores
|
||||
# in the list (i.e. nes, snes) but if you specify folder param,
|
||||
# then it will try to load the ROM file from that folder first.
|
||||
# - width (int) -- broken
|
||||
# - height (int) -- broken
|
||||
# - ratio (float)
|
||||
# - isGlAllowed (bool)
|
||||
# - usesLibCo (bool)
|
||||
# - hasMultitap (bool)
|
||||
list:
|
||||
gba:
|
||||
lib: mgba_libretro
|
||||
roms: [ "gba", "gbc" ]
|
||||
pcsx:
|
||||
lib: pcsx_rearmed_libretro
|
||||
roms: [ "cue" ]
|
||||
# example of folder override
|
||||
folder: psx
|
||||
# MAME core requires additional manual setup, please read:
|
||||
# https://docs.libretro.com/library/fbneo/
|
||||
mame:
|
||||
lib: fbneo_libretro
|
||||
roms: [ "zip" ]
|
||||
nes:
|
||||
lib: nestopia_libretro
|
||||
roms: [ "nes" ]
|
||||
snes:
|
||||
lib: snes9x_libretro
|
||||
roms: [ "smc", "sfc", "swc", "fig", "bs" ]
|
||||
hasMultitap: true
|
||||
n64:
|
||||
lib: mupen64plus_next_libretro
|
||||
config: mupen64plus_next_libretro.cfg
|
||||
roms: [ "n64", "v64", "z64" ]
|
||||
isGlAllowed: true
|
||||
usesLibCo: true
|
||||
|
||||
encoder:
|
||||
audio:
|
||||
channels: 2
|
||||
# audio frame duration needed for WebRTC (Opus)
|
||||
frame: 20
|
||||
frequency: 48000
|
||||
video:
|
||||
# h264, vpx (VP8)
|
||||
codec: h264
|
||||
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
h264:
|
||||
# Constant Rate Factor (CRF) 0-51 (default: 23)
|
||||
crf: 17
|
||||
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
|
||||
preset: veryfast
|
||||
# baseline, main, high, high10, high422, high444
|
||||
profile: main
|
||||
# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
|
||||
tune: zerolatency
|
||||
# 0-3
|
||||
logLevel: 0
|
||||
# see: https://www.webmproject.org/docs/encoder-parameters
|
||||
vpx:
|
||||
# target bitrate (KBit/s)
|
||||
bitrate: 1200
|
||||
# force keyframe interval
|
||||
keyframeInterval: 5
|
||||
# run without a game
|
||||
# (experimental)
|
||||
withoutGame: false
|
||||
|
||||
webrtc:
|
||||
# turn off default Pion interceptors for performance reasons
|
||||
# (experimental)
|
||||
disableDefaultInterceptors:
|
||||
# a list of STUN/TURN servers for the client
|
||||
iceServers:
|
||||
- url: stun:stun.l.google.com:19302
|
||||
# instead of random unlimited port range for
|
||||
# WebRTC UDP connections, these params
|
||||
# define ICE candidates port range explicitly
|
||||
icePorts:
|
||||
min:
|
||||
max:
|
||||
# override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
|
||||
# can be used for Docker bridged network internal IP override
|
||||
iceIpMap:
|
||||
|
|
@ -1,21 +1,33 @@
|
|||
version: '3'
|
||||
services:
|
||||
|
||||
cloud-game:
|
||||
build: .
|
||||
image: cloud-game-local
|
||||
container_name: cloud-game-local
|
||||
privileged: true
|
||||
environment:
|
||||
- DISPLAY=:99
|
||||
- MESA_GL_VERSION_OVERRIDE=3.3
|
||||
network_mode: "host"
|
||||
- MESA_GL_VERSION_OVERRIDE=4.5
|
||||
- CLOUD_GAME_WEBRTC_SINGLEPORT=8443
|
||||
# - CLOUD_GAME_WEBRTC_ICEIPMAP=127.0.0.1
|
||||
- CLOUD_GAME_COORDINATOR_DEBUG=true
|
||||
- CLOUD_GAME_WORKER_DEBUG=true
|
||||
# - PION_LOG_TRACE=all
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "9000:9000"
|
||||
- "8443:8443/udp"
|
||||
command: >
|
||||
bash -c "Xvfb :99 & coordinator --v=5 & worker --coordinatorhost localhost:8000"
|
||||
bash -c "./coordinator & ./worker"
|
||||
volumes:
|
||||
# keep cores persistent in the cloud-game_cores volume
|
||||
- cores:/usr/local/share/cloud-game/assets/cores
|
||||
- ${CLOUD_GAME_GAMES_PATH}:/usr/local/share/cloud-game/assets/games
|
||||
- ./assets/cores:/usr/local/share/cloud-game/assets/cores
|
||||
- ./assets/games:/usr/local/share/cloud-game/assets/games
|
||||
- x11:/tmp/.X11-unix
|
||||
|
||||
xvfb:
|
||||
image: kcollins/xvfb:latest
|
||||
volumes:
|
||||
- x11:/tmp/.X11-unix
|
||||
command: [ ":99", "-screen", "0", "320x240x16" ]
|
||||
|
||||
volumes:
|
||||
cores:
|
||||
x11:
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
## Streaming process description
|
||||
|
||||
This document describes the step-by-step process of media streaming in all parts of the application.
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
|
||||
│ USER AGENT │ │ COORDINATOR │ │ WORKER...n │
|
||||
├──────────────┤ ├───────────────┤ ├──────────────┤
|
||||
│ TCP/WS ├──1──►│ WS ──────► WS │◄───┤ TCP/WS │
|
||||
│ │ │ ▲ 2 │ │ │ │
|
||||
│ │ │ └───────────┘ │ │ │
|
||||
│ │ └───────────────┘ │ │
|
||||
│ UDP/RTP │◄─────────────3────────────┤ UDP/RTP │
|
||||
│ AUDIO < │ OPUS │ AUDIO │
|
||||
│ VIDEO < │ VP8/H264 │ VIDEO │
|
||||
│ DATA > │ 010101 │ DATA │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
The app is based on WebRTC technology which allows the server to stream media and exchange data with ultra-low latencies. An essential part of these types of P2P connections is the signaling process. It's implemented as a custom text-based messaging protocol on top of WebSocket (quite similarly to [WAMP](https://wamp-proto.org)). The app supports both STUN and TURN protocols for NAT traversal or ICE. In terms of supported codecs, it can stream h264, VP8, and OPUS media.
|
||||
|
||||
The streaming process begins when a user opens the main application page (index.html) served by the coordinator.
|
||||
- The user's browser tries to open a new WebSocket connection to the coordinator — socket.init(roomId, zone) [web/js/network/socket.js:32](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L32)
|
||||
> In the initial WebSocket Upgrade request query it may send two params: roomId — an identification number for existing game rooms stored in the URL query of the application page (i.e. app.com/?id=xxxxxx), zone — or, more precisely, region — serves the purpose of CDN and geographical segmentation of the streaming.
|
||||
- On the coordinator side this request goes into a dedicated handler (/ws) — func (o *Server) WS(w http.ResponseWriter, r *http.Request) [pkg/coordinator/handlers.go:150](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/pkg/coordinator/handlers.go#L150)
|
||||
- There, it unconditionally accepts the WebSocket connection and tags it with some ID, so it will be listening to messages from the user's side. Here a new client connection should be considered as established.
|
||||
- Next, given provided query params, the coordinator tries to find a suitable worker whose job — directly stream games to a user.
|
||||
> This process of choosing the right worker is following: if there is no roomId param, then the coordinator gathers the full list of available workers, filters them by a zone value (if provided), returns the user a list of public URLs, which he can ping and send results back to the coordinator. After that, the coordinator links the fastest one with the user. Alternatively, if the user did provide some roomId, then the coordinator directly assigns a worker with that room (workers have 1:1 mapping to rooms or games).
|
||||
> All the information exchange initiated from the worker side is handled in a separate endpoint (/wso) [pkg/coordinator/handlers.go#L81](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/handlers.go#L81).
|
||||
- Coordinator sends to the user ICE servers and the list of games available for playing. That's handled in [web/js/network/socket.js:57](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L57).
|
||||
- From this point, the user's browser begins to initialize WebRTC connection to the worker — web/js/controller.js:413 → [web/js/network/rtcp.js:16](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L16).
|
||||
- First, it sends init request through the WebSocket connection to the coordinator handler in [pkg/coordinator/useragenthandlers.go:17](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go#L17).
|
||||
> Following a standard WebRTC call [negotiation procedure](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling), the coordinator acts as a mediator between users and workers. The signaling protocol here is a text messaging through WebSocket transport.
|
||||
- Coordinator notifies the user's worker that it wants to establish a new PeerConnection (call). That part is being handled in [pkg/worker/internalhandlers.go:42](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/worker/internalhandlers.go#L42). It is worth noting that it is a worker who makes SDP offer and waits for an SDP answer.
|
||||
- Worker initializes new WebRTC connection handler in func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error) [pkg/webrtc/webrtc.go:103](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/webrtc/webrtc.go#L103).
|
||||
- Then through the coordinator it makes simultaneously an SDP offer as well as sends ICE candidates that are handled on the coordinator side (from the user) in [pkg/coordinator/useragenthandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go),
|
||||
(from the worker) in [pkg/coordinator/internalhandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/internalhandlers.go), and on the user side both in [web/js/network/socket.js:56](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/socket.js#L56) and inside [web/js/network/rtcp.js](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js).
|
||||
- Browser on the user's side after SDP offer links remote streams to the HTML Video element in [web/js/controller.js:417](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L417), makes SDP answer and gathers remote ICE candidates until it's done (if receive an empty ICE candidate).
|
||||
- For the user's side a successful WebRTC connection should be considered established when WebRTC datachannel is opened here [web/js/network/rtcp.js:31](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L31).
|
||||
*And that should be it for the streaming part.*
|
||||
> At this point all the connections should be successfully established and the user's ready for a game to start. The coordinator should notify the worker about that fact and the worker starts pushing media frames, listen to the input through the direct to the user WebRTC data channel.
|
||||
- Then the user may send the game start request to the coordinator in [web/js/controller.js:153](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L153).
|
||||
|
||||
#### Streaming requirements
|
||||
- Workers should not have any closed UDP ports to be able to provide suitable ICE candidates.
|
||||
- Coordinator should have at least one non-blocked TCP port (default: 8000) for HTTP/WebSocket signaling connections from users and workers.
|
||||
- Browser should not block WebRTC and support it (check [here](https://test.webrtc.org/)).
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Web-based Cloud Gaming Service Design Document
|
||||
|
||||
Web-based Cloud Gaming Service contains multiple workers for gaming stream and a coordinator (Coordinator) for distributing traffic and pairing up connection.
|
||||
|
||||
## Worker
|
||||
|
||||
Worker is responsible for streaming game to frontend
|
||||

|
||||
|
||||
- After Coordinator matches the most appropriate server to the user, webRTC peer-to-peer handshake will be conducted. The coordinator will exchange the signature (WebRTC Session Remote Description) between two peers over Web Socket connection.
|
||||
- On worker, each user session will spawn a new room running a gaming emulator. Image stream and audio stream from emulator is captured and encoded to WebRTC streaming format. We applied Vp8 for Video compression and Opus for audio compression to ensure the smoothest experience. After finish encoded, these stream is then piped out to user and observers joining that room.
|
||||
- On the other hand, input from users is sent to workers over WebRTC DataChannel. Game logic on the emulator will be updated based on the input stream.
|
||||
- Game state is stored in cloud storage, so all workers can collaborate and keep the same understanding with each other. It allows user can continue from the saved state in the next time.
|
||||
|
||||
## Coordinator
|
||||
|
||||
Coordinator is loadbalancer and coordinator, which is in charge of picking the most suitable workers for a user. Every time a user connects to Coordinator, it will collect all the metric from all workers, i.e free CPU resources and latency from worker to user. Coordinator will decide the best candidate based on the metric and setup peer-to-peer connection between worker and user based on WebRTC protocol
|
||||
|
||||

|
||||
|
||||
1. A user connected to Coordinator .
|
||||
2. Coordinator will find the most suitable worker to serve the user.
|
||||
3. Coordinator collects all latencies from workers to users as well as CPU usage on each machine.
|
||||
4. Coordinator setup peer-to-peer handshake between worker and user by exchanging Session Description Protocol.
|
||||
5. A game is hosted on worker and streamed to the user.
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# Web-based Cloud Gaming Service Implementation Document
|
||||
|
||||
## Code structure
|
||||
```
|
||||
.
|
||||
├── cmd: service entrypoint
|
||||
│ ├── main.go: Spawn coordinator or worker based on flag
|
||||
│ └── main_test.go
|
||||
├── static: static file for front end
|
||||
│ ├── js
|
||||
│ │ └── ws.js: client logic
|
||||
│ ├── game.html: frontend with gameboy ui
|
||||
│ └── index_ws.html: raw frontend without ui
|
||||
├── coordinator: coordinator
|
||||
│ ├── handlers.go: coordinator entrypoint
|
||||
│ ├── browser.go: router listening to browser
|
||||
│ └── worker.go: router listening to worker
|
||||
├── games: roms list, no code logic
|
||||
├── worker: integration between emulator + webrtc (communication)
|
||||
│ ├── room:
|
||||
│ │ ├── room.go: room logic
|
||||
│ │ └── media.go: video + audio encoding
|
||||
│ ├── handlers.go: worker entrypoint
|
||||
│ └── coordinator.go: router listening to coordinator
|
||||
├── emulator: emulator internal
|
||||
│ ├── nes: NES device internal
|
||||
│ ├── director.go: coordinator of views
|
||||
│ └── gameview.go: in game logic
|
||||
├── cws
|
||||
│ └── cws.go: socket multiplexer library, used for signaling
|
||||
└── webrtc: webrtc streaming logic
|
||||
```
|
||||
|
||||
## Room
|
||||
Room is a fundamental part of the system. Each user session will spawn a room with a game running inside. There is a pipeline to encode images and audio and stream them out from emulator to user. The pipeline also listens to all input and streams to the emulator.
|
||||
|
||||
## Worker
|
||||
Worker is an instance that can be provisioned to scale up the traffic. There are multiple rooms inside a worker. Worker will listen to coordinator events in `coordinator.go`.
|
||||
|
||||
## Coordinator
|
||||
Coordinator is the coordinator, which handles all communication with workers and frontend.
|
||||
Coordinator will pair up a worker and a user for peer streaming. In WebRTC handshaking, two peers need to exchange their signature (Session Description Protocol) to initiate a peerconnection.
|
||||
Events come from frontend will be handled in `coordinator/browser.go`. Events come from worker will be handled in `coordinator/worker.go`. Coordinator stays in the middle and relays handshake packages between workers and user.
|
||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 8.4 MiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 347 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -1,28 +0,0 @@
|
|||
# Web-based Cloud Gaming Service Game Instruction
|
||||
|
||||
The game can be played on Desktop, Mobile (Android only). You can plug joystick to play with the game.
|
||||
|
||||
Click question mark on the top left to see game instruction.
|
||||
|
||||
## Key map on Desktop
|
||||
Game keymap follows
|
||||
Arrow keys to move
|
||||
H -> Show help
|
||||
C -> Start
|
||||
V -> Select
|
||||
Z -> A
|
||||
X -> B
|
||||
S -> Save (Save state)
|
||||
A -> Load (Load previous saved state)
|
||||
W -> Share your running game to other or you can keep it to continue playing the next time. Multiple people can access the same game for multiplayer or observation.
|
||||
F -> Full screen
|
||||
Q -> Quit the current game and go to menu screen.**NOTE**: we are facing some issue with quit, so it's better to refresh the page.
|
||||
|
||||
## Mobile play
|
||||
You can play the game on Android device. Make sure your Android has the version that support WebRTC. IOS doesn't support WebRTC streaming now.
|
||||
|
||||
The keys map are equivalent to Desktop. Press the button to fire input.
|
||||
|
||||
## Joystick
|
||||
The game also accepts joystick, so you can try plug in one and experience. It will be very fun!
|
||||
|
||||
91
go.mod
|
|
@ -1,37 +1,62 @@
|
|||
module github.com/giongto35/cloud-game/v2
|
||||
module github.com/giongto35/cloud-game/v3
|
||||
|
||||
go 1.13
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.91.1 // indirect
|
||||
cloud.google.com/go/storage v1.16.0
|
||||
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec
|
||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
|
||||
github.com/fsnotify/fsnotify v1.4.9
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/kkyr/fig v0.3.0
|
||||
github.com/pion/ice/v2 v2.1.12 // indirect
|
||||
github.com/pion/interceptor v0.0.15
|
||||
github.com/pion/rtp v1.7.1
|
||||
github.com/pion/srtp/v2 v2.0.5 // indirect
|
||||
github.com/pion/webrtc/v3 v3.0.32
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/common v0.30.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.2 // indirect
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/veandco/go-sdl2 v0.4.8
|
||||
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
||||
golang.org/x/mod v0.5.0 // indirect
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 // indirect
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/api v0.54.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d // indirect
|
||||
github.com/VictoriaMetrics/metrics v1.40.2
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/knadh/koanf/maps v0.1.2
|
||||
github.com/knadh/koanf/v2 v2.3.0
|
||||
github.com/minio/minio-go/v7 v7.0.97
|
||||
github.com/pion/ice/v4 v4.1.0
|
||||
github.com/pion/interceptor v0.1.42
|
||||
github.com/pion/logging v0.2.4
|
||||
github.com/pion/webrtc/v4 v4.1.8
|
||||
github.com/rs/xid v1.6.0
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/veandco/go-sdl2 v0.4.40
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/image v0.34.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.9 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.16 // indirect
|
||||
github.com/pion/rtp v1.8.27 // indirect
|
||||
github.com/pion/sctp v1.8.41 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.17 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.9 // indirect
|
||||
github.com/pion/stun/v3 v3.0.2 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
)
|
||||
|
|
|
|||
879
go.sum
|
|
@ -1,762 +1,133 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
|
||||
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
|
||||
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
|
||||
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
|
||||
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
|
||||
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
|
||||
cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY=
|
||||
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
|
||||
cloud.google.com/go v0.91.1 h1:w+u8ttN/QtYrpvgXNUd2G6kwqrqCIQbkINlXQjHP1ek=
|
||||
cloud.google.com/go v0.91.1/go.mod h1:V358WZfbFQkmC3gv5XCxzZq2e3h7OGvQR0IXtj77ylI=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2Kg=
|
||||
cloud.google.com/go/storage v1.16.0/go.mod h1:ieKBmUyzcftN5tbxwnXClMKH00CfcQ+xL6NN0r5QfmE=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
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/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec h1:4XvMn0XuV7qxCH22gbnR79r+xTUaLOSA0GW/egpO3SQ=
|
||||
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec/go.mod h1:NbXoa59CCAGqtRm7kRrcZIk2dTCJMRVF8QI3BOD7isY=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac=
|
||||
github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
|
||||
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
|
||||
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/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 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
|
||||
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210804190019-f964ff605595/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkyr/fig v0.3.0 h1:5bd1amYKp/gsK2bGEUJYzcCrQPKOZp6HZD9K21v9Guo=
|
||||
github.com/kkyr/fig v0.3.0/go.mod h1:fEnrLjwg/iwSr8ksJF4DxrDmCUir5CaVMLORGYMcz30=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
|
||||
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
|
||||
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
||||
github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8=
|
||||
github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho=
|
||||
github.com/pion/ice/v2 v2.1.10/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0=
|
||||
github.com/pion/ice/v2 v2.1.12 h1:ZDBuZz+fEI7iDifZCYFVzI4p0Foy0YhdSSZ87ZtRcRE=
|
||||
github.com/pion/ice/v2 v2.1.12/go.mod h1:ovgYHUmwYLlRvcCLI67PnQ5YGe+upXZbGgllBDG/ktU=
|
||||
github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo=
|
||||
github.com/pion/interceptor v0.0.15 h1:pQFkBUL8akUHiGoFr+pM94Q/15x7sLFh0K3Nj+DCC6s=
|
||||
github.com/pion/interceptor v0.0.15/go.mod h1:pg3J253eGi5bqyKzA74+ej5Y19ez2jkWANVnF+Z9Dfk=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
|
||||
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
|
||||
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
||||
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.7.1 h1:hCaxfVgPGt13eF/Tu9RhVn04c+dAcRZmhdDWqUE13oY=
|
||||
github.com/pion/rtp v1.7.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
||||
github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY=
|
||||
github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
|
||||
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
||||
github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
|
||||
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
|
||||
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
|
||||
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
|
||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pion/webrtc/v3 v3.0.32 h1:5J+zNep9am8Swh6kEMp+LaGXNvn6qQWpGkLBnVW44L4=
|
||||
github.com/pion/webrtc/v3 v3.0.32/go.mod h1:wX3V5dQQUGCifhT1mYftC2kCrDQX6ZJ3B7Yad0R9JK0=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/rtp v1.8.27 h1:kbWTdZr62RDlYjatVAW4qFwrAu9XcGnwMsofCfAHlOU=
|
||||
github.com/pion/rtp v1.8.27/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
|
||||
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
|
||||
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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
|
||||
github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.2 h1:zE6zJjRS9S916ptrZ326OU0++1XRwHgxkvCFflxx6Fo=
|
||||
github.com/prometheus/procfs v0.7.2/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/veandco/go-sdl2 v0.4.8 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE=
|
||||
github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/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-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ=
|
||||
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s=
|
||||
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
|
||||
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
|
||||
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
|
||||
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
|
||||
google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
|
||||
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
|
||||
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
|
||||
google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7SrU=
|
||||
google.golang.org/api v0.54.0 h1:ECJUVngj71QI6XEm7b1sAf8BljU5inEhMbKPR8Lxhhk=
|
||||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
|
||||
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
|
||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
|
||||
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
|
||||
google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
|
||||
google.golang.org/genproto v0.0.0-20210811021853-ddbe55d93216/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
|
||||
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d h1:fPtHPeysWvGVJwQFKu3B7H2DB2sOEsW7UTayKkWESKw=
|
||||
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
|
||||
google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
||||
github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
|
||||
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
202
pkg/api/api.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
// Package api defines the general API for both coordinator and worker applications.
|
||||
//
|
||||
// Each API call (request and response) is a JSON-encoded "packet" of the following structure:
|
||||
//
|
||||
// id - (optional) a globally unique packet id;
|
||||
// t - (required) one of the predefined unique packet types;
|
||||
// p - (optional) packet payload with arbitrary data.
|
||||
//
|
||||
// The basic idea behind this API is that the packets differentiate by their predefined types
|
||||
// with which it is possible to unwrap the payload into distinct request/response data structures.
|
||||
// And the id field is used for tracking packets through a chain of different network points (apps, devices),
|
||||
// for example, passing a packet from a browser forward to a worker and back through a coordinator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// {"t":4,"p":{"ice":[{"urls":"stun:stun.l.google.com:19302"}],"games":["Sushi The Cat"],"wid":"cfv68irdrc3ifu3jn6bg"}}
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
Id interface {
|
||||
String() string
|
||||
}
|
||||
Stateful struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
Room struct {
|
||||
Rid string `json:"room_id"`
|
||||
}
|
||||
StatefulRoom struct {
|
||||
Id string `json:"id"`
|
||||
Rid string `json:"room_id"`
|
||||
}
|
||||
PT uint8
|
||||
)
|
||||
|
||||
type In[I Id] struct {
|
||||
Id I `json:"id,omitempty"`
|
||||
T PT `json:"t"`
|
||||
Payload json.RawMessage `json:"p,omitempty"` // should be json.RawMessage for 2-pass unmarshal
|
||||
}
|
||||
|
||||
func (i In[I]) GetId() I { return i.Id }
|
||||
func (i In[I]) GetPayload() []byte { return i.Payload }
|
||||
func (i In[I]) GetType() PT { return i.T }
|
||||
|
||||
type Out struct {
|
||||
Id string `json:"id,omitempty"` // string because omitempty won't work as intended with arrays
|
||||
T uint8 `json:"t"`
|
||||
Payload any `json:"p,omitempty"`
|
||||
}
|
||||
|
||||
func (o *Out) SetId(s string) { o.Id = s }
|
||||
func (o *Out) SetType(u uint8) { o.T = u }
|
||||
func (o *Out) SetPayload(a any) { o.Payload = a }
|
||||
func (o *Out) SetGetId(s fmt.Stringer) { o.Id = s.String() }
|
||||
func (o *Out) GetPayload() any { return o.Payload }
|
||||
|
||||
// Packet codes:
|
||||
//
|
||||
// x, 1xx - user codes
|
||||
// 15x - webrtc data exchange codes
|
||||
// 2xx - worker codes
|
||||
const (
|
||||
CheckLatency PT = 3
|
||||
InitSession PT = 4
|
||||
WebrtcInit PT = 100
|
||||
WebrtcOffer PT = 101
|
||||
WebrtcAnswer PT = 102
|
||||
WebrtcIce PT = 103
|
||||
StartGame PT = 104
|
||||
QuitGame PT = 105
|
||||
SaveGame PT = 106
|
||||
LoadGame PT = 107
|
||||
ChangePlayer PT = 108
|
||||
RecordGame PT = 110
|
||||
GetWorkerList PT = 111
|
||||
ErrNoFreeSlots PT = 112
|
||||
ResetGame PT = 113
|
||||
RegisterRoom PT = 201
|
||||
CloseRoom PT = 202
|
||||
IceCandidate = WebrtcIce
|
||||
TerminateSession PT = 204
|
||||
AppVideoChange PT = 150
|
||||
LibNewGameList PT = 205
|
||||
PrevSessions PT = 206
|
||||
)
|
||||
|
||||
func (p PT) String() string {
|
||||
switch p {
|
||||
case CheckLatency:
|
||||
return "CheckLatency"
|
||||
case InitSession:
|
||||
return "InitSession"
|
||||
case WebrtcInit:
|
||||
return "WebrtcInit"
|
||||
case WebrtcOffer:
|
||||
return "WebrtcOffer"
|
||||
case WebrtcAnswer:
|
||||
return "WebrtcAnswer"
|
||||
case WebrtcIce:
|
||||
return "WebrtcIce"
|
||||
case StartGame:
|
||||
return "StartGame"
|
||||
case ChangePlayer:
|
||||
return "ChangePlayer"
|
||||
case QuitGame:
|
||||
return "QuitGame"
|
||||
case SaveGame:
|
||||
return "SaveGame"
|
||||
case LoadGame:
|
||||
return "LoadGame"
|
||||
case RecordGame:
|
||||
return "RecordGame"
|
||||
case GetWorkerList:
|
||||
return "GetWorkerList"
|
||||
case ErrNoFreeSlots:
|
||||
return "NoFreeSlots"
|
||||
case ResetGame:
|
||||
return "ResetGame"
|
||||
case RegisterRoom:
|
||||
return "RegisterRoom"
|
||||
case CloseRoom:
|
||||
return "CloseRoom"
|
||||
case TerminateSession:
|
||||
return "TerminateSession"
|
||||
case AppVideoChange:
|
||||
return "AppVideoChange"
|
||||
case LibNewGameList:
|
||||
return "LibNewGameList"
|
||||
case PrevSessions:
|
||||
return "PrevSessions"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Various codes
|
||||
const (
|
||||
EMPTY = ""
|
||||
OK = "ok"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrForbidden = fmt.Errorf("forbidden")
|
||||
ErrMalformed = fmt.Errorf("malformed")
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyPacket = Out{Payload: ""}
|
||||
ErrPacket = Out{Payload: "err"}
|
||||
OkPacket = Out{Payload: "ok"}
|
||||
)
|
||||
|
||||
func Do[I Id, T any](in In[I], fn func(T)) error {
|
||||
if dat := Unwrap[T](in.Payload); dat != nil {
|
||||
fn(*dat)
|
||||
return nil
|
||||
}
|
||||
return ErrMalformed
|
||||
}
|
||||
|
||||
func DoE[I Id, T any](in In[I], fn func(T) error) error {
|
||||
if dat := Unwrap[T](in.Payload); dat != nil {
|
||||
return fn(*dat)
|
||||
}
|
||||
return ErrMalformed
|
||||
}
|
||||
|
||||
func Unwrap[T any](data []byte) *T {
|
||||
out := new(T)
|
||||
if err := json.Unmarshal(data, out); err != nil {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func UnwrapChecked[T any](bytes []byte, err error) (*T, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Unwrap[T](bytes), nil
|
||||
}
|
||||
|
||||
func Wrap(t any) ([]byte, error) { return json.Marshal(t) }
|
||||
|
||||
const separator = "___"
|
||||
|
||||
func ExplodeDeepLink(link string) (string, string) {
|
||||
p := strings.SplitN(link, separator, 2)
|
||||
|
||||
if len(p) == 1 {
|
||||
return p[0], ""
|
||||
}
|
||||
|
||||
return p[0], p[1]
|
||||
}
|
||||
42
pkg/api/coordinator.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package api
|
||||
|
||||
type (
|
||||
CloseRoomRequest string
|
||||
ConnectionRequest[T Id] struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Id T `json:"id,omitempty"`
|
||||
IsHTTPS bool `json:"is_https,omitempty"`
|
||||
PingURL string `json:"ping_url,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
}
|
||||
GetWorkerListResponse struct {
|
||||
Servers []Server `json:"servers"`
|
||||
}
|
||||
RegisterRoomRequest string
|
||||
)
|
||||
|
||||
const (
|
||||
DataQueryParam = "data"
|
||||
RoomIdQueryParam = "room_id"
|
||||
ZoneQueryParam = "zone"
|
||||
WorkerIdParam = "wid"
|
||||
)
|
||||
|
||||
// Server contains a list of server groups.
|
||||
// Server is a separate machine that may contain
|
||||
// multiple sub-processes.
|
||||
type Server struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Id Id `json:"id,omitempty"`
|
||||
IsBusy bool `json:"is_busy,omitempty"`
|
||||
InGroup bool `json:"in_group,omitempty"`
|
||||
Machine string `json:"machine,omitempty"`
|
||||
PingURL string `json:"ping_url"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
Room string `json:"room,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
}
|
||||
36
pkg/api/user.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package api
|
||||
|
||||
type (
|
||||
ChangePlayerUserRequest int
|
||||
CheckLatencyUserResponse []string
|
||||
CheckLatencyUserRequest map[string]int64
|
||||
GameStartUserRequest struct {
|
||||
GameName string `json:"game_name"`
|
||||
RoomId string `json:"room_id"`
|
||||
Record bool `json:"record,omitempty"`
|
||||
RecordUser string `json:"record_user,omitempty"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
GameStartUserResponse struct {
|
||||
RoomId string `json:"roomId"`
|
||||
Av *AppVideoInfo `json:"av"`
|
||||
KbMouse bool `json:"kb_mouse"`
|
||||
}
|
||||
IceServer struct {
|
||||
Urls string `json:"urls,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
InitSessionUserResponse struct {
|
||||
Ice []IceServer `json:"ice"`
|
||||
Games []AppMeta `json:"games"`
|
||||
Wid string `json:"wid"`
|
||||
}
|
||||
AppMeta struct {
|
||||
Alias string `json:"alias,omitempty"`
|
||||
Title string `json:"title"`
|
||||
System string `json:"system"`
|
||||
}
|
||||
WebrtcAnswerUserRequest string
|
||||
WebrtcUserIceCandidate string
|
||||
)
|
||||
70
pkg/api/worker.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package api
|
||||
|
||||
type (
|
||||
ChangePlayerRequest struct {
|
||||
StatefulRoom
|
||||
Index int `json:"index"`
|
||||
}
|
||||
ChangePlayerResponse int
|
||||
GameQuitRequest StatefulRoom
|
||||
LoadGameRequest StatefulRoom
|
||||
LoadGameResponse string
|
||||
ResetGameRequest StatefulRoom
|
||||
ResetGameResponse string
|
||||
SaveGameRequest StatefulRoom
|
||||
SaveGameResponse string
|
||||
StartGameRequest struct {
|
||||
StatefulRoom
|
||||
Record bool
|
||||
RecordUser string
|
||||
Game string `json:"game"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
GameInfo struct {
|
||||
Alias string `json:"alias"`
|
||||
Base string `json:"base"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
System string `json:"system"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
StartGameResponse struct {
|
||||
Room
|
||||
AV *AppVideoInfo `json:"av"`
|
||||
Record bool `json:"record"`
|
||||
KbMouse bool `json:"kb_mouse"`
|
||||
}
|
||||
RecordGameRequest struct {
|
||||
StatefulRoom
|
||||
Active bool `json:"active"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
RecordGameResponse string
|
||||
TerminateSessionRequest Stateful
|
||||
WebrtcAnswerRequest struct {
|
||||
Stateful
|
||||
Sdp string `json:"sdp"`
|
||||
}
|
||||
WebrtcIceCandidateRequest struct {
|
||||
Stateful
|
||||
Candidate string `json:"candidate"` // Base64-encoded ICE candidate
|
||||
}
|
||||
WebrtcInitRequest Stateful
|
||||
WebrtcInitResponse string
|
||||
|
||||
AppVideoInfo struct {
|
||||
W int `json:"w"`
|
||||
H int `json:"h"`
|
||||
S int `json:"s"`
|
||||
A float32 `json:"a"`
|
||||
}
|
||||
|
||||
LibGameListInfo struct {
|
||||
T int
|
||||
List []GameInfo
|
||||
}
|
||||
|
||||
PrevSessionInfo struct {
|
||||
List []string
|
||||
}
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package codec
|
||||
|
||||
type VideoCodec string
|
||||
|
||||
const (
|
||||
H264 VideoCodec = "h264"
|
||||
VPX VideoCodec = "vpx"
|
||||
)
|
||||
121
pkg/com/com.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package com
|
||||
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
|
||||
type stringer interface {
|
||||
comparable
|
||||
String() string
|
||||
}
|
||||
|
||||
type NetClient[K stringer] interface {
|
||||
Disconnect()
|
||||
Id() K
|
||||
}
|
||||
|
||||
type NetMap[K stringer, T NetClient[K]] struct{ Map[K, T] }
|
||||
|
||||
func NewNetMap[K stringer, T NetClient[K]]() NetMap[K, T] {
|
||||
return NetMap[K, T]{Map: Map[K, T]{m: make(map[K]T, 10)}}
|
||||
}
|
||||
|
||||
func (m *NetMap[K, T]) Add(client T) bool { return m.Put(client.Id(), client) }
|
||||
func (m *NetMap[K, T]) Empty() bool { return m.Map.Len() == 0 }
|
||||
func (m *NetMap[K, T]) Remove(client T) { m.Map.Remove(client.Id()) }
|
||||
func (m *NetMap[K, T]) RemoveL(client T) int { return m.Map.RemoveL(client.Id()) }
|
||||
func (m *NetMap[K, T]) Reset() { m.Map = Map[K, T]{m: make(map[K]T, 10)} }
|
||||
func (m *NetMap[K, T]) RemoveDisconnect(client T) { client.Disconnect(); m.Remove(client) }
|
||||
func (m *NetMap[K, T]) Find(id string) T {
|
||||
v, _ := m.Map.FindBy(func(v T) bool {
|
||||
return v.Id().String() == id
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
type SocketClient[T ~uint8, P Packet[T], X any, P2 Packet2[X]] struct {
|
||||
id Uid
|
||||
rpc *RPC[T, P]
|
||||
sock *Connection
|
||||
log *logger.Logger // a special logger for showing x -> y directions
|
||||
}
|
||||
|
||||
func NewConnection[T ~uint8, P Packet[T], X any, P2 Packet2[X]](conn *Connection, id Uid, log *logger.Logger) *SocketClient[T, P, X, P2] {
|
||||
if id.IsNil() {
|
||||
id = NewUid()
|
||||
}
|
||||
dir := logger.MarkOut
|
||||
if conn.IsServer() {
|
||||
dir = logger.MarkIn
|
||||
}
|
||||
dirClLog := log.Extend(log.With().
|
||||
Str("cid", id.Short()).
|
||||
Str(logger.DirectionField, dir),
|
||||
)
|
||||
dirClLog.Debug().Msg("Connect")
|
||||
return &SocketClient[T, P, X, P2]{sock: conn, id: id, log: dirClLog}
|
||||
}
|
||||
|
||||
func (c *SocketClient[T, P, _, _]) ProcessPackets(fn func(in P) error) chan struct{} {
|
||||
c.rpc = NewRPC[T, P]()
|
||||
c.rpc.Handler = func(p P) {
|
||||
c.log.Debug().Str(logger.DirectionField, logger.MarkIn).Msgf("%v", p.GetType())
|
||||
if err := fn(p); err != nil { // 3rd handler
|
||||
c.log.Error().Err(err).Send()
|
||||
}
|
||||
}
|
||||
c.sock.conn.SetMessageHandler(c.handleMessage) // 1st handler
|
||||
return c.sock.conn.Listen()
|
||||
}
|
||||
|
||||
func (c *SocketClient[T, P, X, P2]) SetErrorHandler(h func(error)) { c.sock.conn.SetErrorHandler(h) }
|
||||
|
||||
func (c *SocketClient[T, P, X, P2]) SetMaxMessageSize(s int64) { c.sock.conn.SetMaxMessageSize(s) }
|
||||
|
||||
func (c *SocketClient[_, _, _, _]) handleMessage(message []byte, err error) {
|
||||
if err != nil {
|
||||
c.log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
if err = c.rpc.handleMessage(message); err != nil { // 2nd handler
|
||||
c.log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SocketClient[_, P, X, P2]) Route(in P, out P2) {
|
||||
rq := P2(new(X))
|
||||
rq.SetId(in.GetId().String())
|
||||
rq.SetType(uint8(in.GetType()))
|
||||
rq.SetPayload(out.GetPayload())
|
||||
if err := c.rpc.Send(c.sock.conn, rq); err != nil {
|
||||
c.log.Error().Err(err).Msgf("message route fail")
|
||||
}
|
||||
}
|
||||
|
||||
// Send makes a blocking call.
|
||||
func (c *SocketClient[T, P, X, P2]) Send(t T, data any) ([]byte, error) {
|
||||
c.log.Debug().Str(logger.DirectionField, logger.MarkOut).Msgf("ᵇ%v", t)
|
||||
rq := P2(new(X))
|
||||
rq.SetType(uint8(t))
|
||||
rq.SetPayload(data)
|
||||
return c.rpc.Call(c.sock.conn, rq)
|
||||
}
|
||||
|
||||
// Notify just sends a message and goes further.
|
||||
func (c *SocketClient[T, P, X, P2]) Notify(t T, data any) {
|
||||
c.log.Debug().Str(logger.DirectionField, logger.MarkOut).Msgf("%v", t)
|
||||
rq := P2(new(X))
|
||||
rq.SetType(uint8(t))
|
||||
rq.SetPayload(data)
|
||||
if err := c.rpc.Send(c.sock.conn, rq); err != nil {
|
||||
c.log.Error().Err(err).Msgf("notify fail")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SocketClient[_, _, _, _]) Disconnect() {
|
||||
c.sock.conn.Close()
|
||||
c.rpc.Cleanup()
|
||||
c.log.Debug().Str(logger.DirectionField, logger.MarkCross).Msg("Close")
|
||||
}
|
||||
|
||||
func (c *SocketClient[_, _, _, _]) Id() Uid { return c.id }
|
||||
func (c *SocketClient[_, _, _, _]) String() string { return c.Id().String() }
|
||||
127
pkg/com/map.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Map defines a concurrent-safe map structure.
|
||||
// Keep in mind that the underlying map structure will grow indefinitely.
|
||||
type Map[K comparable, V any] struct {
|
||||
m map[K]V
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *Map[K, _]) Len() int {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return len(m.m)
|
||||
}
|
||||
|
||||
func (m *Map[K, _]) Has(key K) bool {
|
||||
m.mu.RLock()
|
||||
_, ok := m.m[key]
|
||||
m.mu.RUnlock()
|
||||
return ok
|
||||
}
|
||||
|
||||
// Get returns the value and exists flag (standard map comma-ok idiom).
|
||||
func (m *Map[K, V]) Get(key K) (V, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
val, ok := m.m[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Find(key K) V {
|
||||
v, _ := m.Get(key)
|
||||
return v
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) String() string {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return fmt.Sprintf("%v", m.m)
|
||||
}
|
||||
|
||||
// FindBy searches for the first value satisfying the predicate.
|
||||
// Note: This holds a Read Lock during iteration.
|
||||
func (m *Map[K, V]) FindBy(predicate func(v V) bool) (V, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
for _, v := range m.m {
|
||||
if predicate(v) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
var zero V
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// Put sets the value and returns true if the key already existed.
|
||||
func (m *Map[K, V]) Put(key K, v V) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.m == nil {
|
||||
m.m = make(map[K]V)
|
||||
}
|
||||
|
||||
_, exists := m.m[key]
|
||||
m.m[key] = v
|
||||
return exists
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Remove(key K) {
|
||||
m.mu.Lock()
|
||||
delete(m.m, key)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Pop returns the value and removes it from the map.
|
||||
// Returns zero value if not found.
|
||||
func (m *Map[K, V]) Pop(key K) V {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
val, ok := m.m[key]
|
||||
if ok {
|
||||
delete(m.m, key)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// RemoveL removes the key and returns the new length of the map.
|
||||
func (m *Map[K, _]) RemoveL(key K) int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.m, key)
|
||||
return len(m.m)
|
||||
}
|
||||
|
||||
// Clear empties the map.
|
||||
func (m *Map[K, V]) Clear() {
|
||||
m.mu.Lock()
|
||||
m.m = make(map[K]V)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Values returns an iterator for values only.
|
||||
//
|
||||
// Usage: for k, v := range m.Values() { ... }
|
||||
//
|
||||
// Warning: This holds a Read Lock (RLock) during iteration.
|
||||
// Do not call Put/Remove on this map inside the loop (Deadlock).
|
||||
func (m *Map[K, V]) Values() iter.Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, v := range m.m {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
pkg/com/map_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package com
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMap_Base(t *testing.T) {
|
||||
// map map
|
||||
m := Map[int, int]{m: make(map[int]int)}
|
||||
|
||||
if m.Len() > 0 {
|
||||
t.Errorf("should be empty, %v %v", m.Len(), m.m)
|
||||
}
|
||||
k := 0
|
||||
m.Put(k, 0)
|
||||
if m.Len() == 0 {
|
||||
t.Errorf("should not be empty, %v", m.m)
|
||||
}
|
||||
if !m.Has(k) {
|
||||
t.Errorf("should have the key %v, %v", k, m.m)
|
||||
}
|
||||
v, ok := m.Get(k)
|
||||
if v != 0 && !ok {
|
||||
t.Errorf("should have the key %v and ok, %v %v", k, ok, m.m)
|
||||
}
|
||||
_, ok = m.Get(k + 1)
|
||||
if ok {
|
||||
t.Errorf("should not find anything, %v %v", ok, m.m)
|
||||
}
|
||||
m.Put(1, 1)
|
||||
v, ok = m.FindBy(func(v int) bool { return v == 1 })
|
||||
if v != 1 && !ok {
|
||||
t.Errorf("should have the key %v and ok, %v %v", 1, ok, m.m)
|
||||
}
|
||||
sum := 0
|
||||
for v := range m.Values() {
|
||||
sum += v
|
||||
}
|
||||
if sum != 1 {
|
||||
t.Errorf("shoud have exact sum of 1, but have %v", sum)
|
||||
}
|
||||
m.Remove(1)
|
||||
if !m.Has(0) || m.Len() > 1 {
|
||||
t.Errorf("should remove only one element, but has %v", m.m)
|
||||
}
|
||||
m.Put(3, 3)
|
||||
v = m.Pop(3)
|
||||
if v != 3 {
|
||||
t.Errorf("should have value %v, but has %v %v", 3, v, m.m)
|
||||
}
|
||||
m.Remove(3)
|
||||
m.Remove(0)
|
||||
if m.Len() != 0 {
|
||||
t.Errorf("should be completely empty, but %v", m.m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap_Concurrency(t *testing.T) {
|
||||
m := Map[int, int]{m: make(map[int]int)}
|
||||
for i := range 100 {
|
||||
go m.Put(i, i)
|
||||
go m.Has(i)
|
||||
go m.Pop(i)
|
||||
}
|
||||
}
|
||||
178
pkg/com/net.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/network/websocket"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
type Uid struct {
|
||||
xid.ID
|
||||
}
|
||||
|
||||
var NilUid = Uid{xid.NilID()}
|
||||
|
||||
func NewUid() Uid { return Uid{xid.New()} }
|
||||
|
||||
func UidFromString(id string) (Uid, error) {
|
||||
x, err := xid.FromString(id)
|
||||
if err != nil {
|
||||
return NilUid, err
|
||||
}
|
||||
return Uid{x}, nil
|
||||
}
|
||||
|
||||
func (u Uid) Short() string { return u.String()[:3] + "." + u.String()[len(u.String())-3:] }
|
||||
|
||||
type HasCallId interface {
|
||||
SetGetId(fmt.Stringer)
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write([]byte)
|
||||
}
|
||||
|
||||
type Packet[T ~uint8] interface {
|
||||
GetId() Uid
|
||||
GetType() T
|
||||
GetPayload() []byte
|
||||
}
|
||||
|
||||
type Packet2[T any] interface {
|
||||
SetId(string)
|
||||
SetType(uint8)
|
||||
SetPayload(any)
|
||||
SetGetId(fmt.Stringer)
|
||||
GetPayload() any
|
||||
*T // non-interface type constraint element
|
||||
}
|
||||
|
||||
type Transport interface {
|
||||
SetMessageHandler(func([]byte, error))
|
||||
}
|
||||
|
||||
type RPC[T ~uint8, P Packet[T]] struct {
|
||||
CallTimeout time.Duration
|
||||
Handler func(P)
|
||||
Transport Transport
|
||||
|
||||
calls Map[Uid, *request]
|
||||
}
|
||||
|
||||
type request struct {
|
||||
done chan struct{}
|
||||
err error
|
||||
response []byte
|
||||
}
|
||||
|
||||
const DefaultCallTimeout = 10 * time.Second
|
||||
|
||||
var errCanceled = errors.New("canceled")
|
||||
var errTimeout = errors.New("timeout")
|
||||
|
||||
type (
|
||||
Client struct {
|
||||
websocket.Client
|
||||
}
|
||||
Server struct {
|
||||
websocket.Server
|
||||
}
|
||||
Connection struct {
|
||||
conn *websocket.Connection
|
||||
}
|
||||
)
|
||||
|
||||
func (c *Client) Connect(addr url.URL) (*Connection, error) { return connect(c.Client.Connect(addr)) }
|
||||
|
||||
func (s *Server) Origin(host string) { s.Upgrader = websocket.NewUpgrader(host) }
|
||||
|
||||
func (s *Server) Connect(w http.ResponseWriter, r *http.Request) (*Connection, error) {
|
||||
return connect(s.Server.Connect(w, r, nil))
|
||||
}
|
||||
|
||||
func (c *Connection) IsServer() bool { return c.conn.IsServer() }
|
||||
|
||||
func (c *Connection) SetMaxReadSize(s int64) { c.conn.SetMaxMessageSize(s) }
|
||||
|
||||
func connect(conn *websocket.Connection, err error) (*Connection, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Connection{conn: conn}, nil
|
||||
}
|
||||
|
||||
func NewRPC[T ~uint8, P Packet[T]]() *RPC[T, P] {
|
||||
return &RPC[T, P]{calls: Map[Uid, *request]{m: make(map[Uid]*request, 10)}}
|
||||
}
|
||||
|
||||
func (t *RPC[_, _]) Send(w Writer, packet any) error {
|
||||
r, err := json.Marshal(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RPC[_, _]) Call(w Writer, rq HasCallId) ([]byte, error) {
|
||||
id := NewUid()
|
||||
// set new request id for the external request structure as string
|
||||
rq.SetGetId(id)
|
||||
|
||||
r, err := json.Marshal(rq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
task := &request{done: make(chan struct{})}
|
||||
t.calls.Put(id, task)
|
||||
w.Write(r)
|
||||
select {
|
||||
case <-task.done:
|
||||
case <-time.After(t.callTimeout()):
|
||||
task.err = errTimeout
|
||||
}
|
||||
return task.response, task.err
|
||||
}
|
||||
|
||||
func (t *RPC[_, P]) handleMessage(message []byte) error {
|
||||
res := *new(P)
|
||||
if err := json.Unmarshal(message, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
// if we have an id, then unblock blocking call with that id
|
||||
id := res.GetId()
|
||||
if id != NilUid {
|
||||
if blocked := t.calls.Pop(id); blocked != nil {
|
||||
blocked.response = res.GetPayload()
|
||||
close(blocked.done)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if t.Handler != nil {
|
||||
t.Handler(res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *RPC[_, _]) callTimeout() time.Duration {
|
||||
if t.CallTimeout > 0 {
|
||||
return t.CallTimeout
|
||||
}
|
||||
return DefaultCallTimeout
|
||||
}
|
||||
|
||||
func (t *RPC[_, _]) Cleanup() {
|
||||
// drain cancels all what's left in the task queue.
|
||||
for task := range t.calls.Values() {
|
||||
if task.err == nil {
|
||||
task.err = errCanceled
|
||||
}
|
||||
close(task.done)
|
||||
}
|
||||
}
|
||||
225
pkg/com/net_test.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/network/websocket"
|
||||
)
|
||||
|
||||
type TestIn struct {
|
||||
Id Uid
|
||||
T uint8
|
||||
Payload json.RawMessage
|
||||
}
|
||||
|
||||
func (i TestIn) GetId() Uid { return i.Id }
|
||||
func (i TestIn) GetType() uint8 { return i.T }
|
||||
func (i TestIn) GetPayload() []byte { return i.Payload }
|
||||
|
||||
type TestOut struct {
|
||||
Id string
|
||||
T uint8
|
||||
Payload any
|
||||
}
|
||||
|
||||
func (o *TestOut) SetId(s string) { o.Id = s }
|
||||
func (o *TestOut) SetType(u uint8) { o.T = u }
|
||||
func (o *TestOut) SetPayload(a any) { o.Payload = a }
|
||||
func (o *TestOut) SetGetId(stringer fmt.Stringer) { o.Id = stringer.String() }
|
||||
func (o *TestOut) GetPayload() any { return o.Payload }
|
||||
|
||||
func TestWebsocket(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
test func(t *testing.T)
|
||||
}{
|
||||
{"If WebSocket implementation is OK in general", testWebsocket},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, tc.test)
|
||||
}
|
||||
}
|
||||
|
||||
func testWebsocket(t *testing.T) {
|
||||
port, err := getFreePort()
|
||||
if err != nil {
|
||||
t.Logf("couldn't get any free port")
|
||||
t.Skip()
|
||||
}
|
||||
addr := fmt.Sprintf(":%v", port)
|
||||
|
||||
server := newServer(addr, t)
|
||||
client := newClient(t, url.URL{Scheme: "ws", Host: "localhost" + addr, Path: "/ws"})
|
||||
clDone := client.ProcessPackets(func(in TestIn) error { return nil })
|
||||
|
||||
if server.conn == nil {
|
||||
t.Fatalf("couldn't make new socket")
|
||||
}
|
||||
|
||||
calls := []struct {
|
||||
packet TestOut
|
||||
concurrent bool
|
||||
value any
|
||||
}{
|
||||
{packet: TestOut{T: 10, Payload: "test"}, value: "test", concurrent: true},
|
||||
{packet: TestOut{T: 10, Payload: "test2"}, value: "test2"},
|
||||
{packet: TestOut{T: 11, Payload: "test3"}, value: "test3"},
|
||||
{packet: TestOut{T: 99, Payload: ""}, value: ""},
|
||||
{packet: TestOut{T: 0}},
|
||||
{packet: TestOut{T: 12, Payload: 123}, value: 123},
|
||||
{packet: TestOut{T: 10, Payload: false}, value: false},
|
||||
{packet: TestOut{T: 10, Payload: true}, value: true},
|
||||
{packet: TestOut{T: 11, Payload: []string{"test", "test", "test"}}, value: []string{"test", "test", "test"}},
|
||||
{packet: TestOut{T: 22, Payload: []string{}}, value: []string{}},
|
||||
}
|
||||
|
||||
const n = 42
|
||||
var wait sync.WaitGroup
|
||||
wait.Add(n * len(calls))
|
||||
|
||||
// test
|
||||
for _, call := range calls {
|
||||
if call.concurrent {
|
||||
for range n {
|
||||
packet := call.packet
|
||||
go func() {
|
||||
defer wait.Done()
|
||||
time.Sleep(time.Duration(rand.IntN(200-100)+100) * time.Millisecond)
|
||||
vv, err := client.rpc.Call(client.sock.conn, &packet)
|
||||
err = checkCall(vv, err, call.value)
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
for range n {
|
||||
packet := call.packet
|
||||
vv, err := client.rpc.Call(client.sock.conn, &packet)
|
||||
err = checkCall(vv, err, call.value)
|
||||
if err != nil {
|
||||
wait.Done()
|
||||
t.Fatalf("%v", err)
|
||||
} else {
|
||||
wait.Done()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wait.Wait()
|
||||
|
||||
client.sock.conn.Close()
|
||||
client.rpc.Cleanup()
|
||||
<-clDone
|
||||
server.conn.Close()
|
||||
<-server.done
|
||||
}
|
||||
|
||||
func newClient(t *testing.T, addr url.URL) *SocketClient[uint8, TestIn, TestOut, *TestOut] {
|
||||
connector := Client{}
|
||||
conn, err := connector.Connect(addr)
|
||||
if err != nil {
|
||||
t.Fatalf("error: couldn't connect to %v because of %v", addr.String(), err)
|
||||
}
|
||||
rpc := new(RPC[uint8, TestIn])
|
||||
rpc.calls = Map[Uid, *request]{m: make(map[Uid]*request, 10)}
|
||||
return &SocketClient[uint8, TestIn, TestOut, *TestOut]{sock: conn, log: logger.Default(), rpc: rpc}
|
||||
}
|
||||
|
||||
func checkCall(v []byte, err error, need any) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var value any
|
||||
if v != nil {
|
||||
if err = json.Unmarshal(v, &value); err != nil {
|
||||
return fmt.Errorf("can't unmarshal %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
nice := true
|
||||
// cast values after default unmarshal
|
||||
switch value.(type) {
|
||||
default:
|
||||
nice = value == need
|
||||
case bool:
|
||||
nice = value == need.(bool)
|
||||
case float64:
|
||||
nice = value == float64(need.(int))
|
||||
case string:
|
||||
nice = value == need.(string)
|
||||
case []any:
|
||||
// let's assume that's strings
|
||||
vv := value.([]any)
|
||||
for i := 0; i < len(need.([]string)); i++ {
|
||||
if vv[i].(string) != need.([]string)[i] {
|
||||
nice = false
|
||||
break
|
||||
}
|
||||
}
|
||||
case map[string]any:
|
||||
// ???
|
||||
}
|
||||
|
||||
if !nice {
|
||||
return fmt.Errorf("expected %v, but got %v", need, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type serverHandler struct {
|
||||
conn *websocket.Connection // ws server reference made dynamically on HTTP request
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (s *serverHandler) serve(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
|
||||
connector := Server{}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
sock, err := connector.Server.Connect(w, r, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't init socket server")
|
||||
}
|
||||
s.conn = sock
|
||||
s.conn.SetMessageHandler(func(m []byte, err error) { s.conn.Write(m) }) // echo
|
||||
s.done = s.conn.Listen()
|
||||
}
|
||||
}
|
||||
|
||||
func newServer(addr string, t *testing.T) *serverHandler {
|
||||
var wg sync.WaitGroup
|
||||
handler := serverHandler{}
|
||||
http.HandleFunc("/ws", handler.serve(t))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Done()
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
t.Errorf("no server, %v", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return &handler
|
||||
}
|
||||
|
||||
func getFreePort() (port int, err error) {
|
||||
var a *net.TCPAddr
|
||||
var l *net.TCPListener
|
||||
if a, err = net.ResolveTCPAddr("tcp", ":0"); err == nil {
|
||||
if l, err = net.ListenTCP("tcp", a); err == nil {
|
||||
defer func() { _ = l.Close() }()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
446
pkg/config/config.yaml
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
# The main config file
|
||||
|
||||
# Note.
|
||||
# Be aware that when this configuration is being overwritten
|
||||
# by another configuration, any empty nested part
|
||||
# in the further configurations will reset (empty out) all the values.
|
||||
# For example:
|
||||
# the main config second config result
|
||||
# ... ... ...
|
||||
# list: list: list:
|
||||
# gba: gba: gba:
|
||||
# lib: mgba_libretro lib: ""
|
||||
# roms: [ "gba", "gbc" ] roms: []
|
||||
# ... ...
|
||||
#
|
||||
# So do not leave empty nested keys.
|
||||
|
||||
# for the compatibility purposes
|
||||
version: 3
|
||||
|
||||
# new decentralized library of games
|
||||
library:
|
||||
# optional alias file for overriding game names from the basePath path
|
||||
aliasFile: alias.txt
|
||||
# root folder for the library (where games are stored)
|
||||
basePath: assets/games
|
||||
# a list of ignored words in the ROM filenames
|
||||
ignored:
|
||||
- neogeo
|
||||
- pgm
|
||||
# DOSBox filesystem state
|
||||
- .pure
|
||||
# an explicit list of supported file extensions
|
||||
# which overrides Libretro emulator ROMs configs
|
||||
supported:
|
||||
# print some additional info
|
||||
verbose: true
|
||||
# enable library directory live reload
|
||||
# (experimental)
|
||||
watchMode: false
|
||||
|
||||
coordinator:
|
||||
# debugging switch
|
||||
# - shows debug logs
|
||||
# - allows selecting worker instances
|
||||
debug: false
|
||||
# selects free workers:
|
||||
# - empty value (default, any free)
|
||||
# - ping (with the lowest ping)
|
||||
selector:
|
||||
monitoring:
|
||||
port: 6601
|
||||
# enable Go profiler HTTP server
|
||||
profilingEnabled: false
|
||||
metricEnabled: false
|
||||
urlPrefix: /coordinator
|
||||
# a custom Origins for incoming Websocket connections:
|
||||
# "" -- checks same origin policy
|
||||
# "*" -- allows all
|
||||
# "your address" -- checks for that address
|
||||
origin:
|
||||
userWs:
|
||||
workerWs:
|
||||
# max websocket message size in bytes
|
||||
maxWsSize: 32000000
|
||||
# HTTP(S) server config
|
||||
server:
|
||||
address: :8000
|
||||
cacheControl: "max-age=259200, must-revalidate"
|
||||
frameOptions: ""
|
||||
https: false
|
||||
# Letsencrypt or self cert config
|
||||
tls:
|
||||
address: :443
|
||||
# allowed host name
|
||||
domain:
|
||||
# if both are set then will use certs
|
||||
# and Letsencryt instead
|
||||
httpsCert:
|
||||
httpsKey:
|
||||
analytics:
|
||||
inject: false
|
||||
gtag:
|
||||
|
||||
worker:
|
||||
# show more logs
|
||||
debug: false
|
||||
library:
|
||||
# root folder for the library (where games are stored)
|
||||
basePath: assets/games
|
||||
network:
|
||||
# a coordinator address to connect to
|
||||
coordinatorAddress: localhost:8000
|
||||
# where to connect
|
||||
endpoint: /wso
|
||||
# ping endpoint
|
||||
pingEndpoint: /echo
|
||||
# set public ping address (IP or hostname)
|
||||
publicAddress:
|
||||
# make coordinator connection secure (wss)
|
||||
secure: false
|
||||
# ISO Alpha-2 country code to group workers by zones
|
||||
zone:
|
||||
monitoring:
|
||||
# monitoring server port
|
||||
port: 6602
|
||||
profilingEnabled: false
|
||||
# monitoring server URL prefix
|
||||
metricEnabled: false
|
||||
urlPrefix: /worker
|
||||
server:
|
||||
address: :9000
|
||||
https: false
|
||||
tls:
|
||||
address: :444
|
||||
# LetsEncrypt config
|
||||
# allowed host name
|
||||
domain:
|
||||
# Own certs config
|
||||
httpsCert:
|
||||
httpsKey:
|
||||
# optional server tag
|
||||
tag:
|
||||
|
||||
emulator:
|
||||
# set the total number of threads for the image processing
|
||||
# (removed)
|
||||
threads: 0
|
||||
|
||||
# enable autosave for emulator states if set to a non-zero value of seconds
|
||||
autosaveSec: 0
|
||||
|
||||
# save directory for emulator states
|
||||
# special tag {user} will be replaced with current user's home dir
|
||||
storage: "{user}/.cr/save"
|
||||
|
||||
# path for storing emulator generated files
|
||||
localPath: "./libretro"
|
||||
|
||||
# checks if the system supports running an emulator at startup
|
||||
failFast: true
|
||||
|
||||
# do not send late video frames
|
||||
skipLateFrames: false
|
||||
|
||||
# log dropped frames (temp)
|
||||
logDroppedFrames: false
|
||||
|
||||
libretro:
|
||||
# use zip compression for emulator save states
|
||||
saveCompression: true
|
||||
# Sets a limiter function for some spammy core callbacks.
|
||||
# 0 - disabled, otherwise -- time in milliseconds for ignoring repeated calls except the last.
|
||||
debounceMs: 0
|
||||
# Allow duplicate frames
|
||||
dup: true
|
||||
# Libretro cores logging level: DEBUG = 0, INFO, WARN, ERROR, DUMMY = INT_MAX
|
||||
logLevel: 1
|
||||
cores:
|
||||
paths:
|
||||
libs: assets/cores
|
||||
# Config params for Libretro cores repository,
|
||||
# available types are:
|
||||
# - buildbot (the default Libretro nightly repository)
|
||||
# - github (GitHub raw repository with a similar structure to buildbot)
|
||||
# - raw (just a link to a zip file extracted as is)
|
||||
repo:
|
||||
# enable auto-download for the list of cores (list->lib)
|
||||
sync: true
|
||||
# external cross-process mutex lock
|
||||
extLock: "{user}/.cr/cloud-game.lock"
|
||||
map:
|
||||
darwin:
|
||||
amd64:
|
||||
arch: x86_64
|
||||
ext: .dylib
|
||||
os: osx
|
||||
vendor: apple
|
||||
arm64:
|
||||
arch: arm64
|
||||
ext: .dylib
|
||||
os: osx
|
||||
vendor: apple
|
||||
linux:
|
||||
amd64:
|
||||
arch: x86_64
|
||||
ext: .so
|
||||
os: linux
|
||||
arm:
|
||||
arch: armv7-neon-hf
|
||||
ext: .so
|
||||
os: linux
|
||||
windows:
|
||||
amd64:
|
||||
arch: x86_64
|
||||
ext: .dll
|
||||
os: windows
|
||||
main:
|
||||
type: buildbot
|
||||
url: https://buildbot.libretro.com/nightly
|
||||
# if repo has file compression
|
||||
compression: zip
|
||||
# a secondary repo to use i.e. for not found in the main cores
|
||||
secondary:
|
||||
type: github
|
||||
url: https://github.com/sergystepanov/libretro-spiegel/raw/main
|
||||
compression: zip
|
||||
# Libretro core configuration
|
||||
#
|
||||
# The emulator selection will happen in this order:
|
||||
# - based on the folder name in the folder param
|
||||
# - based on the folder name (core name) in the list (i.e. nes, snes)
|
||||
# - based on the rom names in the roms param
|
||||
#
|
||||
# Available config params:
|
||||
# - altRepo (bool) prioritize secondary repo as the download source
|
||||
# - lib (string)
|
||||
# - roms ([]string)
|
||||
# - scale (int) scales the output video frames by this factor.
|
||||
# - folder (string)
|
||||
# By default emulator selection is based on the folder named as cores
|
||||
# in the list (i.e. nes, snes) but if you specify folder param,
|
||||
# then it will try to load the ROM file from that folder first.
|
||||
# - width (int) -- broken
|
||||
# - height (int) -- broken
|
||||
# - ratio (float)
|
||||
# - isGlAllowed (bool)
|
||||
# - usesLibCo (bool)
|
||||
# - hasMultitap (bool) -- (removed)
|
||||
# - coreAspectRatio (bool) -- (deprecated) correct the aspect ratio on the client with the info from the core.
|
||||
# - hid (map[int][]int)
|
||||
# A list of device IDs to bind to the input ports.
|
||||
# Can be seen in human readable form in the console when worker.debug is enabled.
|
||||
# Some cores allow binding multiple devices to a single port (DosBox), but typically,
|
||||
# you should bind just one device to one port.
|
||||
# - kbMouseSupport (bool) -- (temp) a flag if the core needs the keyboard and mouse on the client
|
||||
# - nonBlockingSave (bool) -- write save file in a non-blocking way, needed for huge save files
|
||||
# - vfr (bool)
|
||||
# (experimental)
|
||||
# Enable variable frame rate only for cores that can't produce a constant frame rate.
|
||||
# By default, we assume that cores output frames at a constant rate which equals
|
||||
# their tick rate (1/system FPS), but OpenGL cores like N64 may have significant
|
||||
# frame rendering time inconsistencies. In general, VFR for CFR cores leads to
|
||||
# noticeable video stutter (with the current frame rendering time calculations).
|
||||
# - options ([]string) a list of Libretro core options for tweaking.
|
||||
# All keys of the options should be in the double quotes in order to preserve upper-case symbols.
|
||||
# - options4rom (rom[[]string])
|
||||
# A list of core options to override for a specific core depending on the current ROM name.
|
||||
# - hacks ([]string) a list of hacks.
|
||||
# Available:
|
||||
# - skip_hw_context_destroy -- don't destroy OpenGL context during Libretro core deinit.
|
||||
# May help with crashes, for example, with PPSSPP.
|
||||
# - skip_same_thread_save -- skip thread lock save (used with PPSSPP).
|
||||
# - uniqueSaveDir (bool) -- needed only for cores (like DosBox) that persist their state into one shared file.
|
||||
# This will allow for concurrent reading and saving of current states.
|
||||
# - saveStateFs (string) -- the name of the file that will be initially copied into the save folder.
|
||||
# All * symbols will be replaced to the name of the ROM.
|
||||
list:
|
||||
gba:
|
||||
lib: mgba_libretro
|
||||
roms: [ "gba", "gbc" ]
|
||||
options:
|
||||
mgba_audio_low_pass_filter: disabled
|
||||
mgba_audio_low_pass_range: 50
|
||||
pcsx:
|
||||
lib: pcsx_rearmed_libretro
|
||||
roms: [ "cue", "chd" ]
|
||||
# example of folder override
|
||||
folder: psx
|
||||
# see: https://github.com/libretro/pcsx_rearmed/blob/master/frontend/libretro_core_options.h
|
||||
options:
|
||||
"pcsx_rearmed_show_bios_bootlogo": enabled
|
||||
"pcsx_rearmed_drc": enabled
|
||||
"pcsx_rearmed_display_internal_fps": disabled
|
||||
# MAME core requires additional manual setup, please read:
|
||||
# https://docs.libretro.com/library/fbneo/
|
||||
mame:
|
||||
lib: fbneo_libretro
|
||||
folder: mame
|
||||
roms: [ "zip" ]
|
||||
nes:
|
||||
lib: nestopia_libretro
|
||||
roms: [ "nes" ]
|
||||
options:
|
||||
nestopia_aspect: "uncorrected"
|
||||
snes:
|
||||
lib: snes9x_libretro
|
||||
roms: [ "smc", "sfc", "swc", "fig", "bs" ]
|
||||
hid:
|
||||
# set the 2nd port to RETRO_DEVICE_JOYPAD_MULTITAP ((1<<8) | 1) as SNES9x requires it
|
||||
# in order to support up to 5-player games
|
||||
# see: https://nintendo.fandom.com/wiki/Super_Multitap
|
||||
1: 257
|
||||
n64:
|
||||
lib: mupen64plus_next_libretro
|
||||
roms: [ "n64", "v64", "z64" ]
|
||||
isGlAllowed: true
|
||||
usesLibCo: true
|
||||
vfr: true
|
||||
# see: https://github.com/libretro/mupen64plus-libretro-nx/blob/master/libretro/libretro_core_options.h
|
||||
options:
|
||||
"mupen64plus-169screensize": 640x360
|
||||
"mupen64plus-43screensize": 320x240
|
||||
"mupen64plus-EnableCopyColorToRDRAM": Off
|
||||
"mupen64plus-EnableCopyDepthToRDRAM": Off
|
||||
"mupen64plus-EnableEnhancedTextureStorage": True
|
||||
"mupen64plus-EnableFBEmulation": True
|
||||
"mupen64plus-EnableLegacyBlending": True
|
||||
"mupen64plus-FrameDuping": True
|
||||
"mupen64plus-MaxTxCacheSize": 8000
|
||||
"mupen64plus-ThreadedRenderer": False
|
||||
"mupen64plus-cpucore": dynamic_recompiler
|
||||
"mupen64plus-pak1": memory
|
||||
"mupen64plus-rdp-plugin": gliden64
|
||||
"mupen64plus-rsp-plugin": hle
|
||||
"mupen64plus-astick-sensitivity": 100
|
||||
dos:
|
||||
lib: dosbox_pure_libretro
|
||||
roms: [ "zip", "cue" ]
|
||||
folder: dos
|
||||
kbMouseSupport: true
|
||||
nonBlockingSave: true
|
||||
saveStateFs: "*.pure.zip"
|
||||
hid:
|
||||
0: [ 257, 513 ]
|
||||
1: [ 257, 513 ]
|
||||
2: [ 257, 513 ]
|
||||
3: [ 257, 513 ]
|
||||
options:
|
||||
"dosbox_pure_conf": "outside"
|
||||
"dosbox_pure_force60fps": "true"
|
||||
|
||||
encoder:
|
||||
audio:
|
||||
# audio frame duration needed for WebRTC (Opus)
|
||||
# most of the emulators have ~1400 samples per a video frame,
|
||||
# so we keep the frame buffer roughly half of that size or 2 RTC packets per frame
|
||||
# (deprecated) due to frames
|
||||
frame: 10
|
||||
# dynamic frames for Opus encoder
|
||||
frames:
|
||||
- 10
|
||||
- 5
|
||||
# speex (2), linear (1) or nearest neighbour (0) audio resampler
|
||||
# linear should sound slightly better than 0
|
||||
resampler: 2
|
||||
video:
|
||||
# h264, vpx (vp8) or vp9
|
||||
codec: h264
|
||||
# Threaded encoder if supported, 0 - auto, 1 - nope, >1 - multi-threaded
|
||||
threads: 0
|
||||
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
h264:
|
||||
# crf, cbr
|
||||
mode: crf
|
||||
# Constant Rate Factor (CRF) 0-51 (default: 23)
|
||||
crf: 23
|
||||
# Rate control options
|
||||
# set the maximum bitrate
|
||||
maxRate: 0
|
||||
# set the expected client buffer size
|
||||
bufSize: 0
|
||||
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
|
||||
preset: superfast
|
||||
# baseline, main, high, high10, high422, high444
|
||||
profile: baseline
|
||||
# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
|
||||
tune: zerolatency
|
||||
# 0-3
|
||||
logLevel: 0
|
||||
# see: https://www.webmproject.org/docs/encoder-parameters
|
||||
vpx:
|
||||
# target bitrate (KBit/s)
|
||||
bitrate: 1200
|
||||
# force keyframe interval
|
||||
keyframeInterval: 5
|
||||
|
||||
# game recording
|
||||
# (experimental)
|
||||
# recording allows export RAW a/v streams of games
|
||||
# by default, it will export audio as WAV files,
|
||||
# video as a list of PNG-encoded images, and
|
||||
# one additional FFMPEG concat demux file
|
||||
recording:
|
||||
enabled: false
|
||||
# name contains the name of the recording dir (or zip)
|
||||
# format:
|
||||
# %date:go_time_format% -- refer: https://go.dev/src/time/format.go
|
||||
# %user% -- user name who started the recording
|
||||
# %game% -- game name (game ROM name)
|
||||
# %rand:len% -- a random string of given length
|
||||
# as example: 20210101101010_yeE_user1_badApple
|
||||
name: "%date:20060102150405%_%rand:3%_%user%_%game%"
|
||||
# zip and remove recording dir on completion
|
||||
zip: true
|
||||
# save directory
|
||||
folder: ./recording
|
||||
|
||||
# cloud storage options
|
||||
# it is mandatory to use a cloud storage when running
|
||||
# a distributed multi-server configuration in order to
|
||||
# share save states between nodes (resume games on a different worker)
|
||||
storage:
|
||||
# cloud storage provider:
|
||||
# - empty (No op storage stub)
|
||||
# - s3 (S3 API compatible object storage)
|
||||
provider:
|
||||
s3Endpoint:
|
||||
s3BucketName:
|
||||
s3AccessKeyId:
|
||||
s3SecretAccessKey:
|
||||
|
||||
webrtc:
|
||||
# turn off default Pion interceptors (see: https://github.com/pion/interceptor)
|
||||
# (performance)
|
||||
disableDefaultInterceptors: false
|
||||
# indicates the role of the DTLS transport (see: https://github.com/pion/webrtc/blob/master/dtlsrole.go)
|
||||
# (debug)
|
||||
# - (default)
|
||||
# - 1 (auto)
|
||||
# - 2 (client)
|
||||
# - 3 (server)
|
||||
dtlsRole:
|
||||
# a list of STUN/TURN servers to use
|
||||
iceServers:
|
||||
- urls: stun:stun.l.google.com:19302
|
||||
# configures whether the ice agent should be a lite agent (true/false)
|
||||
# (performance)
|
||||
# don't use iceServers when enabled
|
||||
iceLite: false
|
||||
# ICE configuration
|
||||
# by default, ICE ports are random and unlimited
|
||||
# alternatives:
|
||||
# 1. instead of random unlimited port range for
|
||||
# WebRTC connections, these params limit port range of ICE connections
|
||||
icePorts:
|
||||
min:
|
||||
max:
|
||||
# 2. select a single port to forward all ICE connections there
|
||||
singlePort:
|
||||
# override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
|
||||
# can be used for Docker bridged network internal IP override
|
||||
iceIpMap:
|
||||
# set additional log level for WebRTC separately
|
||||
# -1 - trace, 6 - nothing, ..., debug - 0
|
||||
logLevel: 6
|
||||
52
pkg/config/coordinator.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package config
|
||||
|
||||
import "flag"
|
||||
|
||||
type CoordinatorConfig struct {
|
||||
Coordinator Coordinator
|
||||
Emulator Emulator
|
||||
Library Library
|
||||
Recording Recording
|
||||
Version Version
|
||||
Webrtc Webrtc
|
||||
}
|
||||
|
||||
type Coordinator struct {
|
||||
Analytics Analytics
|
||||
Debug bool
|
||||
Library Library
|
||||
MaxWsSize int64
|
||||
Monitoring Monitoring
|
||||
Origin struct {
|
||||
UserWs string
|
||||
WorkerWs string
|
||||
}
|
||||
Selector string
|
||||
Server Server
|
||||
}
|
||||
|
||||
// Analytics is optional Google Analytics
|
||||
type Analytics struct {
|
||||
Inject bool
|
||||
Gtag string
|
||||
}
|
||||
|
||||
const SelectByPing = "ping"
|
||||
|
||||
// allows custom config path
|
||||
var coordinatorConfigPath string
|
||||
|
||||
func NewCoordinatorConfig() (conf CoordinatorConfig, paths []string) {
|
||||
paths, err := LoadConfig(&conf, coordinatorConfigPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CoordinatorConfig) ParseFlags() {
|
||||
c.Coordinator.Server.WithFlags()
|
||||
flag.IntVar(&c.Coordinator.Monitoring.Port, "monitoring.port", c.Coordinator.Monitoring.Port, "Monitoring server port")
|
||||
flag.StringVar(&coordinatorConfigPath, "c-conf", coordinatorConfigPath, "Set custom configuration file path")
|
||||
flag.Parse()
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/emulator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/monitoring"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/shared"
|
||||
webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Coordinator struct {
|
||||
DebugHost string
|
||||
Library games.Config
|
||||
Monitoring monitoring.Config
|
||||
Server shared.Server
|
||||
Analytics Analytics
|
||||
}
|
||||
Emulator emulator.Emulator
|
||||
Environment shared.Environment
|
||||
Webrtc webrtcConfig.Webrtc
|
||||
}
|
||||
|
||||
// Analytics is optional Google Analytics
|
||||
type Analytics struct {
|
||||
Inject bool
|
||||
Gtag string
|
||||
}
|
||||
|
||||
// allows custom config path
|
||||
var configPath string
|
||||
|
||||
func NewConfig() (conf Config) {
|
||||
err := config.LoadConfig(&conf, configPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) ParseFlags() {
|
||||
c.Environment.WithFlags()
|
||||
c.Coordinator.Server.WithFlags()
|
||||
flag.IntVar(&c.Coordinator.Monitoring.Port, "monitoring.port", c.Coordinator.Monitoring.Port, "Monitoring server port")
|
||||
flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
|
||||
flag.Parse()
|
||||
}
|
||||
154
pkg/config/emulator.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Emulator struct {
|
||||
FailFast bool
|
||||
Threads int
|
||||
Storage string
|
||||
LocalPath string
|
||||
Libretro LibretroConfig
|
||||
AutosaveSec int
|
||||
SkipLateFrames bool
|
||||
LogDroppedFrames bool
|
||||
}
|
||||
|
||||
type LibretroConfig struct {
|
||||
Cores struct {
|
||||
Paths struct {
|
||||
Libs string
|
||||
}
|
||||
Repo LibretroRemoteRepo
|
||||
List map[string]LibretroCoreConfig
|
||||
}
|
||||
DebounceMs int
|
||||
Dup bool
|
||||
SaveCompression bool
|
||||
LogLevel int
|
||||
}
|
||||
|
||||
type LibretroRemoteRepo struct {
|
||||
Sync bool
|
||||
ExtLock string
|
||||
Map map[string]map[string]LibretroRepoMapInfo
|
||||
Main LibretroRepoConfig
|
||||
Secondary LibretroRepoConfig
|
||||
}
|
||||
|
||||
// LibretroRepoMapInfo contains Libretro core lib platform info.
|
||||
// And the cores are just C-compiled libraries.
|
||||
// See: https://buildbot.libretro.com/nightly.
|
||||
type LibretroRepoMapInfo struct {
|
||||
Arch string // bottom: x86_64, x86, ...
|
||||
Ext string // platform dependent library file extension (dot-prefixed)
|
||||
Os string // middle: windows, ios, ...
|
||||
Vendor string // top level: apple, nintendo, ...
|
||||
}
|
||||
|
||||
type LibretroRepoConfig struct {
|
||||
Type string
|
||||
Url string
|
||||
Compression string
|
||||
}
|
||||
|
||||
// Guess tries to map OS + CPU architecture to the corresponding remote URL path.
|
||||
// See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63.
|
||||
func (lrp LibretroRemoteRepo) Guess() (LibretroRepoMapInfo, error) {
|
||||
if os, ok := lrp.Map[runtime.GOOS]; ok {
|
||||
if arch, ok2 := os[runtime.GOARCH]; ok2 {
|
||||
return arch, nil
|
||||
}
|
||||
}
|
||||
return LibretroRepoMapInfo{},
|
||||
errors.New("core mapping not found for " + runtime.GOOS + ":" + runtime.GOARCH)
|
||||
}
|
||||
|
||||
type LibretroCoreConfig struct {
|
||||
AltRepo bool
|
||||
AutoGlContext bool // hack: keep it here to pass it down the emulator
|
||||
CoreAspectRatio bool
|
||||
Folder string
|
||||
Hacks []string
|
||||
Height int
|
||||
Hid map[int][]int
|
||||
IsGlAllowed bool
|
||||
KbMouseSupport bool
|
||||
Lib string
|
||||
NonBlockingSave bool
|
||||
Options map[string]string
|
||||
Options4rom map[string]map[string]string // <(^_^)>
|
||||
Roms []string
|
||||
SaveStateFs string
|
||||
Scale float64
|
||||
UniqueSaveDir bool
|
||||
UsesLibCo bool
|
||||
VFR bool
|
||||
Width int
|
||||
}
|
||||
|
||||
type CoreInfo struct {
|
||||
Id string
|
||||
Name string
|
||||
AltRepo bool
|
||||
}
|
||||
|
||||
// GetLibretroCoreConfig returns a core config with expanded paths.
|
||||
func (e Emulator) GetLibretroCoreConfig(emulator string) LibretroCoreConfig {
|
||||
cores := e.Libretro.Cores
|
||||
conf := cores.List[emulator]
|
||||
conf.Lib = path.Join(cores.Paths.Libs, conf.Lib)
|
||||
return conf
|
||||
}
|
||||
|
||||
// GetEmulator tries to find a suitable emulator.
|
||||
// !to remove quadratic complexity
|
||||
func (e Emulator) GetEmulator(rom string, path string) string {
|
||||
found := ""
|
||||
for emu, core := range e.Libretro.Cores.List {
|
||||
for _, romName := range core.Roms {
|
||||
if rom == romName {
|
||||
found = emu
|
||||
if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
|
||||
folder := p[0]
|
||||
if (folder != "" && folder == core.Folder) || folder == emu {
|
||||
return emu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func (e Emulator) GetSupportedExtensions() []string {
|
||||
var extensions []string
|
||||
for _, core := range e.Libretro.Cores.List {
|
||||
extensions = append(extensions, core.Roms...)
|
||||
}
|
||||
return extensions
|
||||
}
|
||||
|
||||
func (e Emulator) SessionStoragePath() string {
|
||||
return e.Storage
|
||||
}
|
||||
|
||||
func (l *LibretroConfig) GetCores() (cores []CoreInfo) {
|
||||
for k, core := range l.Cores.List {
|
||||
cores = append(cores, CoreInfo{Id: k, Name: core.Lib, AltRepo: core.AltRepo})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *LibretroConfig) GetCoresStorePath() string {
|
||||
pth, err := filepath.Abs(l.Cores.Paths.Libs)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return pth
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
package emulator
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Emulator struct {
|
||||
Scale int
|
||||
AspectRatio struct {
|
||||
Keep bool
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
Storage string
|
||||
Libretro LibretroConfig
|
||||
}
|
||||
|
||||
type LibretroConfig struct {
|
||||
Cores struct {
|
||||
Paths struct {
|
||||
Libs string
|
||||
Configs string
|
||||
}
|
||||
Repo struct {
|
||||
Sync bool
|
||||
ExtLock string
|
||||
Main LibretroRepoConfig
|
||||
Secondary LibretroRepoConfig
|
||||
}
|
||||
List map[string]LibretroCoreConfig
|
||||
}
|
||||
}
|
||||
|
||||
type LibretroRepoConfig struct {
|
||||
Type string
|
||||
Url string
|
||||
Compression string
|
||||
}
|
||||
|
||||
type LibretroCoreConfig struct {
|
||||
Lib string
|
||||
Config string
|
||||
Roms []string
|
||||
Folder string
|
||||
Width int
|
||||
Height int
|
||||
Ratio float64
|
||||
IsGlAllowed bool
|
||||
UsesLibCo bool
|
||||
HasMultitap bool
|
||||
|
||||
// hack: keep it here to pass it down the emulator
|
||||
AutoGlContext bool
|
||||
}
|
||||
|
||||
// GetLibretroCoreConfig returns a core config with expanded paths.
|
||||
func (e Emulator) GetLibretroCoreConfig(emulator string) LibretroCoreConfig {
|
||||
cores := e.Libretro.Cores
|
||||
conf := cores.List[emulator]
|
||||
conf.Lib = path.Join(cores.Paths.Libs, conf.Lib)
|
||||
if conf.Config != "" {
|
||||
conf.Config = path.Join(cores.Paths.Configs, conf.Config)
|
||||
}
|
||||
return conf
|
||||
}
|
||||
|
||||
// GetEmulator tries to find a suitable emulator.
|
||||
// !to remove quadratic complexity
|
||||
func (e Emulator) GetEmulator(rom string, path string) string {
|
||||
found := ""
|
||||
for emu, core := range e.Libretro.Cores.List {
|
||||
for _, romName := range core.Roms {
|
||||
if rom == romName {
|
||||
found = emu
|
||||
if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
|
||||
folder := p[0]
|
||||
if (folder != "" && folder == core.Folder) || folder == emu {
|
||||
return emu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func (e Emulator) GetSupportedExtensions() []string {
|
||||
var extensions []string
|
||||
for _, core := range e.Libretro.Cores.List {
|
||||
extensions = append(extensions, core.Roms...)
|
||||
}
|
||||
return extensions
|
||||
}
|
||||
|
||||
func (l *LibretroConfig) GetCores() []string {
|
||||
var cores []string
|
||||
for _, core := range l.Cores.List {
|
||||
cores = append(cores, core.Lib)
|
||||
}
|
||||
return cores
|
||||
}
|
||||
|
||||
func (l *LibretroConfig) GetCoresStorePath() string {
|
||||
pth, err := filepath.Abs(l.Cores.Paths.Libs)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return pth
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package emulator
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
|
|
@ -29,20 +29,17 @@ func TestGetEmulator(t *testing.T) {
|
|||
},
|
||||
{
|
||||
rom: "nes",
|
||||
path: "test/game.nes",
|
||||
path: "test2/game.nes",
|
||||
config: map[string]LibretroCoreConfig{
|
||||
"snes": {Roms: []string{"nes"}},
|
||||
"snes": {Roms: []string{"snes"}},
|
||||
"nes": {Roms: []string{"nes"}},
|
||||
},
|
||||
emulator: "nes",
|
||||
},
|
||||
}
|
||||
|
||||
emu := Emulator{
|
||||
Libretro: LibretroConfig{},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
emu := Emulator{Libretro: LibretroConfig{}}
|
||||
emu.Libretro.Cores.List = test.config
|
||||
em := emu.GetEmulator(test.rom, test.path)
|
||||
if test.emulator != em {
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package encoder
|
||||
|
||||
type Encoder struct {
|
||||
Audio Audio
|
||||
Video Video
|
||||
WithoutGame bool
|
||||
}
|
||||
|
||||
type Audio struct {
|
||||
Channels int
|
||||
Frame int
|
||||
Frequency int
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
Codec string
|
||||
H264 struct {
|
||||
Crf uint8
|
||||
Preset string
|
||||
Profile string
|
||||
Tune string
|
||||
LogLevel int
|
||||
}
|
||||
Vpx struct {
|
||||
Bitrate uint
|
||||
KeyframeInterval uint
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Audio) GetFrameDuration() int {
|
||||
return a.Frequency * a.Frame / 1000 * a.Channels
|
||||
}
|
||||
|
|
@ -1,26 +1,163 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kkyr/fig"
|
||||
"github.com/knadh/koanf/maps"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const EnvPrefix = "CLOUD_GAME_"
|
||||
|
||||
var (
|
||||
//go:embed config.yaml
|
||||
conf embed.FS
|
||||
)
|
||||
|
||||
type Kv = map[string]any
|
||||
type Bytes []byte
|
||||
|
||||
func (b *Bytes) ReadBytes() ([]byte, error) { return *b, nil }
|
||||
func (b *Bytes) Read() (Kv, error) { return nil, nil }
|
||||
|
||||
type File string
|
||||
|
||||
func (f *File) ReadBytes() ([]byte, error) { return os.ReadFile(string(*f)) }
|
||||
func (f *File) Read() (Kv, error) { return nil, nil }
|
||||
|
||||
type YAML struct{}
|
||||
|
||||
func (p *YAML) Marshal(Kv) ([]byte, error) { return nil, nil }
|
||||
func (p *YAML) Unmarshal(b []byte) (Kv, error) {
|
||||
var out Kv
|
||||
klw := keysToLower(b)
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(klw))
|
||||
if err := decoder.Decode(&out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// keysToLower iterates YAML bytes and tries to lower the keys.
|
||||
// Used for merging with environment vars which are lowered as well.
|
||||
func keysToLower(in []byte) []byte {
|
||||
l, r, ignore := 0, 0, false
|
||||
for i, b := range in {
|
||||
switch b {
|
||||
case '#': // skip comments
|
||||
ignore = true
|
||||
case ':': // lower left chunk before the next : symbol
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
r = i
|
||||
ignore = true
|
||||
for j := l; j <= r; j++ {
|
||||
c := in[j]
|
||||
// we skip the line with the first explicit " string symbol
|
||||
if c == '"' {
|
||||
break
|
||||
}
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
in[j] += 'a' - 'A'
|
||||
}
|
||||
}
|
||||
case '\n':
|
||||
l = i
|
||||
ignore = false
|
||||
}
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
type Env string
|
||||
|
||||
func (e *Env) ReadBytes() ([]byte, error) { return nil, nil }
|
||||
func (e *Env) Read() (Kv, error) {
|
||||
var keys []string
|
||||
for _, k := range os.Environ() {
|
||||
if strings.HasPrefix(k, string(*e)) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
mp := make(Kv)
|
||||
for _, k := range keys {
|
||||
parts := strings.SplitN(k, "=", 2)
|
||||
if parts == nil {
|
||||
continue
|
||||
}
|
||||
n := strings.ToLower(strings.TrimPrefix(parts[0], string(*e)))
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
// convert VAR_VAR to VAR.VAR or if we need to preserve _
|
||||
// i.e. VAR_VAR__KEY_HAS_SLASHES to VAR.VAR.KEY_HAS_SLASHES
|
||||
// with the result: VAR: { VAR: { KEY_HAS_SLASHES: '' } } }
|
||||
x := strings.Index(n, "__")
|
||||
var key string
|
||||
if x == -1 {
|
||||
key = strings.Replace(n, "_", ".", -1)
|
||||
} else {
|
||||
key = strings.Replace(n[:x+1], "_", ".", -1) + n[x+2:]
|
||||
}
|
||||
if len(parts) > 1 {
|
||||
mp[key] = parts[1]
|
||||
}
|
||||
}
|
||||
return maps.Unflatten(mp, "."), nil
|
||||
}
|
||||
|
||||
// LoadConfig loads a configuration file into the given struct.
|
||||
// The path param specifies a custom path to the configuration file.
|
||||
// Reads and puts environment variables with the prefix CLOUD_GAME_.
|
||||
// Params from the config should be in uppercase separated with _.
|
||||
func LoadConfig(config interface{}, path string) error {
|
||||
envPrefix := "CLOUD_GAME"
|
||||
dirs := []string{path}
|
||||
if path == "" {
|
||||
dirs = append(dirs, ".", "configs", "../../../configs")
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
dirs = append(dirs, home+"/.cr")
|
||||
func LoadConfig(config any, path string) (loaded []string, err error) {
|
||||
dirs := []string{".", "configs", "../../../configs"}
|
||||
if path != "" {
|
||||
dirs = append([]string{path}, dirs...)
|
||||
}
|
||||
|
||||
homeDir := ""
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
homeDir = home + "/.cr"
|
||||
dirs = append(dirs, homeDir)
|
||||
}
|
||||
|
||||
k := koanf.New("_") // move to global scope if configs become dynamic
|
||||
defer k.Delete("")
|
||||
data, err := conf.ReadFile("config.yaml")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf := Bytes(data)
|
||||
if err := k.Load(&conf, &YAML{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loaded = append(loaded, "default")
|
||||
|
||||
for _, dir := range dirs {
|
||||
path := filepath.Join(filepath.Clean(dir), "config.yaml")
|
||||
f := File(path)
|
||||
if _, err := os.Stat(string(f)); !os.IsNotExist(err) {
|
||||
if err := k.Load(&f, &YAML{}); err != nil {
|
||||
return loaded, err
|
||||
}
|
||||
loaded = append(loaded, path)
|
||||
}
|
||||
}
|
||||
if err := fig.Load(config, fig.Dirs(dirs...), fig.UseEnv(envPrefix)); err != nil {
|
||||
return err
|
||||
|
||||
env := Env(EnvPrefix)
|
||||
if err := k.Load(&env, nil); err != nil {
|
||||
return loaded, err
|
||||
}
|
||||
return nil
|
||||
|
||||
if err := k.Unmarshal("", config); err != nil {
|
||||
return loaded, err
|
||||
}
|
||||
|
||||
return loaded, nil
|
||||
}
|
||||
|
|
|
|||
63
pkg/config/loader_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigEnv(t *testing.T) {
|
||||
var out WorkerConfig
|
||||
|
||||
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[0]", "10")
|
||||
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[1]", "5")
|
||||
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[0]") }()
|
||||
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[1]") }()
|
||||
|
||||
_ = os.Setenv("CLOUD_GAME_EMULATOR_LIBRETRO_CORES_LIST_PCSX_OPTIONS__PCSX_REARMED_DRC", "x")
|
||||
defer func() {
|
||||
_ = os.Unsetenv("CLOUD_GAME_EMULATOR_LIBRETRO_CORES_LIST_PCSX_OPTIONS__PCSX_REARMED_DRC")
|
||||
}()
|
||||
|
||||
_, err := LoadConfig(&out, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i, x := range []float32{10, 5} {
|
||||
if out.Encoder.Audio.Frames[i] != x {
|
||||
t.Errorf("%v is not [10, 5]", out.Encoder.Audio.Frames)
|
||||
t.Failed()
|
||||
}
|
||||
}
|
||||
|
||||
v := out.Emulator.Libretro.Cores.List["pcsx"].Options["pcsx_rearmed_drc"]
|
||||
if v != "x" {
|
||||
t.Errorf("%v is not x", v)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_keysToLower(t *testing.T) {
|
||||
type args struct {
|
||||
in []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{name: "empty", args: args{in: []byte{}}, want: []byte{}},
|
||||
{name: "case", args: args{
|
||||
in: []byte("KEY:1\n#Comment with:\n KeY123_NamE: 1\n\n\n\nAAA:123\n \"KeyKey\":2\n"),
|
||||
},
|
||||
want: []byte("key:1\n#Comment with:\n key123_name: 1\n\n\n\naaa:123\n \"KeyKey\":2\n"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := keysToLower(tt.args.in); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("keysToLower() = %v, want %v", string(got), string(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package monitoring
|
||||
|
||||
type Config struct {
|
||||
Port int
|
||||
URLPrefix string
|
||||
MetricEnabled bool `json:"metric_enabled"`
|
||||
ProfilingEnabled bool `json:"profiling_enabled"`
|
||||
}
|
||||
|
||||
func (c *Config) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }
|
||||
66
pkg/config/shared.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package config
|
||||
|
||||
import "flag"
|
||||
|
||||
type Version int
|
||||
|
||||
type Library struct {
|
||||
// filename of the alias' file
|
||||
AliasFile string
|
||||
// some directory which is going to be
|
||||
// the root folder for the library
|
||||
BasePath string
|
||||
// a list of supported file extensions
|
||||
Supported []string
|
||||
// a list of ignored words in the files
|
||||
Ignored []string
|
||||
// print some additional info
|
||||
Verbose bool
|
||||
// enable directory changes watch
|
||||
WatchMode bool
|
||||
}
|
||||
|
||||
func (l Library) GetSupportedExtensions() []string { return l.Supported }
|
||||
|
||||
type Monitoring struct {
|
||||
Port int
|
||||
URLPrefix string
|
||||
MetricEnabled bool `json:"metric_enabled"`
|
||||
ProfilingEnabled bool `json:"profiling_enabled"`
|
||||
}
|
||||
|
||||
func (c *Monitoring) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }
|
||||
|
||||
type Server struct {
|
||||
Address string
|
||||
CacheControl string
|
||||
FrameOptions string
|
||||
Https bool
|
||||
Tls struct {
|
||||
Address string
|
||||
Domain string
|
||||
HttpsKey string
|
||||
HttpsCert string
|
||||
}
|
||||
}
|
||||
|
||||
type Recording struct {
|
||||
Enabled bool
|
||||
Name string
|
||||
Folder string
|
||||
Zip bool
|
||||
}
|
||||
|
||||
func (s *Server) WithFlags() {
|
||||
flag.StringVar(&s.Address, "address", s.Address, "HTTP server address (host:port)")
|
||||
flag.StringVar(&s.Tls.Address, "httpsAddress", s.Tls.Address, "HTTPS server address (host:port)")
|
||||
flag.StringVar(&s.Tls.HttpsKey, "httpsKey", s.Tls.HttpsKey, "HTTPS key")
|
||||
flag.StringVar(&s.Tls.HttpsCert, "httpsCert", s.Tls.HttpsCert, "HTTPS chain")
|
||||
}
|
||||
|
||||
func (s *Server) GetAddr() string {
|
||||
if s.Https {
|
||||
return s.Tls.Address
|
||||
}
|
||||
return s.Address
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v2/pkg/environment"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type Environment environment.Env
|
||||
|
||||
type Server struct {
|
||||
Address string
|
||||
Https bool
|
||||
Tls struct {
|
||||
Address string
|
||||
Domain string
|
||||
HttpsKey string
|
||||
HttpsCert string
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) WithFlags() {
|
||||
flag.StringVar(&s.Address, "address", s.Address, "HTTP server address (host:port)")
|
||||
flag.StringVar(&s.Tls.Address, "httpsAddress", s.Tls.Address, "HTTPS server address (host:port)")
|
||||
flag.StringVar(&s.Tls.HttpsKey, "httpsKey", s.Tls.HttpsKey, "HTTPS key")
|
||||
flag.StringVar(&s.Tls.HttpsCert, "httpsCert", s.Tls.HttpsCert, "HTTPS chain")
|
||||
}
|
||||
|
||||
func (s *Server) GetAddr() string {
|
||||
if s.Https {
|
||||
return s.Tls.Address
|
||||
}
|
||||
return s.Address
|
||||
}
|
||||
|
||||
func (env *Environment) Get() environment.Env {
|
||||
return (environment.Env)(*env)
|
||||
}
|
||||
|
||||
func (env *Environment) WithFlags() {
|
||||
flag.StringVar((*string)(env), "env", string(*env), "Specify environment type: [dev, staging, prod]")
|
||||
}
|
||||
26
pkg/config/webrtc.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package config
|
||||
|
||||
type Webrtc struct {
|
||||
DisableDefaultInterceptors bool
|
||||
DtlsRole byte
|
||||
IceServers []IceServer
|
||||
IcePorts struct {
|
||||
Min uint16
|
||||
Max uint16
|
||||
}
|
||||
IceIpMap string
|
||||
IceLite bool
|
||||
SinglePort int
|
||||
LogLevel int
|
||||
}
|
||||
|
||||
type IceServer struct {
|
||||
Urls string `json:"urls,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
func (w *Webrtc) HasDtlsRole() bool { return w.DtlsRole > 0 }
|
||||
func (w *Webrtc) HasPortRange() bool { return w.IcePorts.Min > 0 && w.IcePorts.Max > 0 }
|
||||
func (w *Webrtc) HasSinglePort() bool { return w.SinglePort > 0 }
|
||||
func (w *Webrtc) HasIceIpMap() bool { return w.IceIpMap != "" }
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package webrtc
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/config/encoder"
|
||||
|
||||
type Webrtc struct {
|
||||
DisableDefaultInterceptors bool
|
||||
IceServers []IceServer
|
||||
IcePorts struct {
|
||||
Min uint16
|
||||
Max uint16
|
||||
}
|
||||
IceIpMap string
|
||||
}
|
||||
|
||||
type IceServer struct {
|
||||
Url string
|
||||
Username string
|
||||
Credential string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Encoder encoder.Encoder
|
||||
Webrtc Webrtc
|
||||
}
|
||||
|
|
@ -1,31 +1,38 @@
|
|||
package worker
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/emulator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/encoder"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/monitoring"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/shared"
|
||||
webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/environment"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/os"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Encoder encoder.Encoder
|
||||
Emulator emulator.Emulator
|
||||
Environment shared.Environment
|
||||
Worker Worker
|
||||
Webrtc webrtcConfig.Webrtc
|
||||
type WorkerConfig struct {
|
||||
Encoder Encoder
|
||||
Emulator Emulator
|
||||
Library Library
|
||||
Recording Recording
|
||||
Storage Storage
|
||||
Worker Worker
|
||||
Webrtc Webrtc
|
||||
Version Version
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Provider string
|
||||
S3Endpoint string
|
||||
S3BucketName string
|
||||
S3AccessKeyId string
|
||||
S3SecretAccessKey string
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Monitoring monitoring.Config
|
||||
Debug bool
|
||||
Monitoring Monitoring
|
||||
Network struct {
|
||||
CoordinatorAddress string
|
||||
Endpoint string
|
||||
|
|
@ -34,43 +41,85 @@ type Worker struct {
|
|||
Secure bool
|
||||
Zone string
|
||||
}
|
||||
Server shared.Server
|
||||
Server Server
|
||||
Tag string
|
||||
}
|
||||
|
||||
type Encoder struct {
|
||||
Audio Audio
|
||||
Video Video
|
||||
}
|
||||
|
||||
type Audio struct {
|
||||
Frames []float32
|
||||
Resampler int
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
Codec string
|
||||
Threads int
|
||||
H264 struct {
|
||||
Mode string
|
||||
Crf uint8
|
||||
MaxRate int
|
||||
BufSize int
|
||||
LogLevel int32
|
||||
Preset string
|
||||
Profile string
|
||||
Tune string
|
||||
}
|
||||
Vpx struct {
|
||||
Bitrate uint
|
||||
KeyframeInterval uint
|
||||
}
|
||||
}
|
||||
|
||||
// allows custom config path
|
||||
var configPath string
|
||||
var workerConfigPath string
|
||||
|
||||
func NewConfig() (conf Config) {
|
||||
_ = config.LoadConfig(&conf, configPath)
|
||||
func NewWorkerConfig() (conf WorkerConfig, paths []string) {
|
||||
paths, err := LoadConfig(&conf, workerConfigPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conf.expandSpecialTags()
|
||||
conf.fixValues()
|
||||
return
|
||||
}
|
||||
|
||||
// ParseFlags updates config values from passed runtime flags.
|
||||
// Define own flags with default value set to the current config param.
|
||||
// Don't forget to call flag.Parse().
|
||||
func (c *Config) ParseFlags() {
|
||||
c.Environment.WithFlags()
|
||||
func (c *WorkerConfig) ParseFlags() {
|
||||
c.Worker.Server.WithFlags()
|
||||
flag.IntVar(&c.Worker.Monitoring.Port, "monitoring.port", c.Worker.Monitoring.Port, "Monitoring server port")
|
||||
flag.StringVar(&c.Worker.Network.CoordinatorAddress, "coordinatorhost", c.Worker.Network.CoordinatorAddress, "Worker URL to connect")
|
||||
flag.StringVar(&c.Worker.Network.Zone, "zone", c.Worker.Network.Zone, "Worker network zone (us, eu, etc.)")
|
||||
flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
|
||||
flag.StringVar(&workerConfigPath, "w-conf", workerConfigPath, "Set custom configuration file path")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
// expandSpecialTags replaces all the special tags in the config.
|
||||
func (c *Config) expandSpecialTags() {
|
||||
func (c *WorkerConfig) expandSpecialTags() {
|
||||
tag := "{user}"
|
||||
for _, dir := range []*string{&c.Emulator.Storage, &c.Emulator.Libretro.Cores.Repo.ExtLock} {
|
||||
if *dir == "" || !strings.Contains(*dir, tag) {
|
||||
continue
|
||||
}
|
||||
userHomeDir, err := environment.GetUserHome()
|
||||
userHomeDir, err := os.GetUserHome()
|
||||
if err != nil {
|
||||
log.Fatalln("couldn't read user home directory", err)
|
||||
panic(fmt.Sprintf("couldn't read user home directory, %v", err))
|
||||
}
|
||||
*dir = strings.Replace(*dir, tag, userHomeDir, -1)
|
||||
*dir = filepath.FromSlash(*dir)
|
||||
}
|
||||
}
|
||||
|
||||
// fixValues tries to fix some values otherwise hard to set externally.
|
||||
func (c *WorkerConfig) fixValues() {
|
||||
// with ICE lite we clear ICE servers
|
||||
if c.Webrtc.IceLite {
|
||||
c.Webrtc.IceServers = []IceServer{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +127,7 @@ func (c *Config) expandSpecialTags() {
|
|||
func (w *Worker) GetAddr() string { return w.Server.GetAddr() }
|
||||
|
||||
// GetPingAddr returns exposed to clients server ping endpoint address.
|
||||
func (w *Worker) GetPingAddr(address string) string {
|
||||
func (w *Worker) GetPingAddr(address string) url.URL {
|
||||
_, srcPort, _ := net.SplitHostPort(w.GetAddr())
|
||||
dstHost, _, _ := net.SplitHostPort(address)
|
||||
address = net.JoinHostPort(dstHost, srcPort)
|
||||
|
|
@ -98,5 +147,10 @@ func (w *Worker) GetPingAddr(address string) string {
|
|||
if w.Server.Https {
|
||||
pingURL.Scheme = "https"
|
||||
}
|
||||
return pingURL.String()
|
||||
return pingURL
|
||||
}
|
||||
|
||||
func (w *Worker) GetPort(address string) string {
|
||||
_, port, _ := net.SplitHostPort(address)
|
||||
return port
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type BrowserClient struct {
|
||||
*cws.Client
|
||||
SessionID string
|
||||
RoomID string
|
||||
WorkerID string // TODO: how about pointer to workerClient?
|
||||
}
|
||||
|
||||
// NewCoordinatorClient returns a client connecting to browser.
|
||||
// This connection exchanges information between browser and coordinator.
|
||||
func NewBrowserClient(c *websocket.Conn, browserID string) *BrowserClient {
|
||||
return &BrowserClient{
|
||||
Client: cws.NewClient(c),
|
||||
SessionID: browserID,
|
||||
}
|
||||
}
|
||||
|
||||
// Register new log
|
||||
func (bc *BrowserClient) Printf(format string, args ...interface{}) {
|
||||
log.Printf(fmt.Sprintf("Browser %s] %s", bc.SessionID, format), args...)
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) Println(args ...interface{}) {
|
||||
log.Println(fmt.Sprintf("Browser %s] %s", bc.SessionID, fmt.Sprint(args...)))
|
||||
}
|
||||
|
|
@ -1,27 +1,119 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/monitoring"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/service"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/monitoring"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/network/httpx"
|
||||
)
|
||||
|
||||
func New(conf coordinator.Config) (services service.Group) {
|
||||
srv := NewServer(conf, games.NewLibWhitelisted(conf.Coordinator.Library, conf.Emulator))
|
||||
httpSrv, err := NewHTTPServer(conf, func(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/ws", srv.WS)
|
||||
mux.HandleFunc("/wso", srv.WSO)
|
||||
type Coordinator struct {
|
||||
hub *Hub
|
||||
services [2]interface {
|
||||
Run()
|
||||
Stop() error
|
||||
}
|
||||
}
|
||||
|
||||
func New(conf config.CoordinatorConfig, log *logger.Logger) (*Coordinator, error) {
|
||||
coordinator := &Coordinator{hub: NewHub(conf, log)}
|
||||
h, err := NewHTTPServer(conf, log, func(mux *httpx.Mux) *httpx.Mux {
|
||||
mux.HandleFunc("/ws", coordinator.hub.handleUserConnection())
|
||||
mux.HandleFunc("/wso", coordinator.hub.handleWorkerConnection())
|
||||
return mux
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("http init fail: %v", err)
|
||||
return nil, fmt.Errorf("http init fail: %w", err)
|
||||
}
|
||||
services.Add(srv, httpSrv)
|
||||
coordinator.services[0] = h
|
||||
if conf.Coordinator.Monitoring.IsEnabled() {
|
||||
services.Add(monitoring.New(conf.Coordinator.Monitoring, httpSrv.GetHost(), "cord"))
|
||||
coordinator.services[1] = monitoring.New(conf.Coordinator.Monitoring, h.GetHost(), log)
|
||||
}
|
||||
return
|
||||
return coordinator, nil
|
||||
}
|
||||
|
||||
func (c *Coordinator) Start() {
|
||||
for _, s := range c.services {
|
||||
if s != nil {
|
||||
s.Run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Coordinator) Stop() error {
|
||||
var err error
|
||||
for _, s := range c.services {
|
||||
if s != nil {
|
||||
err0 := s.Stop()
|
||||
err = errors.Join(err, err0)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func NewHTTPServer(conf config.CoordinatorConfig, log *logger.Logger, fnMux func(*httpx.Mux) *httpx.Mux) (*httpx.Server, error) {
|
||||
return httpx.NewServer(
|
||||
conf.Coordinator.Server.GetAddr(),
|
||||
func(s *httpx.Server) httpx.Handler { return fnMux(s.Mux().Handle("/", index(conf, log))) },
|
||||
httpx.WithServerConfig(conf.Coordinator.Server),
|
||||
httpx.WithLogger(log),
|
||||
)
|
||||
}
|
||||
|
||||
func index(conf config.CoordinatorConfig, log *logger.Logger) httpx.Handler {
|
||||
const indexHTML = "./web/index.html"
|
||||
|
||||
indexTpl := template.Must(template.ParseFiles(indexHTML))
|
||||
|
||||
// render index page with some tpl values
|
||||
tplData := struct {
|
||||
Analytics config.Analytics
|
||||
Recording config.Recording
|
||||
}{conf.Coordinator.Analytics, conf.Recording}
|
||||
|
||||
handler := func(tpl *template.Template, w httpx.ResponseWriter, r *httpx.Request) {
|
||||
if err := tpl.Execute(w, tplData); err != nil {
|
||||
log.Error().Err(err).Msg("error with the analytics template file")
|
||||
}
|
||||
}
|
||||
|
||||
h := httpx.FileServer("./web")
|
||||
|
||||
if conf.Coordinator.Debug {
|
||||
log.Info().Msgf("Using auto-reloading index.html")
|
||||
return httpx.HandlerFunc(func(w httpx.ResponseWriter, r *httpx.Request) {
|
||||
if conf.Coordinator.Server.CacheControl != "" {
|
||||
w.Header().Add("Cache-Control", conf.Coordinator.Server.CacheControl)
|
||||
}
|
||||
if conf.Coordinator.Server.FrameOptions != "" {
|
||||
w.Header().Add("X-Frame-Options", conf.Coordinator.Server.FrameOptions)
|
||||
}
|
||||
if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, "/index.html") {
|
||||
tpl := template.Must(template.ParseFiles(indexHTML))
|
||||
handler(tpl, w, r)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
return httpx.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if conf.Coordinator.Server.CacheControl != "" {
|
||||
w.Header().Add("Cache-Control", conf.Coordinator.Server.CacheControl)
|
||||
}
|
||||
if conf.Coordinator.Server.FrameOptions != "" {
|
||||
w.Header().Add("X-Frame-Options", conf.Coordinator.Server.FrameOptions)
|
||||
}
|
||||
if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, "/index.html") {
|
||||
handler(indexTpl, w, r)
|
||||
return
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,381 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/environment"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/ice"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/service"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/util"
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
service.Service
|
||||
|
||||
cfg coordinator.Config
|
||||
// games library
|
||||
library games.GameLibrary
|
||||
// roomToWorker map roomID to workerID
|
||||
roomToWorker map[string]string
|
||||
// workerClients are the map workerID to worker Client
|
||||
workerClients map[string]*WorkerClient
|
||||
// browserClients are the map sessionID to browser Client
|
||||
browserClients map[string]*BrowserClient
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
func NewServer(cfg coordinator.Config, library games.GameLibrary) *Server {
|
||||
// scan the lib right away
|
||||
library.Scan()
|
||||
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
library: library,
|
||||
// Mapping roomID to server
|
||||
roomToWorker: map[string]string{},
|
||||
// Mapping workerID to worker
|
||||
workerClients: map[string]*WorkerClient{},
|
||||
// Mapping sessionID to browser
|
||||
browserClients: map[string]*BrowserClient{},
|
||||
}
|
||||
}
|
||||
|
||||
// WSO handles all connections from a new worker to coordinator
|
||||
func (s *Server) WSO(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Coordinator: A worker is connecting...")
|
||||
|
||||
connRt, err := GetConnectionRequest(r.URL.Query().Get("data"))
|
||||
if err != nil {
|
||||
log.Printf("Coordinator: got a malformed request: %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if connRt.PingAddr == "" {
|
||||
log.Printf("Warning! Ping address is not set.")
|
||||
}
|
||||
|
||||
if s.cfg.Coordinator.Server.Https && !connRt.IsHTTPS {
|
||||
log.Printf("Warning! Unsecure connection. The worker may not work properly without HTTPS on its side!")
|
||||
}
|
||||
|
||||
// be aware of ReadBufferSize, WriteBufferSize (default 4096)
|
||||
// https://pkg.go.dev/github.com/gorilla/websocket?tab=doc#Upgrader
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Coordinator: [!] WS upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate workerID
|
||||
var workerID string
|
||||
for {
|
||||
workerID = uuid.Must(uuid.NewV4()).String()
|
||||
// check duplicate
|
||||
if _, ok := s.workerClients[workerID]; !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create a workerClient instance
|
||||
wc := NewWorkerClient(c, workerID)
|
||||
wc.Println("Generated worker ID")
|
||||
wc.Zone = connRt.Zone
|
||||
wc.PingServer = connRt.PingAddr
|
||||
|
||||
// Register to workersClients map the client connection
|
||||
address := util.GetRemoteAddress(c)
|
||||
public := util.IsPublicIP(address)
|
||||
|
||||
wc.Printf("addr: %v | zone: %v | pub: %v | ping: %v", address, wc.Zone, public, wc.PingServer)
|
||||
|
||||
// In case worker and coordinator in the same host
|
||||
if !public && s.cfg.Environment.Get() == environment.Production {
|
||||
// Don't accept private IP for worker's address in prod mode
|
||||
// However, if the worker in the same host with coordinator, we can get public IP of worker
|
||||
wc.Printf("[!] Address %s is invalid", address)
|
||||
|
||||
address = util.GetHostPublicIP()
|
||||
wc.Printf("Find public address: %s", address)
|
||||
|
||||
if address == "" || !util.IsPublicIP(address) {
|
||||
// Skip this worker because we cannot find public IP
|
||||
wc.Println("[!] Unable to find public address, reject worker")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create a workerClient instance
|
||||
wc.Address = address
|
||||
wc.StunTurnServer = ice.ToJson(s.cfg.Webrtc.IceServers, ice.Replacement{From: "server-ip", To: address})
|
||||
|
||||
// Attach to Server instance with workerID, add defer
|
||||
s.workerClients[workerID] = wc
|
||||
defer s.cleanWorker(wc, workerID)
|
||||
|
||||
wc.Send(api.ServerIdPacket(workerID), nil)
|
||||
|
||||
s.workerRoutes(wc)
|
||||
wc.Listen()
|
||||
}
|
||||
|
||||
// WSO handles all connections from user/frontend to coordinator
|
||||
func (s *Server) WS(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Coordinator: A user is connecting...")
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Warn: Something wrong. Recovered in ", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// be aware of ReadBufferSize, WriteBufferSize (default 4096)
|
||||
// https://pkg.go.dev/github.com/gorilla/websocket?tab=doc#Upgrader
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Coordinator: [!] WS upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate sessionID for browserClient
|
||||
var sessionID string
|
||||
for {
|
||||
sessionID = uuid.Must(uuid.NewV4()).String()
|
||||
// check duplicate
|
||||
if _, ok := s.browserClients[sessionID]; !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create browserClient instance
|
||||
bc := NewBrowserClient(c, sessionID)
|
||||
bc.Println("Generated worker ID")
|
||||
|
||||
// Run browser listener first (to capture ping)
|
||||
go bc.Listen()
|
||||
|
||||
/* Create a session - mapping browserClient with workerClient */
|
||||
var wc *WorkerClient
|
||||
|
||||
// get roomID if it is embeded in request. Server will pair the frontend with the server running the room. It only happens when we are trying to access a running room over share link.
|
||||
// TODO: Update link to the wiki
|
||||
roomID := r.URL.Query().Get("room_id")
|
||||
// zone param is to pick worker in that zone only
|
||||
// if there is no zone param, we can pic
|
||||
userZone := r.URL.Query().Get("zone")
|
||||
|
||||
bc.Printf("Get Room %s Zone %s From URL %v", roomID, userZone, r.URL)
|
||||
|
||||
if roomID != "" {
|
||||
bc.Printf("Detected roomID %v from URL", roomID)
|
||||
if workerID, ok := s.roomToWorker[roomID]; ok {
|
||||
wc = s.workerClients[workerID]
|
||||
if userZone != "" && wc.Zone != userZone {
|
||||
// if there is zone param, we need to ensure ther worker in that zone
|
||||
// if not we consider the room is missing
|
||||
wc = nil
|
||||
} else {
|
||||
bc.Printf("Found running server with id=%v client=%v", workerID, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no existing server to connect to, we find the best possible worker for the frontend
|
||||
if wc == nil {
|
||||
// Get best server for frontend to connect to
|
||||
wc, err = s.getBestWorkerClient(bc, userZone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Assign available worker to browserClient
|
||||
bc.WorkerID = wc.WorkerID
|
||||
|
||||
wc.ChangeUserQuantityBy(1)
|
||||
defer wc.ChangeUserQuantityBy(-1)
|
||||
|
||||
// Everything is cool
|
||||
// Attach to Server instance with sessionID
|
||||
s.browserClients[sessionID] = bc
|
||||
defer s.cleanBrowser(bc, sessionID)
|
||||
|
||||
// Routing browserClient message
|
||||
s.useragentRoutes(bc)
|
||||
|
||||
bc.Send(cws.WSPacket{
|
||||
ID: "init",
|
||||
Data: createInitPackage(wc.StunTurnServer, s.library.GetAll()),
|
||||
}, nil)
|
||||
|
||||
// If peerconnection is done (client.Done is signalled), we close peerconnection
|
||||
<-bc.Done
|
||||
|
||||
// Notify worker to clean session
|
||||
wc.Send(api.TerminateSessionPacket(sessionID), nil)
|
||||
}
|
||||
|
||||
func (s *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) {
|
||||
conf := s.cfg.Coordinator
|
||||
if conf.DebugHost != "" {
|
||||
client.Println("Connecting to debug host instead prod servers", conf.DebugHost)
|
||||
wc := s.getWorkerFromAddress(conf.DebugHost)
|
||||
if wc != nil {
|
||||
return wc, nil
|
||||
}
|
||||
// if there is not debugHost, continue usual flow
|
||||
client.Println("Not found, connecting to all available servers")
|
||||
}
|
||||
|
||||
workerClients := s.getAvailableWorkers()
|
||||
|
||||
serverID, err := s.findBestServerFromBrowser(workerClients, client, zone)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.workerClients[serverID], nil
|
||||
}
|
||||
|
||||
// getAvailableWorkers returns the list of available worker
|
||||
func (s *Server) getAvailableWorkers() map[string]*WorkerClient {
|
||||
workerClients := map[string]*WorkerClient{}
|
||||
for k, w := range s.workerClients {
|
||||
if w.HasGameSlot() {
|
||||
workerClients[k] = w
|
||||
}
|
||||
}
|
||||
|
||||
return workerClients
|
||||
}
|
||||
|
||||
// getWorkerFromAddress returns the worker has given address
|
||||
func (s *Server) getWorkerFromAddress(address string) *WorkerClient {
|
||||
for _, w := range s.workerClients {
|
||||
if w.HasGameSlot() && w.Address == address {
|
||||
return w
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findBestServerFromBrowser returns the best server for a session
|
||||
// All workers addresses are sent to user and user will ping to get latency
|
||||
func (s *Server) findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient, zone string) (string, error) {
|
||||
// TODO: Find best Server by latency, currently return by ping
|
||||
if len(workerClients) == 0 {
|
||||
return "", errors.New("no server found")
|
||||
}
|
||||
|
||||
latencies := s.getLatencyMapFromBrowser(workerClients, client)
|
||||
client.Println("Latency map", latencies)
|
||||
|
||||
if len(latencies) == 0 {
|
||||
return "", errors.New("no server found")
|
||||
}
|
||||
|
||||
var bestWorker *WorkerClient
|
||||
var minLatency int64 = math.MaxInt64
|
||||
|
||||
// get the worker with lowest latency to user
|
||||
for wc, l := range latencies {
|
||||
if zone != "" && wc.Zone != zone {
|
||||
// skip worker not in the zone if zone param is given
|
||||
continue
|
||||
}
|
||||
|
||||
if l < minLatency {
|
||||
bestWorker = wc
|
||||
minLatency = l
|
||||
}
|
||||
}
|
||||
|
||||
return bestWorker.WorkerID, nil
|
||||
}
|
||||
|
||||
// getLatencyMapFromBrowser get all latencies from worker to user
|
||||
func (s *Server) getLatencyMapFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) map[*WorkerClient]int64 {
|
||||
var workersList []*WorkerClient
|
||||
var addressList []string
|
||||
uniqueAddresses := map[string]bool{}
|
||||
latencyMap := map[*WorkerClient]int64{}
|
||||
|
||||
// addressList is the list of worker addresses
|
||||
for _, workerClient := range workerClients {
|
||||
if _, ok := uniqueAddresses[workerClient.PingServer]; !ok {
|
||||
addressList = append(addressList, workerClient.PingServer)
|
||||
}
|
||||
uniqueAddresses[workerClient.PingServer] = true
|
||||
workersList = append(workersList, workerClient)
|
||||
}
|
||||
|
||||
// send this address to user and get back latency
|
||||
client.Println("Send sync", addressList, strings.Join(addressList, ","))
|
||||
data := client.SyncSend(cws.WSPacket{
|
||||
ID: "checkLatency",
|
||||
Data: strings.Join(addressList, ","),
|
||||
})
|
||||
|
||||
respLatency := map[string]int64{}
|
||||
err := json.Unmarshal([]byte(data.Data), &respLatency)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return latencyMap
|
||||
}
|
||||
|
||||
for _, workerClient := range workersList {
|
||||
if latency, ok := respLatency[workerClient.PingServer]; ok {
|
||||
latencyMap[workerClient] = latency
|
||||
}
|
||||
}
|
||||
return latencyMap
|
||||
}
|
||||
|
||||
// cleanBrowser is called when a browser is disconnected
|
||||
func (s *Server) cleanBrowser(bc *BrowserClient, sessionID string) {
|
||||
bc.Println("Disconnect from coordinator")
|
||||
delete(s.browserClients, sessionID)
|
||||
bc.Close()
|
||||
}
|
||||
|
||||
// cleanWorker is called when a worker is disconnected
|
||||
// connection from worker to coordinator is also closed
|
||||
func (s *Server) cleanWorker(wc *WorkerClient, workerID string) {
|
||||
wc.Println("Unregister worker from coordinator")
|
||||
// Remove workerID from workerClients
|
||||
delete(s.workerClients, workerID)
|
||||
// Clean all rooms connecting to that server
|
||||
for roomID, roomServer := range s.roomToWorker {
|
||||
if roomServer == workerID {
|
||||
wc.Printf("Remove room %s", roomID)
|
||||
delete(s.roomToWorker, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
wc.Close()
|
||||
}
|
||||
|
||||
// createInitPackage returns serverhost + game list in encoded wspacket format
|
||||
// This package will be sent to initialize
|
||||
func createInitPackage(stunturn string, games []games.GameMetadata) string {
|
||||
var gameName []string
|
||||
for _, game := range games {
|
||||
gameName = append(gameName, game.Name)
|
||||
}
|
||||
|
||||
initPackage := append([]string{stunturn}, gameName...)
|
||||
encodedList, _ := json.Marshal(initPackage)
|
||||
return string(encodedList)
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network/httpx"
|
||||
)
|
||||
|
||||
func NewHTTPServer(conf coordinator.Config, fnMux func(mux *http.ServeMux)) (*httpx.Server, error) {
|
||||
return httpx.NewServer(
|
||||
conf.Coordinator.Server.GetAddr(),
|
||||
func(*httpx.Server) http.Handler {
|
||||
h := http.NewServeMux()
|
||||
h.Handle("/", index(conf))
|
||||
h.Handle("/static/", static("./web"))
|
||||
fnMux(h)
|
||||
return h
|
||||
},
|
||||
httpx.WithServerConfig(conf.Coordinator.Server),
|
||||
)
|
||||
}
|
||||
|
||||
func index(conf coordinator.Config) http.Handler {
|
||||
tpl, err := template.ParseFiles("./web/index.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// return 404 on unknown
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
// render index page with some tpl values
|
||||
if err = tpl.Execute(w, conf.Coordinator.Analytics); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func static(dir string) http.Handler {
|
||||
return http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))
|
||||
}
|
||||
337
pkg/coordinator/hub.go
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
type Connection interface {
|
||||
Disconnect()
|
||||
Id() com.Uid
|
||||
ProcessPackets(func(api.In[com.Uid]) error) chan struct{}
|
||||
|
||||
Send(api.PT, any) ([]byte, error)
|
||||
Notify(api.PT, any)
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
conf config.CoordinatorConfig
|
||||
log *logger.Logger
|
||||
users com.NetMap[com.Uid, *User]
|
||||
workers com.NetMap[com.Uid, *Worker]
|
||||
}
|
||||
|
||||
func NewHub(conf config.CoordinatorConfig, log *logger.Logger) *Hub {
|
||||
return &Hub{
|
||||
conf: conf,
|
||||
users: com.NewNetMap[com.Uid, *User](),
|
||||
workers: com.NewNetMap[com.Uid, *Worker](),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// handleUserConnection handles all connections from user/frontend.
|
||||
func (h *Hub) handleUserConnection() http.HandlerFunc {
|
||||
var connector com.Server
|
||||
connector.Origin(h.conf.Coordinator.Origin.UserWs)
|
||||
|
||||
log := h.log.Extend(h.log.With().
|
||||
Str(logger.ClientField, "u").
|
||||
Str(logger.DirectionField, logger.MarkIn),
|
||||
)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Debug().Msgf("Handshake %v", r.Host)
|
||||
|
||||
conn, err := connector.Connect(w, r)
|
||||
if err != nil {
|
||||
h.log.Error().Err(err).Msg("user connection fail")
|
||||
return
|
||||
}
|
||||
|
||||
user := NewUser(conn, log)
|
||||
defer h.users.RemoveDisconnect(user)
|
||||
done := user.HandleRequests(h, h.conf)
|
||||
params := r.URL.Query()
|
||||
|
||||
worker := h.findWorkerFor(user, params, h.log.Extend(h.log.With().Str("cid", user.Id().Short())))
|
||||
if worker == nil {
|
||||
user.Notify(api.ErrNoFreeSlots, "")
|
||||
h.log.Info().Msg("no free workers")
|
||||
return
|
||||
}
|
||||
|
||||
// Link the user to the selected worker. Slot reservation is handled later
|
||||
// on game start; this keeps connections lightweight and lets deep-link
|
||||
// joins share a worker without consuming its single game slot.
|
||||
user.w = worker
|
||||
|
||||
h.users.Add(user)
|
||||
|
||||
apps := worker.AppNames()
|
||||
list := make([]api.AppMeta, len(apps))
|
||||
for i := range apps {
|
||||
list[i] = api.AppMeta{Alias: apps[i].Alias, Title: apps[i].Name, System: apps[i].System}
|
||||
}
|
||||
|
||||
user.InitSession(worker.Id().String(), h.conf.Webrtc.IceServers, list)
|
||||
log.Info().Str(logger.DirectionField, logger.MarkPlus).Msgf("user %s", user.Id())
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func RequestToHandshake(data string) (*api.ConnectionRequest[com.Uid], error) {
|
||||
if data == "" {
|
||||
return nil, api.ErrMalformed
|
||||
}
|
||||
handshake, err := api.UnwrapChecked[api.ConnectionRequest[com.Uid]](base64.URLEncoding.DecodeString(data))
|
||||
if err != nil || handshake == nil {
|
||||
return nil, fmt.Errorf("%w (%v)", err, handshake)
|
||||
}
|
||||
return handshake, nil
|
||||
}
|
||||
|
||||
// handleWorkerConnection handles all connections from a new worker to coordinator.
|
||||
func (h *Hub) handleWorkerConnection() http.HandlerFunc {
|
||||
var connector com.Server
|
||||
connector.Origin(h.conf.Coordinator.Origin.WorkerWs)
|
||||
|
||||
log := h.log.Extend(h.log.With().
|
||||
Str(logger.ClientField, "w").
|
||||
Str(logger.DirectionField, logger.MarkIn),
|
||||
)
|
||||
|
||||
h.log.Debug().Msgf("WS max message size: %vb", h.conf.Coordinator.MaxWsSize)
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Debug().Msgf("Handshake %v", r.Host)
|
||||
|
||||
handshake, err := RequestToHandshake(r.URL.Query().Get(api.DataQueryParam))
|
||||
if err != nil {
|
||||
h.log.Error().Err(err).Msg("handshake fail")
|
||||
return
|
||||
}
|
||||
|
||||
if handshake.PingURL == "" {
|
||||
h.log.Warn().Msg("Ping address is not set")
|
||||
}
|
||||
|
||||
if h.conf.Coordinator.Server.Https && !handshake.IsHTTPS {
|
||||
h.log.Warn().Msg("Unsecure worker connection. Unsecure to secure may be bad.")
|
||||
}
|
||||
|
||||
// set connection uid from the handshake
|
||||
if handshake.Id != com.NilUid {
|
||||
h.log.Debug().Msgf("Worker uid will be set to %v", handshake.Id)
|
||||
}
|
||||
|
||||
conn, err := connector.Connect(w, r)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("worker connection fail")
|
||||
return
|
||||
}
|
||||
conn.SetMaxReadSize(h.conf.Coordinator.MaxWsSize)
|
||||
|
||||
worker := NewWorker(conn, *handshake, log)
|
||||
defer h.workers.RemoveDisconnect(worker)
|
||||
done := worker.HandleRequests(&h.users)
|
||||
h.workers.Add(worker)
|
||||
log.Info().
|
||||
Str(logger.DirectionField, logger.MarkPlus).
|
||||
Msgf("worker %s", worker.PrintInfo())
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) GetServerList() (r []api.Server) {
|
||||
debug := h.conf.Coordinator.Debug
|
||||
for w := range h.workers.Values() {
|
||||
server := api.Server{
|
||||
Addr: w.Addr,
|
||||
Id: w.Id(),
|
||||
IsBusy: !w.HasSlot(),
|
||||
Machine: string(w.Id().Machine()),
|
||||
PingURL: w.PingServer,
|
||||
Port: w.Port,
|
||||
Tag: w.Tag,
|
||||
Zone: w.Zone,
|
||||
}
|
||||
if debug {
|
||||
server.Room = w.RoomId
|
||||
}
|
||||
r = append(r, server)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// findWorkerFor searches a free worker for the user depending on
|
||||
// various conditions.
|
||||
func (h *Hub) findWorkerFor(usr *User, q url.Values, log *logger.Logger) *Worker {
|
||||
log.Debug().Msg("Search available workers")
|
||||
roomIdRaw := q.Get(api.RoomIdQueryParam)
|
||||
sessionId, deepRoomId := api.ExplodeDeepLink(roomIdRaw)
|
||||
roomId := roomIdRaw
|
||||
if deepRoomId != "" {
|
||||
roomId = deepRoomId
|
||||
}
|
||||
zone := q.Get(api.ZoneQueryParam)
|
||||
wid := q.Get(api.WorkerIdParam)
|
||||
|
||||
var worker *Worker
|
||||
|
||||
if wid != "" {
|
||||
if worker = h.findWorkerById(wid, h.conf.Coordinator.Debug); worker != nil {
|
||||
log.Debug().Msgf("Worker with id: %v has been found", wid)
|
||||
return worker
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if worker = h.findWorkerByRoom(roomIdRaw, roomId, zone); worker != nil {
|
||||
log.Debug().Str("room", roomId).Msg("An existing worker has been found")
|
||||
} else if worker = h.findWorkerByPreviousRoom(sessionId); worker != nil {
|
||||
log.Debug().Msgf("Worker %v with the previous room: %v is found", wid, roomId)
|
||||
} else {
|
||||
switch h.conf.Coordinator.Selector {
|
||||
case config.SelectByPing:
|
||||
log.Debug().Msgf("Searching fastest free worker...")
|
||||
if worker = h.findFastestWorker(zone,
|
||||
func(servers []string) (map[string]int64, error) { return usr.CheckLatency(servers) }); worker != nil {
|
||||
log.Debug().Msg("The fastest worker has been found")
|
||||
}
|
||||
default:
|
||||
log.Debug().Msgf("Searching any free worker...")
|
||||
if worker = h.find1stFreeWorker(zone); worker != nil {
|
||||
log.Debug().Msgf("Found next free worker")
|
||||
}
|
||||
}
|
||||
}
|
||||
return worker
|
||||
}
|
||||
|
||||
func (h *Hub) findWorkerByPreviousRoom(id string) *Worker {
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
w, _ := h.workers.FindBy(func(w *Worker) bool {
|
||||
// session and room id are the same
|
||||
return w.HadSession(id) && w.HasSlot()
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
func (h *Hub) findWorkerByRoom(id string, deepId string, region string) *Worker {
|
||||
if id == "" && deepId == "" {
|
||||
return nil
|
||||
}
|
||||
// if there is zone param, we need to ensure the worker in that zone,
|
||||
// if not we consider the room is missing
|
||||
w, _ := h.workers.FindBy(func(w *Worker) bool {
|
||||
matchId := w.RoomId == id
|
||||
if !matchId && deepId != "" {
|
||||
matchId = w.RoomId == deepId
|
||||
}
|
||||
return matchId && w.In(region)
|
||||
})
|
||||
return w
|
||||
}
|
||||
|
||||
func (h *Hub) getAvailableWorkers(region string) []*Worker {
|
||||
var workers []*Worker
|
||||
for w := range h.workers.Values() {
|
||||
if w.HasSlot() && w.In(region) {
|
||||
workers = append(workers, w)
|
||||
}
|
||||
}
|
||||
return workers
|
||||
}
|
||||
|
||||
func (h *Hub) find1stFreeWorker(region string) *Worker {
|
||||
workers := h.getAvailableWorkers(region)
|
||||
if len(workers) > 0 {
|
||||
return workers[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFastestWorker returns the best server for a session.
|
||||
// All workers addresses are sent to user and user will ping to get latency.
|
||||
// !to rewrite
|
||||
func (h *Hub) findFastestWorker(region string, fn func(addresses []string) (map[string]int64, error)) *Worker {
|
||||
workers := h.getAvailableWorkers(region)
|
||||
if len(workers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var addresses []string
|
||||
group := map[string][]struct{}{}
|
||||
for _, w := range workers {
|
||||
if _, ok := group[w.PingServer]; !ok {
|
||||
addresses = append(addresses, w.PingServer)
|
||||
}
|
||||
group[w.PingServer] = append(group[w.PingServer], struct{}{})
|
||||
}
|
||||
|
||||
latencies, err := fn(addresses)
|
||||
if len(latencies) == 0 || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
workers = h.getAvailableWorkers(region)
|
||||
if len(workers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bestWorker *Worker
|
||||
var minLatency int64 = 1<<31 - 1
|
||||
// get a worker with the lowest latency
|
||||
for addr, ping := range latencies {
|
||||
if ping < minLatency {
|
||||
for _, w := range workers {
|
||||
if w.PingServer == addr {
|
||||
bestWorker = w
|
||||
}
|
||||
}
|
||||
minLatency = ping
|
||||
}
|
||||
}
|
||||
return bestWorker
|
||||
}
|
||||
|
||||
func (h *Hub) findWorkerById(id string, useAllWorkers bool) *Worker {
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
uid, err := com.UidFromString(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, w := range h.getAvailableWorkers("") {
|
||||
if w.Id() == com.NilUid {
|
||||
continue
|
||||
}
|
||||
if useAllWorkers {
|
||||
if uid == w.Id() {
|
||||
return w
|
||||
}
|
||||
} else {
|
||||
// select any worker on the same machine when workers are grouped on the client
|
||||
if bytes.Equal(uid.Machine(), w.Id().Machine()) {
|
||||
return w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
)
|
||||
|
||||
func (wc *WorkerClient) handleHeartbeat() cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func GetConnectionRequest(data string) (api.ConnectionRequest, error) {
|
||||
req := api.ConnectionRequest{}
|
||||
if data == "" {
|
||||
return req, nil
|
||||
}
|
||||
decodeString, err := base64.URLEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
err = json.Unmarshal(decodeString, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
// handleRegisterRoom event from a worker, when worker created a new room.
|
||||
// RoomID is global so it is managed by coordinator.
|
||||
func (wc *WorkerClient) handleRegisterRoom(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Printf("Coordinator: Received registerRoom room %s from worker %s", resp.Data, wc.WorkerID)
|
||||
s.roomToWorker[resp.Data] = wc.WorkerID
|
||||
log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
|
||||
return api.RegisterRoomPacket(api.NoData)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetRoom returns the server ID based on requested roomID.
|
||||
func (wc *WorkerClient) handleGetRoom(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Println("Coordinator: Received a get room request")
|
||||
log.Println("Result: ", s.roomToWorker[resp.Data])
|
||||
return api.GetRoomPacket(s.roomToWorker[resp.Data])
|
||||
}
|
||||
}
|
||||
|
||||
// handleCloseRoom event from a worker, when worker close a room.
|
||||
func (wc *WorkerClient) handleCloseRoom(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Printf("Coordinator: Received closeRoom room %s from worker %s", resp.Data, wc.WorkerID)
|
||||
delete(s.roomToWorker, resp.Data)
|
||||
log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
|
||||
return api.CloseRoomPacket(api.NoData)
|
||||
}
|
||||
}
|
||||
|
||||
// handleIceCandidate passes an ICE candidate (WebRTC) to the browser.
|
||||
func (wc *WorkerClient) handleIceCandidate(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
wc.Println("Received IceCandidate from worker -> relay to browser")
|
||||
bc, ok := s.browserClients[resp.SessionID]
|
||||
if ok {
|
||||
// Remove SessionID while sending back to browser
|
||||
resp.SessionID = ""
|
||||
bc.Send(resp, nil)
|
||||
} else {
|
||||
wc.Println("Error: unknown SessionID:", resp.SessionID)
|
||||
}
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
|
||||
// workerRoutes adds all worker request routes.
|
||||
func (s *Server) workerRoutes(wc *WorkerClient) {
|
||||
if wc == nil {
|
||||
return
|
||||
}
|
||||
wc.Receive(api.Heartbeat, wc.handleHeartbeat())
|
||||
wc.Receive(api.RegisterRoom, wc.handleRegisterRoom(s))
|
||||
wc.Receive(api.GetRoom, wc.handleGetRoom(s))
|
||||
wc.Receive(api.CloseRoom, wc.handleCloseRoom(s))
|
||||
wc.Receive(api.IceCandidate, wc.handleIceCandidate(s))
|
||||
}
|
||||
|
||||
// useragentRoutes adds all useragent (browser) request routes.
|
||||
func (s *Server) useragentRoutes(bc *BrowserClient) {
|
||||
if bc == nil {
|
||||
return
|
||||
}
|
||||
bc.Receive(api.Heartbeat, bc.handleHeartbeat())
|
||||
bc.Receive(api.InitWebrtc, bc.handleInitWebrtc(s))
|
||||
bc.Receive(api.Answer, bc.handleAnswer(s))
|
||||
bc.Receive(api.IceCandidate, bc.handleIceCandidate(s))
|
||||
bc.Receive(api.GameStart, bc.handleGameStart(s))
|
||||
bc.Receive(api.GameQuit, bc.handleGameQuit(s))
|
||||
bc.Receive(api.GameSave, bc.handleGameSave(s))
|
||||
bc.Receive(api.GameLoad, bc.handleGameLoad(s))
|
||||
bc.Receive(api.GamePlayerSelect, bc.handleGamePlayerSelect(s))
|
||||
bc.Receive(api.GameMultitap, bc.handleGameMultitap(s))
|
||||
}
|
||||
81
pkg/coordinator/user.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Connection
|
||||
w *Worker // linked worker
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
type HasServerInfo interface {
|
||||
GetServerList() []api.Server
|
||||
}
|
||||
|
||||
func NewUser(sock *com.Connection, log *logger.Logger) *User {
|
||||
conn := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](sock, com.NewUid(), log)
|
||||
return &User{
|
||||
Connection: conn,
|
||||
log: log.Extend(log.With().
|
||||
Str(logger.ClientField, logger.MarkNone).
|
||||
Str(logger.DirectionField, logger.MarkNone).
|
||||
Str("cid", conn.Id().Short())),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) Bind(w *Worker) bool {
|
||||
u.w = w
|
||||
// Binding only links the worker; slot reservation is handled lazily on
|
||||
// game start to avoid blocking deep-link joins or parallel connections
|
||||
// that haven't started a game yet.
|
||||
return true
|
||||
}
|
||||
|
||||
func (u *User) Disconnect() {
|
||||
u.Connection.Disconnect()
|
||||
if u.w != nil {
|
||||
u.w.TerminateSession(u.Id().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleRequests(info HasServerInfo, conf config.CoordinatorConfig) chan struct{} {
|
||||
return u.ProcessPackets(func(x api.In[com.Uid]) (err error) {
|
||||
switch x.T {
|
||||
case api.WebrtcInit:
|
||||
if u.w != nil {
|
||||
u.HandleWebrtcInit()
|
||||
}
|
||||
case api.WebrtcAnswer:
|
||||
err = api.Do(x, u.HandleWebrtcAnswer)
|
||||
case api.WebrtcIce:
|
||||
err = api.Do(x, u.HandleWebrtcIceCandidate)
|
||||
case api.StartGame:
|
||||
err = api.Do(x, func(d api.GameStartUserRequest) { u.HandleStartGame(d, conf) })
|
||||
case api.QuitGame:
|
||||
err = api.Do(x, u.HandleQuitGame)
|
||||
case api.SaveGame:
|
||||
err = u.HandleSaveGame()
|
||||
case api.LoadGame:
|
||||
err = u.HandleLoadGame()
|
||||
case api.ChangePlayer:
|
||||
err = api.Do(x, u.HandleChangePlayer)
|
||||
case api.ResetGame:
|
||||
err = api.Do(x, u.HandleResetGame)
|
||||
case api.RecordGame:
|
||||
if !conf.Recording.Enabled {
|
||||
return api.ErrForbidden
|
||||
}
|
||||
err = api.Do(x, u.HandleRecordGame)
|
||||
case api.GetWorkerList:
|
||||
u.handleGetWorkerList(conf.Coordinator.Debug, info)
|
||||
default:
|
||||
u.log.Warn().Msgf("Unknown packet: %+v", x)
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/session"
|
||||
)
|
||||
|
||||
func (bc *BrowserClient) handleHeartbeat() cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket { return resp }
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleInitWebrtc(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
// initWebrtc now only sends signal to worker, asks it to createOffer
|
||||
bc.Printf("Received init_webrtc request -> relay to worker: %s", bc.WorkerID)
|
||||
// relay request to target worker
|
||||
// worker creates a PeerConnection, and createOffer
|
||||
// send SDP back to browser
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
sdp := wc.SyncSend(resp)
|
||||
bc.Println("Received SDP from worker -> sending back to browser")
|
||||
return sdp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleAnswer(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
// contains SDP of browser createAnswer
|
||||
// forward to worker
|
||||
bc.Println("Received browser answered SDP -> relay to worker")
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
wc.Send(resp, nil)
|
||||
// no need to response
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleIceCandidate(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
// contains ICE candidate of browser
|
||||
// forward to worker
|
||||
bc.Println("Received IceCandidate from browser -> relay to worker")
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
wc.Send(resp, nil)
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameStart(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received start request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
|
||||
// +injects game data into the original game request
|
||||
gameStartCall, err := newGameStartCall(resp.RoomID, resp.Data, o.library)
|
||||
if err != nil {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
if packet, err := gameStartCall.To(); err != nil {
|
||||
return cws.EmptyPacket
|
||||
} else {
|
||||
resp.Data = packet
|
||||
}
|
||||
workerResp := wc.SyncSend(resp)
|
||||
|
||||
// Response from worker contains initialized roomID. Set roomID to the session
|
||||
bc.RoomID = workerResp.RoomID
|
||||
bc.Println("Received room response from browser: ", workerResp.RoomID)
|
||||
|
||||
return workerResp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameQuit(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received quit request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
// Send but, waiting
|
||||
wc.SyncSend(resp)
|
||||
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameSave(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received save request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameLoad(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received load request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGamePlayerSelect(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received update player index request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameMultitap(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received multitap request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
// newGameStartCall gathers data for a new game start call of the worker
|
||||
func newGameStartCall(roomId string, data string, library games.GameLibrary) (api.GameStartCall, error) {
|
||||
request := api.GameStartRequest{}
|
||||
if err := request.From(data); err != nil {
|
||||
return api.GameStartCall{}, errors.New("invalid request")
|
||||
}
|
||||
|
||||
// the name of the game either in the `room id` field or
|
||||
// it's in the initial request
|
||||
game := request.GameName
|
||||
if roomId != "" {
|
||||
// ! should be moved into coordinator
|
||||
name := session.GetGameNameFromRoomID(roomId)
|
||||
if name == "" {
|
||||
return api.GameStartCall{}, errors.New("couldn't decode game name from the room id")
|
||||
}
|
||||
game = name
|
||||
}
|
||||
|
||||
gameInfo := library.FindGameByName(game)
|
||||
if gameInfo.Path == "" {
|
||||
return api.GameStartCall{}, fmt.Errorf("couldn't find game info for the game %v", game)
|
||||
}
|
||||
|
||||
return api.GameStartCall{
|
||||
Name: gameInfo.Name,
|
||||
Base: gameInfo.Base,
|
||||
Path: gameInfo.Path,
|
||||
Type: gameInfo.Type,
|
||||
}, nil
|
||||
}
|
||||
38
pkg/coordinator/userapi.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
)
|
||||
|
||||
// CheckLatency sends a list of server addresses to the user
|
||||
// and waits get back this list with tested ping times for each server.
|
||||
func (u *User) CheckLatency(req api.CheckLatencyUserResponse) (api.CheckLatencyUserRequest, error) {
|
||||
dat, err := api.UnwrapChecked[api.CheckLatencyUserRequest](u.Send(api.CheckLatency, req))
|
||||
if dat == nil {
|
||||
return api.CheckLatencyUserRequest{}, err
|
||||
}
|
||||
return *dat, nil
|
||||
}
|
||||
|
||||
// InitSession signals the user that the app is ready to go.
|
||||
func (u *User) InitSession(wid string, ice []config.IceServer, games []api.AppMeta) {
|
||||
u.Notify(api.InitSession, api.InitSessionUserResponse{
|
||||
Ice: *(*[]api.IceServer)(unsafe.Pointer(&ice)), // don't do this at home
|
||||
Games: games,
|
||||
Wid: wid,
|
||||
})
|
||||
}
|
||||
|
||||
// SendWebrtcOffer sends SDP offer back to the user.
|
||||
func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
|
||||
|
||||
// SendWebrtcIceCandidate sends remote ICE candidate back to the user.
|
||||
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }
|
||||
|
||||
// StartGame signals the user that everything is ready to start a game.
|
||||
func (u *User) StartGame(av *api.AppVideoInfo, kbMouse bool) {
|
||||
u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av, KbMouse: kbMouse})
|
||||
}
|
||||
196
pkg/coordinator/userhandlers.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/config"
|
||||
)
|
||||
|
||||
func (u *User) HandleWebrtcInit() {
|
||||
uid := u.Id().String()
|
||||
resp, err := u.w.WebrtcInit(uid)
|
||||
if err != nil || resp == nil || *resp == api.EMPTY {
|
||||
u.log.Error().Err(err).Msg("malformed WebRTC init response")
|
||||
return
|
||||
}
|
||||
u.SendWebrtcOffer(string(*resp))
|
||||
}
|
||||
|
||||
func (u *User) HandleWebrtcAnswer(rq api.WebrtcAnswerUserRequest) {
|
||||
u.w.WebrtcAnswer(u.Id().String(), string(rq))
|
||||
}
|
||||
|
||||
func (u *User) HandleWebrtcIceCandidate(rq api.WebrtcUserIceCandidate) {
|
||||
u.w.WebrtcIceCandidate(u.Id().String(), string(rq))
|
||||
}
|
||||
|
||||
func (u *User) HandleStartGame(rq api.GameStartUserRequest, conf config.CoordinatorConfig) {
|
||||
// Worker slot / room gating:
|
||||
// - If the worker is BUSY (no free slot), we must not create another room.
|
||||
// * If the worker has already reported a room id, only allow requests
|
||||
// for that same room (deep-link joins / reloads).
|
||||
// * If the worker hasn't reported a room yet, deny any new StartGame to
|
||||
// avoid racing concurrent room creation on the worker.
|
||||
// * When the user is starting a NEW game (empty room id), we give the
|
||||
// worker a short grace period to close the previous room and free the
|
||||
// slot before rejecting with "no slots".
|
||||
// - If the worker is FREE, reserve the slot lazily before starting the
|
||||
// game; the room id (if any) comes from the request / worker.
|
||||
|
||||
// Grace period: when there's no room id in the request (new game) but the
|
||||
// worker still appears busy, wait a bit for the previous room to close.
|
||||
if rq.RoomId == "" && !u.w.HasSlot() {
|
||||
const waitTotal = 3 * time.Second
|
||||
const step = 100 * time.Millisecond
|
||||
waited := time.Duration(0)
|
||||
for waited < waitTotal {
|
||||
if u.w.HasSlot() {
|
||||
break
|
||||
}
|
||||
time.Sleep(step)
|
||||
waited += step
|
||||
}
|
||||
}
|
||||
|
||||
busy := !u.w.HasSlot()
|
||||
if busy {
|
||||
if u.w.RoomId == "" {
|
||||
u.Notify(api.ErrNoFreeSlots, "")
|
||||
return
|
||||
}
|
||||
if rq.RoomId == "" {
|
||||
// No room id but worker is busy -> assume user wants to continue
|
||||
// the existing room instead of starting a parallel game.
|
||||
rq.RoomId = u.w.RoomId
|
||||
} else if rq.RoomId != u.w.RoomId {
|
||||
u.Notify(api.ErrNoFreeSlots, "")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Worker is free: try to reserve the single slot for this new room.
|
||||
if !u.w.TryReserve() {
|
||||
u.Notify(api.ErrNoFreeSlots, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
startGameResp, err := u.w.StartGame(u.Id().String(), rq)
|
||||
if err != nil || startGameResp == nil {
|
||||
u.log.Error().Err(err).Msg("malformed game start response")
|
||||
return
|
||||
}
|
||||
if startGameResp.Rid == "" {
|
||||
u.log.Error().Msg("there is no room")
|
||||
return
|
||||
}
|
||||
u.log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
|
||||
u.StartGame(startGameResp.AV, startGameResp.KbMouse)
|
||||
|
||||
// send back recording status
|
||||
if conf.Recording.Enabled && rq.Record {
|
||||
u.Notify(api.RecordGame, api.OK)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleQuitGame(rq api.GameQuitRequest) {
|
||||
if rq.Rid == u.w.RoomId {
|
||||
u.w.QuitGame(u.Id().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleResetGame(rq api.ResetGameRequest) {
|
||||
if rq.Rid != u.w.RoomId {
|
||||
return
|
||||
}
|
||||
u.w.ResetGame(u.Id().String())
|
||||
}
|
||||
|
||||
func (u *User) HandleSaveGame() error {
|
||||
resp, err := u.w.SaveGame(u.Id().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *resp == api.OK {
|
||||
if id, _ := api.ExplodeDeepLink(u.w.RoomId); id != "" {
|
||||
u.w.AddSession(id)
|
||||
}
|
||||
}
|
||||
|
||||
u.Notify(api.SaveGame, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HandleLoadGame() error {
|
||||
resp, err := u.w.LoadGame(u.Id().String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Notify(api.LoadGame, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) {
|
||||
resp, err := u.w.ChangePlayer(u.Id().String(), int(rq))
|
||||
// !to make it a little less convoluted
|
||||
if err != nil || resp == nil || *resp == -1 {
|
||||
u.log.Error().Err(err).Msgf("player select fail, req: %v", rq)
|
||||
return
|
||||
}
|
||||
u.Notify(api.ChangePlayer, rq)
|
||||
}
|
||||
|
||||
func (u *User) HandleRecordGame(rq api.RecordGameRequest) {
|
||||
if u.w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.log.Debug().Msgf("??? room: %v, rec: %v user: %v", u.w.RoomId, rq.Active, rq.User)
|
||||
|
||||
if u.w.RoomId == "" {
|
||||
u.log.Error().Msg("Recording in the empty room is not allowed!")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := u.w.RecordGame(u.Id().String(), rq.Active, rq.User)
|
||||
if err != nil {
|
||||
u.log.Error().Err(err).Msg("malformed game record request")
|
||||
return
|
||||
}
|
||||
u.Notify(api.RecordGame, resp)
|
||||
}
|
||||
|
||||
func (u *User) handleGetWorkerList(debug bool, info HasServerInfo) {
|
||||
response := api.GetWorkerListResponse{}
|
||||
servers := info.GetServerList()
|
||||
|
||||
if debug {
|
||||
response.Servers = servers
|
||||
} else {
|
||||
unique := map[string]*api.Server{}
|
||||
for _, s := range servers {
|
||||
mid := s.Machine
|
||||
if _, ok := unique[mid]; !ok {
|
||||
unique[mid] = &api.Server{Addr: s.Addr, PingURL: s.PingURL, Id: s.Id, InGroup: true}
|
||||
}
|
||||
v := unique[mid]
|
||||
if v != nil {
|
||||
v.Replicas++
|
||||
}
|
||||
}
|
||||
for _, v := range unique {
|
||||
response.Servers = append(response.Servers, *v)
|
||||
}
|
||||
}
|
||||
if len(response.Servers) > 0 {
|
||||
sort.SliceStable(response.Servers, func(i, j int) bool {
|
||||
if response.Servers[i].Addr != response.Servers[j].Addr {
|
||||
return response.Servers[i].Addr < response.Servers[j].Addr
|
||||
}
|
||||
return response.Servers[i].Port < response.Servers[j].Port
|
||||
})
|
||||
}
|
||||
u.Notify(api.GetWorkerList, response)
|
||||
}
|
||||
|
|
@ -1,63 +1,191 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v3/pkg/logger"
|
||||
)
|
||||
|
||||
type WorkerClient struct {
|
||||
*cws.Client
|
||||
type Worker struct {
|
||||
AppLibrary
|
||||
Connection
|
||||
RegionalClient
|
||||
Session
|
||||
slotted
|
||||
|
||||
WorkerID string
|
||||
Address string // ip address of worker
|
||||
// public server used for ping check
|
||||
PingServer string
|
||||
StunTurnServer string
|
||||
userCount int // may be atomic
|
||||
Zone string
|
||||
Addr string
|
||||
PingServer string
|
||||
Port string
|
||||
RoomId string // room reference
|
||||
Tag string
|
||||
Zone string
|
||||
|
||||
mu sync.Mutex
|
||||
Lib []api.GameInfo
|
||||
Sessions map[string]struct{}
|
||||
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
// NewWorkerClient returns a client connecting to worker.
|
||||
// This connection exchanges information between workers and server.
|
||||
func NewWorkerClient(c *websocket.Conn, workerID string) *WorkerClient {
|
||||
return &WorkerClient{
|
||||
Client: cws.NewClient(c),
|
||||
WorkerID: workerID,
|
||||
type RegionalClient interface {
|
||||
In(region string) bool
|
||||
}
|
||||
|
||||
type HasUserRegistry interface {
|
||||
Find(id string) *User
|
||||
}
|
||||
|
||||
type AppLibrary interface {
|
||||
SetLib([]api.GameInfo)
|
||||
AppNames() []api.GameInfo
|
||||
}
|
||||
|
||||
type Session interface {
|
||||
AddSession(id string)
|
||||
// HadSession is true when an old session is found
|
||||
HadSession(id string) bool
|
||||
SetSessions(map[string]struct{})
|
||||
}
|
||||
|
||||
type AppMeta struct {
|
||||
Alias string
|
||||
Base string
|
||||
Name string
|
||||
Path string
|
||||
System string
|
||||
Type string
|
||||
}
|
||||
|
||||
func NewWorker(sock *com.Connection, handshake api.ConnectionRequest[com.Uid], log *logger.Logger) *Worker {
|
||||
conn := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](sock, handshake.Id, log)
|
||||
return &Worker{
|
||||
Connection: conn,
|
||||
Addr: handshake.Addr,
|
||||
PingServer: handshake.PingURL,
|
||||
Port: handshake.Port,
|
||||
Tag: handshake.Tag,
|
||||
Zone: handshake.Zone,
|
||||
log: log.Extend(log.With().
|
||||
Str(logger.ClientField, logger.MarkNone).
|
||||
Str(logger.DirectionField, logger.MarkNone).
|
||||
Str("cid", conn.Id().Short())),
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeUserQuantityBy increases or decreases the total amount of
|
||||
// users connected to the current worker.
|
||||
// We count users to determine when the worker becomes new game ready.
|
||||
func (wc *WorkerClient) ChangeUserQuantityBy(n int) {
|
||||
wc.mu.Lock()
|
||||
wc.userCount += n
|
||||
// just to be on a safe side
|
||||
if wc.userCount < 0 {
|
||||
wc.userCount = 0
|
||||
func (w *Worker) HandleRequests(users HasUserRegistry) chan struct{} {
|
||||
return w.ProcessPackets(func(p api.In[com.Uid]) (err error) {
|
||||
switch p.T {
|
||||
case api.RegisterRoom:
|
||||
err = api.Do(p, func(d api.RegisterRoomRequest) {
|
||||
w.log.Info().Msgf("set room [%v] = %v", w.Id(), d)
|
||||
w.HandleRegisterRoom(d)
|
||||
})
|
||||
case api.CloseRoom:
|
||||
err = api.Do(p, w.HandleCloseRoom)
|
||||
case api.IceCandidate:
|
||||
err = api.DoE(p, func(d api.WebrtcIceCandidateRequest) error {
|
||||
return w.HandleIceCandidate(d, users)
|
||||
})
|
||||
case api.LibNewGameList:
|
||||
err = api.DoE(p, w.HandleLibGameList)
|
||||
case api.PrevSessions:
|
||||
err = api.DoE(p, w.HandlePrevSessionList)
|
||||
default:
|
||||
w.log.Warn().Msgf("Unknown packet: %+v", p)
|
||||
}
|
||||
if err != nil && !errors.Is(err, api.ErrMalformed) {
|
||||
w.log.Error().Err(err).Send()
|
||||
err = api.ErrMalformed
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Worker) SetLib(list []api.GameInfo) { w.Lib = list }
|
||||
|
||||
func (w *Worker) AppNames() []api.GameInfo {
|
||||
return w.Lib
|
||||
}
|
||||
|
||||
func (w *Worker) AddSession(id string) {
|
||||
// sessions can be uninitialized until the coordinator pushes them to the worker
|
||||
if w.Sessions == nil {
|
||||
return
|
||||
}
|
||||
wc.mu.Unlock()
|
||||
|
||||
w.Sessions[id] = struct{}{}
|
||||
}
|
||||
|
||||
// HasGameSlot tells whether the current worker has a
|
||||
// free slot to start a new game.
|
||||
// Workers support only one game at a time.
|
||||
func (wc *WorkerClient) HasGameSlot() bool {
|
||||
wc.mu.Lock()
|
||||
defer wc.mu.Unlock()
|
||||
return wc.userCount == 0
|
||||
func (w *Worker) HadSession(id string) bool {
|
||||
_, ok := w.Sessions[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (wc *WorkerClient) Printf(format string, args ...interface{}) {
|
||||
log.Printf(fmt.Sprintf("Worker %s] %s", wc.WorkerID, format), args...)
|
||||
func (w *Worker) SetSessions(sessions map[string]struct{}) {
|
||||
w.Sessions = sessions
|
||||
}
|
||||
|
||||
func (wc *WorkerClient) Println(args ...interface{}) {
|
||||
log.Println(fmt.Sprintf("Worker %s] %s", wc.WorkerID, fmt.Sprint(args...)))
|
||||
// In say whether some worker from this region (zone).
|
||||
// Empty region always returns true.
|
||||
func (w *Worker) In(region string) bool { return region == "" || region == w.Zone }
|
||||
|
||||
// slotted used for tracking user slots and the availability.
|
||||
type slotted int32
|
||||
|
||||
// HasSlot checks if the current worker has a free slot to start a new game.
|
||||
// Workers support only one game at a time, so it returns true in case if
|
||||
// there are no players in the room (worker).
|
||||
func (s *slotted) HasSlot() bool { return atomic.LoadInt32((*int32)(s)) == 0 }
|
||||
|
||||
// TryReserve reserves the slot only when it's free.
|
||||
func (s *slotted) TryReserve() bool {
|
||||
for {
|
||||
current := atomic.LoadInt32((*int32)(s))
|
||||
if current != 0 {
|
||||
return false
|
||||
}
|
||||
if atomic.CompareAndSwapInt32((*int32)(s), 0, 1) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnReserve decrements user counter of the worker.
|
||||
func (s *slotted) UnReserve() {
|
||||
for {
|
||||
current := atomic.LoadInt32((*int32)(s))
|
||||
if current <= 0 {
|
||||
// reset to zero
|
||||
if current < 0 {
|
||||
if atomic.CompareAndSwapInt32((*int32)(s), current, 0) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Regular decrement for positive values
|
||||
newVal := current - 1
|
||||
if atomic.CompareAndSwapInt32((*int32)(s), current, newVal) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *slotted) FreeSlots() { atomic.StoreInt32((*int32)(s), 0) }
|
||||
|
||||
func (w *Worker) Disconnect() {
|
||||
w.Connection.Disconnect()
|
||||
w.RoomId = ""
|
||||
w.FreeSlots()
|
||||
}
|
||||
|
||||
func (w *Worker) PrintInfo() string {
|
||||
return fmt.Sprintf("id: %v, addr: %v, port: %v, zone: %v, ping addr: %v, tag: %v",
|
||||
w.Id(), w.Addr, w.Port, w.Zone, w.PingServer, w.Tag)
|
||||
}
|
||||
|
|
|
|||
193
pkg/coordinator/worker_test.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSlotted(t *testing.T) {
|
||||
t.Run("UnReserve", func(t *testing.T) {
|
||||
t.Run("BasicDecrement", testUnReserveBasic)
|
||||
t.Run("PreventUnderflow", testUnReserveUnderflow)
|
||||
t.Run("ConcurrentDecrement", testUnReserveConcurrent)
|
||||
})
|
||||
|
||||
t.Run("TryReserve", func(t *testing.T) {
|
||||
t.Run("SuccessWhenZero", testTryReserveSuccess)
|
||||
t.Run("FailWhenNonZero", testTryReserveFailure)
|
||||
t.Run("ConcurrentReservations", testTryReserveConcurrent)
|
||||
})
|
||||
|
||||
t.Run("Integration", func(t *testing.T) {
|
||||
t.Run("ReserveUnreserveFlow", testReserveUnreserveFlow)
|
||||
t.Run("FreeSlots", testFreeSlots)
|
||||
t.Run("HasSlot", testHasSlot)
|
||||
})
|
||||
}
|
||||
|
||||
func testUnReserveBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
// Initial state
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Fatal("initial state not zero")
|
||||
}
|
||||
|
||||
// Test normal decrement
|
||||
s.TryReserve() // 0 -> 1
|
||||
s.UnReserve()
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("failed to decrement to zero")
|
||||
}
|
||||
|
||||
// Test multiple decrements
|
||||
s.TryReserve() // 0 -> 1
|
||||
s.TryReserve() // 1 -> 2
|
||||
s.UnReserve()
|
||||
s.UnReserve()
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("failed to decrement multiple times")
|
||||
}
|
||||
}
|
||||
|
||||
func testUnReserveUnderflow(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
t.Run("PreventNewUnderflow", func(t *testing.T) {
|
||||
s.UnReserve() // Start at 0
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("should remain at 0 when unreserving from 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FixExistingNegative", func(t *testing.T) {
|
||||
atomic.StoreInt32((*int32)(&s), -5)
|
||||
s.UnReserve()
|
||||
if current := atomic.LoadInt32((*int32)(&s)); current != 0 {
|
||||
t.Errorf("should fix negative value to 0, got %d", current)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testUnReserveConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var s slotted
|
||||
const workers = 100
|
||||
var wg sync.WaitGroup
|
||||
|
||||
atomic.StoreInt32((*int32)(&s), int32(workers))
|
||||
wg.Add(workers)
|
||||
|
||||
for range workers {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.UnReserve()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if current := atomic.LoadInt32((*int32)(&s)); current != 0 {
|
||||
t.Errorf("unexpected final value: %d (want 0)", current)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReserveSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
if !s.TryReserve() {
|
||||
t.Error("should succeed when zero")
|
||||
}
|
||||
if atomic.LoadInt32((*int32)(&s)) != 1 {
|
||||
t.Error("failed to increment")
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReserveFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
atomic.StoreInt32((*int32)(&s), 1)
|
||||
if s.TryReserve() {
|
||||
t.Error("should fail when non-zero")
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReserveConcurrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
const workers = 100
|
||||
var success int32
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(workers)
|
||||
for range workers {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if s.TryReserve() {
|
||||
atomic.AddInt32(&success, 1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if success != 1 {
|
||||
t.Errorf("unexpected success count: %d (want 1)", success)
|
||||
}
|
||||
if atomic.LoadInt32((*int32)(&s)) != 1 {
|
||||
t.Error("counter not properly incremented")
|
||||
}
|
||||
}
|
||||
|
||||
func testReserveUnreserveFlow(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
// Successful reservation
|
||||
if !s.TryReserve() {
|
||||
t.Fatal("failed initial reservation")
|
||||
}
|
||||
|
||||
// Second reservation should fail
|
||||
if s.TryReserve() {
|
||||
t.Error("unexpected successful second reservation")
|
||||
}
|
||||
|
||||
// Unreserve and try again
|
||||
s.UnReserve()
|
||||
if !s.TryReserve() {
|
||||
t.Error("failed reservation after unreserve")
|
||||
}
|
||||
}
|
||||
|
||||
func testFreeSlots(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
// Set to arbitrary value
|
||||
atomic.StoreInt32((*int32)(&s), 5)
|
||||
s.FreeSlots()
|
||||
if atomic.LoadInt32((*int32)(&s)) != 0 {
|
||||
t.Error("FreeSlots failed to reset counter")
|
||||
}
|
||||
}
|
||||
|
||||
func testHasSlot(t *testing.T) {
|
||||
t.Parallel()
|
||||
var s slotted
|
||||
|
||||
if !s.HasSlot() {
|
||||
t.Error("should have slot when zero")
|
||||
}
|
||||
|
||||
s.TryReserve()
|
||||
if s.HasSlot() {
|
||||
t.Error("shouldn't have slot when reserved")
|
||||
}
|
||||
}
|
||||
68
pkg/coordinator/workerapi.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package coordinator
|
||||
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
|
||||
func (w *Worker) WebrtcInit(id string) (*api.WebrtcInitResponse, error) {
|
||||
return api.UnwrapChecked[api.WebrtcInitResponse](
|
||||
w.Send(api.WebrtcInit, api.WebrtcInitRequest{Id: id}))
|
||||
}
|
||||
|
||||
func (w *Worker) WebrtcAnswer(id string, sdp string) {
|
||||
w.Notify(api.WebrtcAnswer,
|
||||
api.WebrtcAnswerRequest{Stateful: api.Stateful{Id: id}, Sdp: sdp})
|
||||
}
|
||||
|
||||
func (w *Worker) WebrtcIceCandidate(id string, candidate string) {
|
||||
w.Notify(api.WebrtcIce,
|
||||
api.WebrtcIceCandidateRequest{Stateful: api.Stateful{Id: id}, Candidate: candidate})
|
||||
}
|
||||
|
||||
func (w *Worker) StartGame(id string, req api.GameStartUserRequest) (*api.StartGameResponse, error) {
|
||||
return api.UnwrapChecked[api.StartGameResponse](
|
||||
w.Send(api.StartGame, api.StartGameRequest{
|
||||
StatefulRoom: api.StatefulRoom{Id: id, Rid: req.RoomId},
|
||||
Game: req.GameName,
|
||||
PlayerIndex: req.PlayerIndex,
|
||||
Record: req.Record,
|
||||
RecordUser: req.RecordUser,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) QuitGame(id string) {
|
||||
w.Notify(api.QuitGame, api.GameQuitRequest{Id: id, Rid: w.RoomId})
|
||||
}
|
||||
|
||||
func (w *Worker) SaveGame(id string) (*api.SaveGameResponse, error) {
|
||||
return api.UnwrapChecked[api.SaveGameResponse](
|
||||
w.Send(api.SaveGame, api.SaveGameRequest{Id: id, Rid: w.RoomId}))
|
||||
}
|
||||
|
||||
func (w *Worker) LoadGame(id string) (*api.LoadGameResponse, error) {
|
||||
return api.UnwrapChecked[api.LoadGameResponse](
|
||||
w.Send(api.LoadGame, api.LoadGameRequest{Id: id, Rid: w.RoomId}))
|
||||
}
|
||||
|
||||
func (w *Worker) ChangePlayer(id string, index int) (*api.ChangePlayerResponse, error) {
|
||||
return api.UnwrapChecked[api.ChangePlayerResponse](
|
||||
w.Send(api.ChangePlayer, api.ChangePlayerRequest{
|
||||
StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
|
||||
Index: index,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) ResetGame(id string) {
|
||||
w.Notify(api.ResetGame, api.ResetGameRequest{Id: id, Rid: w.RoomId})
|
||||
}
|
||||
|
||||
func (w *Worker) RecordGame(id string, rec bool, recUser string) (*api.RecordGameResponse, error) {
|
||||
return api.UnwrapChecked[api.RecordGameResponse](
|
||||
w.Send(api.RecordGame, api.RecordGameRequest{
|
||||
StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
|
||||
Active: rec,
|
||||
User: recUser,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) TerminateSession(id string) {
|
||||
_, _ = w.Send(api.TerminateSession, api.TerminateSessionRequest{Id: id})
|
||||
}
|
||||
39
pkg/coordinator/workerhandlers.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package coordinator
|
||||
|
||||
import "github.com/giongto35/cloud-game/v3/pkg/api"
|
||||
|
||||
func (w *Worker) HandleRegisterRoom(rq api.RegisterRoomRequest) { w.RoomId = string(rq) }
|
||||
|
||||
func (w *Worker) HandleCloseRoom(rq api.CloseRoomRequest) {
|
||||
if string(rq) == w.RoomId {
|
||||
w.RoomId = ""
|
||||
w.FreeSlots()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) HandleIceCandidate(rq api.WebrtcIceCandidateRequest, users HasUserRegistry) error {
|
||||
if usr := users.Find(rq.Id); usr != nil {
|
||||
usr.SendWebrtcIceCandidate(rq.Candidate)
|
||||
} else {
|
||||
w.log.Warn().Str("id", rq.Id).Msg("unknown session")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worker) HandleLibGameList(inf api.LibGameListInfo) error {
|
||||
w.SetLib(inf.List)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Worker) HandlePrevSessionList(sess api.PrevSessionInfo) error {
|
||||
if len(sess.List) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range sess.List {
|
||||
m[v] = struct{}{}
|
||||
}
|
||||
w.SetSessions(m)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package api
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// This list of postfixes is used in the API:
|
||||
// - *Request postfix denotes clients calls (i.e. from a browser to the HTTP-server).
|
||||
// - *Call postfix denotes IPC calls (from the coordinator to a worker).
|
||||
|
||||
func from(source interface{}, data string) error {
|
||||
err := json.Unmarshal([]byte(data), source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func to(target interface{}) (string, error) {
|
||||
b, err := json.Marshal(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package api
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
|
||||
const (
|
||||
GetRoom = "get_room"
|
||||
CloseRoom = "close_room"
|
||||
RegisterRoom = "register_room"
|
||||
Heartbeat = "heartbeat"
|
||||
IceCandidate = "ice_candidate"
|
||||
|
||||
NoData = ""
|
||||
|
||||
InitWebrtc = "init_webrtc"
|
||||
Answer = "answer"
|
||||
|
||||
GameStart = "start"
|
||||
GameQuit = "quit"
|
||||
GameSave = "save"
|
||||
GameLoad = "load"
|
||||
GamePlayerSelect = "player_index"
|
||||
GameMultitap = "multitap"
|
||||
)
|
||||
|
||||
type GameStartRequest struct {
|
||||
GameName string `json:"game_name"`
|
||||
}
|
||||
|
||||
func (packet *GameStartRequest) From(data string) error { return from(packet, data) }
|
||||
|
||||
type GameStartCall struct {
|
||||
Name string `json:"name"`
|
||||
Base string `json:"base"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (packet *GameStartCall) From(data string) error { return from(packet, data) }
|
||||
func (packet *GameStartCall) To() (string, error) { return to(packet) }
|
||||
|
||||
type ConnectionRequest struct {
|
||||
Zone string `json:"zone,omitempty"`
|
||||
PingAddr string `json:"ping_addr,omitempty"`
|
||||
IsHTTPS bool `json:"is_https,omitempty"`
|
||||
}
|
||||
|
||||
// packets
|
||||
|
||||
func RegisterRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: RegisterRoom, Data: data} }
|
||||
func GetRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: GetRoom, Data: data} }
|
||||
func CloseRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: CloseRoom, Data: data} }
|
||||
func IceCandidatePacket(data string, sessionId string) cws.WSPacket {
|
||||
return cws.WSPacket{ID: IceCandidate, Data: data, SessionID: sessionId}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package api
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
|
||||
const (
|
||||
ServerId = "server_id"
|
||||
TerminateSession = "terminateSession"
|
||||
)
|
||||
|
||||
type ConfPushCall struct {
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
func (packet *ConfPushCall) From(data string) error { return from(packet, data) }
|
||||
func (packet *ConfPushCall) To() (string, error) { return to(packet) }
|
||||
|
||||
func ServerIdPacket(id string) cws.WSPacket { return cws.WSPacket{ID: ServerId, Data: id} }
|
||||
func ConfigRequestPacket(conf []byte) cws.WSPacket { return cws.WSPacket{Data: string(conf)} }
|
||||
func TerminateSessionPacket(sessionId string) cws.WSPacket {
|
||||
return cws.WSPacket{ID: TerminateSession, SessionID: sessionId}
|
||||
}
|
||||
221
pkg/cws/cws.go
|
|
@ -1,221 +0,0 @@
|
|||
package cws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
Client struct {
|
||||
id string
|
||||
|
||||
conn *websocket.Conn
|
||||
|
||||
sendLock sync.Mutex
|
||||
// sendCallback is callback based on packetID
|
||||
sendCallback map[string]func(req WSPacket)
|
||||
sendCallbackLock sync.Mutex
|
||||
// recvCallback is callback when receive based on ID of the packet
|
||||
recvCallback map[string]func(req WSPacket)
|
||||
|
||||
Done chan struct{}
|
||||
}
|
||||
|
||||
WSPacket struct {
|
||||
ID string `json:"id"`
|
||||
// TODO: Make Data generic: map[string]interface{} for more usecases
|
||||
Data string `json:"data"`
|
||||
|
||||
RoomID string `json:"room_id"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
|
||||
PacketID string `json:"packet_id"`
|
||||
// Globally ID of a browser session
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
PacketHandler func(resp WSPacket) (req WSPacket)
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyPacket = WSPacket{}
|
||||
HeartbeatPacket = WSPacket{ID: "heartbeat"}
|
||||
)
|
||||
|
||||
const WSWait = 20 * time.Second
|
||||
|
||||
func NewClient(conn *websocket.Conn) *Client {
|
||||
id := uuid.Must(uuid.NewV4()).String()
|
||||
sendCallback := map[string]func(WSPacket){}
|
||||
recvCallback := map[string]func(WSPacket){}
|
||||
|
||||
return &Client{
|
||||
id: id,
|
||||
conn: conn,
|
||||
|
||||
sendCallback: sendCallback,
|
||||
recvCallback: recvCallback,
|
||||
|
||||
Done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends a packet and trigger callback when the packet comes back
|
||||
func (c *Client) Send(request WSPacket, callback func(response WSPacket)) {
|
||||
request.PacketID = uuid.Must(uuid.NewV4()).String()
|
||||
data, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Consider using lock free
|
||||
// Wrap callback with sessionID and packetID
|
||||
if callback != nil {
|
||||
wrapperCallback := func(resp WSPacket) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("Recovered from err in client callback ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
resp.PacketID = request.PacketID
|
||||
resp.SessionID = request.SessionID
|
||||
callback(resp)
|
||||
}
|
||||
c.sendCallbackLock.Lock()
|
||||
c.sendCallback[request.PacketID] = wrapperCallback
|
||||
c.sendCallbackLock.Unlock()
|
||||
}
|
||||
|
||||
c.sendLock.Lock()
|
||||
c.conn.SetWriteDeadline(time.Now().Add(WSWait))
|
||||
c.conn.WriteMessage(websocket.TextMessage, data)
|
||||
c.sendLock.Unlock()
|
||||
}
|
||||
|
||||
// Receive receive and response back
|
||||
func (c *Client) Receive(id string, f PacketHandler) {
|
||||
c.recvCallback[id] = func(response WSPacket) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("Recovered from err ", err)
|
||||
log.Println(debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
req := f(response)
|
||||
// Add Meta data
|
||||
req.PacketID = response.PacketID
|
||||
req.SessionID = response.SessionID
|
||||
|
||||
// Skip response if it is EmptyPacket
|
||||
if response == EmptyPacket {
|
||||
return
|
||||
}
|
||||
resp, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Println("[!] json marshal error:", err)
|
||||
}
|
||||
c.sendLock.Lock()
|
||||
c.conn.SetWriteDeadline(time.Now().Add(WSWait))
|
||||
c.conn.WriteMessage(websocket.TextMessage, resp)
|
||||
c.sendLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SyncSend sends a packet and wait for callback till the packet comes back
|
||||
func (c *Client) SyncSend(request WSPacket) (response WSPacket) {
|
||||
res := make(chan WSPacket)
|
||||
f := func(resp WSPacket) {
|
||||
res <- resp
|
||||
}
|
||||
c.Send(request, f)
|
||||
return <-res
|
||||
}
|
||||
|
||||
// SendAwait sends some packet while waiting for a tile-limited response
|
||||
//func (c *Client) SendAwait(packet WSPacket) WSPacket {
|
||||
// ch := make(chan WSPacket)
|
||||
// defer close(ch)
|
||||
// c.Send(packet, func(response WSPacket) { ch <- response })
|
||||
//
|
||||
// for {
|
||||
// select {
|
||||
// case packet := <-ch:
|
||||
// return packet
|
||||
// case <-time.After(config.WsIpcTimeout):
|
||||
// log.Printf("Packet receive timeout!")
|
||||
// return EmptyPacket
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// Heartbeat maintains connection to coordinator.
|
||||
// Blocking.
|
||||
func (c *Client) Heartbeat() {
|
||||
// send heartbeat every 1s
|
||||
t := time.NewTicker(time.Second)
|
||||
// don't wait 1 second
|
||||
c.Send(HeartbeatPacket, nil)
|
||||
for {
|
||||
select {
|
||||
case <-c.Done:
|
||||
t.Stop()
|
||||
log.Printf("Close heartbeat")
|
||||
return
|
||||
case <-t.C:
|
||||
c.Send(HeartbeatPacket, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Listen() {
|
||||
for {
|
||||
c.conn.SetReadDeadline(time.Now().Add(WSWait))
|
||||
_, rawMsg, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("[!] read:", err)
|
||||
// TODO: Check explicit disconnect error to break
|
||||
close(c.Done)
|
||||
break
|
||||
}
|
||||
wspacket := WSPacket{}
|
||||
err = json.Unmarshal(rawMsg, &wspacket)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Warn: error decoding", rawMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if some async send is waiting for the response based on packetID
|
||||
// TODO: Change to read lock.
|
||||
//c.sendCallbackLock.Lock()
|
||||
callback, ok := c.sendCallback[wspacket.PacketID]
|
||||
//c.sendCallbackLock.Unlock()
|
||||
if ok {
|
||||
go callback(wspacket)
|
||||
//c.sendCallbackLock.Lock()
|
||||
delete(c.sendCallback, wspacket.PacketID)
|
||||
//c.sendCallbackLock.Unlock()
|
||||
// Skip receiveCallback to avoid duplication
|
||||
continue
|
||||
}
|
||||
// Check if some receiver with the ID is registered
|
||||
if callback, ok := c.recvCallback[wspacket.ID]; ok {
|
||||
go callback(wspacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
if c == nil || c.conn == nil {
|
||||
return
|
||||
}
|
||||
c.conn.Close()
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package backend
|
||||
|
||||
type Download struct {
|
||||
Key string
|
||||
Address string
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Request(dest string, urls ...Download) ([]string, []string)
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/cavaliercoder/grab"
|
||||
)
|
||||
|
||||
type GrabDownloader struct {
|
||||
client *grab.Client
|
||||
concurrency int
|
||||
}
|
||||
|
||||
func NewGrabDownloader() GrabDownloader {
|
||||
client := grab.Client{
|
||||
UserAgent: "Cloud-Game/2.2",
|
||||
HTTPClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
return GrabDownloader{
|
||||
client: &client,
|
||||
concurrency: 5,
|
||||
}
|
||||
}
|
||||
|
||||
func (d GrabDownloader) Request(dest string, urls ...Download) (ok []string, nook []string) {
|
||||
reqs := make([]*grab.Request, 0)
|
||||
for _, url := range urls {
|
||||
req, err := grab.NewRequest(dest, url.Address)
|
||||
if err != nil {
|
||||
log.Printf("error: couldn't make request URL: %v, %v", url, err)
|
||||
} else {
|
||||
req.Label = url.Key
|
||||
reqs = append(reqs, req)
|
||||
}
|
||||
}
|
||||
|
||||
// check each response
|
||||
for resp := range d.client.DoBatch(d.concurrency, reqs...) {
|
||||
r := resp.Request
|
||||
if err := resp.Err(); err != nil {
|
||||
log.Printf("error: download [%s] %s failed: %v\n", r.Label, r.URL(), err)
|
||||
if resp.HTTPResponse.StatusCode == 404 {
|
||||
nook = append(nook, resp.Request.Label)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Downloaded [%v] [%s] -> %s\n", resp.HTTPResponse.Status, r.Label, resp.Filename)
|
||||
ok = append(ok, resp.Filename)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package downloader
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v2/pkg/downloader/backend"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/downloader/pipe"
|
||||
)
|
||||
|
||||
type Downloader struct {
|
||||
backend backend.Client
|
||||
// pipe contains a sequential list of
|
||||
// operations applied to some files and
|
||||
// each operation will return a list of
|
||||
// successfully processed files
|
||||
pipe []Process
|
||||
}
|
||||
|
||||
|
||||
type Process func(string, []string) []string
|
||||
|
||||
func NewDefaultDownloader() Downloader {
|
||||
return Downloader{
|
||||
backend: backend.NewGrabDownloader(),
|
||||
pipe: []Process{
|
||||
pipe.Unpack,
|
||||
pipe.Delete,
|
||||
}}
|
||||
}
|
||||
|
||||
// Download tries to download specified with URLs list of files and
|
||||
// put them into the destination folder.
|
||||
// It will return a partial or full list of downloaded files,
|
||||
// a list of processed files if some pipe processing functions are set.
|
||||
func (d *Downloader) Download(dest string, urls ...backend.Download) ([]string, []string) {
|
||||
files, fails := d.backend.Request(dest, urls...)
|
||||
for _, op := range d.pipe {
|
||||
files = op(dest, files)
|
||||
}
|
||||
return files, fails
|
||||
}
|
||||