mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-04-18 17:30:25 +08:00
Compare commits
1497 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
d59aa5392c | ||
|
fe7992c698 | ||
|
c614b59445 | ||
|
079f046d03 | ||
|
6c039af16f | ||
|
cb767de61f | ||
|
1bd34c70e7 | ||
|
4d44129424 | ||
|
660c42e4d7 | ||
|
23e5eded55 | ||
|
39ac65a425 | ||
|
bace05284b | ||
|
dee299bcf8 | ||
|
e17fb9e2e5 | ||
|
5a002fc504 | ||
|
52888d25ff | ||
|
4d78bb756a | ||
|
588beda158 | ||
|
572a91c247 | ||
|
bb0632fc1a | ||
|
32968947ae | ||
|
fcd1808b73 | ||
|
245669f8b8 | ||
|
2055a1dd85 | ||
|
d0a65fe1a1 | ||
|
ff17c03894 | ||
|
ac3c30939a | ||
|
703a951ffb | ||
|
09a8885d68 | ||
|
d00d97cf97 | ||
|
e875650842 | ||
|
903c1ae415 | ||
|
05accb3a91 | ||
|
7d86d1129c | ||
|
77fd7876cc | ||
|
3175897a6f | ||
|
2e7bf36de8 | ||
|
2ff0f7e29c | ||
|
6962066605 | ||
|
e57cd413ce | ||
|
176c8ad02e | ||
|
a22621a436 | ||
|
186a8fe134 | ||
|
3579fe529f | ||
|
311455c532 | ||
|
38744a40ef | ||
|
921c01dd2c | ||
|
0dd75ac7a7 | ||
|
96ecc0ce77 | ||
|
1e3b99d5c6 | ||
|
e61f5b3a6e | ||
|
d801a46e1c | ||
|
3fea34745a | ||
|
f757a27738 | ||
|
83dbf56ace | ||
|
ed9af31a97 | ||
|
ef818a876d | ||
|
8982aebc83 | ||
|
91aa370a2d | ||
|
165b337584 | ||
|
aad44fa7cb | ||
|
73ad4a79a5 | ||
|
c96601b8a8 | ||
|
d6fab115c9 | ||
|
2e4b1a979c | ||
|
42e6019427 | ||
|
77316b25ce | ||
|
ec69be678f | ||
|
8d68116d97 | ||
|
483ee5e6b4 | ||
|
f99cab3f0a | ||
|
c1285cdb10 | ||
|
02d986d25e | ||
|
1fcef8866d | ||
|
e4abb18302 | ||
|
57cc3ddd4f | ||
|
8fc6cec023 | ||
|
b5aae32f08 | ||
|
27c0a9b75c | ||
|
e0cdf74da3 | ||
|
0efa85a162 | ||
|
e2ebd62100 | ||
|
ec36b86175 | ||
|
b7b6835ff1 | ||
|
ad45502f56 | ||
|
42a8e8dbfe | ||
|
58e06b5221 | ||
|
f0a44a4686 | ||
|
0b19bd7fc1 | ||
|
c3929d61b8 | ||
|
fb0693903e | ||
|
3f28f79368 | ||
|
76fa65ce25 | ||
|
61450c41cb | ||
|
954f5e3241 | ||
|
d49e800508 | ||
|
37c2ee2bcf | ||
|
2f974e8728 | ||
|
1748fcd751 | ||
|
386388822d | ||
|
0ba3e32abc | ||
|
d6cf7323de | ||
|
f0060c3c69 | ||
|
51f2d02eaf | ||
|
cbe03d0074 | ||
|
60d3a4a17c | ||
|
ec5beae2a9 | ||
|
2e79b2dcfe | ||
|
bd3e2bb711 | ||
|
47c86b2bc5 | ||
|
c66c880eb3 | ||
|
f9e6702557 | ||
|
19f2284c2d | ||
|
89ad69b21f | ||
|
56eb8c1640 | ||
|
763943a007 | ||
|
a060540995 | ||
|
8be2efca4c | ||
|
79f69eba4b | ||
|
457e7d499f | ||
|
3b8017e24c | ||
|
9938a8590e | ||
|
52c8ac4ff9 | ||
|
d562cb0448 | ||
|
04031cf2db | ||
|
d2be793eea | ||
|
853793a964 | ||
|
3146352d85 | ||
|
0fbc373c94 | ||
|
3bbd01a0ae | ||
|
b9aff22c5a | ||
|
ba42e1f6b3 | ||
|
7e174a3f71 | ||
|
69459c82d5 | ||
|
96f145acdc | ||
|
0b61eb408e | ||
|
3a22884898 | ||
|
412f5132fe | ||
|
fca4690f21 | ||
|
5c0d98ee74 | ||
|
3af2c1437e | ||
|
d78deac2a4 | ||
|
8a027fb2f1 | ||
|
b3c2a4d19c | ||
|
c6222a5f85 | ||
|
92e63ee44a | ||
|
55a39c2b7b | ||
|
2c24401b4a | ||
|
681da12893 | ||
|
2dd1dc384c | ||
|
086a831cd2 | ||
|
3195753e0e | ||
|
cfe70b6be4 | ||
|
4f61f3e5d0 | ||
|
e521258b9d | ||
|
5fd3564b47 | ||
|
46ff718a8f | ||
|
d63914eb0b | ||
|
1bb3ce33f1 | ||
|
ed4f68f670 | ||
|
629e5fc2e6 | ||
|
3336b49695 | ||
|
6a6bdeb220 | ||
|
8941f34624 | ||
|
e17cddf9da | ||
|
5d6e13f478 | ||
|
c0f39d6950 | ||
|
8522fc261e | ||
|
c0e0e04105 | ||
|
ec79de28df | ||
|
9ade8e9462 | ||
|
a4f871b8fd | ||
|
586bc4a9ef | ||
|
137808048d | ||
|
0bd617d5ed | ||
|
39a662eb52 | ||
|
d7a5da9261 | ||
|
1572a6104f | ||
|
cfd962c159 | ||
|
46a2192f4a | ||
|
a7ec8a12b1 | ||
|
15ac453054 | ||
|
6b7d42e61f | ||
|
8e1c54abef | ||
|
17d8ee3b79 | ||
|
aa33f228a0 | ||
|
c74811898b | ||
|
38322c38c8 | ||
|
6967bb736c | ||
|
d71592b766 | ||
|
470d569fec | ||
|
547d1d6c40 | ||
|
8799afb02c | ||
|
76e0bc023c | ||
|
1dcafd9128 | ||
|
a141de7cc7 | ||
|
16d88f768b | ||
|
7c824a419d | ||
|
c05038c1f1 | ||
|
e2a7e20a49 | ||
|
1dd417f05c | ||
|
ab7bb109e0 | ||
|
d6f0691828 | ||
|
3822a7bfb4 | ||
|
78dcc1de4f | ||
|
4d9959f666 | ||
|
b3caf3aa05 | ||
|
8b21e5feb7 | ||
|
019ccf04d1 | ||
|
781bd32c48 | ||
|
ea49d3f36d | ||
|
6a4145ae72 | ||
|
264b8facaf | ||
|
c3e6fb7f0e | ||
|
abb20ad0f1 | ||
|
92438f876a | ||
|
95973a6c77 | ||
|
d260e737d8 | ||
|
0a97ad9d70 | ||
|
dd6cc84461 | ||
|
3934e26e21 | ||
|
a6f6f34e37 | ||
|
0e7da93735 | ||
|
d95b952c77 | ||
|
bb825ec931 | ||
|
f0ce2f3bd0 | ||
|
573fb80f65 | ||
|
d277497861 | ||
|
3fa9c1e784 | ||
|
358c7f4402 | ||
|
d3aaeb39f1 | ||
|
ef91fdb2a7 | ||
|
336dd6b19f | ||
|
b3d6c04b23 | ||
|
c11910a14e | ||
|
adf7c50981 | ||
|
90bedcd985 | ||
|
50c85cc8e3 | ||
|
903417f7b9 | ||
|
aa28a14ed5 | ||
|
bf2b3ed439 | ||
|
b14d594fc2 | ||
|
6c97db269f | ||
|
fcc082b71d | ||
|
3bb4eff36b | ||
|
cb36d76685 | ||
|
4348d72e1c | ||
|
43bb852bb6 | ||
|
984dee266a | ||
|
da7948b6ad | ||
|
ce94503b96 | ||
|
fdaa87db68 | ||
|
6e707e4b6c | ||
|
6f49a48655 | ||
|
e705fc5f4e | ||
|
67ca5e00bb | ||
|
aea1bfcc60 | ||
|
18af4739fc | ||
|
8fa678ca3f | ||
|
0a05f33936 | ||
|
8fe62b25b4 | ||
|
1d0867d6cb | ||
|
ca6c1ff18f | ||
|
5f400873ff | ||
|
6e06d8145d | ||
|
089f57eff3 | ||
|
d851260407 | ||
|
42918e64dd | ||
|
779388daf8 | ||
|
66e7ded849 | ||
|
c0fa65fc5a | ||
|
d9661483c3 | ||
|
ccd50605ea | ||
|
43c161c8c7 | ||
|
c63ecbb6c1 | ||
|
59f409551f | ||
|
65e3cc8a5f | ||
|
05638596aa | ||
|
d1bc1204ad | ||
|
ec3204488b | ||
|
c07af372cc | ||
|
1280e3ff2f | ||
|
6fef06821a | ||
|
5f25f5d59c | ||
|
06482a7ee1 | ||
|
31d7637d67 | ||
|
ec2963e29d | ||
|
1b02257be8 | ||
|
f35ff84158 | ||
|
0c8da65d1d | ||
|
d3673c7377 | ||
|
23e083bb06 | ||
|
0b2f7af67d | ||
|
1f24cded67 | ||
|
a8d6947d81 | ||
|
94874a5a26 | ||
|
2152380d20 | ||
|
3cffd4c7b3 | ||
|
23109d98b5 | ||
|
fbe8e25f82 | ||
|
fb5a5a5298 | ||
|
860a5d1e42 | ||
|
6bc247a98e | ||
|
705b4988b0 | ||
|
6c4f49d85e | ||
|
514e921ea4 | ||
|
f539708d8e | ||
|
83f9b4108c | ||
|
5bd093ec94 | ||
|
9170dbc557 | ||
|
cbf5c66d1e | ||
|
b4e20469ac | ||
|
f54eb4fdb1 | ||
|
b267922870 | ||
|
16fe45bd0c | ||
|
6f58cef401 | ||
|
8dabd2405f | ||
|
98a7097fda | ||
|
8b79853c47 | ||
|
51b65b1df6 | ||
|
c0db6e28e1 | ||
|
5a54c19806 | ||
|
9c41800f53 | ||
|
bbb534f703 | ||
|
27e63eb203 | ||
|
f6ab4ed435 | ||
|
d9a62e13d0 | ||
|
4d74b06b5a | ||
|
c33a40837b | ||
|
43cfca2185 | ||
|
317af39265 | ||
|
f85ae5eddf | ||
|
bcda59c280 | ||
|
43b39b7544 | ||
|
c2f806e70d | ||
|
aef539633d | ||
|
8958637e08 | ||
|
c89a326adf | ||
|
45cb00e87b | ||
|
e401623e01 | ||
|
ea1971b8e2 | ||
|
b638ee42e4 | ||
|
5943039c42 | ||
|
a89df0266a | ||
|
65a2664cf7 | ||
|
cebd5d281e | ||
|
d326814e0d | ||
|
04c4c97857 | ||
|
874ac73a2a | ||
|
772d9537ee | ||
|
aed4ac1550 | ||
|
454fdd43bf | ||
|
cd61d08dc0 | ||
|
955be538ad | ||
|
5032ce7872 | ||
|
2e68959f6c | ||
|
107dede6b8 | ||
|
9c2c285f0f | ||
|
d6eac1039c | ||
|
7a5c366811 | ||
|
07cde2f488 | ||
|
4dbbba2372 | ||
|
3151846809 | ||
|
1f4e534787 | ||
|
7c54c59fc9 | ||
|
ffecb48b16 | ||
|
f4181c9095 | ||
|
3531c17ab3 | ||
|
5bfd42185b | ||
|
729709342b | ||
|
9149c6ecf1 | ||
|
169397db19 | ||
|
011061aeb7 | ||
|
8bbae5a9c2 | ||
|
52ecb939cb | ||
|
4c1baf9e25 | ||
|
3e03f7d336 | ||
|
02d1fdbf31 | ||
|
81b8290322 | ||
|
269113538d | ||
|
8ed4529e81 | ||
|
a21d92b09f | ||
|
901ec82a24 | ||
|
167c319a51 | ||
|
5af8827db6 | ||
|
7f218daf54 | ||
|
7aa44df4cb | ||
|
3671a7df11 | ||
|
3485afa983 | ||
|
08d8553fa4 | ||
|
90c54d96b2 | ||
|
707cfd87d2 | ||
|
91ba633341 | ||
|
3ff0df8ae8 | ||
|
33f4ceee4c | ||
|
10deb88e38 | ||
|
a22ad88ddb | ||
|
64324f268b | ||
|
5f7965db64 | ||
|
2b355ba5a2 | ||
|
be6a904ea1 | ||
|
d7689201a0 | ||
|
3c3f5060e7 | ||
|
3fad403b22 | ||
|
83e9829c6d | ||
|
7f761c6183 | ||
|
2faefa83c4 | ||
|
3a725bfb74 | ||
|
8441d9591c | ||
|
ef63996eac | ||
|
ad07fe69b7 | ||
|
e0a4b9a9f4 | ||
|
069f6d078d | ||
|
3cc2bc718c | ||
|
67d7429bf0 | ||
|
44156a4829 | ||
|
377ca75aef | ||
|
dc89a6c9b9 | ||
|
c95f12f518 | ||
|
885429dead | ||
|
b094f1de74 | ||
|
8222bbe7c9 | ||
|
eba1fb5d0c | ||
|
ab4e60485d | ||
|
e3e190ec3f | ||
|
b8477f851d | ||
|
4d5e820e92 | ||
|
10942ab81a | ||
|
98778fbe60 | ||
|
af84b7d8d8 | ||
|
6dc5fd7689 | ||
|
a2c0605148 | ||
|
63f3eb7be7 | ||
|
5aa182675e | ||
|
969030b6e7 | ||
|
b7995a77e4 | ||
|
ab5ae5f5ac | ||
|
c4f7816ec3 | ||
|
c85ca51929 | ||
|
da549c53be | ||
|
bb712730d5 | ||
|
bd43643729 | ||
|
5b4560e1a8 | ||
|
f1cda3f923 | ||
|
5b23d54067 | ||
|
a095e702af | ||
|
64b08ed209 | ||
|
bd690cf374 | ||
|
6b17927cf6 | ||
|
fc2022937b | ||
|
381c58eaf4 | ||
|
7df503bd1b | ||
|
697624c998 | ||
|
baee1f1c88 | ||
|
efe0a101ef | ||
|
ca7ecba75f | ||
|
e28fede6fa | ||
|
d0d9e85d77 | ||
|
e335750240 | ||
|
a86ce00392 | ||
|
7820568147 | ||
|
790b3f5e0d | ||
|
2380fc2400 | ||
|
e3a8aa02ac | ||
|
146c6a15d8 | ||
|
227ffc3493 | ||
|
1349d31042 | ||
|
ef63ae4528 | ||
|
fb030941fd | ||
|
176b0eba1d | ||
|
65b8c11a7e | ||
|
634ab0b964 | ||
|
529b08d77a | ||
|
59f3aeaea6 | ||
|
9b75954273 | ||
|
4fbadbe259 | ||
|
f9a4d2e158 | ||
|
44f419c11c | ||
|
dfce94d315 | ||
|
ea1be3fb47 | ||
|
f576027de7 | ||
|
61a00a1fd9 | ||
|
4a255c5d39 | ||
|
111539d1e6 | ||
|
8cdde1a3ad | ||
|
d8c2405bcc | ||
|
6e2f6cfb17 | ||
|
37737bd456 | ||
|
fe2e8477ba | ||
|
25c08536eb | ||
|
feb784d1ab | ||
|
22b4065873 | ||
|
9cdf471348 | ||
|
e9d251ad47 | ||
|
ae16e07e2b | ||
|
86dfbc6bde | ||
|
a9793ce875 | ||
|
80a64250a1 | ||
|
8b2c14e017 | ||
|
8c1914e6bb | ||
|
ee19316672 | ||
|
758c75835c | ||
|
4b37194ee6 | ||
|
76e2253529 | ||
|
bf0cd95686 | ||
|
cd8d3777d2 | ||
|
25044f8e22 | ||
|
0042db539f | ||
|
7c92b882b6 | ||
|
f3380180df | ||
|
6608e25f09 | ||
|
15fa5397a1 | ||
|
8021f0a9e8 | ||
|
6e04974e6f | ||
|
d9bccda215 | ||
|
445e2d2273 | ||
|
edc22a6ae3 | ||
|
5967321ef6 | ||
|
48628c5c0b | ||
|
1c976f944f | ||
|
8f55e9341b | ||
|
321cb77477 | ||
|
86b3c9fddc | ||
|
d219ca3ee0 | ||
|
7f29aeb5f9 | ||
|
21bb4d390d | ||
|
f1566913c2 | ||
|
18e7b13a37 | ||
|
1d34bf7789 | ||
|
be0d6152fc | ||
|
e925dccf61 | ||
|
feba4a1315 | ||
|
8ada692db6 | ||
|
65681abe3b | ||
|
39c9010103 | ||
|
a14a72e2e5 | ||
|
375dc888eb | ||
|
b7d6777648 | ||
|
e62df7c655 | ||
|
8cddacdaec | ||
|
1bcbb075e2 | ||
|
8f69298407 | ||
|
82e77acba8 | ||
|
7843ac7b90 | ||
|
c5c59677c5 | ||
|
61736e50cd | ||
|
b717ac5546 | ||
|
1a51f39a59 | ||
|
93879d9729 | ||
|
5557f69dec | ||
|
b1bc822d2e | ||
|
c01aead34c | ||
|
0022f54c6f | ||
|
806cf04034 | ||
|
1df59f41a2 | ||
|
0f0c4f4d03 | ||
|
3b9ff531e2 | ||
|
28315bb916 | ||
|
b1d1cd965c | ||
|
c5013b1d33 | ||
|
166bc0006b | ||
|
4aadd9efa4 | ||
|
d31dbd1435 | ||
|
d034dad522 | ||
|
cacc01e644 | ||
|
3cb4480d64 | ||
|
627bcd8153 | ||
|
cc5b606807 | ||
|
6fa194ab9e | ||
|
aa2b0147be | ||
|
5c5f57aa60 | ||
|
7d9c874f2b | ||
|
82bcb48ee8 | ||
|
677c9493ca | ||
|
f4c6c3fb79 | ||
|
d64d6009d4 | ||
|
620c9e4eba | ||
|
ba0120a2c1 | ||
|
792b9c77d9 | ||
|
389679781d | ||
|
56887deaa4 | ||
|
de82842bab | ||
|
f792dce8e8 | ||
|
7c168de25a | ||
|
8cb5b099f5 | ||
|
07e3022512 | ||
|
e0fad03549 | ||
|
f8b23e6915 | ||
|
cfefd9bafd | ||
|
494994b9e2 | ||
|
5f19e3a166 | ||
|
ce68bdfe86 | ||
|
5135eea8e0 | ||
|
413b9d41a4 | ||
|
69f1dc28a3 | ||
|
daf2bbd6b4 | ||
|
68da0409fd | ||
|
9bc7e789c4 | ||
|
925a98c78f | ||
|
2e38633f03 | ||
|
573aafc1a9 | ||
|
3fb375e1fd | ||
|
5bab302efd | ||
|
2b909da87f | ||
|
8c95e6e95b | ||
|
0b9a6bf1dc | ||
|
198ab5f6c0 | ||
|
2a836e9441 | ||
|
d3a65d43e8 | ||
|
3bf99ce05b | ||
|
1f007a7aac | ||
|
757a8ac3ba | ||
|
25cd33858f | ||
|
d4fc0aae21 | ||
|
2da1b63c95 | ||
|
d6af1d5a9e | ||
|
047e87d36d | ||
|
d7ebeddc85 | ||
|
175cfae87b | ||
|
6b1eb6c084 | ||
|
80884524c6 | ||
|
e2612067f6 | ||
|
d85b48f9b2 | ||
|
12be1ec81a | ||
|
02a638bd50 | ||
|
8adfacce4f | ||
|
4af1effd3b | ||
|
5c05f7225d | ||
|
21512c2715 | ||
|
10bdb8a40f | ||
|
cd17503496 | ||
|
f5a6f8a95c | ||
|
ce238baa53 | ||
|
a3d8ec539e | ||
|
ae206e4438 | ||
|
4e4631442d | ||
|
3e66a8282a | ||
|
49589bee11 | ||
|
e6538cf930 | ||
|
e38433bf18 | ||
|
867c43180c | ||
|
20239f0813 | ||
|
203ea1046c | ||
|
88444bc820 | ||
|
2ec80af08e | ||
|
d68647fe40 | ||
|
9e5976024b | ||
|
7709a989be | ||
|
8af7777c8c | ||
|
e625b2f369 | ||
|
62fd7ccd7c | ||
|
5a249eca52 | ||
|
d441b8edca | ||
|
34cbdab6b1 | ||
|
e55f93e8dd | ||
|
ae4d2d0516 | ||
|
994ea2f58a | ||
|
147aab6a26 | ||
|
b9f9166510 | ||
|
2dcacc6b6a | ||
|
be75b8de2d | ||
|
b5aa91d75c | ||
|
4d99679e56 | ||
|
e182397c95 | ||
|
42c2199914 | ||
|
bfbbb305ed | ||
|
3e7307e2a3 | ||
|
8a8603b46e | ||
|
16a6235041 | ||
|
40a50fc855 | ||
|
619756527a | ||
|
90cced1475 | ||
|
e6ac14ff25 | ||
|
7a821dd7ad | ||
|
a41584eeab | ||
|
6c928ba2aa | ||
|
3b3974cec0 | ||
|
f3d2a5397b | ||
|
99ed130baf | ||
|
21019beb24 | ||
|
42d073113b | ||
|
70019f8701 | ||
|
50a7a5cb9e | ||
|
161909f0e7 | ||
|
ed4795224a | ||
|
3d74181699 | ||
|
f0fbef118b | ||
|
c90bdcd9cb | ||
|
3799186b52 | ||
|
799c9be80e | ||
|
4b9a919807 | ||
|
6f8b46db13 | ||
|
c1831cbf9e | ||
|
9f1904230d | ||
|
570e25b232 | ||
|
39a3cd7ca1 | ||
|
b662a82923 | ||
|
96b54d71be | ||
|
3864ea1c8e | ||
|
aab850fd24 | ||
|
12a72a1944 | ||
|
e1f3a34896 | ||
|
da9ae21ea3 | ||
|
29eba4db25 | ||
|
d489dfead2 | ||
|
1c5540970a | ||
|
f6f17cc57e | ||
|
aaf46cbad5 | ||
|
e3ad658089 | ||
|
5f90c364e1 | ||
|
7dcd5c2835 | ||
|
fca2e978ae | ||
|
148265ae8f | ||
|
8200b654b2 | ||
|
643b32a2d4 | ||
|
8ba4bb99bb | ||
|
be4f718ad8 | ||
|
99d4963e22 | ||
|
a82465f466 | ||
|
a8a3f636b2 | ||
|
e886fc8281 | ||
|
826adb79e0 | ||
|
19793255eb | ||
|
33f936b9fb | ||
|
94847563bc | ||
|
f8c90f72e2 | ||
|
72a1a997ea | ||
|
0b0c43b921 | ||
|
1e9f20b8d9 | ||
|
f1b569bd55 | ||
|
b329731838 | ||
|
7752ddff5f | ||
|
774362c726 | ||
|
7cb881e50e | ||
|
3cc56edb5a | ||
|
dd4a09213c | ||
|
9edf71e175 | ||
|
0ace3c0161 | ||
|
02b81fbbd5 | ||
|
c589312fc7 | ||
|
d9eed72a32 | ||
|
73a982955e | ||
|
81c009465b | ||
|
044c3e0f3a | ||
|
31e99a3192 | ||
|
67230f0858 | ||
|
6193ad54e1 | ||
|
6bafc8030f | ||
|
691230be01 | ||
|
0ca0021835 | ||
|
73d0e2c1b9 | ||
|
810c0a6861 | ||
|
dd5a66e1a4 | ||
|
6d5addced8 | ||
|
78c9b5701a | ||
|
8d4eea861b | ||
|
98256db4a1 | ||
|
4aa94256d5 | ||
|
6b0f0ea57e | ||
|
7f8128f994 | ||
|
21cfcdfcd4 | ||
|
9f1451b369 | ||
|
cd4484b577 | ||
|
26dc32b48c | ||
|
5bb929c4f8 | ||
|
7b2ee6e5bc | ||
|
76da11697a | ||
|
d9710adff6 | ||
|
725fd1ebdf | ||
|
38a341ec41 | ||
|
4d0c64df25 | ||
|
dc000d433e | ||
|
0a9f2d153f | ||
|
92d28088c9 | ||
|
caf156bee7 | ||
|
086a21f35a | ||
|
1a24c7ee78 | ||
|
1613225cb4 | ||
|
4735c4011c | ||
|
435ec7687f | ||
|
2c40b1c2dc | ||
|
9c086737b0 | ||
|
c8721e3181 | ||
|
55a6548f9b | ||
|
41fbee2317 | ||
|
8a9089c298 | ||
|
73cf3d2127 | ||
|
17725f66e8 | ||
|
8e82e7045f | ||
|
807e259549 | ||
|
d85198b1c5 | ||
|
b0877b0700 | ||
|
8740cbc393 | ||
|
fe17648f7b | ||
|
8f4ca196f3 | ||
|
9e80577b9b | ||
|
3ef56bd4d8 | ||
|
177697f906 | ||
|
9d7ed246b9 | ||
|
a43b2173b1 | ||
|
98d3d3ebe0 | ||
|
f2b96f5a40 | ||
|
b0d6510f27 | ||
|
5eaa2217ce | ||
|
4b436a5349 | ||
|
c7dbc8e608 | ||
|
0e7a9b6f16 | ||
|
e5517e4806 | ||
|
28ce9646db | ||
|
6ba705e2a7 | ||
|
410957cd0c | ||
|
521269fe89 | ||
|
01ff8e30c6 | ||
|
782ada4f14 | ||
|
962989ca88 | ||
|
085703fad5 | ||
|
f787de5a93 | ||
|
9751345f57 | ||
|
fb5332659b | ||
|
04d9e7e7b4 | ||
|
e50c4c2613 | ||
|
f9a4224578 | ||
|
880cb9aa1c | ||
|
a6dd990721 | ||
|
f17fcf9e0c | ||
|
8355a18c85 | ||
|
13b721a5b1 | ||
|
5e6119605a | ||
|
72d40f2643 | ||
|
1022086740 | ||
|
be47f7bd94 | ||
|
1535a0258b | ||
|
3a0093c35d | ||
|
4b06cd42cc | ||
|
42a61d4123 | ||
|
f1a6051bd8 | ||
|
d5dd4c18f1 | ||
|
4d1dd85160 | ||
|
1ad1c00710 | ||
|
6c89c30948 | ||
|
9b07f1568c | ||
|
58d1762e88 | ||
|
80601bef27 | ||
|
b1e12ac813 | ||
|
0c706badb4 | ||
|
68659d7dd8 | ||
|
89b44df63b | ||
|
af18883342 | ||
|
3dc0be8833 | ||
|
aa0023989f | ||
|
9aa1c1ce62 | ||
|
a0d901b909 | ||
|
5d70f49502 | ||
|
a4c584a230 | ||
|
316967da42 | ||
|
b9f583c7eb | ||
|
f86ca39fed | ||
|
8481edb6fc | ||
|
dfc26c0495 | ||
|
835b0c4778 | ||
|
dfb2665530 | ||
|
e379f71efb | ||
|
8ef7cd4e4b | ||
|
52657675b0 | ||
|
18241c136b | ||
|
2b4c76c1ef | ||
|
38a3d1c573 | ||
|
4b94a3d24d | ||
|
06aaa6af16 | ||
|
7a1e108fc6 | ||
|
bf368fae63 | ||
|
e574e71784 | ||
|
2c7ebd4e99 | ||
|
126a37b847 | ||
|
087c566c59 | ||
|
8c527f10bf | ||
|
5550c11050 | ||
|
15b58d4426 | ||
|
54f2e9df4f | ||
|
a63ece10bc | ||
|
2c8adc4253 | ||
|
2873784c7f | ||
|
46638b233a | ||
|
307e5483ab | ||
|
834d4ba664 | ||
|
169e57968d | ||
|
6723d8fd89 | ||
|
44cedab976 | ||
|
544bebad96 | ||
|
8b2e658340 | ||
|
a97efa50af | ||
|
f0795c83d9 | ||
|
dcd4dd095a | ||
|
a524211300 | ||
|
043d11045a | ||
|
75644e7ce7 | ||
|
0c1a3dcf15 | ||
|
6ba399157e | ||
|
a0cdc37a7b | ||
|
e99d2b907c | ||
|
650f080345 | ||
|
2dbd456011 | ||
|
4adf878bc8 | ||
|
cd2358000e | ||
|
d01d24d653 | ||
|
05cb8f2c59 | ||
|
cb7d0b6115 | ||
|
eefb7e4c5d | ||
|
608c8d9de9 | ||
|
d441e5cf9f | ||
|
12f6b7a8d0 | ||
|
55ad4b2b3c | ||
|
766064750f | ||
|
8abff6590a | ||
|
aafcd324de | ||
|
57db900fb9 | ||
|
6c02c21920 | ||
|
4cd60ecd87 | ||
|
aafebeab53 | ||
|
80052e6afc | ||
|
498b92363c | ||
|
9abbdb1cd7 | ||
|
b21f50a0c5 | ||
|
43e1187862 | ||
|
d42adcebc3 | ||
|
300ad76b46 | ||
|
eeae5c93d0 | ||
|
fa2aa095a8 | ||
|
5bf3e7bac9 | ||
|
5747603fea | ||
|
d3e918b9a7 | ||
|
fe76c7cd96 | ||
|
0fcb60e324 | ||
|
9aefac8f71 | ||
|
e17087a2eb | ||
|
2d66ecd419 | ||
|
d36361883f | ||
|
3b459f1d4d | ||
|
3aaffdc195 | ||
|
06742a3f34 | ||
|
fd1f32655c | ||
|
e87472550a | ||
|
f042e7a057 | ||
|
cb3761d077 | ||
|
f6d940d507 | ||
|
04bb67d1eb | ||
|
506402e1c4 | ||
|
86ea265e7d | ||
|
fd61337d97 | ||
|
6486e8086c | ||
|
f69af39be1 | ||
|
fa021f9d32 | ||
|
cfd16fdf8a | ||
|
da0ab86c0c | ||
|
2760578f96 | ||
|
cd10bd9902 | ||
|
0adeb3f64c | ||
|
c062f06978 | ||
|
3a3c194484 | ||
|
7b4f596f23 | ||
|
d5aa22cdae | ||
|
3d82293143 | ||
|
03ccafc996 | ||
|
dcbaf1e7e4 | ||
|
525a04793f | ||
|
6a6a8e0022 | ||
|
5db6bc2225 | ||
|
0e731835cc | ||
|
2becbe45e7 | ||
|
723b336273 | ||
|
fc9a1c833b | ||
|
5a2453e96a | ||
|
58df8994d2 | ||
|
fbed728039 | ||
|
b7ac2ef8d5 | ||
|
fdea2202ba | ||
|
dab7b39416 | ||
|
8a1a8813de | ||
|
381ceb6b86 | ||
|
e67db37bfe | ||
|
e8e4db6586 | ||
|
5b0393dca4 | ||
|
c730adb413 | ||
|
fd2fbd6e6a | ||
|
13b861765a | ||
|
f1eae98244 | ||
|
fc44b8ef80 | ||
|
2c3b02c768 | ||
|
ef5568428b | ||
|
ddebdc2e0a | ||
|
6a38095713 | ||
|
2c8794065c | ||
|
8c74e2dfff | ||
|
6c2c171f20 | ||
|
96cd9b94a4 | ||
|
6d3241c6f3 | ||
|
e340547008 | ||
|
802444e809 | ||
|
5b06184a88 | ||
|
345e0bf363 | ||
|
0fc1dd1f46 | ||
|
b2825e86a7 | ||
|
d0a3aed0f2 | ||
|
840af89188 | ||
|
dfd0016bdb | ||
|
feb39f9b54 | ||
|
7514690ece | ||
|
762b90a95f | ||
|
cadc0637a1 | ||
|
6f49364da9 | ||
|
98f6b93225 | ||
|
0347eb1392 | ||
|
b7ad5f6a03 | ||
|
634172bfcc | ||
|
9e26f9e66e | ||
|
19ea7177d9 | ||
|
68fc8c97ce | ||
|
360db90365 | ||
|
a93a3d40d8 | ||
|
26edaadf5a | ||
|
4d7c9b72cd | ||
|
2f73d95985 | ||
|
994d540b9e | ||
|
6f41802c9a | ||
|
c066828a13 | ||
|
6824077ef0 | ||
|
58188a8c9d | ||
|
2f1988ebe3 | ||
|
2086277047 | ||
|
c443b2712b | ||
|
1e1549450a | ||
|
486c59f388 | ||
|
2c86663a45 | ||
|
3249d95629 | ||
|
8fa8c24d1d | ||
|
5f78781ecf | ||
|
f390de613e | ||
|
64dc34e6eb | ||
|
e2d035e95e | ||
|
55d334aa30 | ||
|
fa8370ee45 | ||
|
c41af91a9f | ||
|
223a534ba9 | ||
|
f6d158f819 | ||
|
093140588f | ||
|
51cbdd5696 | ||
|
6e0be288f8 | ||
|
ca5b24bf4f | ||
|
98ba5b9587 | ||
|
65c28c1409 | ||
|
5fab8b9640 | ||
|
85365557ce | ||
|
858763c2c3 | ||
|
524049947f | ||
|
60d2bce003 | ||
|
c323239990 | ||
|
dbe5532815 | ||
|
41ee819aef | ||
|
7f46cbd1d9 | ||
|
f5c10aad18 | ||
|
5643b38631 | ||
|
15fe2c07f3 | ||
|
ac9962f622 | ||
|
9c9e8927b0 | ||
|
bd9f3272b2 | ||
|
940984b68a | ||
|
3cdd61234c | ||
|
1aac828b2b | ||
|
8e31869591 | ||
|
0075c65efb | ||
|
adfe2b494b | ||
|
a09a5e4310 | ||
|
112b010c62 | ||
|
39e6ea569f | ||
|
245462e057 | ||
|
56f3042425 | ||
|
92388b70ba | ||
|
d7ffd80983 | ||
|
a177da765a | ||
|
b6fdd3a707 | ||
|
f7e406177b | ||
|
3dcbc6bb51 | ||
|
89b1c9866a | ||
|
7a95f640a0 | ||
|
99e93b6f07 | ||
|
1a9238033a | ||
|
682fdfd38e | ||
|
54efaa3e09 | ||
|
b516b690b8 | ||
|
ea88552aec | ||
|
0b6d9950e3 | ||
|
a0b8944f13 | ||
|
ecc77480dc | ||
|
adcb208c9f | ||
|
34b0949a19 | ||
|
01c5d70438 | ||
|
a111d6a6c9 | ||
|
acd6d3db8e | ||
|
6b923babeb | ||
|
b1d1759ea2 | ||
|
3297bb7066 | ||
|
ecbe25b235 | ||
|
c145f28b57 | ||
|
a6e94e4e50 | ||
|
3914863ff3 | ||
|
611c66728b | ||
|
fd4982e08e | ||
|
5e3e1042e1 | ||
|
64a7a2727e | ||
|
eea610aa89 | ||
|
a795a5e735 | ||
|
95020bf62c | ||
|
7b15d519fc | ||
|
f2fd7f4146 | ||
|
825de48143 | ||
|
ddb20ce11d | ||
|
d250c298bb | ||
|
f42ba4a819 | ||
|
649d0147d0 | ||
|
414f0f28ab | ||
|
4ca10c3b60 | ||
|
99117962d1 | ||
|
faf25d8c31 | ||
|
5713d6caf2 | ||
|
ee47fda7d3 | ||
|
d03188febb | ||
|
89b5f9e334 | ||
|
dbb103d205 | ||
|
ef26081af9 | ||
|
d12973a0bc | ||
|
c90df420df | ||
|
3052764d38 | ||
|
fd58b2b7a8 | ||
|
0ce70a22d9 | ||
|
ff850dff74 | ||
|
2c80091c0f | ||
|
4d0e56a270 | ||
|
13be258283 | ||
|
14b6f82f7b | ||
|
3e5222e53c | ||
|
c287e713f2 | ||
|
98847521dc | ||
|
fcb0c813ed | ||
|
347c52c66d | ||
|
6541549732 | ||
|
5fd9c3f876 | ||
|
9d448b61ad | ||
|
dac9612397 | ||
|
479b58a57d | ||
|
5ad53f11e3 | ||
|
8821002739 | ||
|
10adeab6cd | ||
|
d4b9529c5d | ||
|
af8a25bcb5 | ||
|
083de6e4dc | ||
|
71e68dc30c | ||
|
6313675130 | ||
|
d357a1e940 | ||
|
6bd723aefa | ||
|
b2eca11db4 | ||
|
3deabd1707 | ||
|
1fef03da48 | ||
|
92badf3483 | ||
|
717c69e09d | ||
|
798fb85ce9 | ||
|
fa53d5304b | ||
|
a64834d3ab | ||
|
9e119763d1 | ||
|
7d47394321 | ||
|
c1a1de639a | ||
|
0ee0b0402f | ||
|
e77cfb50b1 | ||
|
ae1a14bab8 | ||
|
25a12e37ce | ||
|
6910cd1b45 | ||
|
b69331c960 | ||
|
00eb53ee8d | ||
|
cbfdaaa585 | ||
|
51d336b973 | ||
|
ad31e054cf | ||
|
8dfb6cfc44 | ||
|
dcda4e35bd | ||
|
03760c754c | ||
|
a7f87f59c4 | ||
|
cb949e7bc3 | ||
|
a78a586f09 | ||
|
95b963f497 | ||
|
66dcd6fc15 | ||
|
c0faa5e681 | ||
|
7030ab18c6 | ||
|
f26cf94c52 | ||
|
7f940743b1 | ||
|
5caa590d6c | ||
|
bd39cd0ba8 | ||
|
1c2c1145e8 | ||
|
a90f11867c | ||
|
29447daf99 | ||
|
32e29d0d1f | ||
|
81d0c457e4 | ||
|
3fa6114bbb | ||
|
c1b8343a93 | ||
|
b91d455e7a | ||
|
62a468cfa1 | ||
|
d22b69dc27 | ||
|
c4a169b1ac | ||
|
6721f1e271 | ||
|
eb01bd67a0 | ||
|
942cc30a86 | ||
|
26c945b6fc | ||
|
09c1dbd55d | ||
|
17b635f9c7 | ||
|
3ba3d2b293 | ||
|
154b3acdf3 | ||
|
c19ec75232 | ||
|
10020b59ad | ||
|
6abe0c37d0 | ||
|
7942f310f7 | ||
|
6929d3a3e2 | ||
|
8e651f1c60 | ||
|
b7d4798896 | ||
|
cea5fb9664 | ||
|
befdc7266c | ||
|
b98a0adede | ||
|
f4e5079542 | ||
|
2ad6ab4f4f | ||
|
870cf20d2f | ||
|
4ea1cb13ac | ||
|
b7bc00d8e3 | ||
|
4d7583d527 | ||
|
51a4273d42 | ||
|
8cb053b390 | ||
|
ff451aaab5 | ||
|
7a80a9c6c7 | ||
|
930deb49ec | ||
|
a11fdb45b9 | ||
|
6a76f8ac3f | ||
|
e310d8a271 | ||
|
7bfe043f64 | ||
|
31b843488b | ||
|
50d4a7d3c7 | ||
|
f1853a58e2 | ||
|
e1baa54639 | ||
|
6877b79a27 | ||
|
a861046a3e | ||
|
171082ba50 | ||
|
7e10c9193b | ||
|
a67783d4ba | ||
|
2e5b54b19c | ||
|
37a72eb758 | ||
|
4ddc3e08cf | ||
|
c5bc1956e1 | ||
|
cfa9158c33 | ||
|
4bd41fc28c | ||
|
84a3923a61 | ||
|
314aec6661 | ||
|
245ad7b31c | ||
|
2784d4891c | ||
|
78b11b90db | ||
|
9d9b421e22 | ||
|
0fd43ffc86 | ||
|
6affb4eb8b | ||
|
1e2b6ec3ce | ||
|
9ee690edaa | ||
|
318205fe94 | ||
|
49c0cf2193 | ||
|
f3cb59360f | ||
|
cc2a11e653 | ||
|
a2792eb6dd | ||
|
bddd2f654a | ||
|
99702f4146 | ||
|
888ea09759 | ||
|
82ba10708b | ||
|
c1b663ba7f | ||
|
d06bd6f981 | ||
|
5287f5dc87 | ||
|
9ed671357a | ||
|
f26190b0ab | ||
|
2894ec7290 | ||
|
9b773adcc7 | ||
|
4346146b3a | ||
|
d54327d54a | ||
|
23448d8d75 | ||
|
2c36ee38c1 | ||
|
974afd3fe4 | ||
|
a38c31d771 | ||
|
dcae252cd3 | ||
|
2afc06b33f | ||
|
7f37fc1c1e | ||
|
3a36c5c9fa | ||
|
5013e61fad | ||
|
7118a0039d | ||
|
3458dff436 | ||
|
1ea6f5fc8d | ||
|
2feb07fecf | ||
|
3eaf6fd06b | ||
|
a6547ea109 | ||
|
3d11d8f644 | ||
|
61cef923ca | ||
|
8637b4010c | ||
|
41c6d6b5ee | ||
|
aa4ae683c7 | ||
|
68e12b4544 | ||
|
f5d0cf0a16 | ||
|
6e2f849321 | ||
|
e66cf71ef1 | ||
|
1ffec7046d | ||
|
cc1ad3d484 | ||
|
9974bdac23 | ||
|
edf3e30ba6 | ||
|
d19d8bd0e9 | ||
|
c3834dc12b | ||
|
48319aada3 | ||
|
5eedfd536f | ||
|
d087c1fac6 | ||
|
25fada03e3 | ||
|
d059dadc12 | ||
|
6eb6eb7990 | ||
|
693836a678 | ||
|
e3adbb738b | ||
|
83290c9083 | ||
|
ae295d21f9 | ||
|
201fea79db | ||
|
fa3c5adbc3 | ||
|
8e0722e470 | ||
|
d2fc0ccaaf | ||
|
0148c58e44 | ||
|
66e66ce1f1 | ||
|
9067ef9e77 | ||
|
94cf34f09b | ||
|
6d9885b459 | ||
|
4a4f900717 | ||
|
e30853bec5 | ||
|
bfe1730fba | ||
|
c81d6b4b73 | ||
|
809223ad37 | ||
|
40ee4bfa89 | ||
|
bf8791893a | ||
|
b0ae78f5f9 | ||
|
67feaa4f05 | ||
|
53485dcf67 | ||
|
b815af387c | ||
|
ba5fbbf11b | ||
|
736a71b130 | ||
|
908037c14a | ||
|
095911201b | ||
|
dbd2a672db | ||
|
398e51b87a | ||
|
df6c7d307f | ||
|
9feda9ceb7 | ||
|
7667fb13e0 | ||
|
434aeae3c8 | ||
|
07931f899c | ||
|
db81fa3334 | ||
|
17720d32c0 | ||
|
73d8020f4d | ||
|
220f36115c | ||
|
d5f64afb7f | ||
|
7db91a9cf2 | ||
|
d83183f0ae | ||
|
c2f7c6af47 | ||
|
eb09b65ee6 | ||
|
73750b1070 | ||
|
85340a390d | ||
|
346fc74b51 | ||
|
bc7c8f5bb5 | ||
|
6fbeabae21 | ||
|
c3ec7f1326 | ||
|
b79a38a27b | ||
|
025e68b7b1 | ||
|
6e47a5407e | ||
|
3532be602c | ||
|
539925b21f | ||
|
25394dc61a | ||
|
04464a3200 | ||
|
02a2fa0aef | ||
|
10e66f6a2b | ||
|
a9833f47f0 | ||
|
e82ab043cf | ||
|
b042fa481b | ||
|
6e1508c912 | ||
|
fc3126f141 | ||
|
6bbb5faa8d | ||
|
9a05ee7490 | ||
|
cfd58ad8e4 | ||
|
f8bb6732d0 | ||
|
20cd9fb06d | ||
|
8109a02116 | ||
|
e5a04cc759 | ||
|
2a28a0042a | ||
|
4ace238f54 | ||
|
3b9237b121 | ||
|
00b3837843 | ||
|
386dc0c7fb | ||
|
de6e404d9b | ||
|
cc538f56be | ||
|
276dc729ad | ||
|
993df610c3 | ||
|
e5047cca17 | ||
|
3994e69cc7 | ||
|
9fbdc8f10f | ||
|
75dbbeec5f | ||
|
d977350123 | ||
|
b8ca8d1701 | ||
|
3af138d2fe | ||
|
1b4f01bd8a | ||
|
7d5960a558 | ||
|
b6f45dd7a7 | ||
|
a6689cdac2 | ||
|
5dae7dd684 | ||
|
3a52460729 | ||
|
0af37e2f88 | ||
|
fa5c5b8fd6 | ||
|
655c3c27ef | ||
|
6993b321e2 | ||
|
b4dd21e861 | ||
|
e5304c1f09 | ||
|
8573940e0b | ||
|
4f90544ab9 | ||
|
0ce48aa186 | ||
|
c0c5abe92f | ||
|
dac1839cca | ||
|
1214de8702 | ||
|
3b2348465f | ||
|
d7916b17af | ||
|
baf14600f8 | ||
|
ccf8aa9c57 | ||
|
5bd92a04eb | ||
|
cfc5f7000e | ||
|
64218faeb9 | ||
|
b809162160 | ||
|
1802ea915e | ||
|
0be4ea10be | ||
|
21179f6f71 | ||
|
e283ab154f | ||
|
400242dba8 | ||
|
fb21a4942a | ||
|
0aa7e7bf21 | ||
|
ab372c057a | ||
|
0d75af4e91 | ||
|
b094bf860f | ||
|
86ad7aa188 | ||
|
97a3d96213 | ||
|
ed6c0ab620 | ||
|
10fdbe1b3b | ||
|
a35873fc23 | ||
|
21a6ba64ea | ||
|
c3dc5ce0de | ||
|
4e38561ee5 | ||
|
c38e90e12d | ||
|
8c2c1f65b2 | ||
|
2ec3dd4c02 | ||
|
dc1c8ce471 | ||
|
54cbee7de4 | ||
|
7aff9c9b2d | ||
|
92cc11a089 | ||
|
7310638661 | ||
|
323de1726a | ||
|
2bd2a67127 | ||
|
9683addf56 | ||
|
cf02114970 | ||
|
dbbfdc7227 | ||
|
3d149d59e2 | ||
|
3c106f642a | ||
|
cdcdfea6f9 | ||
|
c059d6222f | ||
|
ab42daba1b | ||
|
7f451ac1b4 | ||
|
20dab89096 | ||
|
83b3217064 | ||
|
40f7e2388d | ||
|
b742d51965 | ||
|
7fe298b051 | ||
|
02b28fa932 | ||
|
09129bbfee | ||
|
78b8af7de6 | ||
|
e6c8cad1d1 | ||
|
94cb5ad0e4 | ||
|
fd12c9a5dd | ||
|
577dd0fca4 | ||
|
d83818c904 | ||
|
6fe996a834 | ||
|
2af25b808b | ||
|
cbafe1e284 | ||
|
ac903b5df7 | ||
|
b8050b62f0 | ||
|
9b982b2b6f | ||
|
db98312ff4 | ||
|
77c852d95e | ||
|
e7e68d328e | ||
|
415d0961be | ||
|
20bb46c214 | ||
|
d1a1699b97 | ||
|
356e124a01 | ||
|
49cb682d48 | ||
|
160a4e293b | ||
|
ef26b4a864 | ||
|
1bb15a231f |
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@ -0,0 +1 @@
|
||||
.gitignore
|
14
.eslintrc.js
14
.eslintrc.js
@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: "eslint:recommended",
|
||||
parserOptions: {
|
||||
ecmaVersion: 12
|
||||
},
|
||||
rules: {}
|
||||
};
|
16
.github/ISSUE_TEMPLATE/-bug-report-.md
vendored
16
.github/ISSUE_TEMPLATE/-bug-report-.md
vendored
@ -1,16 +0,0 @@
|
||||
---
|
||||
name: "[Bug report]"
|
||||
about: Bug report
|
||||
title: ''
|
||||
labels: BUG
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**MCSManager Version**
|
||||
|
||||
**System Info/Version**
|
||||
|
||||
**Node Version**
|
||||
|
||||
**Step**
|
10
.github/ISSUE_TEMPLATE/-feature-request--.md
vendored
10
.github/ISSUE_TEMPLATE/-feature-request--.md
vendored
@ -1,10 +0,0 @@
|
||||
---
|
||||
name: "[Feature request] "
|
||||
about: Feature request
|
||||
title: ''
|
||||
labels: Feature Request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
10
.github/ISSUE_TEMPLATE/-question-.md
vendored
10
.github/ISSUE_TEMPLATE/-question-.md
vendored
@ -1,10 +0,0 @@
|
||||
---
|
||||
name: "[Question]"
|
||||
about: Create a question
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
48
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: "[Bug Report] "
|
||||
labels: "Bug Report"
|
||||
body:
|
||||
- type: input
|
||||
id: Platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Please enter the platform on which you encountered the bug.
|
||||
placeholder: e.g. Windows 11, Ubuntu 14.04
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: panel-version
|
||||
attributes:
|
||||
label: Panel Version
|
||||
description: MCSManager Panel Version
|
||||
placeholder: e.g. 10.1.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: daemon-version
|
||||
attributes:
|
||||
label: Daemon Version (Optional)
|
||||
description: MCSManager Daemon Version
|
||||
placeholder: e.g. 4.3.0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: recurrence-probability
|
||||
attributes:
|
||||
label: Recurrence Probability
|
||||
description: Recurrence Probability
|
||||
placeholder: 1%~100%
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Bug Description
|
||||
description: Please describe the problem you met in as much detail as possible. Besides, please describe how to trigger this problem again.
|
||||
placeholder: |
|
||||
1. Click a button named ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: English | Discord Channel
|
||||
url: https://discord.gg/BNpYMVX7Cd
|
||||
about: The official Discord channel of MCSManager!
|
||||
- name: 中文用户 | QQ Group
|
||||
url: http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=6ryJRaHMaAM9aSeSdRWn6qm9gQ3lI8vu&authKey=BMZ5gA9Rqw%2B5714yAW1ZxiWnFbkk2KRKgBF%2BMiIaOw0VVsSR7tsP10cFwhmDdlgU&noverify=0&group_code=198646856
|
||||
about: The official QQ Group of MCSManager!
|
||||
- name: 中文用户 | Telegram Group
|
||||
url: https://t.me/MCSManager_dev
|
||||
about: The official Telegram Group of MCSManager!
|
22
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Feature Request
|
||||
description: A new feature for MCSManager
|
||||
title: "[Feature] "
|
||||
labels: "Feature Request"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure that no duplicated issues has already been delivered.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please describe in detail the new functionality you would like to add.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reason
|
||||
attributes:
|
||||
label: Reason
|
||||
description: Please describe why you want to add the feature into MCSManager.
|
||||
validations:
|
||||
required: true
|
1
.github/github.txt
vendored
1
.github/github.txt
vendored
@ -0,0 +1 @@
|
||||
|
BIN
.github/panel-custom-layout.gif
vendored
Normal file
BIN
.github/panel-custom-layout.gif
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 MiB |
BIN
.github/panel-image.png
vendored
Normal file
BIN
.github/panel-image.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 274 KiB |
BIN
.github/panel-instances.png
vendored
Normal file
BIN
.github/panel-instances.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 740 KiB |
61
.github/workflows/codeql.yml
vendored
61
.github/workflows/codeql.yml
vendored
@ -13,12 +13,12 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
branches: ["next"]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
branches: ["next"]
|
||||
schedule:
|
||||
- cron: '34 21 * * 6'
|
||||
- cron: "37 18 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@ -32,41 +32,40 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
language: ["javascript", "typescript"]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
20
.github/workflows/dependency-review.yml
vendored
20
.github/workflows/dependency-review.yml
vendored
@ -1,20 +0,0 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v2
|
102
.github/workflows/docker.yml
vendored
Normal file
102
.github/workflows/docker.yml
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
name: Release Docker Build
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-web:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta web
|
||||
id: meta_web
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-web
|
||||
name=githubyumao/mcsmanager-web,enable=${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push Web
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: dockerfile/web.dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta_web.outputs.tags }}
|
||||
labels: ${{ steps.meta_web.outputs.labels }}
|
||||
build-args: |
|
||||
BUILDPLATFORM=linux/amd64
|
||||
|
||||
build-daemon:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
java_version: [8, 11, 17, 21]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta daemon
|
||||
id: meta_daemon
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-daemon,enable=${{ matrix.java_version == 21 }}
|
||||
name=githubyumao/mcsmanager-daemon,enable=${{ github.repository == 'MCSManager/MCSManager' && matrix.java_version == 21 }}
|
||||
name=ghcr.io/${{ github.repository }}-daemon-jdk${{ matrix.java_version }}
|
||||
name=githubyumao/mcsmanager-daemon-jdk${{ matrix.java_version }},enable=${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ghcr.io
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: ${{ github.repository == 'MCSManager/MCSManager' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Build and Push Daemon
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: dockerfile/daemon.dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta_daemon.outputs.tags }}
|
||||
labels: ${{ steps.meta_daemon.outputs.labels }}
|
||||
build-args: |
|
||||
BUILDPLATFORM=linux/amd64
|
||||
JAVA_RUNTIME=${{ matrix.java_version }}
|
30
.github/workflows/node.js.yml
vendored
30
.github/workflows/node.js.yml
vendored
@ -1,30 +0,0 @@
|
||||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run dev
|
57
.github/workflows/release.yml
vendored
Normal file
57
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
chmod a+x ./install-dependents.sh
|
||||
chmod a+x ./build.sh
|
||||
./install-dependents.sh
|
||||
./build.sh
|
||||
|
||||
- name: Add binaries to production files
|
||||
run: wget --input-file=lib-urls.txt --directory-prefix=production-code/daemon/lib/
|
||||
|
||||
- name: Create linux and windows build
|
||||
run: |
|
||||
cp -r production-code dist_linux
|
||||
mv production-code dist_windows
|
||||
|
||||
- name: Copy startup scripts
|
||||
run: |
|
||||
cp prod-scripts/linux/* dist_linux/
|
||||
cp prod-scripts/windows/* dist_windows/
|
||||
|
||||
- name: Copy node runtime to windows build
|
||||
run: |
|
||||
wget https://nodejs.org/download/release/latest-v20.x/win-x64/node.exe -O dist_windows/daemon/node_app.exe
|
||||
cp dist_windows/daemon/node_app.exe dist_windows/web/node_app.exe
|
||||
|
||||
- name: Create archive
|
||||
run: |
|
||||
mv dist_linux/ mcsmanager/
|
||||
tar czf mcsmanager_linux_release.tar.gz mcsmanager/
|
||||
rm -rf mcsmanager/
|
||||
mv dist_windows/ mcsmanager/
|
||||
zip -r mcsmanager_windows_release.zip mcsmanager/
|
||||
|
||||
- name: Upload assets to release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
mcsmanager_windows_release.zip
|
||||
mcsmanager_linux_release.tar.gz
|
31
.github/workflows/webpack.yml
vendored
Executable file
31
.github/workflows/webpack.yml
vendored
Executable file
@ -0,0 +1,31 @@
|
||||
name: Build MCSManager
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Run Build Script
|
||||
run: |
|
||||
chmod a+x ./install-dependents.sh
|
||||
chmod a+x ./build.sh
|
||||
./install-dependents.sh
|
||||
./build.sh
|
||||
|
19
.gitignore
vendored
19
.gitignore
vendored
@ -5,20 +5,17 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
test.txt
|
||||
*.bat
|
||||
|
||||
src/public
|
||||
panel/public/
|
||||
*.code-workspace
|
||||
vscode/
|
||||
.vscode/
|
||||
Daemon/
|
||||
data/
|
||||
dist/
|
||||
out/
|
||||
public/
|
||||
lib/
|
||||
production/
|
||||
.DS_Store
|
||||
production-code/
|
||||
test.js
|
||||
.idea/
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
@ -28,6 +25,7 @@ pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
mcsmanager_packages.json
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
@ -121,4 +119,7 @@ workspace.code-workspace
|
||||
src/public/1.png
|
||||
|
||||
# IntelliJ Idea project files
|
||||
/.idea/*
|
||||
/.idea/*
|
||||
*/.idea/*
|
||||
|
||||
.turbo
|
||||
|
19
.vscode/settings.json
vendored
Normal file
19
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
// Please do not commit changes to this file.
|
||||
// You need to install the "i18n ally" VSCode plugin.
|
||||
{
|
||||
// Change this to your language, available options: "languages/*.json"
|
||||
// "i18n-ally.displayLanguage": "en_US"
|
||||
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.extract.keyMaxLength": 10000,
|
||||
"cSpell.words": ["BUKKIT", "BUNGEECORD", "MINECRAFT"],
|
||||
"i18n-ally.localesPaths": [
|
||||
"languages",
|
||||
"frontend/src/lang",
|
||||
"daemon/src/i18n",
|
||||
"panel/src/app/i18n"
|
||||
],
|
||||
"i18n-ally.keystyle": "flat",
|
||||
"i18n-ally.enabledFrameworks": ["vue"],
|
||||
"editor.wordSeparators": "`~!@#$%^&*()=+[{]}\\|;:'\",.<>/?"
|
||||
}
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
support@mcsmanager.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
49
CONTRIBUTING.md
Normal file
49
CONTRIBUTING.md
Normal file
@ -0,0 +1,49 @@
|
||||
## English 🌎
|
||||
|
||||
### Purpose
|
||||
|
||||
To prevent uncontrolled growth of features and wasted effort after submitting PRs that are not merged, these notices are hereby written.
|
||||
|
||||
### Submitting New Features
|
||||
|
||||
If you wish to submit a new feature via a Pull Request, please first open an issue to inform the developer @unitwk. After discussion, we will decide together whether the feature should be developed.
|
||||
|
||||
We will consider whether to merge this feature into the master branch from three perspectives: `scope of code impact`, `practicality of the feature`, and `estimated number of applicable users`.
|
||||
|
||||
Otherwise, unrestrained addition of features will inevitably lead to the continuous bloating of software functionality, which will sooner or later result in very serious security vulnerabilities.
|
||||
|
||||
### Iterating Old Features
|
||||
|
||||
Fixing or upgrading an existing feature, **without exceeding the responsibilities of the old feature**, **does not require opening an issue for discussion**. You can directly submit a Pull Request, and after testing and Code Review, it will be merged.
|
||||
|
||||
**Code in open-source projects often exists for several years or even decades. Due to differences in developer habits, implementation approaches, and code styles, we need some rules and constraints to ensure the long-term health of the code. Thank you for your understanding.**
|
||||
|
||||
### How to Contribute Code?
|
||||
|
||||
Please refer to the `Development` section in the README.md file.
|
||||
|
||||
<br />
|
||||
|
||||
## 中文 🌏
|
||||
|
||||
### 目的
|
||||
|
||||
为了防止功能无节制增长和提交 PR 之后未得到合并浪费工作精力,特此写下这些须知通告。
|
||||
|
||||
### 提交新功能
|
||||
|
||||
如果你想 `Pull Request` 一个新功能,请先开启一个 `issue` 告知开发者 @unitwk ,我们一起经过讨论之后,再决定这个功能是否应该开发。
|
||||
|
||||
我们会从`代码影响范围`,`功能实用性`和`估算的适用群体人数` 三个角度来考虑这个功能是否应该合并到 master 分支。
|
||||
|
||||
否则,无节制的新增功能必将导致软件功能不断臃肿,从而迟早会有一天会出现非常严重的安全漏洞。
|
||||
|
||||
### 迭代旧功能
|
||||
|
||||
修复或升级某个现有的功能,在**不超出旧功能的职责**情况下,**无需开 `issue` 进行讨论**,你可以直接提交 `Pull Request`,在经过测试和 `Code Review`后,都会给予合并。
|
||||
|
||||
**开源项目的代码通常会存在几年甚至十年以上,由于每个开发者习惯不同,实现思路不同,代码风格不同,所以我们需要有一定的规则约束来让代码长期健康的活下去,谢谢你的理解。**
|
||||
|
||||
### 如何贡献代码?
|
||||
|
||||
请阅读 README.MD 文件的 `Development` 段落。
|
322
LICENSE
Executable file → Normal file
322
LICENSE
Executable file → Normal file
@ -2,190 +2,200 @@
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
Copyright 2022 unitwk
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright 2025 MCSManager
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
279
README.md
Executable file → Normal file
279
README.md
Executable file → Normal file
@ -1,243 +1,224 @@
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">MCSManager Panel</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[Official Website](http://mcsmanager.com/) | [Web Project](https://github.com/MCSManager/MCSManager) | [UI Project](https://github.com/MCSManager/UI) | [Daemon Project](https://github.com/MCSManager/Daemon)
|
||||
[Official Website](http://mcsmanager.com/) | [Docs](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
[English](readme.md) | [简体中文](README_CN.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## Introduction
|
||||
## What is MCSManager?
|
||||
|
||||
MCSManager Panel(abbr: MCSM Panel)is a multilingual, lightweight, out-of-the-box, and multi-instance Minecraft server control panel with Docker support.
|
||||
**MCSManager Panel** (MCSM) is a **modern, secure, and distributed control panel** designed for managing Minecraft and Steam game servers.
|
||||
|
||||
MCSManager panel can help you manage multiple physical servers in one place, and create game servers at any host dynamically. It also provides a secure and reliable user permission system for a seamless multi-user experience.
|
||||
MCSManager has already gained a certain level of popularity within the community, specifically because of Minecraft. MCSManager excels in offering a centralized management solution for multiple server instances and provides a secure and reliable multi-user permission system. In addition, we are committed to support server administrators, not only for Minecraft but also for Terraria and various Steam games. Our goal is to foster a thriving and supportive community for game server management.
|
||||
|
||||

|
||||

|
||||
MCSManager **supports English, French, German, Italian, Japanese, Portuguese, Simplified Chinese, and Traditional Chinese**, with plans to add support for more languages in the future!
|
||||
|
||||
**Terminal**
|
||||
|
||||

|
||||
|
||||
**Instance List**
|
||||
|
||||

|
||||
|
||||
**Custom Layout**
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
1. One-click deployment of `Minecraft` Java/Bedrock Server
|
||||
2. Compatible with most `Steam` game servers. (e.g. `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.)
|
||||
3. Customizable UI, create your own layout
|
||||
4. Supports all images on `Docker Hub`, supports multiple users and supports commercial services!
|
||||
5. Manage multiple servers with a single web interface
|
||||
6. The technology stack is simple, and you only need to be good at Typescript to complete the entire MCSManager development.
|
||||
7. And More!
|
||||
|
||||
<br />
|
||||
|
||||
## Runtime Environment
|
||||
|
||||
MCSManager panel can run on both Windows and Linux platforms without a database or complicated system configuration. As a lightweight control panel, you only need Node.js to run it.
|
||||
MCSM supports both `Windows` and `Linux`. The only requirement is `Node.js` and some libraries **for decompression**.
|
||||
|
||||
Required Node.js version: **14.17.6** or above.
|
||||
Requires [Node.js 16.20.2](https://nodejs.org/en) or above.
|
||||
|
||||
<br />
|
||||
|
||||
## Install
|
||||
## Installation
|
||||
|
||||
### Windows
|
||||
|
||||
For the Windows systems, the MCSM panel has been **compiled into a click-to-run version**.
|
||||
For Windows, we provide packaged executable files:
|
||||
|
||||
Download it from the official site: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
Go to: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**Quick Install with one command**
|
||||
**One-Command Deployment**
|
||||
|
||||
> The script needs to register system services and requires root permissions because of that.
|
||||
|
||||
```bash
|
||||
wget -qO- https://raw.githubusercontent.com/mcsmanager/Script/master/setup_en.sh | bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
- The script is designed for Ubuntu/Centos/Debian/Archlinux of AMD64 architecture only.
|
||||
- Use `systemctl start mcsm-{web,daemon}` to start service after installtion.
|
||||
- Directory for panel components and runtime: `/opt/mcsmanager/`
|
||||
**Usage**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Only supports Ubuntu/Centos/Debian/Archlinux.
|
||||
- Installation directory: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Linux Manual Installation**
|
||||
|
||||
- If the installation script does not work, you can try the following steps to install manually.
|
||||
- If the installation script fails to execute correctly, you can try to install it manually.
|
||||
|
||||
```bash
|
||||
# switch to the installation directory. Please create it in advance with 'mkdir /opt/' if not exist.
|
||||
# Create /opt directory if not already
|
||||
mkdir /opt
|
||||
# Switch to /opt
|
||||
cd /opt/
|
||||
# Download runtime environment (Node.js). Ignore this step if you have Node.js 14+ installed already.
|
||||
wget https://nodejs.org/dist/v14.17.6/node-v14.17.6-linux-x64.tar.gz
|
||||
# Decompress archive
|
||||
tar -zxvf node-v14.17.6-linux-x64.tar.gz
|
||||
# Add program to system PATH
|
||||
ln -s /opt/node-v14.17.6-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v14.17.6-linux-x64/bin/npm /usr/bin/npm
|
||||
# Download Node.js 20.11. If you already have Node.js 16+ installed, ignore this step.
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Decompress Node.js source
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Add Node.js to system PATH
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# Prepare installation directory
|
||||
# Prepare MCSM's installation directory
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# Download the web project
|
||||
git clone https://github.com/MCSManager/MCSManager-Web-Production.git web
|
||||
cd web
|
||||
# Download MCSManager
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# Install dependencies
|
||||
npm install --production
|
||||
cd /opt/mcsmanager/
|
||||
./install.sh
|
||||
|
||||
# Download the Daemon
|
||||
git clone https://github.com/MCSManager/MCSManager-Daemon-Production.git daemon
|
||||
cd daemon
|
||||
# Install dependencies
|
||||
npm install --production
|
||||
# Please open two terminals or screens.
|
||||
|
||||
# Please open two terminals or Screen
|
||||
# Start the daemon first
|
||||
cd /opt/mcsmanager/daemon
|
||||
# Start the daemon
|
||||
node app.js
|
||||
# Start the daemon first.
|
||||
./start-daemon.sh
|
||||
|
||||
# Start the web project (in the second terminal/screen)
|
||||
cd /opt/mcsmanager/web
|
||||
# start the application
|
||||
node app.js
|
||||
# Start the web interface at the second terminal or screen.
|
||||
./start-web.sh
|
||||
|
||||
# Access http://localhost:23333/ for web interface
|
||||
# In general, the web application will scan and connect to the local daemon automatically.
|
||||
# For web access, go to http://localhost:23333/
|
||||
# In general, the web interface will automatically scan and add the local daemon.
|
||||
```
|
||||
|
||||
- Note, the above steps do not register the panel components to system service. You have to use 'screen' to manage it or register the system service manually.
|
||||
This installation approach does not automatically set up MCSManager as a system service. Therefore, it is necessary to use `screen` for management. For those interested in managing MCSManager through a system service, please refer to our wiki/documentation.
|
||||
|
||||
<br />
|
||||
|
||||
## Development
|
||||
|
||||
## Data Directories
|
||||
This section is specifically designed for developers. General users may disregard this portion without concern.
|
||||
|
||||
Web Config & Data: `/opt/mcsmanager/web/data/`
|
||||
### Plugins
|
||||
|
||||
Daemon Config & Data `/opt/mcsmanager/daemon/data/`
|
||||
We use "VS Code" to develop MCSManager. You may need to install these plugins:
|
||||
|
||||
<br />
|
||||
- i18n display support (I18n Ally)
|
||||
- Code formatter (Prettier)
|
||||
- Vue - Offcial
|
||||
- ESLint
|
||||
|
||||
|
||||
## Update
|
||||
|
||||
Reference: https://github.com/MCSManager/MCSManager/wiki/Update-MCSManager
|
||||
|
||||
> Note, backup of `data` directory before each update is highly recommended.
|
||||
|
||||
<br />
|
||||
|
||||
## Projects
|
||||
|
||||
This software requires all three projects to run. The code you use for installation is the result of compilation and integration.
|
||||
|
||||
[**Web Backend**](https://github.com/MCSManager/MCSManager)
|
||||
|
||||
- Role: Control Center
|
||||
- Responsible for: Backend APIs, user data management, and communication & authentication with daemons.
|
||||
|
||||
[**Frontend/UI**](https://github.com/MCSManager/UI)
|
||||
|
||||
- Role: The user interfaces for the backend.
|
||||
- Responsible for: Displaying statistics via the web interface, sending requests, and communicating with daemons. The final product of this project is pure static files.
|
||||
|
||||
[**Daemon**](https://github.com/MCSManager/Daemon)
|
||||
|
||||
- Role: Slave/controlled remote node
|
||||
- Responsible for: Controlling all instances on localhost and managing the actual instance process. It is capable to communicate with all objects.
|
||||
|
||||
<br />
|
||||
|
||||
## Build the Development Environment
|
||||
|
||||
This is intended for developers. If you are not a developer, you can safely ignore these.
|
||||
|
||||
You can continue to develop or preview all the projects once they are running under the development environment. Please make sure to be in compliance with the license.
|
||||
|
||||
**Web Project**
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
cd MCSManager
|
||||
npm install
|
||||
npm run start
|
||||
# By default, use ts-node to run Typescript code directly
|
||||
# By default, run on port 23333.
|
||||
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
**UI Project**
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/UI.git
|
||||
cd UI
|
||||
npm install
|
||||
npm run serve
|
||||
# Preview the interface at http://localhost:8080/
|
||||
# All the requests will be redirected to port 23333.
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
**Daemon Project**
|
||||
### Dependency Files
|
||||
|
||||
You'll need to go to the [PTY](https://github.com/MCSManager/PTY) and [Zip-Tools](https://github.com/MCSManager/Zip-Tools) projects to download the corresponding binary files and place them in the `daemon/lib` directory to ensure the proper functioning of the `Emulation Terminal` and `File Decompression`.
|
||||
|
||||
### Build Production Version
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/Daemon.git
|
||||
cd Daemon
|
||||
npm install
|
||||
npm run start
|
||||
# After running, please connect the daemon to the control panel via the web interface.
|
||||
# By default, run on port 24444
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Output Directory: "production-code"
|
||||
|
||||
<br />
|
||||
|
||||
## Code Contributing
|
||||
|
||||
If you experience any problems while using MCSManager, you are welcome to [submit an Issue](https://github.com/MCSManager/MCSManager/issues/new/choose). Alternatively, you can fork the project and contribute directly by submitting a Pull Request.
|
||||
|
||||
Please ensure that any submitted code adheres to our existing coding style. For more details, refer to the guidelines provided in [this issue](https://github.com/MCSManager/MCSManager/issues/544).
|
||||
|
||||
<br />
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Support mainstream modern browsers like `Chrome` `Firefox` `Safari` `Opera`.
|
||||
- `IE` support has been dropped.
|
||||
- Supported on modern browsers including `Chrome`, `Firefox`, and `Safari`.
|
||||
- Support for `IE` has been discontinued.
|
||||
|
||||
<br />
|
||||
|
||||
## i18n
|
||||
## BUG Reporting
|
||||
|
||||
The MCSManager internationalization was done by [Lazy](https://github.com/LazyCreeper), [KevinLu2000](https://github.com/KevinLu2000), [zijiren233](https://github.com/zijiren233) and [Unitwk](https://github.com/unitwk).
|
||||
**Open Issue:** [Click here](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**Security Vulnerability Report:** [SECURITY.md](SECURITY.md)
|
||||
|
||||
<br />
|
||||
|
||||
## Panel Permission
|
||||
## Internationalization
|
||||
|
||||
The control panel will check the user list while running. If there is no user available, a default administrator user will be created.
|
||||
Thanks to these contributors for providing a substantial amount of translation:
|
||||
|
||||
If you forget your only administrator account, you can back up all the current user data, regenerate a new admin account, and overwrite the previous one.
|
||||
|
||||
> User Data: /opt/mcsmanager/web/data/Users/*.json
|
||||
|
||||
<br />
|
||||
|
||||
## Contribution
|
||||
|
||||
If you encounter any issue while using, you can [submit an Issue](https://github.com/MCSManager/MCSManager/issues/new/choose) or submit Pull Request after you fix it in a fork.
|
||||
|
||||
The code needs to be in its existing format, and no extra codes should be formatted. For details: [click here](https://github.com/MCSManager/MCSManager/issues/544)。
|
||||
|
||||
<br />
|
||||
|
||||
## Report bug
|
||||
|
||||
Feedback on any problem encountered is welcomed and will be responded to in a timely manner.
|
||||
|
||||
If you find a serious security vulnerability, you can email mcsmanager-dev@outlook.com for a private submission.
|
||||
|
||||
After the security issue has been resolved, your name will be listed as the bug-finder.
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2022 [MCSManager Dev](https://github.com/mcsmanager), Apache-2.0 license.
|
||||
|
||||
**Additional Requirements:**
|
||||
|
||||
You must keep all copyright information and place "Powered by MCSManager" in a conspicuous position.
|
||||
|
||||
|
||||
The source code of MCSManager is licensed under the [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) License.
|
||||
|
||||
Copyright ©2025 MCSManager.
|
||||
|
255
README_CN.md
255
README_CN.md
@ -1,255 +0,0 @@
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
|
||||
<br />
|
||||
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[官方网站](http://mcsmanager.com/) | [使用文档](https://docs.mcsmanager.com/) | [团队主页](https://github.com/MCSManager) | [面板端项目](https://github.com/MCSManager/MCSManager) | [网页前端项目](https://github.com/MCSManager/UI) | [守护进程项目](https://github.com/MCSManager/Daemon)
|
||||
|
||||
|
||||
中文 QQ 群:https://jq.qq.com/?_wv=1027&k=Pgl9ScGw
|
||||
|
||||
中文 TG 群:https://t.me/MCSManager_dev
|
||||
|
||||
爱发电赞助:https://afdian.net/a/mcsmanager
|
||||
|
||||
<br />
|
||||
|
||||
## 这是什么?
|
||||
|
||||
|
||||
MCSManager 面板(简称:MCSM 面板)是一款开源,分布式,轻量级,快速部署,支持大部分游戏服务端和控制台程序的管理面板
|
||||
|
||||
<br />
|
||||
|
||||
## 软件特性
|
||||
|
||||
软件在 Minecraft 和其他游戏社区内中已有一定的流行程度,它可以帮助你集中管理多个物理服务器,动态在任何主机上创建游戏服务端,并且提供安全可靠的多用户权限系统,可以很轻松的帮助你管理多个服务器,一直在为 Minecraft,Terraria,Steam 游戏服务器管理员,运维人员和个人开发者提供健康的软件支持。
|
||||
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<img width="1322" alt="QQ20221207-174328@2x" src="https://user-images.githubusercontent.com/18360009/206144481-7f57b40d-f71b-4d7e-a617-846da69ca1a3.png">
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
## 运行环境
|
||||
|
||||
控制面板可运行在 Windows 与 Linux 平台,无需数据库与任何系统配置,只需安装 node 环境即可快速运行,属于轻量级的 Minecraft 服务端控制面板。
|
||||
|
||||
必须 `Node 14.17.0` 以上,无需数据库和更改任何系统配置,开箱即可运行。
|
||||
|
||||
<br />
|
||||
|
||||
## 配置/数据文件
|
||||
|
||||
配置文件: `data/SystemConfig/config.json`
|
||||
|
||||
用户数据文件:`data/User/*.json`
|
||||
|
||||
远程守护进程配置:`data/RemoteServiceConfig/*.json`
|
||||
|
||||
<br />
|
||||
|
||||
## 软件文档
|
||||
|
||||
地址:[https://docs.mcsmanager.com/](https://docs.mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
## 安装
|
||||
|
||||
### Windows
|
||||
|
||||
对于 Windows 系统,**已整合成直接运行版本,下载即可运行**(使用管理员权限运行):
|
||||
|
||||
前往:[https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
**一行命令快速安装**
|
||||
|
||||
```bash
|
||||
// 国内用户专用 gitee 加速源
|
||||
wget -qO- https://gitee.com/mcsmanager/script/raw/master/setup.sh | bash
|
||||
|
||||
// 或原始源(科学上网)
|
||||
wget -qO- https://raw.githubusercontent.com/mcsmanager/Script/master/setup.sh | bash
|
||||
```
|
||||
|
||||
- 脚本仅适用于 AMD64 架构 Ubuntu/Centos/Debian/Archlinux。
|
||||
- 执行完成后,使用 `systemctl start mcsm-{web,daemon}` 即可启动面板服务。
|
||||
- 面板代码与运行环境自动安装在 `/opt/mcsmanager/` 目录下。
|
||||
|
||||
<br />
|
||||
|
||||
**Linux 手动安装**
|
||||
|
||||
- 若一键安装不起作用,则可以尝试此步骤手动安装。
|
||||
|
||||
```bash
|
||||
# 切换到安装目录,没有此目录请执行 mkdir /opt/
|
||||
cd /opt/
|
||||
# 下载运行环境(已有 Node 14+ 可忽略)
|
||||
wget https://npmmirror.com/mirrors/node/v14.17.6/node-v14.17.6-linux-x64.tar.gz
|
||||
# 解压文件
|
||||
tar -zxvf node-v14.17.6-linux-x64.tar.gz
|
||||
# 链接程序到环境变量中
|
||||
ln -s /opt/node-v14.17.6-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v14.17.6-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# 准备安装目录
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# 下载面板端(Web)程序
|
||||
git clone https://github.com/MCSManager/MCSManager-Web-Production.git web
|
||||
cd web
|
||||
# 安装依赖库
|
||||
npm install --production --registry=https://registry.npmmirror.com/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# 下载守护进程(Daemon)程序
|
||||
git clone https://github.com/MCSManager/MCSManager-Daemon-Production.git daemon
|
||||
cd daemon
|
||||
# 安装依赖库
|
||||
npm install --production --registry=https://registry.npmmirror.com/
|
||||
|
||||
# 打开两个终端或两个 Screen 软件的终端窗口
|
||||
# 先启动守护进程
|
||||
cd /opt/mcsmanager/daemon
|
||||
# 启动
|
||||
node app.js
|
||||
|
||||
# 然后启动面板端进程
|
||||
cd /opt/mcsmanager/web
|
||||
# 启动
|
||||
node app.js
|
||||
|
||||
# 访问 http://localhost:23333/ 即可进入面板。
|
||||
# 默认情况下,面板端会自动扫描 daemon 文件夹并且自动连接到守护进程。
|
||||
```
|
||||
|
||||
- 注意,这种安装方式不会自动注册面板到系统服务(Service),所以必须使用 `screen` 软件来管理。
|
||||
|
||||
<br />
|
||||
|
||||
## 更新版本
|
||||
|
||||
参考: https://github.com/MCSManager/MCSManager/wiki/Update-MCSManager
|
||||
|
||||
> 如果你不是特别需要新版本的功能,或者不是为了修复安全隐患,那就不建议更新。
|
||||
|
||||
<br />
|
||||
|
||||
## 项目体系
|
||||
|
||||
整个软件运行需要三个项目的互相配合才可运行,您普通安装的代码是编译再整合后的产物。
|
||||
|
||||
[**控制面板端**](https://github.com/MCSManager/MCSManager)
|
||||
|
||||
- 角色:控制中心
|
||||
- 责任:负责提供网页前端的后端接口,提供 API 接口,用户数据管理和对守护进程进行通信和授权。
|
||||
|
||||
[**网页前端**](https://github.com/MCSManager/UI)
|
||||
|
||||
- 角色:控制中心的用户交互界面
|
||||
- 责任:以网页形式展示数据,发送请求,并且拥有与守护进程通信的能力,此项目最终产物是纯静态文件。
|
||||
|
||||
[**守护进程**](https://github.com/MCSManager/Daemon)
|
||||
|
||||
- 角色:被控端
|
||||
- 责任:控制本地主机的所有实例,真实进程的实际管理者,拥有与任何对象的通信能力。
|
||||
|
||||
<br />
|
||||
|
||||
## 搭建开发环境
|
||||
|
||||
此段落面向开发人员,普通用户无需关注也无需执行。
|
||||
|
||||
所有项目全部以开发环境运行后,便可以进行开发与预览,请务必遵循开源协议。
|
||||
|
||||
**控制面板端(MCSManager)**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
cd MCSManager
|
||||
npm install
|
||||
npm run start
|
||||
# 默认将采用 ts-node 直接执行 Typescript 代码
|
||||
# 默认运行在 23333 端口
|
||||
```
|
||||
|
||||
**网页前端(UI)**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/UI.git
|
||||
cd UI
|
||||
npm install
|
||||
npm run serve
|
||||
# 访问 http://localhost:8080/ 即可预览界面
|
||||
# 所有 API 请求将自动转发到 23333 端口
|
||||
```
|
||||
|
||||
**守护进程(Daemon)**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/Daemon.git
|
||||
cd Daemon
|
||||
npm install
|
||||
npm run start
|
||||
# 运行后请在控制面板端连接本守护进程
|
||||
# 默认运行在 24444 端口
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- 支持 `Chrome` `Firefox` `Safari` `Opera` 等现代主流浏览器。
|
||||
- 已放弃支持 `IE` 浏览器。
|
||||
|
||||
<br />
|
||||
|
||||
## 国际化
|
||||
|
||||
MCSManager 已支持中文,英文两种语言,已经实现国际化全面覆盖。
|
||||
|
||||
软件国际化由 [Lazy](https://github.com/LazyCreeper),[KevinLu2000](https://github.com/KevinLu2000),[zijiren233](https://github.com/zijiren233) 和 [Unitwk](https://github.com/unitwk) 共同完成
|
||||
|
||||
<br />
|
||||
|
||||
## 贡献
|
||||
|
||||
如果你在使用过程中发现任何问题,可以 [提交 Issue](https://github.com/MCSManager/MCSManager/issues/new/choose) 或自行 Fork 修改后提交 Pull Request。
|
||||
|
||||
代码需要保持现有格式,不得格式化多余代码,具体可[参考这里](https://github.com/MCSManager/MCSManager/issues/544)。
|
||||
|
||||
<br />
|
||||
|
||||
## 问题报告
|
||||
|
||||
欢迎发现的任何问题进行反馈,必当及时修复。
|
||||
|
||||
若发现严重安全漏洞又不便公开发布,请发送邮件至: mcsmanager-dev@outlook.com。
|
||||
|
||||
安全问题修复后将在代码中附加漏洞发现者姓名。
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## 源代码协议
|
||||
|
||||
遵循 Apache-2.0 协议。
|
||||
|
||||
**我们对源代码有额外的限制,你必须保留所有版权文字(带Copyright),并在明显位置附上 Powered by MCSManager。**
|
||||
|
||||
版权所有 2022 MCSManager 开发团队。
|
224
README_DE.md
Normal file
224
README_DE.md
Normal file
@ -0,0 +1,224 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">MCSManager Panel</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[Offizielle Website](http://mcsmanager.com/) | [Dokumentation](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[Englisch](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanisch](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## Was ist MCSManager?
|
||||
|
||||
**MCSManager Panel** (MCSM) ist ein **modernes, sicheres und verbreitetes Verwaltungspanel**, designt für die Verwaltung von Minecraft und Steam Spielserver.
|
||||
|
||||
MCSManager hat bereits eine gewisse Popularität in der Community erlangt, insbesondere durch Minecraft. MCSManager zeichnet sich durch eine zentralisierte Verwaltungslösung für mehrere Serverinstanzen aus und bietet ein sicheres und zuverlässiges Berechtigungssystem für mehrere Benutzer. Darüber hinaus engagieren wir uns für die Unterstützung von Serveradministratoren, nicht nur für Minecraft, sondern auch für Terraria und verschiedene Steam Spiele. Unser Ziel ist es, eine florierende und unterstützende Community für die Verwaltung von Spielservern zu fördern.
|
||||
|
||||
MCSManager **unterstützt Englisch, Französisch, Deutsch, Italienisch, Japanisch, Portugiesisch, Chinesisch (Vereinfacht) und Chinesisch (Traditionell)**, mit Plänen mehr Sprachen in der Zukunft zu unterstützen!
|
||||
|
||||
**Terminal**
|
||||
|
||||

|
||||
|
||||
**Instanzliste**
|
||||
|
||||

|
||||
|
||||
**Benutzerdefiniertes Layout**
|
||||
|
||||

|
||||
|
||||
## Funktionen
|
||||
|
||||
1. Ein-Klick-Bereitstellung von `Minecraft` Java/Bedrock Servers
|
||||
2. Kompatibel mit den meisten `Steam` Spielservern. (z.B. `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.)
|
||||
3. Anpassbare Benutzeroberfläche, erstellen Sie Ihr eigenes Layout
|
||||
4. Unterstützt alle Images auf `Docker Hub`, unterstützt mehrere Benutzer und unterstützt kommerzielle Dienste!
|
||||
5. Verwalten Sie mehrere Server mit einer einzigen Weboberfläche
|
||||
6. Der Technologie-Stack ist einfach, und Sie müssen nur gut in Typescript sein, um die gesamte MCSManager-Entwicklung abzuschließen.
|
||||
7. Und mehr!
|
||||
|
||||
<br />
|
||||
|
||||
## Laufzeitumgebung
|
||||
|
||||
MCSM unterstützt `Windows` und `Linux`. Die einzige Anforderung ist `Node.js` und ein Paar andere Bibliotheken **für die Dekomprimierung**.
|
||||
|
||||
Benötigt [Node.js 16.20.2](https://nodejs.org/en) oder höher.
|
||||
|
||||
<br />
|
||||
|
||||
## Installation
|
||||
|
||||
### Windows
|
||||
|
||||
Für Windows, stellen wir gepackte ausführbare Dateien zur Verfügung:
|
||||
|
||||
Gehe zu: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**Bereitstellung mit einem Befehl**
|
||||
|
||||
> Das Skript muss Systemdienste registrieren und benötigt daher Root-Berechtigungen.
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**Verwendung**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Unterstützt nur Ubuntu/Centos/Debian/Archlinux.
|
||||
- Installationsort: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Linux - Manuelle Installation**
|
||||
|
||||
- Wenn das Installationsskript nicht ordnungsgemäß ausgeführt wird, können Sie versuchen, es manuell zu installieren.
|
||||
|
||||
```bash
|
||||
# Erstellen des /opt Verzeichnisses, wenn es nicht bereits existiert
|
||||
mkdir /opt
|
||||
# Wechseln zu /opt/
|
||||
cd /opt/
|
||||
# Node.js 20.11 herunterladen. Wenn Sie bereits Node.js 16+ installiert haben, können sie diesen Schritt ignorieren.
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Node.js entpacken
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Node.js zum System PATH hinzufügen
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# MCSM's Installationsort vorbereiten
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# MCSManager herunterladen
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
./install.sh
|
||||
|
||||
# Bitte öffnen Sie zwei Terminals oder Screens.
|
||||
|
||||
# Starten Sie den Daemon zuerst.
|
||||
./start-daemon.sh
|
||||
|
||||
# Starten Sie anschließend das Web-Interface im zweiten Terminal oder Screen.
|
||||
./start-web.sh
|
||||
|
||||
# Gehen Sie für den Webzugriff zu http://localhost:23333/
|
||||
# Im Allgemeinen scannt das Webinterface automatisch den lokalen Daemon und fügt ihn hinzu.
|
||||
```
|
||||
|
||||
Bei diesem Installationsansatz wird MCSManager nicht automatisch als Systemdienst eingerichtet. Daher ist es notwendig, "Screen" für die Verwaltung zu verwenden. Wenn Sie daran interessiert sind, MCSManager über einen Systemdienst zu verwalten, lesen Sie bitte unser Wiki/unsere Dokumentation.
|
||||
|
||||
<br />
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Dieser Abschnitt wurde speziell für Entwickler entwickelt. Allgemeine Benutzer können diesen Teil ohne Bedenken ignorieren.
|
||||
|
||||
### Plugins
|
||||
|
||||
Wir verwenden "VS Code", um MCSManager zu entwickeln. Möglicherweise müssen Sie diese Plugins installieren:
|
||||
|
||||
- i18n display support (I18n Ally)
|
||||
- Code formatter (Prettier)
|
||||
- Vue - Offcial
|
||||
- ESLint
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Sie müssen zu den Projekten [PTY](https://github.com/MCSManager/PTY) und [Zip-Tools](https://github.com/MCSManager/Zip-Tools) gehen, um die entsprechenden Binärdateien herunterzuladen und sie im Verzeichnis 'daemon/lib' abzulegen, um das ordnungsgemäße Funktionieren des `Emulation Terminals` und der `File Decompression` sicherzustellen.
|
||||
|
||||
### Produktionsversion erstellen
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Ausgabe-Verzeichnis: "production-code"
|
||||
|
||||
<br />
|
||||
|
||||
## Code beitragen
|
||||
|
||||
Sollten Sie Probleme bei der Nutzung von MCSManager haben, können Sie gerne [ein Issue](https://github.com/MCSManager/MCSManager/issues/new/choose) einreichen. Alternativ können Sie das Projekt forken und direkt beitragen, indem Sie einen Pull Request einreichen.
|
||||
|
||||
Bitte stellen Sie sicher, dass der eingereichte Code unserem bestehenden Codierungsstil entspricht. Weitere Informationen finden Sie in den Richtlinien in [diesem Issue](https://github.com/MCSManager/MCSManager/issues/544).
|
||||
|
||||
<br />
|
||||
|
||||
## Browser Kompatibilität
|
||||
|
||||
- Wird in modernen Browsern wie `Chrome`, `Firefox` und `Safari` unterstützt.
|
||||
- Die Unterstützung für `IE` wurde eingestellt.
|
||||
|
||||
<br />
|
||||
|
||||
## BUG Reporting
|
||||
|
||||
**Issue erstellen:** [Hier drücken](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**Bericht über Sicherheitslücken:** [SECURITY.md](SECURITY.md)
|
||||
|
||||
<br />
|
||||
|
||||
## Internationalisierung
|
||||
|
||||
Vielen Dank an diese Mitwirkenden für die Bereitstellung einer beträchtlichen Menge an Übersetzungen:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## Lizens
|
||||
|
||||
Der Quellcode von MCSManager ist unter der [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) Lizenz lizenziert.
|
||||
|
||||
Copyright ©2025 MCSManager.
|
199
README_ES.md
Normal file
199
README_ES.md
Normal file
@ -0,0 +1,199 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">Panel de MCSManager</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[Sitio Oficial](http://mcsmanager.com/) | [Documentación](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[Inglés](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) | [日本語](README_JP.md)
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## ¿Qué es MCSManager?
|
||||
|
||||
**Panel de MCSManager** (MCSM) es un **panel de control moderno, seguro y distribuido** diseñado para gestionar servidores de juego de Minecraft y Steam.
|
||||
|
||||
MCSManager ha ganado popularidad en la comunidad, especialmente en Minecraft. MCSManager ofrece una solución centralizada para gestionar múltiples instancias de servidor y proporciona un sistema de permisos multiusuario seguro y confiable. Nos comprometemos a apoyar a los administradores de servidores no solo para Minecraft, sino también para Terraria y varios juegos de Steam. Nuestro objetivo es fomentar una comunidad próspera y de apoyo en la gestión de servidores de juego.
|
||||
|
||||
MCSManager **admite inglés, francés, alemán, italiano, japonés, portugués, chino simplificado y chino tradicional**, ¡y planea agregar más idiomas en el futuro!
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Características
|
||||
|
||||
1. Implementación con un clic de servidor `Minecraft` Java/Bedrock.
|
||||
2. Compatible con la mayoría de servidores de juegos de `Steam` (p. ej., `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.).
|
||||
3. Interfaz personalizable; crea tu propio diseño.
|
||||
4. Soporte para virtualización con `Docker`, multiusuario y servicios comerciales.
|
||||
5. Gestiona múltiples servidores desde una sola interfaz web.
|
||||
6. ¡Y más!
|
||||
|
||||
<br />
|
||||
|
||||
## Entorno de Ejecución
|
||||
|
||||
MCSM es compatible con `Windows` y `Linux`. El único requisito es `Node.js` y algunas librerías **para descompresión**.
|
||||
|
||||
Requiere [Node.js 16.20.2](https://nodejs.org/en) o superior.
|
||||
|
||||
<br />
|
||||
|
||||
## Instalación
|
||||
|
||||
### Windows
|
||||
|
||||
Para Windows, ofrecemos archivos ejecutables empaquetados:
|
||||
|
||||
Ir a: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**Despliegue con un solo comando**
|
||||
|
||||
> El script necesita registrar servicios del sistema, requiere permisos de root.
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**Uso**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Solo compatible con Ubuntu/Centos/Debian/Archlinux.
|
||||
- Directorio de instalación: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Instalación Manual en Linux**
|
||||
|
||||
- Si el script de instalación falla, puedes intentar instalarlo manualmente.
|
||||
|
||||
```bash
|
||||
# Crear directorio /opt si no existe
|
||||
mkdir /opt
|
||||
# Cambiar a /opt
|
||||
cd /opt/
|
||||
# Descargar Node.js 20.11. Si ya tienes Node.js 16+ instalado, omite este paso.
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Descomprimir Node.js
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Agregar Node.js al PATH del sistema
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# Preparar el directorio de instalación de MCSM
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# Descargar MCSManager
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# Instalar dependencias
|
||||
./install.sh
|
||||
|
||||
# Abrir dos terminales o pantallas.
|
||||
|
||||
# Iniciar el daemon primero.
|
||||
./start-daemon.sh
|
||||
|
||||
# Iniciar la interfaz web en la segunda terminal o pantalla.
|
||||
./start-web.sh
|
||||
|
||||
# Para acceder a la web, ir a http://localhost:23333/
|
||||
# En general, la interfaz web escaneará y añadirá automáticamente el daemon local.
|
||||
```
|
||||
|
||||
Este método de instalación no configura automáticamente MCSManager como un servicio del sistema. Por lo tanto, es necesario usar `screen` para la administración. Para quienes quieran administrar MCSManager a través de un servicio del sistema, por favor consulta nuestra wiki/documentación.
|
||||
|
||||
<br />
|
||||
|
||||
## Compatibilidad del Navegador
|
||||
|
||||
- Compatible con navegadores modernos como `Chrome`, `Firefox` y `Safari`.
|
||||
- El soporte para `IE` ha sido discontinuado.
|
||||
|
||||
<br />
|
||||
|
||||
## Desarrollo
|
||||
|
||||
Esta sección está dirigida específicamente a desarrolladores. Los usuarios generales pueden ignorarla sin problema.
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Construir Versión de Producción
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Luego, deberás ir a los proyectos [PTY](https://github.com/MCSManager/PTY) y [Zip-Tools](https://github.com/MCSManager/Zip-Tools) para descargar los archivos binarios correspondientes y colocarlos en el directorio `daemon/lib` para asegurar el funcionamiento adecuado del `Terminal de Emulación` y la `Descompresión de Archivos`.
|
||||
|
||||
<br />
|
||||
|
||||
## Contribución de Código
|
||||
|
||||
Si experimentas problemas al usar MCSManager, puedes [enviar un Issue](https://github.com/MCSManager/MCSManager/issues/new/choose). Alternativamente, puedes hacer un fork del proyecto y contribuir directamente enviando un Pull Request.
|
||||
|
||||
Asegúrate de que el código enviado siga nuestro estilo de codificación existente. Para más detalles, consulta las pautas en [este issue](https://github.com/MCSManager/MCSManager/issues/544).
|
||||
|
||||
<br />
|
||||
|
||||
## Reporte de Errores
|
||||
|
||||
**Abrir Issue:** [Haz clic aquí](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**Reporte de Vulnerabilidades de Seguridad:** [SECURITY.md](SECURITY.md) (en ingles)
|
||||
|
||||
<br />
|
||||
|
||||
## Internacionalización
|
||||
|
||||
Gracias a estos colaboradores por proporcionar una gran cantidad de traducciones:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## Licencia
|
||||
El código fuente de MCSManager está licenciado bajo la [Licencia Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
Copyright ©2025 MCSManager.
|
205
README_JP.md
Normal file
205
README_JP.md
Normal file
@ -0,0 +1,205 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">MCSManager パネル</a>
|
||||
</h1>
|
||||
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[HP](http://mcsmanager.com/) | [ドキュメント](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
|
||||
## MCSManagerとは?
|
||||
|
||||
**MCSManager パネル**は、マイクラフト(Minecraft)やsteamのゲームサーバーを管理するために開発されました。このパネルは、モダンで安全な分散型コントロールパネルです。
|
||||
|
||||
MCSManagerは、マイクラフトと中心としてコミュニティ内で既に一定の人気を得ています。このパネルは、複数のサーバーインスタンスの集中管理ソリューションを提供し、安全で信頼できるマルチユーザー許可システムも提供します。まだ、マイクラフトだけでなく、Terrariaや色々なSteamゲームサーバーの管理者をサポートします。私たちの目標は、ゲームサーバー管理のために別なコミュニティを育成します。
|
||||
|
||||
今まで、MCSManagerは英語、フランス語、ドイツ語、イタリア語、日本語、簡体中国語、繫体中国語をサポートしており、将来は多くの言語のサポートを追加する予定です。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 特徴
|
||||
|
||||
1. シンプルなマイクラフトサーバーをデプロイメントができます。
|
||||
2. 他のゲームサーバーもコンパチブルです。(例:`Palworld`, `Squad`, `Project Zomboid`, `Terraria`)
|
||||
3. 個人的なUIをカスタマイズすることができます。
|
||||
4. `Docker`デプロイすると商用利用ができます。
|
||||
5. 一つウェブでマルチサーバー管理することができます。
|
||||
6. もっと
|
||||
|
||||
<br />
|
||||
|
||||
## ランタイム環境
|
||||
|
||||
MCSMパネルは`Windows`や`Linux`システムをサポートして、`Node.js`が必要です。
|
||||
|
||||
`Node.js`は16.20.2以上が必要です。
|
||||
|
||||
<br />
|
||||
|
||||
## インスタレーション
|
||||
|
||||
### Windows
|
||||
|
||||
[MCSM HP](https://mcsmanager.com)でファイルパックをダウンロードしって、`.exe`ファイルを実行するです。
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**シンプルデプロイ**
|
||||
|
||||
> スクリプトは自動的にシステムサービスを登録して、実行するときはルートパーミッションが必要です。
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**コマンド**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Ubuntu/Centos/Debian/Archlinuxしかサポートしてないです。
|
||||
- インストールパス: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Linux マニュアルのインストール**
|
||||
|
||||
- スクリプト実行するときでエロがあったら、マニュアルでインストールもできます。
|
||||
|
||||
```bash
|
||||
# /opt パスなかったら、パスを作成します
|
||||
mkdir /opt
|
||||
# /opt パスを開けます
|
||||
cd /opt/
|
||||
# Node.js 20.11 ダウンロードします。Node.js 16+ あったら、しなくていいです。
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Node.jsソースの解凍
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Node.jsをシステムパス追加します
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# MCSMのインストールパスを準備します
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# MCSMダウンロードします
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# 必要なアプリを自動的にインストールします
|
||||
|
||||
./install.sh
|
||||
|
||||
# 二つターミナル用意してください
|
||||
|
||||
# まず、daemon実行します。
|
||||
./start-daemon.sh
|
||||
|
||||
# daemon実行したら、二つ目のターミナルでウェブを実行します。
|
||||
./start-web.sh
|
||||
|
||||
# ウェブのアドレスは http://localhost:23333/
|
||||
# ウェブは自動的に同じデバイスインストールしたdaemonを検測して、追加します。
|
||||
```
|
||||
|
||||
マニュアルインストールは、自動的にシステムサービス追加しません。そして、自分でシステムサービス追加するか`screen`しないといけないです。システム追加する方法は、グーグルやウィキペディアで調べてください。
|
||||
|
||||
<br />
|
||||
|
||||
## ブラウザの互換性
|
||||
|
||||
- Supported on modern browsers including `Chrome`, `Firefox`, and `Safari`.
|
||||
- `Chrome`と`Firefox`と`Safari`がサポートしています。
|
||||
- Support for `IE` has been discontinued.
|
||||
- 現在は、`IE`のサポートしていません。
|
||||
|
||||
<br />
|
||||
|
||||
## 開発
|
||||
|
||||
以下の情報は、開発者関係のみです。普通のユーザーとは関係ないです。
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Build Production Version
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
そのあとは、[PTY](https://github.com/MCSManager/PTY)と[Zip-Tools](https://github.com/MCSManager/Zip-Tools) ダウンロードして、解凍して、対応の`daemon/lib`パスへ移動してください。
|
||||
|
||||
<br />
|
||||
|
||||
## コード貢献
|
||||
|
||||
問題と意見あったら、GitHubのissueへ提出してください。プロジェクトforkしたら、そして、Pull Requestへcommit出して貢献することができます。
|
||||
|
||||
もし、貢献するは、コードのスタイルが現在と似てっる感じしてください。詳細は、[ガイドライン](https://github.com/MCSManager/MCSManager/issues/544)見てください。
|
||||
|
||||
<br />
|
||||
|
||||
## BUG レポート
|
||||
|
||||
**IssueとBUG:** [押してください](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**安全関係のレポート:** [SECURITY.md](SECURITY.md)
|
||||
|
||||
<br />
|
||||
|
||||
## 国際化
|
||||
|
||||
以下の人は、多量の翻訳してありがどうございます:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## ライセンス
|
||||
|
||||
ソースコードは、 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)ライセンス使っています。
|
||||
|
||||
Copyright ©2025 MCSManager.
|
204
README_PTBR.md
Normal file
204
README_PTBR.md
Normal file
@ -0,0 +1,204 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">Painel MCSManager</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[Website Oficial](http://mcsmanager.com/) | [Documentação](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
|
||||
## O que é o MCSManager?
|
||||
|
||||
O **Painel MCSManager** (MCSM) é um **painel de controle moderno, seguro e distribuído** projetado para gerenciar servidores de jogos Minecraft e Steam.
|
||||
|
||||
O MCSManager já conquistou um certo nível de popularidade dentro da comunidade, especificamente a de Minecraft. O MCSManager se destaca por oferecer uma solução de gerenciamento centralizado para várias instâncias de servidores e proporciona um sistema de permissões multiusuário seguro e confiável. Além disso, estamos comprometidos em apoiar os administradores de servidores não apenas para Minecraft, mas também para Terraria e diversos jogos da Steam. Nosso objetivo é fomentar uma comunidade próspera e colaborativa para o gerenciamento de servidores de jogos.
|
||||
|
||||
O MCSManager **suporta Alemão, Chinês Simplificado, Chinês Tradicional, Inglês, Francês, Italiano, Japonês e Português**, com planos de adicionar suporte para mais idiomas no futuro!
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
1. Implantação com um clique do servidor `Minecraft` Java/Bedrock
|
||||
2. Compatível com a maioria dos servidores de jogos da `Steam` (ex.: `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.)
|
||||
3. Interface personalizável, crie seu próprio layout
|
||||
4. Suporte à virtualização `Docker`, multiusuário e serviços comerciais
|
||||
5. Gerencie múltiplos servidores com uma única interface web
|
||||
6. E mais...
|
||||
|
||||
<br />
|
||||
|
||||
## Ambiente de Execução
|
||||
|
||||
O MCSM suporta tanto `Windows` quanto `Linux`. O único requisito é `Node.js` e algumas bibliotecas **para descompressão**.
|
||||
|
||||
Requer [Node.js 16.20.2](https://nodejs.org/en) ou superior.
|
||||
|
||||
<br />
|
||||
|
||||
## Instalação
|
||||
|
||||
### Windows
|
||||
|
||||
Para Windows, fornecemos arquivos executáveis empacotados:
|
||||
|
||||
Acesse: [https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**Implantação com um Comando**
|
||||
|
||||
> O script precisa registrar serviços do sistema, portanto, requer permissões de root.
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**Uso**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon}
|
||||
systemctl stop mcsm-{web,daemon}
|
||||
```
|
||||
|
||||
- Suporta apenas Ubuntu/Centos/Debian/Archlinux.
|
||||
- Diretório de instalação: `/opt/mcsmanager/`.
|
||||
|
||||
<br />
|
||||
|
||||
**Instalação Manual no Linux**
|
||||
|
||||
- Se o script de instalação falhou ao ser executado corretamente, você pode tentar instalar manualmente.
|
||||
|
||||
```bash
|
||||
# Criar o diretório /opt se ainda não existir
|
||||
mkdir /opt
|
||||
# Mudar para o diretório /opt
|
||||
cd /opt/
|
||||
# Baixar o Node.js 20.11. Se você já tiver o Node.js 16+ instalado, ignore este passo.
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# Descompactar o código-fonte do Node.js
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# Adicionar o Node.js ao PATH do sistema
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# Preparar o diretório de instalação do MCSM
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# Baixar o MCSManager
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# Instalar dependências
|
||||
./install.sh
|
||||
|
||||
# Por favor, abra dois terminais ou telas separadas.
|
||||
|
||||
# Inicie o daemon primeiro.
|
||||
./start-daemon.sh
|
||||
|
||||
# Inicie a interface web no segundo terminal ou tela.
|
||||
./start-web.sh
|
||||
|
||||
# Para acessar a web, vá para http://localhost:23333/
|
||||
# Geralmente, a interface web irá escanear e adicionar automaticamente o daemon local.
|
||||
```
|
||||
|
||||
Este método de instalação não configura automaticamente o MCSManager como um serviço do sistema. Portanto, é necessário usar `screen` para gerenciamento. Para aqueles interessados em gerenciar o MCSManager por meio de um serviço do sistema, consulte nossa wiki/documentação.
|
||||
|
||||
<br />
|
||||
|
||||
## Compatibilidade com Navegadores
|
||||
|
||||
- Suportado em navegadores modernos, incluindo `Chrome`, `Firefox` e `Safari`.
|
||||
- O suporte ao `IE` foi descontinuado.
|
||||
|
||||
<br />
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
Esta seção é especificamente projetada para desenvolvedores. Usuários comuns podem ignorar esta parte sem preocupações.
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### Build Production Version
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
Em seguida, você precisará acessar os projetos [PTY](https://github.com/MCSManager/PTY) e [Zip-Tools](https://github.com/MCSManager/Zip-Tools) para baixar os arquivos binários correspondentes e colocá-los no diretório `daemon/lib` para garantir o funcionamento adequado do `Terminal de Emulação` e `Descompressão de Arquivos`.
|
||||
|
||||
<br />
|
||||
|
||||
## Contribuição de Código
|
||||
|
||||
Se você encontrar algum problema ao usar o MCSManager, sinta-se à vontade para [enviar um Issue](https://github.com/MCSManager/MCSManager/issues/new/choose). Alternativamente, você pode fazer um fork do projeto e contribuir diretamente enviando um Pull Request.
|
||||
|
||||
Por favor, certifique-se de que qualquer código enviado siga o estilo de codificação existente. Para mais detalhes, consulte as diretrizes fornecidas [neste issue](https://github.com/MCSManager/MCSManager/issues/544).
|
||||
|
||||
<br />
|
||||
|
||||
## Relatório de BUGs
|
||||
|
||||
**Criar Issue:** [Clique aqui](https://github.com/MCSManager/MCSManager/issues/new/choose)
|
||||
|
||||
**Relatório de Vulnerabilidade de Segurança:** [SECURITY.md](SECURITY.md)
|
||||
|
||||
<br />
|
||||
|
||||
## Internacionalização
|
||||
|
||||
Agradecemos aos seguintes colaboradores por fornecerem uma quantidade substancial de tradução:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## Licença
|
||||
|
||||
O código-fonte do MCSManager é licenciado sob a Licença [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
Copyright ©2025 MCSManager.
|
218
README_TW.md
Normal file
218
README_TW.md
Normal file
@ -0,0 +1,218 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">MCSManager Panel</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[官方網站](http://mcsmanager.com/) | [教學說明](https://docs.mcsmanager.com/#/zh-cn/) | [TG 群組](https://t.me/MCSManager_dev) | [成為贊助者](https://afdian.net/a/mcsmanager)
|
||||
|
||||
[English](README.md) | [简体中文](README_ZH.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## 這是什麼?
|
||||
|
||||
**MCSManager 面板**(簡稱:MCSM 面板)是一款免費,開源,分散式,輕量級,快速部署,支援 Minecraft 和 Steam 遊戲伺服器的 Web 管理面板。
|
||||
|
||||
此軟體在 `Minecraft` 和其他遊戲社群內中已有一定的流行程度,它可以幫助你集中管理多個實體伺服器,實現在任何主機上建立遊戲伺服器,並且提供安全可靠的多使用者權限系統,可以很輕鬆的幫助你管理多個伺服器,一直在為 `Minecraft`,`Terraria` 和 `Steam` 遊戲伺服器的管理員,維護人員和個人開發者提供健康的軟體支援。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<br />
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. 支援快速開服! 輕鬆部署 `Minecraft` Java 版/基岩版遊戲伺服器。
|
||||
2. 相容於大部分 `Steam` 遊戲伺服器,例如 `幻獸帕魯`,`Squad`,`Project Zomboid` 和 `Terraria` 等。
|
||||
3. 網頁支援拖拉式的小卡片佈局,打造自己喜歡的介面佈局。
|
||||
4. 支援 `Docker` 虛擬化,支援多使用者,支援商業出租行為。
|
||||
5. 支援所有 `Docker` 映像,輕鬆打造預設!
|
||||
6. 支援分散式,一個網頁即可同時管理數台機器。
|
||||
7. 更多...
|
||||
|
||||
<br />
|
||||
|
||||
## 執行環境
|
||||
|
||||
控制面板可執行在 `Windows` 與 `Linux` 平台,無需安裝資料庫,只需安裝 `Node.js` 環境和幾個**用於解壓縮**的指令。
|
||||
|
||||
必須使用 [Node.js 16.20.2](https://nodejs.org/en) 以上,建議使用最新版本 LTS 版本。
|
||||
|
||||
<br />
|
||||
|
||||
## 安裝
|
||||
|
||||
### Windows
|
||||
|
||||
對於 Windows 系統,**已整合成快速啟動版本,下載即可執行**:
|
||||
|
||||
前往:[https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**快速安裝**
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
|
||||
```
|
||||
|
||||
**安裝後的使用方法**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon} # 開啟面板
|
||||
systemctl stop mcsm-{web,daemon} # 關閉面板
|
||||
|
||||
```
|
||||
|
||||
- 快速安裝指令只適用於 Ubuntu/Centos/Debian/Archlinux。
|
||||
- 面板程式碼與執行環境自動安裝在 `/opt/mcsmanager/` 資料夾下。
|
||||
|
||||
<br />
|
||||
|
||||
**Linux 手動安裝**
|
||||
|
||||
- 若快速安裝無法使用,則可以嘗試此步驟手動安裝。
|
||||
|
||||
```bash
|
||||
|
||||
# 切換到安裝資料夾。 如果沒有這個資料夾,請先輸入 mkdir /opt/ 新增它。
|
||||
|
||||
cd /opt/
|
||||
|
||||
# 下載執行環境(Node.js)。 如果你已經安裝了 Node.js 16+,請忽略此步驟。
|
||||
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
|
||||
# 解壓縮檔案
|
||||
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
|
||||
# 新增程式到系統環境變數
|
||||
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# 準備好安裝資料夾
|
||||
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# 下載 MCSManager
|
||||
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# 安裝函式庫
|
||||
|
||||
./install.sh
|
||||
|
||||
# 請開啟兩個終端機或 screen
|
||||
|
||||
# 先啟動節點程式
|
||||
|
||||
./start-daemon.sh
|
||||
|
||||
# 啟動網頁服務(在第二個終端機或 screen)
|
||||
|
||||
./start-web.sh
|
||||
|
||||
# 進入 http://localhost:23333/ 開始使用
|
||||
|
||||
# 一般來說,網頁服務會自動掃描並連接到本機節點。
|
||||
|
||||
```
|
||||
|
||||
這種安裝方式不會自動註冊系統服務(Service),所以必須使用 `screen` 軟體來管理,如果你希望由系統服務來接管 MCSManager,請參考文件。
|
||||
|
||||
<br />
|
||||
|
||||
## 瀏覽器相容性
|
||||
|
||||
- 支援 `Chrome` `Firefox` `Safari` `Opera` 等現代主流瀏覽器。
|
||||
- 已放棄支援 `IE` 瀏覽器。
|
||||
|
||||
<br />
|
||||
|
||||
## 建立開發環境
|
||||
|
||||
這是給開發人員使用的,一般使用者可直接跳過這個環節。
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
|
||||
```
|
||||
|
||||
### 編譯正式環境版本
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
|
||||
```
|
||||
|
||||
接下來你還需要前往 [PTY](https://github.com/MCSManager/PTY) 和 [Zip-Tools](https://github.com/MCSManager/Zip-Tools) 兩個專案下載對應的檔案,將他們存放到 `daemon/lib` 目錄下,以確保 `模擬終端機` 和 `解壓縮檔案` 正常運作。
|
||||
|
||||
<br />
|
||||
|
||||
## 貢獻程式碼
|
||||
|
||||
如果你在使用過程中發現任何問題,可以 [提交 Issue](https://github.com/MCSManager/MCSManager/issues/new/choose) 或自行 Fork 修改後提交 Pull Request。
|
||||
|
||||
程式碼需保持現有格式,不得刪除多餘程式碼,具體可[參考這裡](https://github.com/MCSManager/MCSManager/issues/544)。
|
||||
|
||||
<br />
|
||||
|
||||
## 回報 BUG
|
||||
|
||||
歡迎回報發現到的任何問題,將會快速修復。
|
||||
|
||||
若發現嚴重安全漏洞又不方便公開發布,請 E-mail 至: support@mcsmanager.com,安全問題修復後將在程式碼中附加漏洞發現者姓名。
|
||||
|
||||
<br />
|
||||
|
||||
## 國際化
|
||||
|
||||
感謝下列成員提供的**大量**翻譯檔案:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## 原始碼協定
|
||||
|
||||
原始碼遵循 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 協定。
|
||||
|
||||
Copyright ©2025 MCSManager.
|
212
README_ZH.md
Normal file
212
README_ZH.md
Normal file
@ -0,0 +1,212 @@
|
||||
<div align="center">
|
||||
<a href="https://mcsmanager.com/" target="_blank">
|
||||
<img src="https://public-link.oss-cn-shenzhen.aliyuncs.com/mcsm_picture/logo.png" alt="MCSManagerLogo.png" width="510px" />
|
||||
</a>
|
||||
|
||||
<br />
|
||||
|
||||
<h1 id="mcsmanager">
|
||||
<a href="https://mcsmanager.com/" target="_blank">MCSManager Panel</a>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/MCSManager)
|
||||
[](https://www.npmjs.com/)
|
||||
[](https://nodejs.org/en/download/)
|
||||
[](https://github.com/MCSManager)
|
||||
|
||||
[官方网站](http://mcsmanager.com/) | [使用文档](https://docs.mcsmanager.com/#/zh-cn/) | [QQ 群](https://jq.qq.com/?_wv=1027&k=Pgl9ScGw) | [TG 群](https://t.me/MCSManager_dev) | [成为赞助者](https://afdian.net/a/mcsmanager)
|
||||
|
||||
[English](README.md) | [繁體中文](README_TW.md) | [Deutsch](README_DE.md) | [Português BR](README_PTBR.md) |
|
||||
[日本語](README_JP.md) | [Spanish](README_ES.md)
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## 这是什么?
|
||||
|
||||
**MCSManager 面板**(简称:MCSM 面板)是一款免费,开源,分布式,轻量级,快速部署,支持 Minecraft 和 Steam 游戏服务器的 Web 管理面板。
|
||||
|
||||
此软件在 `Minecraft` 和其他游戏社区内中已有一定的流行程度,它可以帮助你集中管理多个物理服务器,实现在任何主机上创建游戏服务器,并且提供安全可靠的多用户权限系统,可以很轻松的帮助你管理多个服务器,一直在为 `Minecraft`,`Terraria` 和 `Steam` 游戏服务器的管理员,运维人员和个人开发者提供健康的软件支持。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
<br />
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. 支持一键开服!轻松部署 `Minecraft` Java 版/基岩版游戏服务器。
|
||||
2. 兼容大部分 `Steam` 游戏服务器,列如 `幻兽帕鲁`,`战术小队`,`僵尸毁灭工程` 和 `泰拉瑞亚` 等。
|
||||
3. 网页支持拖拽式的小卡片布局,打造自己喜欢的界面布局。
|
||||
4. 支持 `Docker Hub` 上的所有镜像,支持多用户,支持商业服务。
|
||||
5. 支持分布式,一个网页即可同时管理数台机器。
|
||||
6. 技术栈简单,仅需擅长 Typescript 即可完成整个 MCSManager 开发!
|
||||
7. 更多...
|
||||
|
||||
<br />
|
||||
|
||||
## 运行环境
|
||||
|
||||
控制面板可运行在 `Windows` 与 `Linux` 平台,无需安装数据库,只需安装 `Node.js` 环境和几个**用于解压缩**的命令。
|
||||
|
||||
必须使用 [Node.js 16.20.2](https://nodejs.org/en) 以上,推荐使用最新版本 LTS 版本。
|
||||
|
||||
<br />
|
||||
|
||||
## 安装
|
||||
|
||||
### Windows
|
||||
|
||||
对于 Windows 系统,**已整合成直接运行版本,下载即可运行**:
|
||||
|
||||
前往:[https://mcsmanager.com/](https://mcsmanager.com/)
|
||||
|
||||
<br />
|
||||
|
||||
### Linux
|
||||
|
||||
**一行命令快速安装**
|
||||
|
||||
```bash
|
||||
sudo su -c "wget -qO- https://script.mcsmanager.com/setup_cn.sh | bash"
|
||||
```
|
||||
|
||||
**安装后的使用方法**
|
||||
|
||||
```bash
|
||||
systemctl start mcsm-{web,daemon} # 开启面板
|
||||
systemctl stop mcsm-{web,daemon} # 关闭面板
|
||||
```
|
||||
|
||||
- 脚本仅适用于 Ubuntu/Centos/Debian/Archlinux。
|
||||
- 面板代码与运行环境自动安装在 `/opt/mcsmanager/` 目录下。
|
||||
|
||||
<br />
|
||||
|
||||
**Linux 手动安装**
|
||||
|
||||
- 若一键安装不起作用,则可以尝试此步骤手动安装。
|
||||
|
||||
```bash
|
||||
# 切换到安装目录。如果不存在,请提前用'mkdir /opt/'创建它。
|
||||
cd /opt/
|
||||
# 下载运行时环境(Node.js)。如果你已经安装了Node.js 16+,请忽略此步骤。
|
||||
wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
|
||||
# 解压档案
|
||||
tar -xvf node-v20.11.0-linux-x64.tar.xz
|
||||
# 添加程序到系统环境变量
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
|
||||
ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
|
||||
|
||||
# 准备好安装目录
|
||||
mkdir /opt/mcsmanager/
|
||||
cd /opt/mcsmanager/
|
||||
|
||||
# 下载MCSManager
|
||||
wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
|
||||
tar -zxf mcsmanager_linux_release.tar.gz
|
||||
|
||||
# 安装依赖库
|
||||
./install.sh
|
||||
|
||||
# 请打开两个终端或screen
|
||||
|
||||
# 先启动节点程序
|
||||
./start-daemon.sh
|
||||
|
||||
# 启动网络服务(在第二个终端或screen)
|
||||
./start-web.sh
|
||||
|
||||
# 为网络界面访问http://localhost:23333/
|
||||
# 一般来说,网络应用会自动扫描并连接到本地守护进程。
|
||||
```
|
||||
|
||||
这种安装方式不会自动注册面板到系统服务(Service),所以必须使用 `screen` 软件来管理,如果你希望由系统服务来接管 MCSManager,请参考文档。
|
||||
|
||||
<br />
|
||||
|
||||
## 搭建开发环境
|
||||
|
||||
此段落面向开发人员,普通用户无需关注也无需执行。
|
||||
|
||||
### 必备插件
|
||||
|
||||
我们使用 “VS Code” 开发 MCSManager,你可能需要安装这些插件:
|
||||
|
||||
- i18n 文案显示支持(I18n Ally)
|
||||
- 代码格式化(Prettier)
|
||||
- Vue - Offcial
|
||||
- ESLint
|
||||
|
||||
### MacOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.sh
|
||||
./npm-dev-macos.sh
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MCSManager/MCSManager.git
|
||||
./install-dependents.bat
|
||||
./npm-dev-windows.bat
|
||||
```
|
||||
|
||||
### 依赖文件
|
||||
|
||||
接下来你还需要前往 [PTY](https://github.com/MCSManager/PTY) 和 [Zip-Tools](https://github.com/MCSManager/Zip-Tools) 两个项目下载对应的二进制文件,将他们存放到 `daemon/lib` 目录下,以确保 `仿真终端` 和 `文件解压缩` 的正常工作。
|
||||
|
||||
### 构建生产环境版本
|
||||
|
||||
```bash
|
||||
./build.bat # Windows
|
||||
./build.sh # MacOS
|
||||
```
|
||||
|
||||
最终产物目录: "production-code"
|
||||
|
||||
<br />
|
||||
|
||||
## 贡献代码
|
||||
|
||||
如果你在使用过程中发现任何问题,可以 [提交 Issue](https://github.com/MCSManager/MCSManager/issues/new/choose) 或自行 Fork 修改后提交 Pull Request。
|
||||
|
||||
代码需要保持现有格式,不得格式化多余代码,具体可[参考这里](https://github.com/MCSManager/MCSManager/issues/544)。
|
||||
|
||||
<br />
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- 支持 `Chrome` `Firefox` `Safari` `Opera` 等现代主流浏览器。
|
||||
- 已放弃支持 `IE` 浏览器。
|
||||
|
||||
<br />
|
||||
|
||||
## BUG 报告
|
||||
|
||||
欢迎发现的任何问题进行反馈,必当及时修复。
|
||||
|
||||
若发现严重安全漏洞又不便公开发布,请发送邮件至: support@mcsmanager.com,安全问题修复后将在代码中附加漏洞发现者姓名。
|
||||
|
||||
<br />
|
||||
|
||||
## 国际化
|
||||
|
||||
感谢下列成员提供的**大量**翻译文件:
|
||||
|
||||
- [KevinLu2000](https://github.com/KevinLu2000)
|
||||
- [Unitwk](https://github.com/unitwk)
|
||||
- [JianyueLab](https://github.com/JianyueLab)
|
||||
- [IceBrick](https://github.com/IceBrick01)
|
||||
|
||||
<br />
|
||||
|
||||
## 源代码协议
|
||||
|
||||
源代码遵循 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 协议。
|
||||
|
||||
Copyright ©2025 MCSManager.
|
11
SECURITY.md
Normal file
11
SECURITY.md
Normal file
@ -0,0 +1,11 @@
|
||||
## Security Vulnerability Reporting
|
||||
|
||||
If you discover a security vulnerability in MCSManager and believe it may harm other users, and you are willing to help us fix it but do not wish to disclose the details publicly.
|
||||
|
||||
**Please send detailed information to our email: support@mcsmanager.com.**
|
||||
|
||||
The email subject format should be: `[Security Vulnerability] <Title>`
|
||||
|
||||
Please provide detailed steps to reproduce the security vulnerability, its impact, and we will reply and express our gratitude upon receiving your email.
|
||||
|
||||
We will acknowledge your contribution by adding your name to the `readme.md` and `code`!
|
21
app.js
21
app.js
@ -1,21 +0,0 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
console.log(`______ _______________________ ___
|
||||
___ |/ /_ ____/_ ___/__ |/ /_____ _____________ _______ _____________
|
||||
__ /|_/ /_ / _____ \\__ /|_/ /_ __ /_ __ \\ __ /_ __ / _ \\_ ___/
|
||||
_ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ /
|
||||
/_/ /_/ \\____/ /____/ /_/ /_/ \\__,_/ /_/ /_/\\__,_/ _\\__, / \\___//_/
|
||||
/____/
|
||||
+ https://github.com/MCSManager
|
||||
`);
|
||||
|
||||
console.log(`The installation and startup method you are using is no longer supported,
|
||||
you may be reading an older version of the MCSManager tutorial or documentation.
|
||||
Please go to https://mcsmanager.com/ official website for the latest installation method.
|
||||
The program has automatically exited.
|
||||
`);
|
||||
|
||||
console.log(`您使用的安装和启动方式已经不再支持,您可能正在阅读旧版本的 MCSManager 教程或文档。
|
||||
请前往 https://mcsmanager.com/ 官方网站了解最新安装方式。
|
||||
程序已自动退出。
|
||||
`);
|
58
build.bat
Executable file
58
build.bat
Executable file
@ -0,0 +1,58 @@
|
||||
call npm run preview-build
|
||||
|
||||
rd /s /q "production-code"
|
||||
rd /s /q ".\daemon\dist"
|
||||
rd /s /q ".\daemon\production"
|
||||
rd /s /q ".\panel\dist"
|
||||
rd /s /q ".\panel\production"
|
||||
|
||||
echo "Build daemon..."
|
||||
cd daemon
|
||||
call npm run build
|
||||
|
||||
echo "Build panel..."
|
||||
cd ../panel
|
||||
call npm run build
|
||||
|
||||
echo "Build frontend..."
|
||||
cd ../frontend
|
||||
call npm run build
|
||||
|
||||
echo "Collecting files..."
|
||||
cd ..
|
||||
|
||||
mkdir "production-code"
|
||||
mkdir "production-code\daemon"
|
||||
mkdir "production-code\web"
|
||||
mkdir "production-code\web\public"
|
||||
|
||||
copy ".\daemon\production\app.js" ".\production-code\daemon\app.js"
|
||||
copy ".\daemon\production\app.js.map" ".\production-code\daemon\app.js.map"
|
||||
copy ".\daemon\package.json" ".\production-code\daemon\package.json"
|
||||
copy ".\daemon\package-lock.json" ".\production-code\daemon\package-lock.json"
|
||||
|
||||
copy ".\panel\production\app.js" ".\production-code\web\app.js"
|
||||
copy ".\panel\production\app.js.map" ".\production-code\web\app.js.map"
|
||||
copy ".\panel\package.json" ".\production-code\web\package.json"
|
||||
copy ".\panel\package-lock.json" ".\production-code\web\package-lock.json"
|
||||
|
||||
xcopy ".\frontend\dist" ".\production-code\web\public" /E /I /H /Y
|
||||
|
||||
rd /s /q ".\panel\production"
|
||||
rd /s /q ".\daemon\production"
|
||||
rd /s /q ".\daemon\dist"
|
||||
rd /s /q ".\panel\dist"
|
||||
rd /s /q ".\frontend\dist"
|
||||
|
||||
cd "production-code\daemon"
|
||||
call npm install --production
|
||||
cd "../web"
|
||||
call npm install --production
|
||||
cd "../../"
|
||||
|
||||
echo "------------"
|
||||
echo "Compilation completed!"
|
||||
echo "Output Directory: ./production-code/"
|
||||
echo "------------"
|
||||
|
||||
pause
|
57
build.sh
Executable file
57
build.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
BASE_PATH=$(pwd)
|
||||
|
||||
npm run preview-build
|
||||
|
||||
rm -rf production-code
|
||||
rm -rf ./daemon/dist ./daemon/production
|
||||
rm -rf ./panel/dist ./panel/production
|
||||
|
||||
echo "Build daemon..."
|
||||
cd "${BASE_PATH}/daemon"
|
||||
npm run build
|
||||
|
||||
echo "Build panel..."
|
||||
cd "${BASE_PATH}/panel"
|
||||
npm run build
|
||||
|
||||
echo "Build frontend..."
|
||||
cd "${BASE_PATH}/frontend"
|
||||
npm run build
|
||||
|
||||
echo "Collecting files..."
|
||||
cd "${BASE_PATH}"
|
||||
|
||||
mkdir production-code
|
||||
mkdir production-code/daemon
|
||||
mkdir production-code/web
|
||||
mkdir production-code/web/public
|
||||
|
||||
mv "${BASE_PATH}/daemon/production/app.js" "${BASE_PATH}/production-code/daemon"
|
||||
mv "${BASE_PATH}/daemon/production/app.js.map" "${BASE_PATH}/production-code/daemon"
|
||||
cp -f "${BASE_PATH}/daemon/package.json" "${BASE_PATH}/production-code/daemon/package.json"
|
||||
cp -f "${BASE_PATH}/daemon/package-lock.json" "${BASE_PATH}/production-code/daemon/package-lock.json"
|
||||
|
||||
mv "${BASE_PATH}/panel/production/app.js" "${BASE_PATH}/production-code/web"
|
||||
mv "${BASE_PATH}/panel/production/app.js.map" "${BASE_PATH}/production-code/web"
|
||||
cp -f "${BASE_PATH}/panel/package.json" "${BASE_PATH}/production-code/web/package.json"
|
||||
cp -f "${BASE_PATH}/panel/package-lock.json" "${BASE_PATH}/production-code/web/package-lock.json"
|
||||
|
||||
mv "${BASE_PATH}"/frontend/dist/* "${BASE_PATH}/production-code/web/public"
|
||||
|
||||
rm -rf "${BASE_PATH}/daemon/dist" "${BASE_PATH}/daemon/production"
|
||||
rm -rf "${BASE_PATH}/panel/dist" "${BASE_PATH}/panel/production"
|
||||
rm -rf "${BASE_PATH}/frontend/dist"
|
||||
|
||||
cd "${BASE_PATH}/production-code/daemon"
|
||||
npm install --production --no-fund --no-audit
|
||||
cd "${BASE_PATH}/production-code/web"
|
||||
npm install --production --no-fund --no-audit
|
||||
|
||||
echo "------------"
|
||||
echo "Compilation completed!"
|
||||
echo "Output Directory: ./production-code/"
|
||||
echo "------------"
|
223
common/global.d.ts
vendored
Normal file
223
common/global.d.ts
vendored
Normal file
@ -0,0 +1,223 @@
|
||||
/*
|
||||
Please ensure that all NEW fields cannot be divided again,
|
||||
and the old configuration will be ignored.
|
||||
|
||||
Do not write:
|
||||
steamOptions: {
|
||||
ip: boolean;
|
||||
port: boolean;
|
||||
};
|
||||
|
||||
Write:
|
||||
steamIp: ""
|
||||
steamPort: 8080
|
||||
*/
|
||||
export interface IGlobalInstanceConfig {
|
||||
nickname: string;
|
||||
startCommand: string;
|
||||
stopCommand: string;
|
||||
cwd: string;
|
||||
ie: string;
|
||||
oe: string;
|
||||
createDatetime: number;
|
||||
lastDatetime: number;
|
||||
type: string;
|
||||
tag: string[];
|
||||
endTime: number;
|
||||
fileCode: string;
|
||||
processType: string;
|
||||
updateCommand: string;
|
||||
actionCommandList: any[];
|
||||
crlf: number;
|
||||
category: number;
|
||||
|
||||
// Steam RCON
|
||||
enableRcon?: boolean;
|
||||
rconPassword?: string;
|
||||
rconPort?: number;
|
||||
rconIp?: string;
|
||||
|
||||
// Old fields
|
||||
terminalOption: {
|
||||
haveColor: boolean;
|
||||
pty: boolean;
|
||||
};
|
||||
eventTask: {
|
||||
autoStart: boolean;
|
||||
autoRestart: boolean;
|
||||
ignore: boolean;
|
||||
};
|
||||
docker: IGlobalInstanceDockerConfig;
|
||||
pingConfig: {
|
||||
ip?: string;
|
||||
port?: number;
|
||||
type?: number;
|
||||
};
|
||||
extraServiceConfig: {
|
||||
openFrpTunnelId?: string;
|
||||
openFrpToken?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IGlobalInstanceDockerConfig {
|
||||
containerName?: string;
|
||||
image?: string;
|
||||
memory?: number;
|
||||
ports?: string[];
|
||||
extraVolumes?: string[];
|
||||
maxSpace?: number;
|
||||
network?: number;
|
||||
io?: number;
|
||||
networkMode?: string;
|
||||
networkAliases?: string[];
|
||||
cpusetCpus?: string;
|
||||
cpuUsage?: number;
|
||||
workingDir?: string;
|
||||
env?: string[];
|
||||
changeWorkdir?: boolean;
|
||||
}
|
||||
|
||||
export interface IPanelResponseProtocol {
|
||||
data: any;
|
||||
timestamp: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface IPanelOverviewRemoteResponse {
|
||||
version: string;
|
||||
process?: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
cwd: string;
|
||||
};
|
||||
instance?: {
|
||||
running: number;
|
||||
total: number;
|
||||
};
|
||||
system?: {
|
||||
type: string;
|
||||
hostname: string;
|
||||
platform: string;
|
||||
release: string;
|
||||
uptime: number;
|
||||
cwd: string;
|
||||
loadavg: number[];
|
||||
freemem: number;
|
||||
cpuUsage: number;
|
||||
memUsage: number;
|
||||
totalmem: number;
|
||||
processCpu: number;
|
||||
processMem: number;
|
||||
};
|
||||
cpuMemChart?: {
|
||||
cpu: number;
|
||||
mem: number;
|
||||
}[];
|
||||
uuid: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
prefix: string;
|
||||
available: boolean;
|
||||
remarks: string;
|
||||
}
|
||||
|
||||
export interface IPanelOverviewResponse {
|
||||
version: string;
|
||||
specifiedDaemonVersion: string;
|
||||
process: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
cwd: string;
|
||||
};
|
||||
record: {
|
||||
logined: number;
|
||||
illegalAccess: number;
|
||||
banips: number;
|
||||
loginFailed: number;
|
||||
};
|
||||
system: {
|
||||
user: any;
|
||||
time: number;
|
||||
totalmem: number;
|
||||
freemem: number;
|
||||
type: string;
|
||||
version: string;
|
||||
node: string;
|
||||
hostname: string;
|
||||
loadavg: number[];
|
||||
platform: string;
|
||||
release: string;
|
||||
uptime: number;
|
||||
cpu: number;
|
||||
};
|
||||
chart: {
|
||||
system: { cpu: number; mem: number }[];
|
||||
request: { value: number; totalInstance: number; runningInstance: number }[];
|
||||
};
|
||||
remoteCount: {
|
||||
available: number;
|
||||
total: number;
|
||||
};
|
||||
remote: IPanelOverviewRemoteResponse[];
|
||||
}
|
||||
|
||||
export interface IJsonData {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IMapData<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
export interface IPageLayoutConfig {
|
||||
page: string;
|
||||
items: ILayoutCard[];
|
||||
theme?: {
|
||||
backgroundImage: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ILayoutCardParams {
|
||||
field: string;
|
||||
label: string;
|
||||
type: "string" | "number" | "boolean" | "instance";
|
||||
}
|
||||
|
||||
export interface ILayoutCard {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
width: number;
|
||||
height: string;
|
||||
meta: IJsonData;
|
||||
disableAdd?: boolean;
|
||||
onlyPath?: string[];
|
||||
params?: ILayoutCardParams[];
|
||||
followId?: string;
|
||||
description?: string;
|
||||
allowedPages?: Array<string> | null;
|
||||
line?: number;
|
||||
disableDelete?: boolean;
|
||||
}
|
||||
|
||||
export interface IQuickStartPackages {
|
||||
language: string;
|
||||
description: string;
|
||||
title: string;
|
||||
runtime: string;
|
||||
size: string;
|
||||
hardware: string;
|
||||
remark: string;
|
||||
targetLink?: string;
|
||||
author: string;
|
||||
setupInfo?: IJsonData;
|
||||
}
|
||||
|
||||
export interface IQuickStartTemplate {
|
||||
remark: string;
|
||||
languages: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
packages: IQuickStartPackages[];
|
||||
}
|
1598
common/package-lock.json
generated
Normal file
1598
common/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
common/package.json
Normal file
28
common/package.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "common",
|
||||
"version": "1.0.0",
|
||||
"description": "MCSManager Common layer",
|
||||
"main": "dist/index.js",
|
||||
"homepage": "https://mcsmanager.com/",
|
||||
"author": "https://github.com/unitwk",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MCSManager/MCSManager"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/os-utils": "^0.0.4",
|
||||
"archiver": "^6.0.1",
|
||||
"compressing": "^1.10.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"os-utils": "^0.0.14",
|
||||
"socket.io": "^4.7.2",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
5
common/src/array.ts
Normal file
5
common/src/array.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function arrayUnique<T>(arr: T[], felidName?: string): T[] {
|
||||
if (!felidName) return Array.from(new Set(arr));
|
||||
const map = new Map();
|
||||
return arr.filter((v: any) => !map.has(v[felidName]) && map.set(v[felidName], v));
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
export default class GlobalVariable {
|
||||
public static readonly map = new Map<string, any>();
|
||||
|
30
common/src/index.ts
Normal file
30
common/src/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import StorageSubsystem from "./system_storage";
|
||||
import GlobalVariable from "./global_variable";
|
||||
import InstanceStreamListener from "./instance_stream";
|
||||
|
||||
import MCServerStatus from "./mcping";
|
||||
|
||||
export { ProcessWrapper, killProcess } from "./process_tools";
|
||||
export { systemInfo } from "./system_info";
|
||||
export {
|
||||
QueryMapWrapper,
|
||||
IDataSource,
|
||||
MySqlSource,
|
||||
LocalFileSource,
|
||||
QueryWrapper
|
||||
} from "./query_wrapper";
|
||||
|
||||
export {
|
||||
configureEntityParams,
|
||||
toText,
|
||||
toBoolean,
|
||||
toNumber,
|
||||
isEmpty,
|
||||
supposeValue
|
||||
} from "./typecheck";
|
||||
|
||||
export { arrayUnique } from "./array";
|
||||
|
||||
export { removeTrail } from "./string_utils";
|
||||
|
||||
export { MCServerStatus, StorageSubsystem, GlobalVariable, InstanceStreamListener };
|
@ -1,16 +1,15 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
// Application instance data stream forwarding adapter
|
||||
|
||||
export default class InstanceStreamListener {
|
||||
// Instance uuid -> Socket[]
|
||||
public readonly listenMap = new Map<string, Socket[]>();
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public requestForward(socket: Socket, instanceUuid: string) {
|
||||
if (this.listenMap.has(instanceUuid)) {
|
||||
const sockets = this.listenMap.get(instanceUuid);
|
||||
if (!sockets) return;
|
||||
for (const iterator of sockets)
|
||||
if (iterator.id === socket.id)
|
||||
throw new Error(
|
||||
@ -26,14 +25,14 @@ export default class InstanceStreamListener {
|
||||
if (!this.listenMap.has(instanceUuid))
|
||||
throw new Error(`The specified ${instanceUuid} does not exist in the listening table`);
|
||||
const socketList = this.listenMap.get(instanceUuid);
|
||||
socketList.forEach((v, index) => {
|
||||
if (v.id === socket.id) socketList.splice(index, 1);
|
||||
socketList?.forEach((v, index) => {
|
||||
if (v.id === socket.id) socketList?.splice(index, 1);
|
||||
});
|
||||
}
|
||||
|
||||
public forward(instanceUuid: string, data: any) {
|
||||
const sockets = this.listenMap.get(instanceUuid);
|
||||
sockets.forEach((socket) => {
|
||||
sockets?.forEach((socket) => {
|
||||
if (socket && socket.connected) socket.emit("instance/stdout", data);
|
||||
});
|
||||
}
|
||||
@ -41,13 +40,13 @@ export default class InstanceStreamListener {
|
||||
public forwardViaCallback(instanceUuid: string, callback: (socket: Socket) => void) {
|
||||
if (this.listenMap.has(instanceUuid)) {
|
||||
const sockets = this.listenMap.get(instanceUuid);
|
||||
sockets.forEach((socket) => {
|
||||
sockets?.forEach((socket) => {
|
||||
if (socket && socket.connected) callback(socket);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public hasListenInstance(instanceUuid: string) {
|
||||
return this.listenMap.has(instanceUuid) && this.listenMap.get(instanceUuid).length > 0;
|
||||
return this.listenMap.has(instanceUuid) && this.listenMap?.get(instanceUuid)!.length > 0;
|
||||
}
|
||||
}
|
131
common/src/mcping.ts
Executable file
131
common/src/mcping.ts
Executable file
@ -0,0 +1,131 @@
|
||||
// Using SLT (Server List Ping) provided by Minecraft.
|
||||
// https://wiki.vg/Server_List_Ping#Response
|
||||
|
||||
import net from "net";
|
||||
|
||||
export interface MinecraftPingResponse {
|
||||
host: string;
|
||||
port: number;
|
||||
online: boolean;
|
||||
version: string;
|
||||
motd: string;
|
||||
current_players: number;
|
||||
max_players: number;
|
||||
latency: number;
|
||||
}
|
||||
|
||||
export default class PingMinecraftServer {
|
||||
public port: number;
|
||||
public host: string;
|
||||
public status: MinecraftPingResponse;
|
||||
public client?: net.Socket;
|
||||
|
||||
constructor(port: number, host: string) {
|
||||
this.port = port;
|
||||
this.host = host;
|
||||
this.status = {
|
||||
online: false,
|
||||
host,
|
||||
port,
|
||||
version: "",
|
||||
motd: "",
|
||||
current_players: 0,
|
||||
max_players: 0,
|
||||
latency: 0
|
||||
};
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
return new Promise<MinecraftPingResponse>((resolve, reject) => {
|
||||
var start_time = new Date().getTime();
|
||||
this.client = net.connect(
|
||||
{
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
timeout: 1000 * 15
|
||||
},
|
||||
() => {
|
||||
this.status.latency = Math.round(new Date().getTime() - start_time);
|
||||
// 0xFE packet identifier for a server list ping
|
||||
// 0x01 server list ping's payload (always 1)
|
||||
let data = Buffer.from([0xfe, 0x01]);
|
||||
this?.client?.write(data);
|
||||
}
|
||||
);
|
||||
|
||||
// The client can also receive data from the server by reading from its socket.
|
||||
this?.client?.on("data", (response: any) => {
|
||||
// Check the readme for a simple explanation
|
||||
var server_info = response.toString().split("\x00\x00");
|
||||
|
||||
this.status = {
|
||||
online: true,
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
version: server_info[2].replace(/\u0000/g, ""),
|
||||
motd: server_info[3].replace(/\u0000/g, ""),
|
||||
current_players: server_info[4].replace(/\u0000/g, ""),
|
||||
max_players: server_info[5].replace(/\u0000/g, ""),
|
||||
latency: this.status.latency
|
||||
};
|
||||
|
||||
// Request an end to the connection after the data has been received.
|
||||
this?.client?.end();
|
||||
resolve(this.status);
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
this?.client?.on("end", () => {
|
||||
resolve(this.status);
|
||||
this.destroy();
|
||||
});
|
||||
|
||||
this?.client?.on("error", (err: any) => {
|
||||
reject(err);
|
||||
this.destroy();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private destroy() {
|
||||
this.client?.removeAllListeners();
|
||||
}
|
||||
|
||||
async asyncStatus() {
|
||||
let status = await this.getStatus();
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// async function test() {
|
||||
// try {
|
||||
// var status = await new MCServStatus(25565, "localhost").asyncStatus();
|
||||
// // console.log("status: ", status);
|
||||
// } catch (error) {
|
||||
// console.error("错误:", error);
|
||||
// }
|
||||
// const memoryUsage = process.memoryUsage();
|
||||
// console.log(
|
||||
// "RSS (Resident Set Size):",
|
||||
// (memoryUsage.rss / 1024 / 1024).toFixed(2),
|
||||
// "MB",
|
||||
// "Heap Total:",
|
||||
// (memoryUsage.heapTotal / 1024 / 1024).toFixed(2),
|
||||
// "MB",
|
||||
// "Heap Used:",
|
||||
// (memoryUsage.heapUsed / 1024 / 1024).toFixed(2),
|
||||
// "MB",
|
||||
// "External:",
|
||||
// (memoryUsage.external / 1024 / 1024).toFixed(2),
|
||||
// "MB"
|
||||
// );
|
||||
// // console.log("Heap Total:", (memoryUsage.heapTotal / 1024 / 1024).toFixed(2), "MB");
|
||||
// // console.log("External:", (memoryUsage.external / 1024 / 1024).toFixed(2), "MB");
|
||||
// // console.log("Heap Used:", (memoryUsage.heapUsed / 1024 / 1024).toFixed(2), "MB");
|
||||
// }
|
||||
|
||||
// for (let index = 0; index < 10000; index++) {
|
||||
// test();
|
||||
// // @ts-ignore
|
||||
// // global.gc();
|
||||
// }
|
140
common/src/process_tools.ts
Executable file
140
common/src/process_tools.ts
Executable file
@ -0,0 +1,140 @@
|
||||
import { ChildProcess, exec, execSync, SpawnOptionsWithoutStdio } from "child_process";
|
||||
import os from "os";
|
||||
import child_process from "child_process";
|
||||
import path from "path";
|
||||
import EventEmitter from "events";
|
||||
import iconv from "iconv-lite";
|
||||
|
||||
export class StartError extends Error {}
|
||||
|
||||
export class ProcessWrapper extends EventEmitter {
|
||||
public process?: ChildProcess;
|
||||
public pid?: number;
|
||||
|
||||
public errMsg = {
|
||||
timeoutErr: "task timeout!",
|
||||
exitErr: "task error!",
|
||||
startErr: "task start error!"
|
||||
};
|
||||
|
||||
constructor(
|
||||
public readonly file: string,
|
||||
public readonly args: string[],
|
||||
public readonly cwd: string,
|
||||
public readonly timeout: number = 0,
|
||||
public readonly code = "utf-8",
|
||||
public readonly option: SpawnOptionsWithoutStdio = {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public setErrMsg(errMsg: { timeoutErr: string; exitErr: string; startErr: string }) {
|
||||
this.errMsg = errMsg;
|
||||
}
|
||||
|
||||
public start(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeTask: NodeJS.Timeout;
|
||||
const subProcess = child_process.spawn(this.file, this.args, {
|
||||
stdio: "pipe",
|
||||
windowsHide: true,
|
||||
cwd: path.normalize(this.cwd),
|
||||
...this.option
|
||||
});
|
||||
this.process = subProcess;
|
||||
this.pid = subProcess.pid;
|
||||
|
||||
this.emit("start", subProcess.pid);
|
||||
if (!subProcess || !subProcess.pid) return reject(new Error(this.errMsg.startErr));
|
||||
|
||||
subProcess.stdout.on("data", (text) => this.emit("data", iconv.decode(text, this.code)));
|
||||
subProcess.stderr.on("data", (text) => this.emit("data", iconv.decode(text, this.code)));
|
||||
subProcess.on("exit", (code) => {
|
||||
try {
|
||||
this.emit("exit", code);
|
||||
this.destroy();
|
||||
} catch (error: any) {}
|
||||
if (timeTask) clearTimeout(timeTask);
|
||||
if (code !== 0) return reject(new Error(this.errMsg.exitErr));
|
||||
return resolve(true);
|
||||
});
|
||||
|
||||
// timeout, terminate the task
|
||||
if (this.timeout) {
|
||||
timeTask = setTimeout(() => {
|
||||
if (subProcess?.pid && !subProcess.exitCode && subProcess.exitCode !== 0) {
|
||||
killProcess(subProcess.pid, subProcess);
|
||||
reject(new Error(this.errMsg.timeoutErr));
|
||||
} else {
|
||||
reject(new Error(this.errMsg.exitErr));
|
||||
}
|
||||
}, 1000 * this.timeout);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getPid() {
|
||||
return this.process?.pid;
|
||||
}
|
||||
|
||||
public write(data?: any) {
|
||||
return this.process?.stdin?.write(iconv.encode(data, this.code));
|
||||
}
|
||||
|
||||
public kill() {
|
||||
if (this.process?.pid) killProcess(this.process?.pid, this.process);
|
||||
}
|
||||
|
||||
public status() {
|
||||
return !!this.process?.exitCode;
|
||||
}
|
||||
|
||||
public exitCode() {
|
||||
return this.process?.exitCode;
|
||||
}
|
||||
|
||||
private async destroy() {
|
||||
try {
|
||||
for (const n of this.eventNames()) this.removeAllListeners(n);
|
||||
if (this.process?.stdout)
|
||||
for (const eventName of this.process.stdout.eventNames())
|
||||
this.process.stdout.removeAllListeners(eventName);
|
||||
if (this.process?.stderr)
|
||||
for (const eventName of this.process.stderr.eventNames())
|
||||
this.process.stderr.removeAllListeners(eventName);
|
||||
if (this.process)
|
||||
for (const eventName of this.process.eventNames())
|
||||
this.process.removeAllListeners(eventName);
|
||||
this.process?.stdout?.destroy();
|
||||
this.process?.stderr?.destroy();
|
||||
if (this.process?.exitCode === null) {
|
||||
this.process.kill("SIGTERM");
|
||||
this.process.kill("SIGKILL");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log("[ProcessWrapper destroy() Error]", error);
|
||||
} finally {
|
||||
this.process = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function killProcess(
|
||||
pid: string | number,
|
||||
process: { kill(signal?: any): any },
|
||||
signal?: any
|
||||
) {
|
||||
try {
|
||||
if (os.platform() === "win32") {
|
||||
execSync(`taskkill /PID ${pid} /T /F`);
|
||||
return true;
|
||||
}
|
||||
if (os.platform() === "linux") {
|
||||
execSync(`kill -s 9 ${pid}`);
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
return signal ? process.kill(signal) : process.kill("SIGKILL");
|
||||
}
|
||||
return signal ? process.kill(signal) : process.kill("SIGKILL");
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
interface IMap {
|
||||
size: number;
|
||||
forEach: (value: any, key?: any) => void;
|
||||
@ -54,11 +52,22 @@ export interface IDataSource<T> {
|
||||
|
||||
// MYSQL data source
|
||||
export class MySqlSource<T> implements IDataSource<T> {
|
||||
selectPage: (condition: any, page: number, pageSize: number) => Page<T>;
|
||||
select: (condition: any) => any[];
|
||||
update: (condition: any, data: any) => void;
|
||||
delete: (condition: any) => void;
|
||||
insert: (data: any) => void;
|
||||
constructor(public data: any) {}
|
||||
selectPage(condition: any, page: number, pageSize: number) {
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
maxPage: 0,
|
||||
total: 0,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
select(condition: any) {
|
||||
return [];
|
||||
}
|
||||
update(condition: any, data: any) {}
|
||||
delete(condition: any) {}
|
||||
insert(data: any) {}
|
||||
}
|
||||
|
||||
// local file data source (embedded microdatabase)
|
||||
@ -101,7 +110,7 @@ export class LocalFileSource<T> implements IDataSource<T> {
|
||||
}
|
||||
|
||||
select(condition: any): any[] {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
update(condition: any, data: any) {}
|
||||
delete(condition: any) {}
|
7
common/src/string_utils.ts
Normal file
7
common/src/string_utils.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function removeTrail(origin: string, trail: string) {
|
||||
if (origin.endsWith(trail)) {
|
||||
return origin.slice(0, origin.length - trail.length);
|
||||
} else {
|
||||
return origin;
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
import os from "os";
|
||||
import osUtils from "os-utils";
|
||||
import fs from "fs";
|
||||
@ -90,7 +88,7 @@ function setLinuxSystemInfo() {
|
||||
info.totalmem = memTotal * 1024;
|
||||
info.memUsage = (info.totalmem - info.freemem) / info.totalmem;
|
||||
osUtils.cpuUsage((p) => (info.cpuUsage = p));
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
// If the reading is wrong, the default general reading method is automatically used
|
||||
otherSystemInfo();
|
||||
}
|
@ -1,17 +1,9 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
interface IClassz {
|
||||
name: string;
|
||||
}
|
||||
|
||||
class StorageSubsystem {
|
||||
public static readonly STIRAGE_DATA_PATH = path.normalize(path.join(process.cwd(), "data"));
|
||||
public static readonly STIRAGE_INDEX_PATH = path.normalize(
|
||||
path.join(process.cwd(), "data", "index")
|
||||
);
|
||||
export default class StorageSubsystem {
|
||||
public static readonly DATA_PATH = path.normalize(path.join(process.cwd(), "data"));
|
||||
public static readonly INDEX_PATH = path.normalize(path.join(process.cwd(), "data", "index"));
|
||||
|
||||
private checkFileName(name: string) {
|
||||
const blackList = ["\\", "/", ".."];
|
||||
@ -21,9 +13,36 @@ class StorageSubsystem {
|
||||
return true;
|
||||
}
|
||||
|
||||
public writeFile(name: string, data: string) {
|
||||
const targetPath = path.normalize(path.join(StorageSubsystem.DATA_PATH, name));
|
||||
fs.writeFileSync(targetPath, data, { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
public readFile(name: string) {
|
||||
const targetPath = path.normalize(path.join(StorageSubsystem.DATA_PATH, name));
|
||||
return fs.readFileSync(targetPath, { encoding: "utf-8" });
|
||||
}
|
||||
|
||||
public readDir(dirName: string) {
|
||||
const targetPath = path.normalize(path.join(StorageSubsystem.DATA_PATH, dirName));
|
||||
if (!fs.existsSync(targetPath)) return [];
|
||||
const files = fs.readdirSync(targetPath).map((v) => path.normalize(path.join(dirName, v)));
|
||||
return files;
|
||||
}
|
||||
|
||||
public deleteFile(name: string) {
|
||||
const targetPath = path.normalize(path.join(StorageSubsystem.DATA_PATH, name));
|
||||
fs.removeSync(targetPath);
|
||||
}
|
||||
|
||||
public fileExists(name: string) {
|
||||
const targetPath = path.normalize(path.join(StorageSubsystem.DATA_PATH, name));
|
||||
return fs.existsSync(targetPath);
|
||||
}
|
||||
|
||||
// Stored in local file based on class definition and identifier
|
||||
public store(category: string, uuid: string, object: any) {
|
||||
const dirPath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category);
|
||||
const dirPath = path.join(StorageSubsystem.DATA_PATH, category);
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath);
|
||||
if (!this.checkFileName(uuid))
|
||||
throw new Error(`UUID ${uuid} does not conform to specification`);
|
||||
@ -54,7 +73,7 @@ class StorageSubsystem {
|
||||
* Instantiate an object based on the class definition and identifier
|
||||
*/
|
||||
public load(category: string, classz: any, uuid: string) {
|
||||
const dirPath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category);
|
||||
const dirPath = path.join(StorageSubsystem.DATA_PATH, category);
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath);
|
||||
if (!this.checkFileName(uuid))
|
||||
throw new Error(`UUID ${uuid} does not conform to specification`);
|
||||
@ -74,7 +93,7 @@ class StorageSubsystem {
|
||||
* Return all identifiers related to this class through the class definition
|
||||
*/
|
||||
public list(category: string) {
|
||||
const dirPath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category);
|
||||
const dirPath = path.join(StorageSubsystem.DATA_PATH, category);
|
||||
if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath);
|
||||
const files = fs.readdirSync(dirPath);
|
||||
const result = new Array<string>();
|
||||
@ -88,10 +107,8 @@ class StorageSubsystem {
|
||||
* Delete an identifier instance of the specified type through the class definition
|
||||
*/
|
||||
public delete(category: string, uuid: string) {
|
||||
const filePath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category, `${uuid}.json`);
|
||||
const filePath = path.join(StorageSubsystem.DATA_PATH, category, `${uuid}.json`);
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
fs.removeSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
export default new StorageSubsystem();
|
21
src/app/common/typecheck.ts → common/src/typecheck.ts
Normal file → Executable file
21
src/app/common/typecheck.ts → common/src/typecheck.ts
Normal file → Executable file
@ -1,5 +1,3 @@
|
||||
// Copyright (C) 2022 MCSManager <mcsmanager-dev@outlook.com>
|
||||
|
||||
export function configureEntityParams(self: any, args: any, key: string, typeFn?: Function): any {
|
||||
const selfDefaultValue = self[key] ?? null;
|
||||
const v = args[key] != null ? args[key] : selfDefaultValue;
|
||||
@ -35,15 +33,6 @@ export function configureEntityParams(self: any, args: any, key: string, typeFn?
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeFn === Array) {
|
||||
if (v == null) return (self[key] = null);
|
||||
if (!(v instanceof Array))
|
||||
throw new Error(
|
||||
`ConfigureEntityParams Error: Expected type to be Array, but got ${typeof v}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeFn) {
|
||||
self[key] = typeFn(v);
|
||||
} else {
|
||||
@ -51,27 +40,27 @@ export function configureEntityParams(self: any, args: any, key: string, typeFn?
|
||||
}
|
||||
}
|
||||
|
||||
export function toText(v: any) {
|
||||
export function toText(v: any): string | null {
|
||||
if (isEmpty(v)) return null;
|
||||
return String(v);
|
||||
}
|
||||
|
||||
export function toNumber(v: any) {
|
||||
export function toNumber(v: any): number | null {
|
||||
if (isEmpty(v)) return null;
|
||||
if (isNaN(Number(v))) return null;
|
||||
return Number(v);
|
||||
}
|
||||
|
||||
export function toBoolean(v: any) {
|
||||
export function toBoolean(v: any): boolean | null {
|
||||
if (isEmpty(v)) return null;
|
||||
return Boolean(v);
|
||||
}
|
||||
|
||||
export function isEmpty(v: any) {
|
||||
export function isEmpty(v: any): boolean {
|
||||
return v === null || v === undefined;
|
||||
}
|
||||
|
||||
export function supposeValue(v: any, def: any = null) {
|
||||
export function supposeValue<T>(v: any, def?: T) {
|
||||
if (isEmpty(v)) return def;
|
||||
return v;
|
||||
}
|
25
common/tsconfig.json
Executable file
25
common/tsconfig.json
Executable file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": false,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2018",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@languages/*": ["../languages/*"],
|
||||
"*": ["node_modules/*", "src/types/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
16
daemon/.eslintrc.js
Executable file
16
daemon/.eslintrc.js
Executable file
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: "eslint:recommended",
|
||||
globals: {
|
||||
Atomics: "readonly",
|
||||
SharedArrayBuffer: "readonly"
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018
|
||||
},
|
||||
rules: {}
|
||||
};
|
1
daemon/.npmrc
Normal file
1
daemon/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
registry=https://registry.npmjs.org
|
4
daemon/.prettierrc.json
Executable file
4
daemon/.prettierrc.json
Executable file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
9016
daemon/package-lock.json
generated
Normal file
9016
daemon/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
70
daemon/package.json
Normal file
70
daemon/package.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "mcsmanager-daemon",
|
||||
"version": "4.6.0",
|
||||
"description": "Provides remote control capability for MCSManager to manage processes, scheduled tasks, I/O streams, and more",
|
||||
"scripts": {
|
||||
"dev": "ts-node --project tsconfig.json src/app.ts",
|
||||
"build": "webpack --config webpack.config.js"
|
||||
},
|
||||
"homepage": "https://mcsmanager.com/",
|
||||
"author": "https://github.com/unitwk",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MCSManager/MCSManager"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"@languages": "../languages"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@koa/router": "^10.0.0",
|
||||
"archiver": "^5.3.1",
|
||||
"axios": "^1.8.2",
|
||||
"compressing": "^1.10.0",
|
||||
"dockerode": "4.0.5",
|
||||
"formidable": "^3.5.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"i18next": "^21.8.14",
|
||||
"iconv-lite": "^0.6.2",
|
||||
"koa": "^2.16.1",
|
||||
"koa-body-patch": "^6.0.1",
|
||||
"koa-send": "^5.0.1",
|
||||
"log4js": "^6.4.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"node-schedule": "^2.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"os-utils": "0.0.14",
|
||||
"pidusage": "^2.0.21",
|
||||
"properties": "^1.2.1",
|
||||
"rcon-srcds": "^2.1.0",
|
||||
"socket.io": "^4.6.1",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"uuid": "^8.3.2",
|
||||
"yaml": "^1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^5.3.1",
|
||||
"@types/dockerode": "^3.2.7",
|
||||
"@types/formidable": "^3.4.5",
|
||||
"@types/fs-extra": "^9.0.11",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa__router": "^8.0.7",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
"@types/mocha": "^8.2.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/os-utils": "0.0.1",
|
||||
"@types/pidusage": "^2.0.1",
|
||||
"@types/ssh2": "^1.11.7",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"common": "file:../common",
|
||||
"eslint": "^7.13.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
}
|
||||
}
|
169
daemon/src/app.ts
Executable file
169
daemon/src/app.ts
Executable file
@ -0,0 +1,169 @@
|
||||
import "module-alias/register";
|
||||
import http from "http";
|
||||
import fs from "fs-extra";
|
||||
import versionAdapter from "./service/version_adapter";
|
||||
import { checkDependencies } from "./service/dependencies";
|
||||
import { $t, i18next } from "./i18n";
|
||||
import { getVersion, initVersionManager } from "./service/version";
|
||||
import { globalConfiguration } from "./entity/config";
|
||||
import { Server, Socket } from "socket.io";
|
||||
import { LOCAL_PRESET_LANG_PATH } from "./const";
|
||||
import logger from "./service/log";
|
||||
import { GOLANG_ZIP_PATH, PTY_PATH } from "./const";
|
||||
import * as router from "./service/router";
|
||||
import * as koa from "./service/http";
|
||||
import * as protocol from "./service/protocol";
|
||||
import InstanceSubsystem from "./service/system_instance";
|
||||
import "./service/async_task_service";
|
||||
import "./service/async_task_service/quick_install";
|
||||
import "./service/system_visual_data";
|
||||
import { removeTrail } from "common";
|
||||
|
||||
initVersionManager();
|
||||
const VERSION = getVersion();
|
||||
|
||||
console.log(`
|
||||
______ _______________________ ___
|
||||
___ |/ /_ ____/_ ___/__ |/ /_____ _____________ _______ _____________
|
||||
__ /|_/ /_ / _____ \\__ /|_/ /_ __ \`/_ __ \\ __ \`/_ __ \`/ _ \\_ ___/
|
||||
_ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ /
|
||||
/_/ /_/ \\____/ /____/ /_/ /_/ \\__,_/ /_/ /_/\\__,_/ _\\__, / \\___//_/
|
||||
/____/
|
||||
________
|
||||
___ __ \\_____ ____________ ________________
|
||||
__ / / / __ \`/ _ \\_ __ \`__ \\ __ \\_ __ \\
|
||||
_ /_/ // /_/ // __/ / / / / / /_/ / / / /
|
||||
/_____/ \\__,_/ \\___//_/ /_/ /_/\\____//_/ /_/
|
||||
|
||||
|
||||
+ Copyright ${new Date().getFullYear()} MCSManager Dev <https://github.com/MCSManager>
|
||||
+ Version ${VERSION}
|
||||
`);
|
||||
|
||||
// Initialize the global configuration service
|
||||
globalConfiguration.load();
|
||||
const config = globalConfiguration.config;
|
||||
|
||||
// Detect whether the configuration file is from an older version and update it if so.
|
||||
versionAdapter.detectConfig();
|
||||
|
||||
checkDependencies();
|
||||
|
||||
// Set language
|
||||
if (fs.existsSync(LOCAL_PRESET_LANG_PATH)) {
|
||||
i18next.changeLanguage(fs.readFileSync(LOCAL_PRESET_LANG_PATH, "utf-8"));
|
||||
} else {
|
||||
const lang = config.language || "en_us";
|
||||
logger.info(`LANGUAGE: ${lang}`);
|
||||
i18next.changeLanguage(lang);
|
||||
}
|
||||
logger.info($t("TXT_CODE_app.welcome"));
|
||||
|
||||
// Initialize HTTP service
|
||||
const koaApp = koa.initKoa();
|
||||
|
||||
// Listen for Koa errors
|
||||
koaApp.on("error", (error) => {
|
||||
// Block all Koa framework error
|
||||
// When Koa is attacked by a short connection flood, it is easy for error messages to swipe the screen, which may indirectly affect the operation of some applications
|
||||
});
|
||||
|
||||
const httpServer = http.createServer(koaApp.callback());
|
||||
httpServer.on("error", (err) => {
|
||||
logger.error($t("TXT_CODE_app.httpSetupError"));
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
httpServer.listen(config.port, config.ip);
|
||||
|
||||
// Initialize Websocket service to HTTP service
|
||||
const io = new Server(httpServer, {
|
||||
serveClient: false,
|
||||
pingInterval: 5000,
|
||||
pingTimeout: 5000,
|
||||
cookie: false,
|
||||
path: removeTrail(config.prefix, "/") + "/socket.io",
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST", "PUT", "DELETE"]
|
||||
},
|
||||
maxHttpBufferSize: 1e8
|
||||
});
|
||||
|
||||
// Initialize application instance system
|
||||
try {
|
||||
InstanceSubsystem.loadInstances();
|
||||
logger.info($t("TXT_CODE_app.instanceLoad", { n: InstanceSubsystem.getInstances().length }));
|
||||
} catch (err) {
|
||||
logger.error($t("TXT_CODE_app.instanceLoadError"), err);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
(function initCompressModule() {
|
||||
try {
|
||||
fs.chmodSync(GOLANG_ZIP_PATH, 0o755);
|
||||
fs.chmodSync(PTY_PATH, 0o755);
|
||||
} catch (error: any) {
|
||||
logger.error(error?.message);
|
||||
logger.error($t("TXT_CODE_a8b245fa"));
|
||||
}
|
||||
})();
|
||||
|
||||
// Initialize Websocket server
|
||||
io.on("connection", (socket: Socket) => {
|
||||
protocol.addGlobalSocket(socket);
|
||||
router.navigation(socket);
|
||||
|
||||
socket.on("error", (err) => {
|
||||
logger.error("Connection(): Socket.io Error:", err);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
protocol.delGlobalSocket(socket);
|
||||
for (const name of socket.eventNames()) socket.removeAllListeners(name);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("uncaughtException", function (err) {
|
||||
logger.error(`Error: UncaughtException:`, err);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason, p) => {
|
||||
logger.error(`Error: UnhandledRejection:`, reason, p);
|
||||
});
|
||||
|
||||
logger.info("----------------------------");
|
||||
logger.info($t("TXT_CODE_app.started"));
|
||||
logger.info($t("TXT_CODE_app.doc"));
|
||||
logger.info($t("TXT_CODE_app.addr", { port: config.port }));
|
||||
logger.info($t("TXT_CODE_app.configPathTip", { path: "" }));
|
||||
logger.info($t("TXT_CODE_app.password", { key: config.key }));
|
||||
logger.info($t("TXT_CODE_app.passwordTip"));
|
||||
logger.info($t("TXT_CODE_app.exitTip"));
|
||||
logger.info("----------------------------");
|
||||
console.log("");
|
||||
|
||||
async function processExit() {
|
||||
try {
|
||||
console.log("");
|
||||
logger.warn("Program received EXIT command.");
|
||||
await InstanceSubsystem.exit();
|
||||
logger.info("Exit.");
|
||||
} catch (err) {
|
||||
logger.error("ERROR:", err);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
["SIGTERM", "SIGINT", "SIGQUIT"].forEach(function (sig) {
|
||||
process.on(sig, () => {
|
||||
logger.warn(`${sig} close process signal detected.`);
|
||||
processExit();
|
||||
});
|
||||
});
|
||||
|
||||
process.stdin.on("data", (v) => {
|
||||
const command = v.toString().replace("\n", "").replace("\r", "").trim().toLowerCase();
|
||||
if (command === "exit") processExit();
|
||||
});
|
85
daemon/src/common/compress.ts
Executable file
85
daemon/src/common/compress.ts
Executable file
@ -0,0 +1,85 @@
|
||||
import path from "path";
|
||||
import { t } from "i18next";
|
||||
import logger from "../service/log";
|
||||
import { ProcessWrapper } from "common";
|
||||
import fs from "fs-extra";
|
||||
import { GOLANG_ZIP_PATH } from "../const";
|
||||
import { ZIP_TIMEOUT_SECONDS } from "../const";
|
||||
|
||||
const COMPRESS_ERROR_MSG = {
|
||||
invalidName: t("TXT_CODE_3aa9f36"),
|
||||
exitErr: t("TXT_CODE_2be83d36"),
|
||||
startErr: t("TXT_CODE_37d839a4"),
|
||||
timeoutErr: t("TXT_CODE_15c07350")
|
||||
};
|
||||
|
||||
function checkFileName(fileName: string) {
|
||||
const disableList = ['"', "'", "?", "|", "&"];
|
||||
for (const iterator of disableList) {
|
||||
if (fileName.includes(iterator)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function compress(
|
||||
sourceZip: string,
|
||||
files: string[],
|
||||
fileCode?: string
|
||||
): Promise<boolean> {
|
||||
if (!checkFileName(sourceZip) || files.some((v) => !checkFileName(v)))
|
||||
throw new Error(COMPRESS_ERROR_MSG.invalidName);
|
||||
return await useZip(sourceZip, files, fileCode);
|
||||
}
|
||||
|
||||
export async function decompress(
|
||||
zipPath: string,
|
||||
dest: string,
|
||||
fileCode?: string
|
||||
): Promise<boolean> {
|
||||
if (!checkFileName(zipPath) || !checkFileName(dest))
|
||||
throw new Error(COMPRESS_ERROR_MSG.invalidName);
|
||||
return await useUnzip(zipPath, dest, fileCode || "utf-8");
|
||||
}
|
||||
|
||||
// ./file-zip -mode 2 --zipPath aaa.zip --DistDirPath 123412124 --code GBK
|
||||
async function useUnzip(sourceZip: string, destDir: string, code = "utf-8"): Promise<boolean> {
|
||||
const params = [
|
||||
"--mode=2",
|
||||
`--zipPath=${path.basename(sourceZip)}`,
|
||||
`--distDirPath=${path.normalize(destDir)}`,
|
||||
`--code=${code}`
|
||||
];
|
||||
logger.info(`Function useUnzip(): Command: ${GOLANG_ZIP_PATH} ${params.join(" ")}`);
|
||||
const subProcess = new ProcessWrapper(
|
||||
GOLANG_ZIP_PATH,
|
||||
params,
|
||||
path.dirname(sourceZip),
|
||||
ZIP_TIMEOUT_SECONDS,
|
||||
code
|
||||
);
|
||||
subProcess.setErrMsg(COMPRESS_ERROR_MSG);
|
||||
return subProcess.start();
|
||||
}
|
||||
|
||||
// ./file-zip -mode 1 --file main.go --file file-zip --file 123 --file README.md --zipPath aaabb.zip
|
||||
async function useZip(distZip: string, files: string[], code = "utf-8"): Promise<boolean> {
|
||||
if (!files || files.length == 0) throw new Error(t("TXT_CODE_2038ec2c"));
|
||||
const params = ["--mode=1", `--code=${code}`, `--zipPath=${path.basename(distZip)}`];
|
||||
files.forEach((v) => {
|
||||
params.push(`--file=${path.basename(v)}`);
|
||||
});
|
||||
logger.info(
|
||||
`Function useZip(): Command: ${GOLANG_ZIP_PATH} ${params.join(" ")}, CWD: ${path.dirname(
|
||||
distZip
|
||||
)}`
|
||||
);
|
||||
const subProcess = new ProcessWrapper(
|
||||
GOLANG_ZIP_PATH,
|
||||
params,
|
||||
path.dirname(distZip),
|
||||
ZIP_TIMEOUT_SECONDS,
|
||||
code
|
||||
);
|
||||
subProcess.setErrMsg(COMPRESS_ERROR_MSG);
|
||||
return subProcess.start();
|
||||
}
|
2
daemon/src/common/system_storage.ts
Executable file
2
daemon/src/common/system_storage.ts
Executable file
@ -0,0 +1,2 @@
|
||||
import { StorageSubsystem } from "common";
|
||||
export default new StorageSubsystem();
|
27
daemon/src/const.ts
Executable file
27
daemon/src/const.ts
Executable file
@ -0,0 +1,27 @@
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
|
||||
const SYS_INFO = `${os.platform()}_${os.arch()}${os.platform() === "win32" ? ".exe" : ""}`;
|
||||
const ptyName = `pty_${SYS_INFO}`;
|
||||
const frpcName = `frpc_${SYS_INFO}`;
|
||||
const PTY_PATH = path.normalize(path.join(process.cwd(), "lib", ptyName));
|
||||
const FRPC_PATH = path.normalize(path.join(process.cwd(), "lib", frpcName));
|
||||
const FILENAME_BLACKLIST = ["\\", "/", ".", "'", '"', "?", "*", "<", ">"];
|
||||
const LOCAL_PRESET_LANG_PATH = path.normalize(path.join(process.cwd(), "language"));
|
||||
const IGNORE = "[IGNORE_LOG]";
|
||||
const SYSTEM_TYPE = os.platform();
|
||||
const ZIP_TIMEOUT_SECONDS = 60 * 40;
|
||||
const GOLANG_ZIP_NAME = `file_zip_${SYSTEM_TYPE}_${os.arch()}${
|
||||
SYSTEM_TYPE === "win32" ? ".exe" : ""
|
||||
}`;
|
||||
const GOLANG_ZIP_PATH = path.normalize(path.join(process.cwd(), "lib", GOLANG_ZIP_NAME));
|
||||
|
||||
export {
|
||||
GOLANG_ZIP_PATH,
|
||||
FILENAME_BLACKLIST,
|
||||
PTY_PATH,
|
||||
LOCAL_PRESET_LANG_PATH,
|
||||
FRPC_PATH,
|
||||
IGNORE,
|
||||
ZIP_TIMEOUT_SECONDS
|
||||
};
|
5
daemon/src/entity/commands/base/command.ts
Executable file
5
daemon/src/entity/commands/base/command.ts
Executable file
@ -0,0 +1,5 @@
|
||||
export default class InstanceCommand {
|
||||
constructor(public info?: string) {}
|
||||
async exec(instance: any): Promise<any> {}
|
||||
async stop(instance: any) {}
|
||||
}
|
61
daemon/src/entity/commands/base/command_parser.ts
Executable file
61
daemon/src/entity/commands/base/command_parser.ts
Executable file
@ -0,0 +1,61 @@
|
||||
import { $t } from "../../../i18n";
|
||||
|
||||
export function commandStringToArray(cmd: string) {
|
||||
const QUOTES_KEY = "{quotes}";
|
||||
let start = 0;
|
||||
let len = cmd.length;
|
||||
const cmdArray: string[] = [];
|
||||
function _analyze() {
|
||||
for (let index = start; index < len; index++) {
|
||||
const ch = cmd[index];
|
||||
if (ch === " ") {
|
||||
findSpace(index);
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
index = findQuotes(index);
|
||||
}
|
||||
if (index + 1 >= len) {
|
||||
findEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findEnd() {
|
||||
return cmdArray.push(cmd.slice(start));
|
||||
}
|
||||
|
||||
function findSpace(endPoint: number) {
|
||||
if (endPoint != start) {
|
||||
const elem = cmd.slice(start, endPoint);
|
||||
start = endPoint;
|
||||
return cmdArray.push(elem);
|
||||
}
|
||||
}
|
||||
|
||||
function findQuotes(p: number) {
|
||||
for (let index = p + 1; index < len; index++) {
|
||||
const ch = cmd[index];
|
||||
if (ch === '"') return index;
|
||||
}
|
||||
throw new Error($t("TXT_CODE_command.quotes"));
|
||||
}
|
||||
|
||||
_analyze();
|
||||
|
||||
if (cmdArray.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const index in cmdArray) {
|
||||
const element = cmdArray[index];
|
||||
if (element[0] === '"' && element[element.length - 1] === '"')
|
||||
cmdArray[index] = element.slice(1, element.length - 1);
|
||||
while (cmdArray[index].indexOf(QUOTES_KEY) != -1)
|
||||
cmdArray[index] = cmdArray[index].replace(QUOTES_KEY, '"');
|
||||
}
|
||||
|
||||
return cmdArray;
|
||||
}
|
82
daemon/src/entity/commands/dispatcher.ts
Executable file
82
daemon/src/entity/commands/dispatcher.ts
Executable file
@ -0,0 +1,82 @@
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
import NullCommand from "./nullfunc";
|
||||
import GeneralStartCommand from "./general/general_start";
|
||||
import GeneralStopCommand from "./general/general_stop";
|
||||
import GeneralKillCommand from "./general/general_kill";
|
||||
import GeneralSendCommand from "./general/general_command";
|
||||
import GeneralRestartCommand from "./general/general_restart";
|
||||
import DockerStartCommand from "./docker/docker_start";
|
||||
import TimeCheck from "./task/time";
|
||||
import GeneralUpdateCommand from "./general/general_update";
|
||||
import PtyStartCommand from "./pty/pty_start";
|
||||
import RconCommand from "./steam/rcon_command";
|
||||
import DockerResizeCommand from "./docker/docker_pty_resize";
|
||||
import PtyResizeCommand from "./pty/pty_resize";
|
||||
import GeneralInstallCommand from "./general/general_install";
|
||||
import PingJavaMinecraftServerCommand from "./minecraft/mc_ping";
|
||||
import PingMinecraftServerTask from "./task/mc_players";
|
||||
|
||||
// If you add a new "Preset", Please add the definition here.
|
||||
export type IPresetCommand =
|
||||
| "start"
|
||||
| "stop"
|
||||
| "restart"
|
||||
| "kill"
|
||||
| "update"
|
||||
| "refreshPlayers"
|
||||
| "command"
|
||||
| "resize"
|
||||
| "install";
|
||||
|
||||
// Instance function dispatcher
|
||||
// Dispatch and assign different functions according to different types
|
||||
export default class FunctionDispatcher extends InstanceCommand {
|
||||
constructor() {
|
||||
super("FunctionDispatcher");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
// initialize all modules
|
||||
instance.lifeCycleTaskManager.clearLifeCycleTask();
|
||||
instance.clearPreset();
|
||||
|
||||
// the component that the instance must mount
|
||||
instance.lifeCycleTaskManager.registerLifeCycleTask(new TimeCheck());
|
||||
// instance.lifeCycleTaskManager.registerLifeCycleTask(new OpenFrpTask());
|
||||
|
||||
// Instance general preset capabilities
|
||||
instance.setPreset("command", new GeneralSendCommand());
|
||||
instance.setPreset("stop", new GeneralStopCommand());
|
||||
instance.setPreset("kill", new GeneralKillCommand());
|
||||
instance.setPreset("restart", new GeneralRestartCommand());
|
||||
instance.setPreset("update", new GeneralUpdateCommand());
|
||||
instance.setPreset("refreshPlayers", new NullCommand());
|
||||
instance.setPreset("install", new GeneralInstallCommand());
|
||||
|
||||
// Preset the basic operation mode according to the instance startup type
|
||||
if (!instance.config.processType || instance.config.processType === "general") {
|
||||
instance.setPreset("start", new GeneralStartCommand());
|
||||
}
|
||||
|
||||
// Enable emulated terminal mode
|
||||
if (instance.config.terminalOption.pty && instance.config.processType === "general") {
|
||||
instance.setPreset("start", new PtyStartCommand());
|
||||
instance.setPreset("resize", new PtyResizeCommand());
|
||||
}
|
||||
// Whether to enable Docker PTY mode
|
||||
if (instance.config.processType === "docker") {
|
||||
instance.setPreset("start", new DockerStartCommand());
|
||||
instance.setPreset("resize", new DockerResizeCommand());
|
||||
}
|
||||
if (instance.config.enableRcon) {
|
||||
instance.setPreset("command", new RconCommand());
|
||||
}
|
||||
|
||||
// Minecraft Ping
|
||||
if (instance.config.type.includes(Instance.TYPE_MINECRAFT_JAVA)) {
|
||||
instance.setPreset("refreshPlayers", new PingJavaMinecraftServerCommand());
|
||||
instance.lifeCycleTaskManager.registerLifeCycleTask(new PingMinecraftServerTask());
|
||||
}
|
||||
}
|
||||
}
|
20
daemon/src/entity/commands/docker/docker_pty_resize.ts
Normal file
20
daemon/src/entity/commands/docker/docker_pty_resize.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { DockerProcessAdapter } from "../../../service/docker_process_service";
|
||||
|
||||
export default class DockerResizeCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("ResizeTTY");
|
||||
}
|
||||
|
||||
async exec(instance: Instance): Promise<any> {
|
||||
const dockerProcess = instance?.process as Partial<DockerProcessAdapter>;
|
||||
if (typeof dockerProcess?.container?.resize === "function") {
|
||||
const { w, h } = instance.computeTerminalSize();
|
||||
await dockerProcess?.container?.resize({
|
||||
h,
|
||||
w
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
79
daemon/src/entity/commands/docker/docker_pull.ts
Normal file
79
daemon/src/entity/commands/docker/docker_pull.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { t } from "i18next";
|
||||
import { DefaultDocker } from "../../../service/docker_service";
|
||||
|
||||
export async function checkImage(name: string) {
|
||||
const docker = new DefaultDocker();
|
||||
try {
|
||||
const image = docker.getImage(name);
|
||||
const info = await image.inspect();
|
||||
return info.Size > 0 ? true : false;
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default class DockerPullCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("DockerPullCommand");
|
||||
}
|
||||
|
||||
private stopFlag = false;
|
||||
|
||||
private stopped(instance: Instance) {
|
||||
this.stopFlag = true;
|
||||
instance.asynchronousTask = undefined;
|
||||
}
|
||||
|
||||
private awaitImageDone(instance: Instance, name: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let count = 0;
|
||||
const task = setInterval(async () => {
|
||||
count++;
|
||||
instance.println("CONTAINER", t("TXT_CODE_977cb449"));
|
||||
if (await checkImage(name)) {
|
||||
clearInterval(task);
|
||||
resolve(true);
|
||||
}
|
||||
if (count >= 20 * 15) {
|
||||
clearInterval(task);
|
||||
reject(new Error(t("TXT_CODE_9cae6f92")));
|
||||
}
|
||||
if (this.stopFlag) {
|
||||
clearInterval(task);
|
||||
reject(new Error(t("TXT_CODE_361a79c6")));
|
||||
}
|
||||
}, 3 * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
const imageName = instance.config.docker.image;
|
||||
if (!imageName) throw new Error(t("TXT_CODE_17be5f70"));
|
||||
const cachedStartCount = instance.startCount;
|
||||
// If the image exists, there is no need to pull again.
|
||||
if (await checkImage(imageName)) return;
|
||||
|
||||
try {
|
||||
const docker = new DefaultDocker();
|
||||
instance.println("CONTAINER", t("TXT_CODE_2fa46b8c") + imageName);
|
||||
instance.asynchronousTask = this;
|
||||
|
||||
await docker.pull(imageName, {});
|
||||
|
||||
await this.awaitImageDone(instance, imageName);
|
||||
if (cachedStartCount !== instance.startCount) return;
|
||||
instance.println("CONTAINER", t("TXT_CODE_c68b0bef"));
|
||||
} catch (err: any) {
|
||||
if (cachedStartCount !== instance.startCount) return;
|
||||
throw new Error([t("TXT_CODE_db37b7f9"), err?.message].join("\n"));
|
||||
} finally {
|
||||
this.stopped(instance);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(instance: Instance): Promise<void> {
|
||||
this.stopped(instance);
|
||||
}
|
||||
}
|
42
daemon/src/entity/commands/docker/docker_start.ts
Executable file
42
daemon/src/entity/commands/docker/docker_start.ts
Executable file
@ -0,0 +1,42 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import logger from "../../../service/log";
|
||||
import fs from "fs-extra";
|
||||
import DockerPullCommand from "./docker_pull";
|
||||
import {
|
||||
DockerProcessAdapter,
|
||||
SetupDockerContainer,
|
||||
StartupDockerProcessError
|
||||
} from "../../../service/docker_process_service";
|
||||
import AbsStartCommand from "../start";
|
||||
|
||||
export default class DockerStartCommand extends AbsStartCommand {
|
||||
protected async createProcess(instance: Instance) {
|
||||
if (!instance.hasCwdPath() || !instance.config.ie || !instance.config.oe)
|
||||
throw new StartupDockerProcessError($t("TXT_CODE_a6424dcc"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath())) fs.mkdirpSync(instance.absoluteCwdPath());
|
||||
|
||||
// Docker Image check
|
||||
try {
|
||||
await instance.forceExec(new DockerPullCommand());
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Docker docks to the process adapter
|
||||
const processAdapter = new DockerProcessAdapter(new SetupDockerContainer(instance));
|
||||
await processAdapter.start({
|
||||
isTty: instance.config.terminalOption.pty,
|
||||
w: instance.config.terminalOption.ptyWindowCol,
|
||||
h: instance.config.terminalOption.ptyWindowCol
|
||||
});
|
||||
|
||||
instance.started(processAdapter);
|
||||
logger.info(
|
||||
$t("TXT_CODE_instance.successful", {
|
||||
v: `${instance.config.nickname} ${instance.instanceUuid}`
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
38
daemon/src/entity/commands/general/general_command.ts
Executable file
38
daemon/src/entity/commands/general/general_command.ts
Executable file
@ -0,0 +1,38 @@
|
||||
import { $t } from "../../../i18n";
|
||||
|
||||
import Instance from "../../instance/instance";
|
||||
import { encode } from "iconv-lite";
|
||||
import InstanceCommand from "../base/command";
|
||||
|
||||
export const CTRL_C = "\x03";
|
||||
|
||||
export function isExitCommand(instance: Instance, buf: any) {
|
||||
if (String(buf).toLowerCase() === "^c") {
|
||||
instance.process?.kill("SIGINT");
|
||||
return true;
|
||||
}
|
||||
if (buf == CTRL_C) {
|
||||
instance.process?.write(CTRL_C);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default class GeneralSendCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("SendCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance, buf?: any): Promise<any> {
|
||||
if (isExitCommand(instance, buf)) return;
|
||||
// The server shutdown command needs to send a command, but before the server shutdown command is executed, the status will be set to the shutdown state.
|
||||
// So here the command can only be executed by whether the process exists or not
|
||||
if (instance?.process) {
|
||||
instance.process.write(encode(buf, instance.config.ie));
|
||||
if (instance.config.crlf === 2) return instance.process.write("\r\n");
|
||||
return instance.process.write("\n");
|
||||
} else {
|
||||
instance.failure(new Error($t("TXT_CODE_command.instanceNotOpen")));
|
||||
}
|
||||
}
|
||||
}
|
66
daemon/src/entity/commands/general/general_install.ts
Executable file
66
daemon/src/entity/commands/general/general_install.ts
Executable file
@ -0,0 +1,66 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import fs from "fs-extra";
|
||||
import { QuickInstallTask } from "../../../service/async_task_service/quick_install";
|
||||
import { IQuickStartPackages } from "common/global";
|
||||
|
||||
export default class GeneralInstallCommand extends InstanceCommand {
|
||||
private process?: QuickInstallTask;
|
||||
|
||||
constructor() {
|
||||
super("GeneralInstallCommand");
|
||||
}
|
||||
|
||||
private stopped(instance: Instance) {
|
||||
instance.asynchronousTask = undefined;
|
||||
instance.setLock(false);
|
||||
instance.status(Instance.STATUS_STOP);
|
||||
}
|
||||
|
||||
async exec(instance: Instance, params?: IQuickStartPackages) {
|
||||
if (instance.status() !== Instance.STATUS_STOP)
|
||||
return instance.failure(new Error($t("TXT_CODE_general_update.statusErr_notStop")));
|
||||
if (instance.asynchronousTask)
|
||||
return instance.failure(new Error($t("TXT_CODE_general_update.statusErr_otherProgress")));
|
||||
if (!params) throw new Error("GeneralInstallCommand: No params");
|
||||
try {
|
||||
instance.setLock(true);
|
||||
instance.status(Instance.STATUS_BUSY);
|
||||
instance.println($t("TXT_CODE_1704ea49"), $t("TXT_CODE_cbc235ad"));
|
||||
if (instance.hasCwdPath()) {
|
||||
await fs.remove(instance.absoluteCwdPath());
|
||||
await fs.mkdirs(instance.absoluteCwdPath());
|
||||
}
|
||||
instance.println($t("TXT_CODE_1704ea49"), $t("TXT_CODE_906c5d6a"));
|
||||
this.process = new QuickInstallTask(
|
||||
instance.config.nickname,
|
||||
params.targetLink,
|
||||
params.setupInfo,
|
||||
instance
|
||||
);
|
||||
instance.asynchronousTask = this;
|
||||
instance.println($t("TXT_CODE_1704ea49"), $t("TXT_CODE_b9ca022b"));
|
||||
await this.process.start();
|
||||
await this.process.wait();
|
||||
|
||||
instance.println($t("TXT_CODE_1704ea49"), $t("TXT_CODE_f220ed78"));
|
||||
} catch (err: any) {
|
||||
instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.error", { err })
|
||||
);
|
||||
} finally {
|
||||
this.stopped(instance);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(instance: Instance): Promise<void> {
|
||||
instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.killProcess")
|
||||
);
|
||||
this.stopped(instance);
|
||||
await this.process?.stop();
|
||||
}
|
||||
}
|
34
daemon/src/entity/commands/general/general_kill.ts
Executable file
34
daemon/src/entity/commands/general/general_kill.ts
Executable file
@ -0,0 +1,34 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import logger from "../../../service/log";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
|
||||
export default class GeneralKillCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("KillCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
if (instance.status() === Instance.STATUS_STOP) return;
|
||||
|
||||
if (instance.startTimestamp && instance.startTimestamp + 6 * 1000 > Date.now()) {
|
||||
return instance.failure(new Error($t("TXT_CODE_6259357c")));
|
||||
}
|
||||
|
||||
instance.ignoreEventTaskOnce();
|
||||
|
||||
const task = instance?.asynchronousTask;
|
||||
if (task && task.stop) {
|
||||
task
|
||||
.stop(instance)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
logger.error(`Instance ${instance.config.nickname} asynchronousTask stop error:`, err);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.process) {
|
||||
await instance.process.kill("SIGKILL");
|
||||
}
|
||||
}
|
||||
}
|
47
daemon/src/entity/commands/general/general_restart.ts
Executable file
47
daemon/src/entity/commands/general/general_restart.ts
Executable file
@ -0,0 +1,47 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
|
||||
export default class GeneralRestartCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("GeneralRestartCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
try {
|
||||
instance.ignoreEventTaskOnce();
|
||||
instance.println("INFO", $t("TXT_CODE_restart.start"));
|
||||
instance.setLock(true);
|
||||
await instance.execPreset("stop");
|
||||
const startCount = instance.startCount;
|
||||
// Check the instance status every second,
|
||||
// if the instance status is stopped, restart the server immediately
|
||||
const task = setInterval(async () => {
|
||||
try {
|
||||
if (startCount !== instance.startCount) {
|
||||
throw new Error($t("TXT_CODE_restart.error1"));
|
||||
}
|
||||
if (
|
||||
instance.status() !== Instance.STATUS_STOPPING &&
|
||||
instance.status() !== Instance.STATUS_STOP
|
||||
) {
|
||||
throw new Error($t("TXT_CODE_restart.error2"));
|
||||
}
|
||||
if (instance.status() === Instance.STATUS_STOP) {
|
||||
instance.println("INFO", $t("TXT_CODE_restart.restarting"));
|
||||
instance.setLock(false);
|
||||
clearInterval(task);
|
||||
await instance.execPreset("start");
|
||||
}
|
||||
} catch (error: any) {
|
||||
clearInterval(task);
|
||||
instance.setLock(false);
|
||||
instance.failure(error);
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
instance.setLock(false);
|
||||
instance.failure(error);
|
||||
}
|
||||
}
|
||||
}
|
120
daemon/src/entity/commands/general/general_start.ts
Executable file
120
daemon/src/entity/commands/general/general_start.ts
Executable file
@ -0,0 +1,120 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import logger from "../../../service/log";
|
||||
import fs from "fs-extra";
|
||||
import EventEmitter from "events";
|
||||
import { IInstanceProcess } from "../../instance/interface";
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import { commandStringToArray } from "../base/command_parser";
|
||||
import { killProcess } from "common";
|
||||
import AbsStartCommand from "../start";
|
||||
|
||||
// Error exception at startup
|
||||
class StartupError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Docker process adapter
|
||||
class ProcessAdapter extends EventEmitter implements IInstanceProcess {
|
||||
pid?: number | string;
|
||||
|
||||
constructor(private process: ChildProcess) {
|
||||
super();
|
||||
this.pid = this.process.pid;
|
||||
process.stdout?.on("data", (text) => this.emit("data", text));
|
||||
process.stderr?.on("data", (text) => this.emit("data", text));
|
||||
process.on("exit", (code) => this.emit("exit", code));
|
||||
}
|
||||
|
||||
public write(data?: string) {
|
||||
return this.process.stdin?.write(data);
|
||||
}
|
||||
|
||||
public kill(s?: any) {
|
||||
if (this.pid) return killProcess(this.pid, this.process, s);
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
// remove all dynamically added event listeners
|
||||
for (const n of this.eventNames()) this.removeAllListeners(n);
|
||||
if (this.process.stdout)
|
||||
for (const eventName of this.process.stdout.eventNames())
|
||||
this.process.stdout.removeAllListeners(eventName);
|
||||
if (this.process.stderr)
|
||||
for (const eventName of this.process.stderr.eventNames())
|
||||
this.process.stderr.removeAllListeners(eventName);
|
||||
if (this.process)
|
||||
for (const eventName of this.process.eventNames()) this.process.removeAllListeners(eventName);
|
||||
this.process?.stdout?.destroy();
|
||||
this.process?.stderr?.destroy();
|
||||
if (this.process?.exitCode === null) {
|
||||
this.process.kill("SIGTERM");
|
||||
this.process.kill("SIGKILL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class GeneralStartCommand extends AbsStartCommand {
|
||||
async createProcess(instance: Instance, source = "") {
|
||||
if (
|
||||
(!instance.config.startCommand && instance.config.processType === "general") ||
|
||||
!instance.hasCwdPath() ||
|
||||
!instance.config.ie ||
|
||||
!instance.config.oe
|
||||
)
|
||||
throw new StartupError($t("TXT_CODE_general_start.instanceConfigErr"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath())) fs.mkdirpSync(instance.absoluteCwdPath());
|
||||
|
||||
// command parsing
|
||||
const commandList = commandStringToArray(instance.config.startCommand);
|
||||
const commandExeFile = commandList[0];
|
||||
const commandParameters = commandList.slice(1);
|
||||
if (commandList.length === 0) {
|
||||
throw new StartupError($t("TXT_CODE_general_start.cmdEmpty"));
|
||||
}
|
||||
|
||||
logger.info("----------------");
|
||||
logger.info($t("TXT_CODE_general_start.startInstance", { source: source }));
|
||||
logger.info($t("TXT_CODE_general_start.instanceUuid", { uuid: instance.instanceUuid }));
|
||||
logger.info($t("TXT_CODE_general_start.startCmd", { cmdList: JSON.stringify(commandList) }));
|
||||
logger.info($t("TXT_CODE_general_start.cwd", { cwd: instance.absoluteCwdPath() }));
|
||||
logger.info("----------------");
|
||||
|
||||
// create child process
|
||||
// Parameter 1 directly passes the process name or path (including spaces) without double quotes
|
||||
const subProcess = spawn(commandExeFile, commandParameters, {
|
||||
cwd: instance.absoluteCwdPath(),
|
||||
stdio: "pipe",
|
||||
windowsHide: true,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
// child process creation result check
|
||||
if (!subProcess || !subProcess.pid) {
|
||||
instance.println(
|
||||
"ERROR",
|
||||
$t("TXT_CODE_general_start.pidErr", {
|
||||
startCommand: instance.config.startCommand,
|
||||
commandExeFile: commandExeFile,
|
||||
commandParameters: JSON.stringify(commandParameters)
|
||||
})
|
||||
);
|
||||
throw new StartupError($t("TXT_CODE_general_start.startErr"));
|
||||
}
|
||||
|
||||
// create process adapter
|
||||
const processAdapter = new ProcessAdapter(subProcess);
|
||||
|
||||
// generate open event
|
||||
instance.started(processAdapter);
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_start.startSuccess", {
|
||||
instanceUuid: instance.instanceUuid,
|
||||
pid: subProcess.pid
|
||||
})
|
||||
);
|
||||
instance.println("INFO", $t("TXT_CODE_general_start.startOrdinaryTerminal"));
|
||||
}
|
||||
}
|
41
daemon/src/entity/commands/general/general_stop.ts
Executable file
41
daemon/src/entity/commands/general/general_stop.ts
Executable file
@ -0,0 +1,41 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
|
||||
export default class GeneralStopCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("StopCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
const stopCommand = instance.config.stopCommand;
|
||||
if (instance.status() === Instance.STATUS_STOP || !instance.process)
|
||||
return instance.failure(new Error($t("TXT_CODE_general_stop.notRunning")));
|
||||
|
||||
instance.status(Instance.STATUS_STOPPING);
|
||||
instance.ignoreEventTaskOnce();
|
||||
|
||||
const stopCommandList = stopCommand.split("\n");
|
||||
for (const stopCommand of stopCommandList) {
|
||||
await instance.execPreset("command", stopCommand);
|
||||
}
|
||||
|
||||
instance.print("\n");
|
||||
instance.println("INFO", $t("TXT_CODE_pty_stop.execCmd", { stopCommand: `\n${stopCommand}` }));
|
||||
|
||||
const cacheStartCount = instance.startCount;
|
||||
|
||||
// If the instance is still in the stopped state after 10 minutes, restore the state
|
||||
setTimeout(() => {
|
||||
if (
|
||||
instance.status() === Instance.STATUS_STOPPING &&
|
||||
instance.startCount === cacheStartCount
|
||||
) {
|
||||
instance.println("ERROR", $t("TXT_CODE_general_stop.stopErr"));
|
||||
instance.status(Instance.STATUS_RUNNING);
|
||||
}
|
||||
}, 1000 * 60 * 10);
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
60
daemon/src/entity/commands/general/general_update.ts
Executable file
60
daemon/src/entity/commands/general/general_update.ts
Executable file
@ -0,0 +1,60 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import logger from "../../../service/log";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { InstanceUpdateAction } from "../../../service/instance_update_action";
|
||||
|
||||
export default class GeneralUpdateCommand extends InstanceCommand {
|
||||
private updateTask?: InstanceUpdateAction;
|
||||
|
||||
constructor() {
|
||||
super("GeneralUpdateCommand");
|
||||
}
|
||||
|
||||
private stopped(instance: Instance) {
|
||||
instance.asynchronousTask = undefined;
|
||||
instance.setLock(false);
|
||||
instance.status(Instance.STATUS_STOP);
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
if (instance.status() !== Instance.STATUS_STOP && instance.status() !== Instance.STATUS_BUSY)
|
||||
return instance.failure(new Error($t("TXT_CODE_general_update.statusErr_notStop")));
|
||||
if (instance.asynchronousTask)
|
||||
return instance.failure(new Error($t("TXT_CODE_general_update.statusErr_otherProgress")));
|
||||
|
||||
try {
|
||||
instance.setLock(true);
|
||||
instance.asynchronousTask = this;
|
||||
instance.status(Instance.STATUS_BUSY);
|
||||
|
||||
this.updateTask = new InstanceUpdateAction(instance);
|
||||
await this.updateTask.start();
|
||||
await this.updateTask.wait();
|
||||
} catch (err: any) {
|
||||
instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.error", { err: err })
|
||||
);
|
||||
} finally {
|
||||
this.stopped(instance);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(instance: Instance): Promise<void> {
|
||||
instance.asynchronousTask = undefined;
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_update.terminateUpdate", { instanceUuid: instance.instanceUuid })
|
||||
);
|
||||
instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.terminateUpdate", { instanceUuid: instance.instanceUuid })
|
||||
);
|
||||
instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.killProcess")
|
||||
);
|
||||
|
||||
await this.updateTask?.stop();
|
||||
}
|
||||
}
|
32
daemon/src/entity/commands/minecraft/mc_ping.ts
Normal file
32
daemon/src/entity/commands/minecraft/mc_ping.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { MCServerStatus, toNumber } from "common";
|
||||
|
||||
export default class PingJavaMinecraftServerCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("PingJavaMinecraftServerCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
const host = instance.config.pingConfig.ip || "localhost";
|
||||
try {
|
||||
if (instance.config.pingConfig.port) {
|
||||
const result = await new MCServerStatus(instance.config.pingConfig.port, host).getStatus();
|
||||
if (result.online) {
|
||||
instance.info.mcPingOnline = true;
|
||||
instance.info.currentPlayers = toNumber(result.current_players) ?? 0;
|
||||
instance.info.maxPlayers = toNumber(result.max_players) ?? 0;
|
||||
instance.info.version = result.version;
|
||||
instance.info.latency = toNumber(result.latency) ?? 0;
|
||||
} else {
|
||||
instance.resetPingInfo();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
instance.resetPingInfo();
|
||||
// ignore error
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
77
daemon/src/entity/commands/minecraft/mc_ping_bedrock.ts
Executable file
77
daemon/src/entity/commands/minecraft/mc_ping_bedrock.ts
Executable file
@ -0,0 +1,77 @@
|
||||
import dgram from "dgram";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
|
||||
// Get Minecraft Bedrock server MOTD information
|
||||
// Author: https://github.com/Mcayear
|
||||
async function request(ip: string, port: number) {
|
||||
const message = Buffer.from(
|
||||
"01 00 00 00 00 00 06 18 20 00 FF FF 00 FE FE FE FE FD FD FD FD 12 34 56 78 A3 61 1C F8 BA 8F D5 60".replace(
|
||||
/ /g,
|
||||
""
|
||||
),
|
||||
"hex"
|
||||
);
|
||||
const client = dgram.createSocket("udp4");
|
||||
var Config = {
|
||||
ip,
|
||||
port
|
||||
};
|
||||
return new Promise((r, j) => {
|
||||
client.on("error", (err: any) => {
|
||||
try {
|
||||
client.close();
|
||||
} finally {
|
||||
j(err);
|
||||
}
|
||||
});
|
||||
client.on("message", (data: any) => {
|
||||
const result = data.toString().split(";");
|
||||
try {
|
||||
client.close();
|
||||
} finally {
|
||||
r(result);
|
||||
}
|
||||
});
|
||||
client.send(message, Config.port, Config.ip, (err: any) => {
|
||||
if (err) {
|
||||
try {
|
||||
client.close();
|
||||
} finally {
|
||||
j(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
j("request timeout");
|
||||
try {
|
||||
client.close();
|
||||
} catch (error: any) {}
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
// Adapt to MCSManager lifecycle tasks
|
||||
export default class MinecraftBedrockGetPlayersCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("MinecraftBedrockGetPlayersCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
if (instance.config.pingConfig.ip && instance.config.pingConfig.port) {
|
||||
try {
|
||||
const info: any = await request(
|
||||
instance.config.pingConfig.ip,
|
||||
instance.config.pingConfig.port
|
||||
);
|
||||
return {
|
||||
version: info[3],
|
||||
motd: info[0],
|
||||
current_players: info[4],
|
||||
max_players: info[5]
|
||||
};
|
||||
} catch (error: any) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
10
daemon/src/entity/commands/nullfunc.ts
Executable file
10
daemon/src/entity/commands/nullfunc.ts
Executable file
@ -0,0 +1,10 @@
|
||||
import InstanceCommand from "./base/command";
|
||||
|
||||
export default class NullCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("NullCommand");
|
||||
}
|
||||
async exec() {
|
||||
// Do nothing.....
|
||||
}
|
||||
}
|
25
daemon/src/entity/commands/process_info.ts
Executable file
25
daemon/src/entity/commands/process_info.ts
Executable file
@ -0,0 +1,25 @@
|
||||
import { ProcessConfig } from "../instance/process_config";
|
||||
import pidusage from "pidusage";
|
||||
import InstanceCommand from "./base/command";
|
||||
import Instance from "../instance/instance";
|
||||
|
||||
export default class ProcessInfoCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("ProcessInfo");
|
||||
}
|
||||
async exec(instance: Instance): Promise<Object> {
|
||||
let info: any = {
|
||||
cpu: 0, // percentage (from 0 to 100*vcore)
|
||||
memory: 0, // bytes
|
||||
ppid: 0, // PPID
|
||||
pid: 0, // PID
|
||||
ctime: 0, // ms user + system time
|
||||
elapsed: 0, // ms since the start of the process
|
||||
timestamp: 0 // ms since epoch
|
||||
};
|
||||
if (instance.process && instance.process.pid) {
|
||||
info = await pidusage(instance.process.pid);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
}
|
17
daemon/src/entity/commands/pty/pty_resize.ts
Normal file
17
daemon/src/entity/commands/pty/pty_resize.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { GoPtyProcessAdapter } from "./pty_start";
|
||||
|
||||
export default class PtyResizeCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
super("ResizeTTY");
|
||||
}
|
||||
|
||||
async exec(instance: Instance): Promise<any> {
|
||||
const pty = instance.process as Partial<GoPtyProcessAdapter>;
|
||||
if (typeof pty?.resize === "function") {
|
||||
const { w, h } = instance.computeTerminalSize();
|
||||
pty?.resize(w, h);
|
||||
}
|
||||
}
|
||||
}
|
273
daemon/src/entity/commands/pty/pty_start.ts
Executable file
273
daemon/src/entity/commands/pty/pty_start.ts
Executable file
@ -0,0 +1,273 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import os from "os";
|
||||
import Instance from "../../instance/instance";
|
||||
import logger from "../../../service/log";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import readline from "readline";
|
||||
import EventEmitter from "events";
|
||||
import { IInstanceProcess } from "../../instance/interface";
|
||||
import { ChildProcess, ChildProcessWithoutNullStreams, exec, spawn } from "child_process";
|
||||
import { commandStringToArray } from "../base/command_parser";
|
||||
import { killProcess } from "common";
|
||||
import FunctionDispatcher from "../dispatcher";
|
||||
import { PTY_PATH } from "../../../const";
|
||||
import { Writable } from "stream";
|
||||
import { v4 } from "uuid";
|
||||
import AbsStartCommand from "../start";
|
||||
|
||||
interface IPtySubProcessCfg {
|
||||
pid: number;
|
||||
}
|
||||
|
||||
// Error exception at startup
|
||||
class StartupError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
const GO_PTY_MSG_TYPE = {
|
||||
RESIZE: 0x04
|
||||
};
|
||||
|
||||
// process adapter
|
||||
export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProcess {
|
||||
private pipeClient?: Writable;
|
||||
|
||||
constructor(
|
||||
private readonly process: ChildProcess,
|
||||
public readonly pid: number,
|
||||
public readonly pipeName: string
|
||||
) {
|
||||
super();
|
||||
process.stdout?.on("data", (text) => this.emit("data", text));
|
||||
process.stderr?.on("data", (text) => this.emit("data", text));
|
||||
process.on("exit", (code) => this.emit("exit", code));
|
||||
this.initNamedPipe();
|
||||
}
|
||||
|
||||
private async initNamedPipe() {
|
||||
try {
|
||||
const fd = await fs.open(this.pipeName, "w");
|
||||
const writePipe = fs.createWriteStream("", { fd });
|
||||
writePipe.on("close", () => {});
|
||||
writePipe.on("end", () => {});
|
||||
writePipe.on("error", (err) => {
|
||||
logger.error("Pipe error:", this.pipeName, err);
|
||||
});
|
||||
this.pipeClient = writePipe;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
$t("TXT_CODE_9d1d244f", {
|
||||
pipeName: error
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(w: number, h: number) {
|
||||
const MAX_W = 900;
|
||||
if (w > MAX_W) w = MAX_W;
|
||||
if (h > MAX_W) h = MAX_W;
|
||||
const resizeStruct = JSON.stringify({ width: Number(w), height: Number(h) });
|
||||
const len = resizeStruct.length;
|
||||
const lenBuff = Buffer.alloc(2);
|
||||
lenBuff.writeInt16BE(len, 0);
|
||||
const buf = Buffer.from([GO_PTY_MSG_TYPE.RESIZE, ...lenBuff, ...Buffer.from(resizeStruct)]);
|
||||
this.writeToNamedPipe(buf);
|
||||
}
|
||||
|
||||
public writeToNamedPipe(data: Buffer) {
|
||||
this.pipeClient?.write(data);
|
||||
}
|
||||
|
||||
public write(data?: string) {
|
||||
return this.process.stdin?.write(data);
|
||||
}
|
||||
|
||||
public kill(s?: any) {
|
||||
return killProcess(this.pid, this.process, s);
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
for (const n of this.eventNames()) this.removeAllListeners(n);
|
||||
if (this.process.stdout)
|
||||
for (const eventName of this.process.stdout.eventNames())
|
||||
this.process.stdout.removeAllListeners(eventName);
|
||||
if (this.process.stderr)
|
||||
for (const eventName of this.process.stderr.eventNames())
|
||||
this.process.stderr.removeAllListeners(eventName);
|
||||
if (this.process)
|
||||
for (const eventName of this.process.eventNames())
|
||||
this.process.stdout?.removeAllListeners(eventName);
|
||||
if (this.pipeClient)
|
||||
for (const eventName of this.pipeClient.eventNames())
|
||||
this.pipeClient.removeAllListeners(eventName);
|
||||
this.pipeClient?.destroy();
|
||||
this.process?.stdout?.destroy();
|
||||
this.process?.stderr?.destroy();
|
||||
if (this.process?.exitCode === null) {
|
||||
this.process.kill("SIGTERM");
|
||||
this.process.kill("SIGKILL");
|
||||
}
|
||||
fs.remove(this.pipeName, (err) => {});
|
||||
}
|
||||
}
|
||||
|
||||
export default class PtyStartCommand extends AbsStartCommand {
|
||||
readPtySubProcessConfig(subProcess: ChildProcessWithoutNullStreams): Promise<IPtySubProcessCfg> {
|
||||
return new Promise((r, j) => {
|
||||
const errConfig = {
|
||||
pid: 0
|
||||
};
|
||||
const rl = readline.createInterface({
|
||||
input: subProcess.stdout,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
rl.on("line", (line = "") => {
|
||||
try {
|
||||
rl.removeAllListeners();
|
||||
const cfg = JSON.parse(line) as IPtySubProcessCfg;
|
||||
if (cfg.pid == null) throw new Error("Error");
|
||||
r(cfg);
|
||||
} catch (error: any) {
|
||||
r(errConfig);
|
||||
}
|
||||
});
|
||||
setTimeout(() => {
|
||||
r(errConfig);
|
||||
}, 1000 * 3);
|
||||
});
|
||||
}
|
||||
|
||||
async createProcess(instance: Instance) {
|
||||
if (
|
||||
!instance.config.startCommand ||
|
||||
!instance.hasCwdPath() ||
|
||||
!instance.config.ie ||
|
||||
!instance.config.oe
|
||||
)
|
||||
throw new StartupError($t("TXT_CODE_pty_start.cmdErr"));
|
||||
if (!fs.existsSync(instance.absoluteCwdPath())) fs.mkdirpSync(instance.absoluteCwdPath());
|
||||
if (!path.isAbsolute(path.normalize(instance.absoluteCwdPath())))
|
||||
throw new StartupError($t("TXT_CODE_pty_start.mustAbsolutePath"));
|
||||
|
||||
// PTY mode correctness check
|
||||
logger.info($t("TXT_CODE_pty_start.startPty", { source: "" }));
|
||||
let checkPtyEnv = true;
|
||||
|
||||
if (!fs.existsSync(PTY_PATH)) {
|
||||
instance.println("ERROR", $t("TXT_CODE_pty_start.startErr"));
|
||||
checkPtyEnv = false;
|
||||
}
|
||||
|
||||
if (checkPtyEnv === false) {
|
||||
// Close the PTY type, reconfigure the instance function group, and restart the instance
|
||||
instance.config.terminalOption.pty = false;
|
||||
await instance.forceExec(new FunctionDispatcher());
|
||||
await instance.execPreset("start"); // execute the preset command directly
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the startup state & increase the number of startups
|
||||
instance.status(Instance.STATUS_STARTING);
|
||||
instance.startCount++;
|
||||
|
||||
// command parsing
|
||||
let commandList: string[] = [];
|
||||
if (os.platform() === "win32") {
|
||||
// windows: cmd.exe /c {{startCommand}}
|
||||
commandList = [instance.config.startCommand];
|
||||
} else {
|
||||
commandList = commandStringToArray(instance.config.startCommand);
|
||||
}
|
||||
|
||||
if (commandList.length === 0)
|
||||
return instance.failure(new StartupError($t("TXT_CODE_pty_start.cmdEmpty")));
|
||||
|
||||
const pipeId = v4();
|
||||
const pipeLinuxDir = "/tmp/mcsmanager-instance-pipe";
|
||||
if (!fs.existsSync(pipeLinuxDir)) fs.mkdirsSync(pipeLinuxDir);
|
||||
let pipeName = `${pipeLinuxDir}/pipe-${pipeId}`;
|
||||
if (os.platform() === "win32") {
|
||||
pipeName = `\\\\.\\pipe\\mcsmanager-${pipeId}`;
|
||||
}
|
||||
|
||||
const ptyParameter = [
|
||||
"-size",
|
||||
`${instance.config.terminalOption.ptyWindowCol},${instance.config.terminalOption.ptyWindowRow}`,
|
||||
"-coder",
|
||||
instance.config.oe,
|
||||
"-dir",
|
||||
instance.absoluteCwdPath(),
|
||||
"-fifo",
|
||||
pipeName,
|
||||
"-cmd",
|
||||
JSON.stringify(commandList)
|
||||
];
|
||||
|
||||
logger.info("----------------");
|
||||
logger.info($t("TXT_CODE_pty_start.sourceRequest", { source: "" }));
|
||||
logger.info($t("TXT_CODE_pty_start.instanceUuid", { instanceUuid: instance.instanceUuid }));
|
||||
logger.info($t("TXT_CODE_pty_start.startCmd", { cmd: commandList.join(" ") }));
|
||||
logger.info($t("TXT_CODE_pty_start.ptyPath", { path: PTY_PATH }));
|
||||
logger.info($t("TXT_CODE_pty_start.ptyParams", { param: ptyParameter.join(" ") }));
|
||||
logger.info($t("TXT_CODE_pty_start.ptyCwd", { cwd: instance.absoluteCwdPath() }));
|
||||
logger.info("----------------");
|
||||
|
||||
// create pty child process
|
||||
// Parameter 1 directly passes the process name or path (including spaces) without double quotes
|
||||
const subProcess = spawn(PTY_PATH, ptyParameter, {
|
||||
cwd: path.dirname(PTY_PATH),
|
||||
stdio: "pipe",
|
||||
windowsHide: true,
|
||||
env: {
|
||||
...process.env,
|
||||
TERM: "xterm-256color"
|
||||
}
|
||||
});
|
||||
|
||||
// pty child process creation result check
|
||||
if (!subProcess || !subProcess.pid) {
|
||||
instance.println(
|
||||
"ERROR",
|
||||
$t("TXT_CODE_pty_start.pidErr", {
|
||||
startCommand: instance.config.startCommand,
|
||||
path: PTY_PATH,
|
||||
params: JSON.stringify(ptyParameter)
|
||||
})
|
||||
);
|
||||
throw new StartupError($t("TXT_CODE_pty_start.instanceStartErr"));
|
||||
}
|
||||
|
||||
// create process adapter
|
||||
const ptySubProcessCfg = await this.readPtySubProcessConfig(subProcess);
|
||||
const processAdapter = new GoPtyProcessAdapter(subProcess, ptySubProcessCfg.pid, pipeName);
|
||||
|
||||
// After reading the configuration, Need to check the process status
|
||||
// The "processAdapter.pid" here represents the process created by the PTY process
|
||||
if (subProcess.exitCode !== null || processAdapter.pid == null || processAdapter.pid === 0) {
|
||||
instance.println(
|
||||
"ERROR",
|
||||
$t("TXT_CODE_pty_start.pidErr", {
|
||||
startCommand: instance.config.startCommand,
|
||||
path: PTY_PATH,
|
||||
params: JSON.stringify(ptyParameter)
|
||||
})
|
||||
);
|
||||
throw new StartupError($t("TXT_CODE_pty_start.instanceStartErr"));
|
||||
}
|
||||
|
||||
// generate open event
|
||||
instance.started(processAdapter);
|
||||
|
||||
logger.info(
|
||||
$t("TXT_CODE_pty_start.startSuccess", {
|
||||
instanceUuid: instance.instanceUuid,
|
||||
pid: ptySubProcessCfg.pid
|
||||
})
|
||||
);
|
||||
instance.println("INFO", $t("TXT_CODE_pty_start.startEmulatedTerminal"));
|
||||
}
|
||||
}
|
58
daemon/src/entity/commands/start.ts
Executable file
58
daemon/src/entity/commands/start.ts
Executable file
@ -0,0 +1,58 @@
|
||||
import { $t } from "../../i18n";
|
||||
import Instance from "../instance/instance";
|
||||
import InstanceCommand from "./base/command";
|
||||
|
||||
export class StartupError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export default abstract class AbsStartCommand extends InstanceCommand {
|
||||
private async sleep() {
|
||||
return new Promise((ok) => {
|
||||
setTimeout(ok, 1000 * 2);
|
||||
});
|
||||
}
|
||||
|
||||
async exec(instance: Instance) {
|
||||
if (instance.status() !== Instance.STATUS_STOP)
|
||||
return instance.failure(new StartupError($t("TXT_CODE_start.instanceNotDown")));
|
||||
|
||||
try {
|
||||
instance.setLock(true);
|
||||
instance.status(Instance.STATUS_STARTING);
|
||||
instance.startCount++;
|
||||
|
||||
instance.startTimestamp = Date.now();
|
||||
|
||||
if (instance.config.endTime) {
|
||||
const endTime = instance.config.endTime;
|
||||
if (endTime) {
|
||||
if (endTime <= instance.startTimestamp) {
|
||||
throw new Error($t("TXT_CODE_start.instanceMaturity"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instance.print("\n\n");
|
||||
instance.println("INFO", $t("TXT_CODE_start.startInstance"));
|
||||
|
||||
// prevent the dead-loop from starting
|
||||
await this.sleep();
|
||||
|
||||
return await this.createProcess(instance);
|
||||
} catch (error: any) {
|
||||
try {
|
||||
await instance.execPreset("kill");
|
||||
} catch (ignore) {}
|
||||
instance.releaseResources();
|
||||
instance.status(Instance.STATUS_STOP);
|
||||
instance.failure(error);
|
||||
} finally {
|
||||
instance.setLock(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract createProcess(instance: Instance): Promise<void>;
|
||||
}
|
58
daemon/src/entity/commands/steam/rcon_command.ts
Executable file
58
daemon/src/entity/commands/steam/rcon_command.ts
Executable file
@ -0,0 +1,58 @@
|
||||
import { t } from "i18next";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import Rcon from "rcon-srcds";
|
||||
import { isExitCommand } from "../general/general_command";
|
||||
|
||||
async function sendRconCommand(instance: Instance, command: string) {
|
||||
const targetIp = instance.config.rconIp || "localhost";
|
||||
const rconServer = new Rcon({
|
||||
port: instance.config.rconPort,
|
||||
host: targetIp,
|
||||
encoding: "utf8",
|
||||
timeout: 1000 * 6
|
||||
});
|
||||
await rconServer.authenticate(instance.config.rconPassword);
|
||||
if (!rconServer.isAuthenticated()) {
|
||||
throw new Error(t("TXT_CODE_1b1b2934"));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let hasResult = false;
|
||||
setTimeout(() => {
|
||||
if (!hasResult) {
|
||||
rconServer.disconnect().catch(() => {});
|
||||
instance.print(`[RCON] ${t("TXT_CODE_386f2d66")}\n`);
|
||||
resolve("");
|
||||
}
|
||||
}, 1000 * 10);
|
||||
rconServer
|
||||
.execute(command)
|
||||
.then((res) => {
|
||||
hasResult = true;
|
||||
instance.print(`[RCON] ${res}\n`);
|
||||
rconServer.disconnect().catch(() => {});
|
||||
resolve(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
instance.print(`[RCON] <<< ${command}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
export default class RconCommand extends InstanceCommand {
|
||||
constructor(public readonly cmd?: string) {
|
||||
super("RconSendCommand");
|
||||
}
|
||||
|
||||
async exec(instance: Instance, text?: string): Promise<any> {
|
||||
if (isExitCommand(instance, text)) return;
|
||||
try {
|
||||
if (text || this.cmd) {
|
||||
await sendRconCommand(instance, String(text ?? this.cmd));
|
||||
}
|
||||
} catch (error: any) {
|
||||
instance.println("RCON ERROR", error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
23
daemon/src/entity/commands/task/mc_players.ts
Executable file
23
daemon/src/entity/commands/task/mc_players.ts
Executable file
@ -0,0 +1,23 @@
|
||||
import { ILifeCycleTask } from "../../instance/life_cycle";
|
||||
import Instance from "../../instance/instance";
|
||||
import { MCServerStatus } from "common";
|
||||
|
||||
// When the instance is running, continue to check the expiration time
|
||||
export default class PingMinecraftServerTask implements ILifeCycleTask {
|
||||
public status: number = 0;
|
||||
public name: string = "TimeCheck";
|
||||
|
||||
private task?: NodeJS.Timeout;
|
||||
|
||||
async start(instance: Instance) {
|
||||
this.task = setInterval(() => {
|
||||
instance.execPreset("refreshPlayers");
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
async stop(instance: Instance) {
|
||||
instance.resetPingInfo();
|
||||
clearInterval(this.task);
|
||||
this.task = undefined;
|
||||
}
|
||||
}
|
115
daemon/src/entity/commands/task/openfrp.ts
Executable file
115
daemon/src/entity/commands/task/openfrp.ts
Executable file
@ -0,0 +1,115 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { ILifeCycleTask } from "../../instance/life_cycle";
|
||||
import Instance from "../../instance/instance";
|
||||
import logger from "../../../service/log";
|
||||
import { $t } from "../../../i18n";
|
||||
import { ProcessWrapper } from "common";
|
||||
import { FRPC_PATH } from "../../../const";
|
||||
import { downloadFileToLocalFile } from "../../../service/download";
|
||||
export class OpenFrp {
|
||||
public processWrapper?: ProcessWrapper;
|
||||
|
||||
constructor(public readonly token: string, public readonly tunnelId: string) {
|
||||
// ./frpc -u <passowrd> -p <channel id>
|
||||
this.processWrapper = new ProcessWrapper(
|
||||
FRPC_PATH,
|
||||
["-u", this.token, "-p", this.tunnelId],
|
||||
path.dirname(FRPC_PATH)
|
||||
);
|
||||
}
|
||||
|
||||
public open() {
|
||||
logger.info("Start openfrp:", FRPC_PATH);
|
||||
this.processWrapper?.start();
|
||||
if (!this.processWrapper?.getPid()) {
|
||||
throw new Error("pid is null");
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
try {
|
||||
if (this.processWrapper?.exitCode() == null) {
|
||||
this.processWrapper?.kill();
|
||||
}
|
||||
this.processWrapper = undefined;
|
||||
} catch (error: any) {}
|
||||
}
|
||||
}
|
||||
|
||||
export default class OpenFrpTask implements ILifeCycleTask {
|
||||
public status: number = 0;
|
||||
public name: string = "openfrp";
|
||||
public static readonly FRP_EXE_NAME = `frpc_${os.platform()}_${os.arch()}${
|
||||
os.platform() === "win32" ? ".exe" : ""
|
||||
}`;
|
||||
public static readonly FRP_EXE_PATH = path.normalize(
|
||||
path.join(process.cwd(), "lib", OpenFrpTask.FRP_EXE_NAME)
|
||||
);
|
||||
public static readonly FRP_DOWNLOAD_ADDR = "https://mcsmanager.oss-cn-guangzhou.aliyuncs.com/";
|
||||
|
||||
async start(instance: Instance) {
|
||||
const { openFrpToken, openFrpTunnelId } = instance.config?.extraServiceConfig;
|
||||
if (!openFrpToken || !openFrpTunnelId) return;
|
||||
|
||||
if (!fs.existsSync(OpenFrpTask.FRP_EXE_PATH)) {
|
||||
const tmpTask = setInterval(() => {
|
||||
instance.println("FRP", $t("TXT_CODE_frp.installing"));
|
||||
}, 2000);
|
||||
try {
|
||||
await downloadFileToLocalFile(
|
||||
OpenFrpTask.FRP_DOWNLOAD_ADDR + OpenFrpTask.FRP_EXE_NAME,
|
||||
OpenFrpTask.FRP_EXE_PATH
|
||||
);
|
||||
instance.println("FRP", $t("TXT_CODE_frp.done"));
|
||||
} catch (error: any) {
|
||||
logger.error($t("TXT_CODE_frp.downloadErr"), error);
|
||||
instance.println("ERROR", $t("TXT_CODE_frp.downloadErr") + `: ${error}`);
|
||||
fs.remove(OpenFrpTask.FRP_EXE_PATH, () => {});
|
||||
return;
|
||||
} finally {
|
||||
clearInterval(tmpTask);
|
||||
}
|
||||
}
|
||||
|
||||
const frpProcess = new OpenFrp(openFrpToken, openFrpTunnelId);
|
||||
frpProcess.processWrapper?.on("start", (pid: number) => {
|
||||
if (pid) {
|
||||
logger.info(
|
||||
`Instance ${instance.config.nickname}(${instance.instanceUuid}) ${pid} Frp task started!`
|
||||
);
|
||||
logger.info(`Params: ${openFrpTunnelId} | ${openFrpToken}`);
|
||||
instance.openFrp = frpProcess;
|
||||
instance.info.openFrpStatus = true;
|
||||
} else {
|
||||
logger.warn(
|
||||
`Instance ${instance.config.nickname}(${instance.instanceUuid}) Frp task start failed! Process ID is ${pid}`
|
||||
);
|
||||
}
|
||||
});
|
||||
frpProcess.processWrapper?.on("exit", () => {
|
||||
logger.info(
|
||||
`Instance ${instance.config.nickname}(${instance.instanceUuid}) Frp task stopped!`
|
||||
);
|
||||
instance.info.openFrpStatus = false;
|
||||
instance.openFrp = undefined;
|
||||
});
|
||||
|
||||
try {
|
||||
frpProcess.open();
|
||||
} catch (error: any) {
|
||||
logger.warn(
|
||||
`Instance ${instance.config.nickname}(${instance.instanceUuid}) Frp task Start failure! ERR:`
|
||||
);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(instance: Instance) {
|
||||
if (instance.openFrp) {
|
||||
const frpProcess = instance.openFrp;
|
||||
frpProcess.stop();
|
||||
}
|
||||
}
|
||||
}
|
30
daemon/src/entity/commands/task/time.ts
Executable file
30
daemon/src/entity/commands/task/time.ts
Executable file
@ -0,0 +1,30 @@
|
||||
import { ILifeCycleTask } from "../../instance/life_cycle";
|
||||
import Instance from "../../instance/instance";
|
||||
|
||||
// When the instance is running, continue to check the expiration time
|
||||
export default class TimeCheck implements ILifeCycleTask {
|
||||
public status: number = 0;
|
||||
public name: string = "TimeCheck";
|
||||
|
||||
private task: any = null;
|
||||
|
||||
async start(instance: Instance) {
|
||||
this.task = setInterval(async () => {
|
||||
if (instance.config.endTime) {
|
||||
const endTime = instance.config.endTime;
|
||||
if (endTime) {
|
||||
const currentTime = Date.now();
|
||||
if (endTime <= currentTime) {
|
||||
// Expired, execute the end process command
|
||||
await instance.execPreset("kill");
|
||||
clearInterval(this.task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000 * 60 * 60);
|
||||
}
|
||||
|
||||
async stop(instance: Instance) {
|
||||
clearInterval(this.task);
|
||||
}
|
||||
}
|
50
daemon/src/entity/config.ts
Executable file
50
daemon/src/entity/config.ts
Executable file
@ -0,0 +1,50 @@
|
||||
import { v4 } from "uuid";
|
||||
import StorageSubsystem from "../common/system_storage";
|
||||
|
||||
function builderPassword() {
|
||||
const a = `${v4().replace(/\-/gim, "")}`;
|
||||
const b = a.slice(0, a.length / 2 - 1);
|
||||
const c = `${v4().replace(/\-/gim, "")}`;
|
||||
return b + c;
|
||||
}
|
||||
|
||||
// @Entity
|
||||
class Config {
|
||||
public version = 2;
|
||||
public ip = "";
|
||||
public port = 24444;
|
||||
public prefix = "";
|
||||
public key = builderPassword();
|
||||
public maxFileTask = 2;
|
||||
public maxZipFileSize = 200;
|
||||
public language = "en_us";
|
||||
public defaultInstancePath = "";
|
||||
}
|
||||
|
||||
// daemon configuration class
|
||||
class GlobalConfiguration {
|
||||
public config = new Config();
|
||||
private static readonly ID = "global";
|
||||
|
||||
load() {
|
||||
let config: Config = StorageSubsystem.load("Config", Config, GlobalConfiguration.ID);
|
||||
if (config == null) {
|
||||
config = new Config();
|
||||
StorageSubsystem.store("Config", GlobalConfiguration.ID, config);
|
||||
}
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
store() {
|
||||
StorageSubsystem.store("Config", GlobalConfiguration.ID, this.config);
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalEnv {
|
||||
public fileTaskCount = 0;
|
||||
}
|
||||
|
||||
const globalConfiguration = new GlobalConfiguration();
|
||||
const globalEnv = new GlobalEnv();
|
||||
|
||||
export { globalConfiguration, Config, globalEnv };
|
14
daemon/src/entity/ctx.ts
Executable file
14
daemon/src/entity/ctx.ts
Executable file
@ -0,0 +1,14 @@
|
||||
import { Socket } from "socket.io";
|
||||
|
||||
export default class RouterContext {
|
||||
constructor(
|
||||
public uuid: string | null,
|
||||
public socket: Socket,
|
||||
public session?: any,
|
||||
public event?: string
|
||||
) {}
|
||||
|
||||
public response(data: any) {
|
||||
return this;
|
||||
}
|
||||
}
|
83
daemon/src/entity/instance/Instance_config.ts
Executable file
83
daemon/src/entity/instance/Instance_config.ts
Executable file
@ -0,0 +1,83 @@
|
||||
import Instance from "./instance";
|
||||
import os from "os";
|
||||
import { IGlobalInstanceConfig, IGlobalInstanceDockerConfig } from "common/global";
|
||||
interface IActionCommand {
|
||||
name: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
type ProcessType = "general" | "docker";
|
||||
|
||||
// @Entity
|
||||
export default class InstanceConfig implements IGlobalInstanceConfig {
|
||||
public nickname = "Undefined";
|
||||
public startCommand = "";
|
||||
public stopCommand = "^C";
|
||||
public cwd = ".";
|
||||
public ie = "utf-8";
|
||||
public oe = "utf-8";
|
||||
public createDatetime = Date.now();
|
||||
public lastDatetime = Date.now();
|
||||
public type = Instance.TYPE_UNIVERSAL;
|
||||
public tag: string[] = [];
|
||||
public endTime: number = 0;
|
||||
public fileCode: string = "utf-8";
|
||||
public processType: ProcessType = "general";
|
||||
public updateCommand: string = "";
|
||||
public crlf = os.platform() === "win32" ? 2 : 1; // 1: \n 2: \r\n
|
||||
public category = 0;
|
||||
|
||||
// Steam RCON protocol
|
||||
public enableRcon = false;
|
||||
public rconPassword = "";
|
||||
public rconPort = 0;
|
||||
public rconIp = "";
|
||||
|
||||
// custom command list
|
||||
public actionCommandList: IActionCommand[] = [];
|
||||
|
||||
// terminal option
|
||||
public terminalOption = {
|
||||
haveColor: false,
|
||||
pty: true,
|
||||
ptyWindowCol: 164,
|
||||
ptyWindowRow: 40
|
||||
};
|
||||
|
||||
// Event task
|
||||
public eventTask = {
|
||||
autoStart: false,
|
||||
autoRestart: false,
|
||||
ignore: false
|
||||
};
|
||||
|
||||
// Extend
|
||||
public docker: IGlobalInstanceDockerConfig = {
|
||||
containerName: "",
|
||||
image: "",
|
||||
ports: [],
|
||||
extraVolumes: [],
|
||||
memory: 0,
|
||||
networkMode: "bridge",
|
||||
networkAliases: [],
|
||||
cpusetCpus: "",
|
||||
cpuUsage: 0,
|
||||
maxSpace: 0,
|
||||
io: 0,
|
||||
network: 0,
|
||||
workingDir: "/workspace/",
|
||||
env: [],
|
||||
changeWorkdir: true
|
||||
};
|
||||
|
||||
public pingConfig = {
|
||||
ip: "",
|
||||
port: 25565,
|
||||
type: 1
|
||||
};
|
||||
|
||||
public extraServiceConfig = {
|
||||
openFrpTunnelId: "",
|
||||
openFrpToken: ""
|
||||
};
|
||||
}
|
445
daemon/src/entity/instance/instance.ts
Executable file
445
daemon/src/entity/instance/instance.ts
Executable file
@ -0,0 +1,445 @@
|
||||
import { $t } from "../../i18n";
|
||||
import iconv from "iconv-lite";
|
||||
import path from "path";
|
||||
import { EventEmitter } from "events";
|
||||
import { IExecutable } from "./preset";
|
||||
import InstanceCommand from "../commands/base/command";
|
||||
import InstanceConfig from "./Instance_config";
|
||||
import StorageSubsystem from "../../common/system_storage";
|
||||
import { LifeCycleTaskManager } from "./life_cycle";
|
||||
import { PresetCommandManager } from "./preset";
|
||||
import FunctionDispatcher, { IPresetCommand } from "../commands/dispatcher";
|
||||
import { IInstanceProcess } from "./interface";
|
||||
import StartCommand from "../commands/start";
|
||||
import { configureEntityParams, toText } from "common";
|
||||
import { OpenFrp } from "../commands/task/openfrp";
|
||||
import logger from "../../service/log";
|
||||
import { t } from "i18next";
|
||||
|
||||
interface IInstanceInfo {
|
||||
mcPingOnline: boolean;
|
||||
currentPlayers: number;
|
||||
maxPlayers: number;
|
||||
version: string;
|
||||
fileLock: number;
|
||||
playersChart: Array<{ value: string }>;
|
||||
openFrpStatus: boolean;
|
||||
latency: number;
|
||||
}
|
||||
|
||||
interface IWatcherInfo {
|
||||
terminalSize: {
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
}
|
||||
|
||||
const LINE_MAX_SIZE = 1024;
|
||||
const TERM_TEXT_YELLOW = "\x1B[0;33;1m";
|
||||
const TERM_TEXT_GOLD = "\x1B[0;33m"; // Gold §6
|
||||
const TERM_RESET = "\x1B[0m";
|
||||
const IGNORE_TEXT = [
|
||||
"\n\n",
|
||||
TERM_TEXT_GOLD,
|
||||
"[MCSMANAGER] ",
|
||||
TERM_RESET,
|
||||
TERM_TEXT_YELLOW,
|
||||
t("TXT_CODE_c5ed896f"),
|
||||
TERM_RESET,
|
||||
"\n\n"
|
||||
].join("");
|
||||
|
||||
export default class Instance extends EventEmitter {
|
||||
public static readonly STATUS_BUSY = -1;
|
||||
public static readonly STATUS_STOP = 0;
|
||||
public static readonly STATUS_STOPPING = 1;
|
||||
public static readonly STATUS_STARTING = 2;
|
||||
public static readonly STATUS_RUNNING = 3;
|
||||
|
||||
public static readonly TYPE_UNIVERSAL = "universal";
|
||||
public static readonly TYPE_MINECRAFT_JAVA = "minecraft/java";
|
||||
public static readonly TYPE_MINECRAFT_BEDROCK = "minecraft/bedrock";
|
||||
|
||||
public instanceStatus: number = Instance.STATUS_STOP;
|
||||
public instanceUuid: string = "";
|
||||
public lock: boolean = false;
|
||||
public startCount: number = 0;
|
||||
public startTimestamp: number = 0;
|
||||
public asynchronousTask?: IExecutable | null;
|
||||
public openFrp?: OpenFrp;
|
||||
|
||||
public readonly lifeCycleTaskManager = new LifeCycleTaskManager(this);
|
||||
public readonly presetCommandManager = new PresetCommandManager(this);
|
||||
|
||||
public config: InstanceConfig;
|
||||
|
||||
public info: IInstanceInfo = {
|
||||
mcPingOnline: false,
|
||||
currentPlayers: 0,
|
||||
maxPlayers: 0,
|
||||
version: "",
|
||||
fileLock: 0,
|
||||
playersChart: [],
|
||||
openFrpStatus: false,
|
||||
latency: 0
|
||||
};
|
||||
|
||||
public watchers: Map<string, IWatcherInfo> = new Map();
|
||||
|
||||
public process?: IInstanceProcess;
|
||||
|
||||
private outputStack: string[] = [];
|
||||
private outputLoopTask?: NodeJS.Timer;
|
||||
|
||||
// When initializing an instance, the instance must be initialized through uuid and configuration class, otherwise the instance will be unavailable
|
||||
constructor(instanceUuid: string, config: InstanceConfig) {
|
||||
super();
|
||||
|
||||
if (!instanceUuid || !config) throw new Error($t("TXT_CODE_instanceConf.initInstanceErr"));
|
||||
|
||||
// Basic information
|
||||
this.instanceStatus = Instance.STATUS_STOP;
|
||||
this.instanceUuid = instanceUuid;
|
||||
|
||||
// Action lock
|
||||
this.lock = false;
|
||||
|
||||
this.config = config;
|
||||
|
||||
this.process = undefined;
|
||||
this.startCount = 0;
|
||||
}
|
||||
|
||||
isStoppedOrBusy() {
|
||||
return [Instance.STATUS_STOP, Instance.STATUS_BUSY].includes(this.status());
|
||||
}
|
||||
|
||||
// Pass in instance configuration, loosely and dynamically set configuration items for instance parameters
|
||||
parameters(cfg: any, persistence = true) {
|
||||
// If the instance type changes, default commands and lifecycle events must be reset
|
||||
if (cfg?.type && cfg?.type != this.config.type) {
|
||||
if (!this.isStoppedOrBusy())
|
||||
throw new Error($t("TXT_CODE_instanceConf.cantModifyInstanceType"));
|
||||
configureEntityParams(this.config, cfg, "type", String);
|
||||
this.forceExec(new FunctionDispatcher());
|
||||
}
|
||||
|
||||
if (cfg?.enableRcon != null && cfg?.enableRcon !== this.config.enableRcon) {
|
||||
if (!this.isStoppedOrBusy()) throw new Error($t("TXT_CODE_bdfa3457"));
|
||||
configureEntityParams(this.config, cfg, "enableRcon", Boolean);
|
||||
this.forceExec(new FunctionDispatcher());
|
||||
}
|
||||
|
||||
if (cfg?.processType && cfg?.processType !== this.config.processType) {
|
||||
if (!this.isStoppedOrBusy())
|
||||
throw new Error($t("TXT_CODE_instanceConf.cantModifyProcessType"));
|
||||
configureEntityParams(this.config, cfg, "processType", String);
|
||||
this.forceExec(new FunctionDispatcher());
|
||||
}
|
||||
|
||||
// If the terminal type is changed, the default command must be reset
|
||||
if (
|
||||
cfg?.terminalOption?.pty != null &&
|
||||
cfg?.terminalOption?.pty !== this.config.terminalOption.pty
|
||||
) {
|
||||
if (!this.isStoppedOrBusy()) throw new Error($t("TXT_CODE_instanceConf.cantModifyPtyModel"));
|
||||
configureEntityParams(this.config.terminalOption, cfg.terminalOption, "pty", Boolean);
|
||||
this.forceExec(new FunctionDispatcher());
|
||||
}
|
||||
|
||||
// Only allow some configuration items to be modified when the server is stopped
|
||||
if (this.isStoppedOrBusy() && cfg.terminalOption) {
|
||||
configureEntityParams(this.config.terminalOption, cfg.terminalOption, "ptyWindowCol", Number);
|
||||
configureEntityParams(this.config.terminalOption, cfg.terminalOption, "ptyWindowRow", Number);
|
||||
}
|
||||
|
||||
if (cfg.tag instanceof Array) {
|
||||
cfg.tag = cfg.tag.map((tag: any) => String(tag).trim());
|
||||
this.config.tag = cfg.tag;
|
||||
}
|
||||
|
||||
if (cfg?.extraServiceConfig) {
|
||||
configureEntityParams(
|
||||
this.config.extraServiceConfig,
|
||||
cfg.extraServiceConfig,
|
||||
"isOpenFrp",
|
||||
Boolean
|
||||
);
|
||||
configureEntityParams(
|
||||
this.config.extraServiceConfig,
|
||||
cfg.extraServiceConfig,
|
||||
"openFrpToken",
|
||||
String
|
||||
);
|
||||
configureEntityParams(
|
||||
this.config.extraServiceConfig,
|
||||
cfg.extraServiceConfig,
|
||||
"openFrpTunnelId",
|
||||
String
|
||||
);
|
||||
}
|
||||
|
||||
configureEntityParams(this.config, cfg, "nickname", String);
|
||||
configureEntityParams(this.config, cfg, "startCommand", String);
|
||||
configureEntityParams(this.config, cfg, "stopCommand", String);
|
||||
configureEntityParams(this.config, cfg, "updateCommand", String);
|
||||
configureEntityParams(this.config, cfg, "cwd", String);
|
||||
configureEntityParams(this.config, cfg, "ie", String);
|
||||
configureEntityParams(this.config, cfg, "oe", String);
|
||||
configureEntityParams(this.config, cfg, "crlf", Number);
|
||||
configureEntityParams(this.config, cfg, "endTime", Number);
|
||||
configureEntityParams(this.config, cfg, "fileCode", String);
|
||||
configureEntityParams(this.config, cfg, "rconPassword", String);
|
||||
configureEntityParams(this.config, cfg, "rconPort", Number);
|
||||
configureEntityParams(this.config, cfg, "rconIp", String);
|
||||
configureEntityParams(this.config, cfg, "category", Number);
|
||||
|
||||
if (cfg.docker) {
|
||||
configureEntityParams(this.config.docker, cfg.docker, "containerName", String);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "image", String);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "memory", Number);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "ports");
|
||||
configureEntityParams(this.config.docker, cfg.docker, "extraVolumes");
|
||||
configureEntityParams(this.config.docker, cfg.docker, "maxSpace", Number);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "io", Number);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "network", Number);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "networkMode", String);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "networkAliases");
|
||||
configureEntityParams(this.config.docker, cfg.docker, "cpusetCpus", String);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "cpuUsage", Number);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "env");
|
||||
configureEntityParams(this.config.docker, cfg.docker, "workingDir", String);
|
||||
configureEntityParams(this.config.docker, cfg.docker, "changeWorkdir", Boolean);
|
||||
}
|
||||
if (cfg.pingConfig) {
|
||||
configureEntityParams(this.config.pingConfig, cfg.pingConfig, "ip", String);
|
||||
configureEntityParams(this.config.pingConfig, cfg.pingConfig, "port", Number);
|
||||
configureEntityParams(this.config.pingConfig, cfg.pingConfig, "type", Number);
|
||||
}
|
||||
if (cfg.eventTask) {
|
||||
configureEntityParams(this.config.eventTask, cfg.eventTask, "autoStart", Boolean);
|
||||
configureEntityParams(this.config.eventTask, cfg.eventTask, "autoRestart", Boolean);
|
||||
configureEntityParams(this.config.eventTask, cfg.eventTask, "ignore", Boolean);
|
||||
}
|
||||
if (cfg.terminalOption) {
|
||||
configureEntityParams(this.config.terminalOption, cfg.terminalOption, "haveColor", Boolean);
|
||||
}
|
||||
|
||||
if (persistence) StorageSubsystem.store("InstanceConfig", this.instanceUuid, this.config);
|
||||
}
|
||||
|
||||
setLock(bool: boolean) {
|
||||
if (this.lock === true && bool === true) {
|
||||
throw new Error($t("TXT_CODE_ca030197"));
|
||||
}
|
||||
this.lock = bool;
|
||||
}
|
||||
|
||||
// force the command to execute
|
||||
async forceExec(command: InstanceCommand) {
|
||||
return await command.exec(this);
|
||||
}
|
||||
|
||||
// set instance state or get state
|
||||
status(v?: number) {
|
||||
if (v != null) this.instanceStatus = v;
|
||||
return this.instanceStatus;
|
||||
}
|
||||
|
||||
// function that must be executed after the instance starts
|
||||
// Trigger the open event and bind the data and exit events, etc.
|
||||
started(process: IInstanceProcess) {
|
||||
this.config.lastDatetime = Date.now();
|
||||
const outputCode = this.config.terminalOption.pty ? "utf-8" : this.config.oe;
|
||||
process.on("data", (text: any) => {
|
||||
this.pushOutput(iconv.decode(text, outputCode));
|
||||
});
|
||||
process.on("exit", (code: number) => this.stopped(code));
|
||||
this.process = process;
|
||||
this.instanceStatus = Instance.STATUS_RUNNING;
|
||||
this.emit("open", this);
|
||||
|
||||
// start all lifecycle tasks
|
||||
this.lifeCycleTaskManager.execLifeCycleTask(1);
|
||||
this.startOutputLoop();
|
||||
}
|
||||
|
||||
// If the instance performs any operation exception, it must throw an exception through this function
|
||||
// trigger failure event
|
||||
failure(error: Error) {
|
||||
this.emit("failure", error);
|
||||
this.println("Operation Error", error.message ?? String(error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
// function that must be executed after the instance has been closed
|
||||
// trigger exit event
|
||||
stopped(code = 0) {
|
||||
this.releaseResources();
|
||||
if (this.instanceStatus != Instance.STATUS_STOP) {
|
||||
this.instanceStatus = Instance.STATUS_STOP;
|
||||
this.startTimestamp = 0;
|
||||
this.emit("exit", code);
|
||||
StorageSubsystem.store("InstanceConfig", this.instanceUuid, this.config);
|
||||
}
|
||||
|
||||
// Close all lifecycle tasks
|
||||
this.stopOutputLoop();
|
||||
this.lifeCycleTaskManager.execLifeCycleTask(0);
|
||||
|
||||
// If automatic restart is enabled, the startup operation is performed immediately
|
||||
if (!this.config.eventTask.ignore && this.config.eventTask.autoRestart) {
|
||||
this.execPreset("start")
|
||||
.then(() => {
|
||||
this.println($t("TXT_CODE_instanceConf.info"), $t("TXT_CODE_instanceConf.autoRestart"));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.println(
|
||||
$t("TXT_CODE_instanceConf.error"),
|
||||
$t("TXT_CODE_instanceConf.autoRestartErr", { err: err })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.config.eventTask.ignore = false;
|
||||
|
||||
// Turn off the warning immediately after startup, usually the startup command is written incorrectly
|
||||
const currentTimestamp = new Date().getTime();
|
||||
const startThreshold = 2 * 1000;
|
||||
if (currentTimestamp - this.startTimestamp < startThreshold) {
|
||||
this.println("ERROR", $t("TXT_CODE_aae2918f"));
|
||||
}
|
||||
}
|
||||
|
||||
ignoreEventTaskOnce() {
|
||||
if (this.config.eventTask) this.config.eventTask.ignore = true;
|
||||
}
|
||||
|
||||
// custom output method, formatting
|
||||
println(level: string, text: string) {
|
||||
const str = `[${level}] ${text}\n`;
|
||||
this.emit("data", str);
|
||||
}
|
||||
|
||||
// custom output method
|
||||
print(data: any) {
|
||||
this.emit("data", data);
|
||||
}
|
||||
|
||||
// Release resources (mainly release process-related resources)
|
||||
releaseResources() {
|
||||
try {
|
||||
this.process?.destroy();
|
||||
} catch (error: any) {
|
||||
logger.error(`Instance ${this.instanceUuid}, Release resources error: ${error}`);
|
||||
} finally {
|
||||
this.process = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// destroy this instance
|
||||
destroy() {
|
||||
if (this.process && this.process.pid) {
|
||||
this.process.kill("SIGKILL");
|
||||
}
|
||||
this.process = undefined;
|
||||
}
|
||||
|
||||
fullTime() {
|
||||
const date = new Date();
|
||||
return date.toLocaleDateString() + " " + date.getHours() + ":" + date.getMinutes();
|
||||
}
|
||||
|
||||
hasCwdPath() {
|
||||
return !!this.config.cwd;
|
||||
}
|
||||
|
||||
absoluteCwdPath() {
|
||||
if (!this.config || !this.config.cwd) throw new Error("Instance config error, cwd is Null!");
|
||||
if (path.isAbsolute(this.config.cwd)) return path.normalize(this.config.cwd);
|
||||
return path.normalize(path.join(process.cwd(), this.config.cwd));
|
||||
}
|
||||
|
||||
// execute the preset command action
|
||||
async execPreset(action: IPresetCommand, p?: any) {
|
||||
if (this.presetCommandManager) {
|
||||
return await this.presetCommandManager.execPreset(action, p);
|
||||
}
|
||||
throw new Error(`Preset Manager does not exist`);
|
||||
}
|
||||
|
||||
setPreset(action: IPresetCommand, cmd: InstanceCommand) {
|
||||
this.presetCommandManager.setPreset(action, cmd);
|
||||
}
|
||||
|
||||
getPreset(action: IPresetCommand) {
|
||||
return this.presetCommandManager.getPreset(action);
|
||||
}
|
||||
|
||||
clearPreset() {
|
||||
this.presetCommandManager.clearPreset();
|
||||
}
|
||||
|
||||
computeTerminalSize() {
|
||||
let minW = this.config.terminalOption.ptyWindowCol;
|
||||
let minH = this.config.terminalOption.ptyWindowRow;
|
||||
for (const iterator of this.watchers.values()) {
|
||||
const { w, h } = iterator.terminalSize;
|
||||
if (w && h) {
|
||||
if (w < minW) minW = w;
|
||||
if (h < minH) minH = h;
|
||||
}
|
||||
}
|
||||
return {
|
||||
w: minW,
|
||||
h: minH
|
||||
};
|
||||
}
|
||||
|
||||
public resetPingInfo() {
|
||||
this.info.mcPingOnline = false;
|
||||
this.info.currentPlayers = 0;
|
||||
this.info.maxPlayers = 0;
|
||||
this.info.version = "";
|
||||
this.info.latency = 0;
|
||||
}
|
||||
|
||||
public parseTextParams(text: string) {
|
||||
text = text.replace(/\{mcsm_workspace\}/gim, this.absoluteCwdPath());
|
||||
text = text.replace(/\{mcsm_instance_id\}/gim, this.instanceUuid);
|
||||
text = text.replace(/\{mcsm_cwd\}/gim, this.absoluteCwdPath());
|
||||
return text;
|
||||
}
|
||||
|
||||
private pushOutput(data: string) {
|
||||
if (data.length > LINE_MAX_SIZE * 100) {
|
||||
this.outputStack.push(IGNORE_TEXT);
|
||||
} else if (data.length > LINE_MAX_SIZE) {
|
||||
for (let index = 0; index < Math.ceil(data.length / LINE_MAX_SIZE); index++) {
|
||||
const tmp = data.slice(index * LINE_MAX_SIZE, (index + 1) * LINE_MAX_SIZE);
|
||||
if (tmp) this.outputStack.push(tmp);
|
||||
}
|
||||
} else {
|
||||
this.outputStack.push(data);
|
||||
}
|
||||
if (this.outputStack.length >= 100) {
|
||||
this.outputStack.splice(0, 50);
|
||||
this.outputStack.splice(0, 0, IGNORE_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
private startOutputLoop() {
|
||||
this.stopOutputLoop();
|
||||
this.outputLoopTask = setInterval(() => {
|
||||
if (this.outputStack.length > 0) {
|
||||
const buf = this.outputStack.splice(0, 10);
|
||||
this.emit("data", buf.join(""));
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
private stopOutputLoop() {
|
||||
if (this.outputLoopTask) clearInterval(this.outputLoopTask);
|
||||
this.outputLoopTask = undefined;
|
||||
}
|
||||
}
|
9
daemon/src/entity/instance/interface.ts
Executable file
9
daemon/src/entity/instance/interface.ts
Executable file
@ -0,0 +1,9 @@
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
// Instance specific process interface
|
||||
export interface IInstanceProcess extends EventEmitter {
|
||||
pid?: number | string;
|
||||
kill: (signal?: any) => any;
|
||||
destroy: () => void;
|
||||
write: (data?: any) => any;
|
||||
}
|
38
daemon/src/entity/instance/life_cycle.ts
Executable file
38
daemon/src/entity/instance/life_cycle.ts
Executable file
@ -0,0 +1,38 @@
|
||||
import Instance from "./instance";
|
||||
|
||||
export interface ILifeCycleTask {
|
||||
name: string; // task name
|
||||
status: number; // Running status, the default is 0, the task manager will automatically change
|
||||
start: (instance: Instance) => Promise<void>;
|
||||
stop: (instance: Instance) => Promise<void>;
|
||||
}
|
||||
|
||||
export class LifeCycleTaskManager {
|
||||
// list of life cycle tasks
|
||||
public readonly lifeCycleTask: ILifeCycleTask[] = [];
|
||||
|
||||
constructor(private self: any) {}
|
||||
|
||||
registerLifeCycleTask(task: ILifeCycleTask) {
|
||||
this.lifeCycleTask.push(task);
|
||||
}
|
||||
|
||||
execLifeCycleTask(type: 1 | 0) {
|
||||
if (type == 1) {
|
||||
this.lifeCycleTask.forEach((v) => {
|
||||
if (v.status === 0) v.start(this.self);
|
||||
v.status = 1;
|
||||
});
|
||||
} else {
|
||||
this.lifeCycleTask.forEach((v) => {
|
||||
if (v.status === 1) v.stop(this.self);
|
||||
v.status = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
clearLifeCycleTask() {
|
||||
this.execLifeCycleTask(0);
|
||||
this.lifeCycleTask.splice(0, this.lifeCycleTask.length);
|
||||
}
|
||||
}
|
30
daemon/src/entity/instance/preset.ts
Executable file
30
daemon/src/entity/instance/preset.ts
Executable file
@ -0,0 +1,30 @@
|
||||
import { $t } from "../../i18n";
|
||||
import { IPresetCommand } from "../commands/dispatcher";
|
||||
export interface IExecutable {
|
||||
exec: (a: any, b?: any) => Promise<any>;
|
||||
stop?: (a: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export class PresetCommandManager {
|
||||
public readonly preset = new Map<IPresetCommand, IExecutable>();
|
||||
|
||||
constructor(private self: any) {}
|
||||
|
||||
setPreset(action: IPresetCommand, cmd: IExecutable) {
|
||||
this.preset.set(action, cmd);
|
||||
}
|
||||
|
||||
getPreset(action: IPresetCommand) {
|
||||
return this.preset.get(action);
|
||||
}
|
||||
|
||||
async execPreset(action: IPresetCommand, p?: any) {
|
||||
const cmd = this.preset.get(action);
|
||||
if (!cmd) throw new Error($t("TXT_CODE_preset.actionErr", { action: action }));
|
||||
return await cmd.exec(this.self, p);
|
||||
}
|
||||
|
||||
clearPreset() {
|
||||
this.preset.clear();
|
||||
}
|
||||
}
|
77
daemon/src/entity/instance/process_config.ts
Executable file
77
daemon/src/entity/instance/process_config.ts
Executable file
@ -0,0 +1,77 @@
|
||||
import { $t } from "../../i18n";
|
||||
import yaml from "yaml";
|
||||
import toml from "@iarna/toml";
|
||||
import properties from "properties";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
|
||||
const CONFIG_FILE_ENCODE = "utf-8";
|
||||
|
||||
export interface IProcessConfig {
|
||||
fileName: string;
|
||||
path: string;
|
||||
type: string;
|
||||
info: string | null;
|
||||
redirect: string;
|
||||
from?: string;
|
||||
fromLink?: string | null;
|
||||
}
|
||||
|
||||
export class ProcessConfig {
|
||||
constructor(public iProcessConfig: IProcessConfig) {
|
||||
iProcessConfig.path = path.normalize(iProcessConfig.path);
|
||||
}
|
||||
|
||||
// Automatically parse the local file according to the type and return the configuration object
|
||||
read(): any {
|
||||
const text = fs.readFileSync(this.iProcessConfig.path, { encoding: CONFIG_FILE_ENCODE });
|
||||
if (this.iProcessConfig.type === "yml") {
|
||||
return yaml.parse(text);
|
||||
}
|
||||
if (this.iProcessConfig.type == "toml") {
|
||||
return toml.parse(text);
|
||||
}
|
||||
if (this.iProcessConfig.type === "properties") {
|
||||
return properties.parse(text);
|
||||
}
|
||||
if (this.iProcessConfig.type === "json") {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
if (this.iProcessConfig.type === "txt") {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically save to the local configuration file according to the parameter object
|
||||
write(object: Object | toml.JsonMap) {
|
||||
let text = "";
|
||||
if (this.iProcessConfig.type === "yml") {
|
||||
text = yaml.stringify(object);
|
||||
}
|
||||
if (this.iProcessConfig.type === "toml") {
|
||||
text = toml.stringify(<toml.JsonMap>object);
|
||||
}
|
||||
if (this.iProcessConfig.type === "properties") {
|
||||
text = properties.stringify(object, {
|
||||
unicode: true
|
||||
});
|
||||
text = text.replace(/ = /gim, "=");
|
||||
if (this.iProcessConfig.fileName == "server.properties") {
|
||||
text = text.replace(/\\\\u/gim, "\\u");
|
||||
}
|
||||
}
|
||||
if (this.iProcessConfig.type === "json") {
|
||||
text = JSON.stringify(object, null, 4);
|
||||
}
|
||||
if (this.iProcessConfig.type === "txt") {
|
||||
text = object.toString();
|
||||
}
|
||||
if (!text && this.iProcessConfig.type !== "txt")
|
||||
throw new Error($t("TXT_CODE_process_config.writEmpty"));
|
||||
fs.writeFileSync(this.iProcessConfig.path, text, { encoding: CONFIG_FILE_ENCODE });
|
||||
}
|
||||
|
||||
exists() {
|
||||
return fs.existsSync(this.iProcessConfig.path);
|
||||
}
|
||||
}
|
60
daemon/src/i18n/index.ts
Executable file
60
daemon/src/i18n/index.ts
Executable file
@ -0,0 +1,60 @@
|
||||
// I18n init configuration (Daemon)
|
||||
// If you want to add the language of your own country, you need to add the code here.
|
||||
|
||||
import i18next from "i18next";
|
||||
|
||||
import zh_cn from "@languages/zh_CN.json";
|
||||
import en_us from "@languages/en_US.json";
|
||||
import zh_tw from "@languages/zh_TW.json";
|
||||
import ja_JP from "@languages/ja_JP.json";
|
||||
import es_ES from "@languages/es_ES.json";
|
||||
import fr_FR from "@languages/fr_FR.json";
|
||||
import ru_RU from "@languages/ru_RU.json";
|
||||
import ko_KR from "@languages/ko_KR.json";
|
||||
import de_DE from "@languages/de_DE.json";
|
||||
import pt_BR from "@languages/pt_BR.json";
|
||||
|
||||
i18next.init({
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
lng: "en_us",
|
||||
fallbackLng: "en_us",
|
||||
resources: {
|
||||
en_us: {
|
||||
translation: en_us
|
||||
},
|
||||
zh_cn: {
|
||||
translation: zh_cn
|
||||
},
|
||||
zh_tw: {
|
||||
translation: zh_tw
|
||||
},
|
||||
ja_jp: {
|
||||
translation: ja_JP
|
||||
},
|
||||
es_es: {
|
||||
translation: es_ES
|
||||
},
|
||||
fr_fr: {
|
||||
translation: fr_FR
|
||||
},
|
||||
ru_ru: {
|
||||
translation: ru_RU
|
||||
},
|
||||
ko_kr: {
|
||||
translation: ko_KR
|
||||
},
|
||||
de_de: {
|
||||
translation: de_DE
|
||||
},
|
||||
pt_br: {
|
||||
translation: pt_BR
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// alias
|
||||
const $t = i18next.t;
|
||||
|
||||
export { $t, i18next };
|
503
daemon/src/routers/Instance_router.ts
Executable file
503
daemon/src/routers/Instance_router.ts
Executable file
@ -0,0 +1,503 @@
|
||||
import { $t } from "../i18n";
|
||||
import fs from "fs-extra";
|
||||
import * as protocol from "../service/protocol";
|
||||
import { routerApp } from "../service/router";
|
||||
import InstanceSubsystem from "../service/system_instance";
|
||||
import Instance from "../entity/instance/instance";
|
||||
import logger from "../service/log";
|
||||
import path from "path";
|
||||
|
||||
import { IInstanceDetail, IJson } from "../service/interfaces";
|
||||
import ProcessInfoCommand from "../entity/commands/process_info";
|
||||
import FileManager from "../service/system_file";
|
||||
import { ProcessConfig } from "../entity/instance/process_config";
|
||||
import { TaskCenter } from "../service/async_task_service";
|
||||
import { createQuickInstallTask } from "../service/async_task_service/quick_install";
|
||||
import { QuickInstallTask } from "../service/async_task_service/quick_install";
|
||||
import { toNumber } from "common";
|
||||
import { arrayUnique } from "common";
|
||||
|
||||
// Some instances operate router authentication middleware
|
||||
routerApp.use((event, ctx, data, next) => {
|
||||
if (event === "instance/new" && data) return next();
|
||||
if (event === "instance/overview") return next();
|
||||
if (event === "instance/select") return next();
|
||||
if (event === "instance/asynchronous") return next();
|
||||
if (event === "instance/query_asynchronous") return next();
|
||||
if (event === "instance/stop_asynchronous") return next();
|
||||
if (event.startsWith("instance")) {
|
||||
if (data.instanceUuids) return next();
|
||||
const instanceUuid = data.instanceUuid;
|
||||
if (!InstanceSubsystem.exists(instanceUuid)) {
|
||||
return protocol.error(ctx, event, {
|
||||
instanceUuid: instanceUuid,
|
||||
err: `The operation failed, the instance ${instanceUuid} does not exist.`
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Get the list of instances of this daemon (query)
|
||||
routerApp.on("instance/select", (ctx, data) => {
|
||||
const page = toNumber(data.page) ?? 1;
|
||||
const pageSize = toNumber(data.pageSize) ?? 1;
|
||||
const condition = data.condition;
|
||||
const targetTag = data.condition.tag;
|
||||
const overview: IInstanceDetail[] = [];
|
||||
// keyword condition query
|
||||
const queryWrapper = InstanceSubsystem.getQueryMapWrapper();
|
||||
const allTags: string[] = [];
|
||||
|
||||
let searchTags: string[] = [];
|
||||
if (targetTag instanceof Array && targetTag.length > 0) {
|
||||
searchTags = targetTag.map((v) => String(v).trim());
|
||||
}
|
||||
|
||||
let result = queryWrapper.select<Instance>((v) => {
|
||||
if (v.config.tag) allTags.push(...v.config.tag);
|
||||
if (InstanceSubsystem.isGlobalInstance(v)) return false;
|
||||
if (!v.config.nickname.toLowerCase().includes(condition.instanceName.toLowerCase()))
|
||||
return false;
|
||||
if (condition.status && v.instanceStatus !== Number(condition.status)) return false;
|
||||
|
||||
if (searchTags.length > 0) {
|
||||
const myTags = v.config.tag || [];
|
||||
const res = myTags.filter((v) => searchTags.includes(v));
|
||||
if (res.length === 0 || res.length !== searchTags.length) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
result = result.sort((a, b) => (a.config.nickname > b.config.nickname ? 1 : -1));
|
||||
// paging function
|
||||
const pageResult = queryWrapper.page<Instance>(result, page, pageSize);
|
||||
// filter unwanted data
|
||||
pageResult.data.forEach((instance) => {
|
||||
overview.push({
|
||||
instanceUuid: instance.instanceUuid,
|
||||
started: instance.startCount,
|
||||
status: instance.status(),
|
||||
config: instance.config,
|
||||
info: instance.info
|
||||
});
|
||||
});
|
||||
|
||||
overview.sort((a, b) => {
|
||||
if (a.status !== b.status) {
|
||||
return b.status - a.status;
|
||||
}
|
||||
return a.config.nickname >= b.config.nickname ? 1 : -1;
|
||||
});
|
||||
|
||||
protocol.response(ctx, {
|
||||
page: pageResult.page,
|
||||
pageSize: pageResult.pageSize,
|
||||
maxPage: pageResult.maxPage,
|
||||
allTags: arrayUnique(allTags).slice(0, 60),
|
||||
data: overview
|
||||
});
|
||||
});
|
||||
|
||||
// Get an overview of this daemon instance
|
||||
routerApp.on("instance/overview", (ctx) => {
|
||||
const overview: IInstanceDetail[] = [];
|
||||
InstanceSubsystem.getInstances().forEach((instance) => {
|
||||
overview.push({
|
||||
instanceUuid: instance.instanceUuid,
|
||||
started: instance.startCount,
|
||||
status: instance.status(),
|
||||
config: instance.config,
|
||||
info: instance.info
|
||||
});
|
||||
});
|
||||
|
||||
protocol.msg(ctx, "instance/overview", overview);
|
||||
});
|
||||
|
||||
// Get an overview of some instances of this daemon
|
||||
routerApp.on("instance/section", (ctx, data) => {
|
||||
const instanceUuids = data.instanceUuids as string[];
|
||||
const overview: IInstanceDetail[] = [];
|
||||
InstanceSubsystem.getInstances().forEach((instance) => {
|
||||
instanceUuids.forEach((targetUuid) => {
|
||||
if (targetUuid === instance.instanceUuid) {
|
||||
overview.push({
|
||||
instanceUuid: instance.instanceUuid,
|
||||
started: instance.startCount,
|
||||
status: instance.status(),
|
||||
config: instance.config,
|
||||
info: instance.info
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
protocol.msg(ctx, "instance/section", overview);
|
||||
});
|
||||
|
||||
// View details of a single instance
|
||||
routerApp.on("instance/detail", async (ctx, data) => {
|
||||
try {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
let processInfo = null;
|
||||
let space = 0;
|
||||
try {
|
||||
// Parts that may be wrong due to file permissions, avoid affecting the acquisition of the entire configuration
|
||||
processInfo = await instance.forceExec(new ProcessInfoCommand());
|
||||
} catch (err: any) {}
|
||||
protocol.msg(ctx, "instance/detail", {
|
||||
instanceUuid: instance.instanceUuid,
|
||||
started: instance.startCount,
|
||||
status: instance.status(),
|
||||
config: instance.config,
|
||||
info: instance.info,
|
||||
space,
|
||||
processInfo
|
||||
});
|
||||
} catch (err: any) {
|
||||
protocol.error(ctx, "instance/detail", { err: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// create a new application instance
|
||||
routerApp.on("instance/new", (ctx, data) => {
|
||||
const config = data;
|
||||
try {
|
||||
const newInstance = InstanceSubsystem.createInstance(config);
|
||||
protocol.msg(ctx, "instance/new", {
|
||||
instanceUuid: newInstance.instanceUuid,
|
||||
config: newInstance.config
|
||||
});
|
||||
} catch (err: any) {
|
||||
protocol.error(ctx, "instance/new", { instanceUuid: null, err: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// update instance data
|
||||
routerApp.on("instance/update", (ctx, data) => {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const config = data.config;
|
||||
try {
|
||||
InstanceSubsystem.getInstance(instanceUuid)?.parameters(config);
|
||||
protocol.msg(ctx, "instance/update", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
protocol.error(ctx, "instance/update", { instanceUuid: instanceUuid, err: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Request to forward all IO data of an instance
|
||||
routerApp.on("instance/forward", (ctx, data) => {
|
||||
const targetInstanceUuid = data.instanceUuid;
|
||||
const isforward: boolean = data.forward;
|
||||
try {
|
||||
// InstanceSubsystem.getInstance(targetInstanceUuid);
|
||||
if (isforward) {
|
||||
logger.info(
|
||||
$t("TXT_CODE_Instance_router.requestIO", {
|
||||
id: ctx.socket.id,
|
||||
targetInstanceUuid: targetInstanceUuid
|
||||
})
|
||||
);
|
||||
InstanceSubsystem.forward(targetInstanceUuid, ctx.socket);
|
||||
} else {
|
||||
logger.info(
|
||||
$t("TXT_CODE_Instance_router.cancelIO", {
|
||||
id: ctx.socket.id,
|
||||
targetInstanceUuid: targetInstanceUuid
|
||||
})
|
||||
);
|
||||
InstanceSubsystem.stopForward(targetInstanceUuid, ctx.socket);
|
||||
}
|
||||
protocol.msg(ctx, "instance/forward", { instanceUuid: targetInstanceUuid });
|
||||
} catch (err: any) {
|
||||
protocol.error(ctx, "instance/forward", { instanceUuid: targetInstanceUuid, err: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// open the instance
|
||||
routerApp.on("instance/open", async (ctx, data) => {
|
||||
const disableResponse = data.disableResponse;
|
||||
for (const instanceUuid of data.instanceUuids) {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
await instance!.execPreset("start");
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/open", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse) {
|
||||
logger.error(
|
||||
$t("TXT_CODE_Instance_router.openInstanceErr", { instanceUuid: instanceUuid }),
|
||||
err
|
||||
);
|
||||
protocol.error(ctx, "instance/open", { instanceUuid: instanceUuid, err: err.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// close the instance
|
||||
routerApp.on("instance/stop", async (ctx, data) => {
|
||||
const disableResponse = data.disableResponse;
|
||||
for (const instanceUuid of data.instanceUuids) {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
await instance.execPreset("stop");
|
||||
//Note: Removing this reply will cause the front-end response to be slow, because the front-end will wait for the panel-side message to be forwarded
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/stop", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
protocol.error(ctx, "instance/stop", { instanceUuid: instanceUuid, err: err.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// restart the instance
|
||||
routerApp.on("instance/restart", async (ctx, data) => {
|
||||
const disableResponse = data.disableResponse;
|
||||
for (const instanceUuid of data.instanceUuids) {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
await instance.execPreset("restart");
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/restart", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
protocol.error(ctx, "instance/restart", { instanceUuid: instanceUuid, err: err.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// terminate instance method
|
||||
routerApp.on("instance/kill", async (ctx, data) => {
|
||||
const disableResponse = data.disableResponse;
|
||||
for (const instanceUuid of data.instanceUuids) {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
if (!instance) continue;
|
||||
try {
|
||||
await instance.execPreset("kill");
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/kill", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
protocol.error(ctx, "instance/kill", { instanceUuid: instanceUuid, err: err.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Send a command to the application instance
|
||||
routerApp.on("instance/command", async (ctx, data) => {
|
||||
const disableResponse = data.disableResponse;
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const command = data.command || "";
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
try {
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
await instance.execPreset("command", command);
|
||||
if (!disableResponse) protocol.msg(ctx, "instance/command", { instanceUuid });
|
||||
} catch (err: any) {
|
||||
if (!disableResponse)
|
||||
protocol.error(ctx, "instance/command", { instanceUuid: instanceUuid, err: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// delete instance
|
||||
routerApp.on("instance/delete", (ctx, data) => {
|
||||
const instanceUuids = data.instanceUuids;
|
||||
const deleteFile = data.deleteFile;
|
||||
for (const instanceUuid of instanceUuids) {
|
||||
try {
|
||||
InstanceSubsystem.removeInstance(instanceUuid, deleteFile);
|
||||
} catch (err: any) {}
|
||||
}
|
||||
protocol.msg(ctx, "instance/delete", instanceUuids);
|
||||
});
|
||||
|
||||
// perform complex asynchronous tasks
|
||||
routerApp.on("instance/asynchronous", (ctx, data) => {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const taskName = data.taskName;
|
||||
const parameter = data.parameter;
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
|
||||
logger.info(
|
||||
$t("TXT_CODE_Instance_router.performTasks", {
|
||||
id: ctx.socket.id,
|
||||
uuid: instanceUuid,
|
||||
taskName: taskName
|
||||
})
|
||||
);
|
||||
|
||||
// Install instance via preset package
|
||||
if (taskName === "install_instance" && instance) {
|
||||
instance
|
||||
.execPreset("install", parameter)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
logger.error(
|
||||
$t("TXT_CODE_Instance_router.performTasksErr", {
|
||||
uuid: instance.instanceUuid,
|
||||
taskName: taskName,
|
||||
err: err
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Instance software update via Command
|
||||
if (taskName === "update" && instance) {
|
||||
instance
|
||||
.execPreset("update", parameter)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
logger.error(
|
||||
$t("TXT_CODE_Instance_router.performTasksErr", {
|
||||
uuid: instance.instanceUuid,
|
||||
taskName: taskName,
|
||||
err: err
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
// Quick install Minecraft server task
|
||||
if (taskName === "quick_install") {
|
||||
const newInstanceName = String(parameter.newInstanceName);
|
||||
const targetLink = String(parameter.targetLink);
|
||||
logger.info(`Quick install: Name: ${newInstanceName} | Download: ${targetLink}`);
|
||||
const task = createQuickInstallTask(targetLink, newInstanceName, parameter.setupInfo);
|
||||
return protocol.response(ctx, task.toObject());
|
||||
}
|
||||
|
||||
protocol.response(ctx, true);
|
||||
});
|
||||
|
||||
// Terminate the execution of complex asynchronous tasks
|
||||
routerApp.on("instance/stop_asynchronous", (ctx, data) => {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const { taskId } = data.parameter;
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
|
||||
// Multi-instance async task
|
||||
if (taskId && typeof taskId === "string") {
|
||||
const task = TaskCenter.getTask(taskId);
|
||||
if (!task) throw new Error(`Async Task ID: ${taskId} does not exist`);
|
||||
task.stop();
|
||||
return protocol.response(ctx, true);
|
||||
}
|
||||
|
||||
// Singleton async task
|
||||
const task = instance?.asynchronousTask;
|
||||
if (task && task.stop) {
|
||||
task
|
||||
.stop(instance)
|
||||
.then(() => {})
|
||||
.catch((err) => {});
|
||||
} else {
|
||||
return protocol.error(
|
||||
ctx,
|
||||
"instance/stop_asynchronous",
|
||||
$t("TXT_CODE_Instance_router.taskEmpty")
|
||||
);
|
||||
}
|
||||
|
||||
protocol.response(ctx, true);
|
||||
});
|
||||
|
||||
// Query async task status
|
||||
routerApp.on("instance/query_asynchronous", (ctx, data) => {
|
||||
const taskId = data.parameter.taskId as string | undefined;
|
||||
const taskName = data.taskName as string;
|
||||
const taskNameTypeMap: IJson<string> = {
|
||||
quick_install: QuickInstallTask.TYPE
|
||||
};
|
||||
const type = String(taskNameTypeMap[taskName] || QuickInstallTask.TYPE);
|
||||
if (!taskId) {
|
||||
const result = [];
|
||||
for (const task of TaskCenter.getTasks(type)) {
|
||||
result.push({
|
||||
taskId: task.taskId,
|
||||
status: task.status(),
|
||||
detail: task.toObject()
|
||||
});
|
||||
}
|
||||
protocol.response(ctx, result);
|
||||
} else {
|
||||
const task = TaskCenter.getTask(String(taskId));
|
||||
if (task)
|
||||
protocol.response(ctx, {
|
||||
taskId: task.taskId,
|
||||
status: task.status(),
|
||||
detail: task.toObject()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
routerApp.on("instance/process_config/list", (ctx, data) => {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const files = data.files;
|
||||
const result: any[] = [];
|
||||
try {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
const fileManager = new FileManager(instance.absoluteCwdPath());
|
||||
for (const filePath of files) {
|
||||
if (fileManager.check(filePath)) {
|
||||
result.push({
|
||||
file: filePath,
|
||||
check: true
|
||||
});
|
||||
}
|
||||
}
|
||||
protocol.response(ctx, result);
|
||||
} catch (err: any) {
|
||||
protocol.responseError(ctx, err);
|
||||
}
|
||||
});
|
||||
|
||||
// Get or update the content of the instance specified file
|
||||
routerApp.on("instance/process_config/file", (ctx, data) => {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
const fileName = data.fileName;
|
||||
const config = data.config || null;
|
||||
const fileType = data.type;
|
||||
try {
|
||||
const instance = InstanceSubsystem.getInstance(instanceUuid);
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
const fileManager = new FileManager(instance.absoluteCwdPath());
|
||||
if (!fileManager.check(fileName)) throw new Error($t("TXT_CODE_Instance_router.accessFileErr"));
|
||||
const filePath = path.normalize(path.join(instance.absoluteCwdPath(), fileName));
|
||||
const processConfig = new ProcessConfig({
|
||||
fileName: fileName,
|
||||
redirect: fileName,
|
||||
path: filePath,
|
||||
type: fileType,
|
||||
info: null,
|
||||
fromLink: null
|
||||
});
|
||||
if (config) {
|
||||
processConfig.write(config);
|
||||
return protocol.response(ctx, true);
|
||||
} else {
|
||||
const json = processConfig.read();
|
||||
return protocol.response(ctx, json);
|
||||
}
|
||||
} catch (err: any) {
|
||||
protocol.responseError(ctx, err);
|
||||
}
|
||||
});
|
||||
|
||||
// Get instance terminal log
|
||||
routerApp.on("instance/outputlog", async (ctx, data) => {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
try {
|
||||
const filePath = path.join(InstanceSubsystem.LOG_DIR, `${instanceUuid}.log`);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const text = await fs.readFile(filePath, { encoding: "utf-8" });
|
||||
return protocol.response(ctx, text);
|
||||
}
|
||||
protocol.responseError(ctx, new Error($t("TXT_CODE_Instance_router.terminalLogNotExist")), {
|
||||
disablePrint: true
|
||||
});
|
||||
} catch (err: any) {
|
||||
protocol.responseError(ctx, err);
|
||||
}
|
||||
});
|
72
daemon/src/routers/auth_router.ts
Executable file
72
daemon/src/routers/auth_router.ts
Executable file
@ -0,0 +1,72 @@
|
||||
import { $t } from "../i18n";
|
||||
import { routerApp } from "../service/router";
|
||||
import * as protocol from "../service/protocol";
|
||||
import { globalConfiguration } from "../entity/config";
|
||||
import logger from "../service/log";
|
||||
import RouterContext from "../entity/ctx";
|
||||
import { IGNORE } from "../const";
|
||||
import { LOGIN_BY_TOP_LEVEL, loginSuccessful } from "../service/mission_passport";
|
||||
|
||||
// latest verification time
|
||||
const AUTH_TIMEOUT = 6000;
|
||||
|
||||
// Top-level authority authentication middleware (this is the first place for any authority authentication middleware)
|
||||
routerApp.use(async (event, ctx, _, next) => {
|
||||
const socket = ctx.socket;
|
||||
// release all data flow controllers
|
||||
if (event.startsWith("stream")) return next();
|
||||
// Except for the auth controller, which is publicly accessible, other business controllers must be authorized before they can be accessed
|
||||
if (event === "auth") return await next();
|
||||
if (!ctx.session) throw new Error("Session does not exist in authentication middleware.");
|
||||
if (
|
||||
ctx.session.key === globalConfiguration.config.key &&
|
||||
ctx.session.type === LOGIN_BY_TOP_LEVEL &&
|
||||
ctx.session.login &&
|
||||
ctx.session.id
|
||||
) {
|
||||
return await next();
|
||||
}
|
||||
logger.warn(
|
||||
$t("TXT_CODE_auth_router.notAccess", {
|
||||
id: socket.id,
|
||||
address: socket.handshake.address,
|
||||
event: event
|
||||
})
|
||||
);
|
||||
return protocol.error(ctx, "error", IGNORE, {
|
||||
disablePrint: true
|
||||
});
|
||||
});
|
||||
|
||||
// authentication controller
|
||||
routerApp.on("auth", (ctx, data) => {
|
||||
if (data === globalConfiguration.config.key) {
|
||||
// The authentication is passed, and the registered session is a trusted session
|
||||
logger.info(
|
||||
$t("TXT_CODE_auth_router.access", {
|
||||
id: ctx.socket.id,
|
||||
address: ctx.socket.handshake.address
|
||||
})
|
||||
);
|
||||
loginSuccessful(ctx, data);
|
||||
protocol.msg(ctx, "auth", true);
|
||||
} else {
|
||||
protocol.msg(ctx, "auth", false);
|
||||
}
|
||||
});
|
||||
|
||||
// Connected event for timeout authentication close
|
||||
routerApp.on("connection", (ctx) => {
|
||||
const session = ctx.session;
|
||||
setTimeout(() => {
|
||||
if (!session.login) {
|
||||
ctx.socket.disconnect();
|
||||
logger.info(
|
||||
$t("TXT_CODE_auth_router.disconnect", {
|
||||
id: ctx.socket.id,
|
||||
address: ctx.socket.handshake.address
|
||||
})
|
||||
);
|
||||
}
|
||||
}, AUTH_TIMEOUT);
|
||||
});
|
114
daemon/src/routers/environment_router.ts
Executable file
114
daemon/src/routers/environment_router.ts
Executable file
@ -0,0 +1,114 @@
|
||||
import { $t } from "../i18n";
|
||||
import { DockerManager } from "../service/docker_service";
|
||||
import * as protocol from "../service/protocol";
|
||||
import { routerApp } from "../service/router";
|
||||
import * as fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { v4 } from "uuid";
|
||||
import logger from "../service/log";
|
||||
import os from "os";
|
||||
|
||||
// Get the image list of this system
|
||||
routerApp.on("environment/images", async (ctx, data) => {
|
||||
try {
|
||||
const docker = new DockerManager().getDocker();
|
||||
const result = await docker.listImages();
|
||||
protocol.response(ctx, result);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, $t("TXT_CODE_environment_router.dockerInfoErr"));
|
||||
}
|
||||
});
|
||||
|
||||
// Get the list of containers in this system
|
||||
routerApp.on("environment/containers", async (ctx, data) => {
|
||||
try {
|
||||
const docker = new DockerManager().getDocker();
|
||||
const result = await docker.listContainers();
|
||||
protocol.response(ctx, result);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the network list of this system
|
||||
routerApp.on("environment/networkModes", async (ctx, data) => {
|
||||
try {
|
||||
const docker = new DockerManager().getDocker();
|
||||
const result = await docker.listNetworks();
|
||||
protocol.response(ctx, result);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// create image
|
||||
routerApp.on("environment/new_image", async (ctx, data) => {
|
||||
try {
|
||||
const dockerFileText = data.dockerFile;
|
||||
const name = data.name;
|
||||
const tag = data.tag;
|
||||
// Initialize the image file directory and Dockerfile
|
||||
const uuid = v4();
|
||||
const dockerFileDir = path.normalize(path.join(process.cwd(), "tmp", uuid));
|
||||
if (!fs.existsSync(dockerFileDir)) fs.mkdirsSync(dockerFileDir);
|
||||
|
||||
// write to DockerFile
|
||||
const dockerFilepath = path.normalize(path.join(dockerFileDir, "Dockerfile"));
|
||||
await fs.writeFile(dockerFilepath, dockerFileText, { encoding: "utf-8" });
|
||||
|
||||
logger.info(
|
||||
$t("TXT_CODE_environment_router.crateImage", {
|
||||
name: name,
|
||||
tag: tag,
|
||||
dockerFileText: dockerFileText
|
||||
})
|
||||
);
|
||||
|
||||
// pre-response
|
||||
protocol.response(ctx, true);
|
||||
|
||||
// start creating
|
||||
const dockerImageName = `${name}:${tag}`;
|
||||
try {
|
||||
await new DockerManager().startBuildImage(dockerFileDir, dockerImageName);
|
||||
logger.info($t("TXT_CODE_environment_router.crateSuccess", { name: name, tag: tag }));
|
||||
} catch (error: any) {
|
||||
logger.info(
|
||||
$t("TXT_CODE_environment_router.crateErr", { name: name, tag: tag, error: error })
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// delete image
|
||||
routerApp.on("environment/del_image", async (ctx, data) => {
|
||||
try {
|
||||
const imageId = data.imageId;
|
||||
const docker = new DockerManager().getDocker();
|
||||
const image = docker.getImage(imageId);
|
||||
if (image) {
|
||||
logger.info($t("TXT_CODE_environment_router.delImage", { imageId: imageId }));
|
||||
await image.remove();
|
||||
} else {
|
||||
throw new Error("Image does not exist");
|
||||
}
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Get the progress of all mirroring tasks
|
||||
routerApp.on("environment/progress", async (ctx) => {
|
||||
try {
|
||||
const data: any = {};
|
||||
DockerManager.builderProgress.forEach((v, k) => {
|
||||
data[k] = v;
|
||||
});
|
||||
protocol.response(ctx, data);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
197
daemon/src/routers/file_router.ts
Executable file
197
daemon/src/routers/file_router.ts
Executable file
@ -0,0 +1,197 @@
|
||||
import { $t } from "../i18n";
|
||||
import * as protocol from "../service/protocol";
|
||||
import { routerApp } from "../service/router";
|
||||
import InstanceSubsystem from "../service/system_instance";
|
||||
import { getFileManager, getWindowsDisks } from "../service/file_router_service";
|
||||
import { globalConfiguration, globalEnv } from "../entity/config";
|
||||
import os from "os";
|
||||
|
||||
// Some routers operate router authentication middleware
|
||||
routerApp.use((event, ctx, data, next) => {
|
||||
if (event.startsWith("file/")) {
|
||||
const instanceUuid = data.instanceUuid;
|
||||
if (!InstanceSubsystem.exists(instanceUuid)) {
|
||||
return protocol.error(ctx, event, {
|
||||
instanceUuid: instanceUuid,
|
||||
err: $t("TXT_CODE_file_router.instanceNotExist", { instanceUuid: instanceUuid })
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// List the files in the specified instance working directory
|
||||
routerApp.on("file/list", (ctx, data) => {
|
||||
try {
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
const { page, pageSize, target, fileName } = data;
|
||||
fileManager.cd(target);
|
||||
const overview = fileManager.list(page, pageSize, fileName);
|
||||
protocol.response(ctx, overview);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// File chmod (only Linux)
|
||||
routerApp.on("file/chmod", async (ctx, data) => {
|
||||
try {
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
const { chmod, target, deep } = data;
|
||||
await fileManager.chmod(target, chmod, deep);
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Query the status of the file management system
|
||||
routerApp.on("file/status", async (ctx, data) => {
|
||||
try {
|
||||
const instance = InstanceSubsystem.getInstance(data.instanceUuid);
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
protocol.response(ctx, {
|
||||
instanceFileTask: instance.info.fileLock ?? 0,
|
||||
globalFileTask: globalEnv.fileTaskCount ?? 0,
|
||||
platform: os.platform(),
|
||||
isGlobalInstance: data.instanceUuid === InstanceSubsystem.GLOBAL_INSTANCE_UUID,
|
||||
disks: getWindowsDisks()
|
||||
});
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new file
|
||||
routerApp.on("file/touch", (ctx, data) => {
|
||||
try {
|
||||
const target = data.target;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
fileManager.newFile(target);
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Create a directory
|
||||
routerApp.on("file/mkdir", (ctx, data) => {
|
||||
try {
|
||||
const target = data.target;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
fileManager.mkdir(target);
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// copy the file
|
||||
routerApp.on("file/copy", async (ctx, data) => {
|
||||
try {
|
||||
// [["a.txt","b.txt"],["cxz","zzz"]]
|
||||
const targets = data.targets;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
for (const target of targets) {
|
||||
fileManager.copy(target[0], target[1]);
|
||||
}
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// move the file
|
||||
routerApp.on("file/move", async (ctx, data) => {
|
||||
try {
|
||||
// [["a.txt","b.txt"],["cxz","zzz"]]
|
||||
const targets = data.targets;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
for (const target of targets) {
|
||||
await fileManager.move(target[0], target[1]);
|
||||
}
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete Files
|
||||
routerApp.on("file/delete", async (ctx, data) => {
|
||||
try {
|
||||
const targets = data.targets;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
for (const target of targets) {
|
||||
// async delete
|
||||
fileManager.delete(target);
|
||||
}
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// edit file
|
||||
routerApp.on("file/edit", async (ctx, data) => {
|
||||
try {
|
||||
const target = data.target;
|
||||
const text = data.text;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
const result = await fileManager.edit(target, text);
|
||||
protocol.response(ctx, result ? result : true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
||||
|
||||
// compress/decompress the file
|
||||
routerApp.on("file/compress", async (ctx, data) => {
|
||||
const maxFileTask = globalConfiguration.config.maxFileTask;
|
||||
try {
|
||||
const source = data.source;
|
||||
const targets = data.targets;
|
||||
const type = data.type;
|
||||
const code = data.code;
|
||||
const fileManager = getFileManager(data.instanceUuid);
|
||||
const instance = InstanceSubsystem.getInstance(data.instanceUuid);
|
||||
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
|
||||
if (instance.info.fileLock >= maxFileTask) {
|
||||
throw new Error(
|
||||
$t("TXT_CODE_file_router.unzipLimit", {
|
||||
maxFileTask: maxFileTask,
|
||||
fileLock: instance.info.fileLock
|
||||
})
|
||||
);
|
||||
}
|
||||
// Statistics of the number of tasks in a single instance file and the number of tasks in the entire daemon process
|
||||
function fileTaskStart() {
|
||||
if (instance) {
|
||||
instance.info.fileLock++;
|
||||
globalEnv.fileTaskCount++;
|
||||
}
|
||||
}
|
||||
function fileTaskEnd() {
|
||||
if (instance) {
|
||||
instance.info.fileLock--;
|
||||
globalEnv.fileTaskCount--;
|
||||
}
|
||||
}
|
||||
|
||||
// start decompressing or compressing the file
|
||||
fileTaskStart();
|
||||
try {
|
||||
if (type === 1) {
|
||||
await fileManager.zip(source, targets, code);
|
||||
} else {
|
||||
await fileManager.unzip(source, targets, code);
|
||||
}
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
} finally {
|
||||
fileTaskEnd();
|
||||
}
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
139
daemon/src/routers/http_router.ts
Executable file
139
daemon/src/routers/http_router.ts
Executable file
@ -0,0 +1,139 @@
|
||||
import { $t } from "../i18n";
|
||||
import Router from "@koa/router";
|
||||
import send from "koa-send";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { missionPassport } from "../service/mission_passport";
|
||||
import InstanceSubsystem from "../service/system_instance";
|
||||
import FileManager from "../service/system_file";
|
||||
import formidable from "formidable";
|
||||
import { clearUploadFiles } from "../tools/filepath";
|
||||
import logger from "../service/log";
|
||||
|
||||
const router = new Router();
|
||||
|
||||
// Define the HTTP home page display route
|
||||
router.all("/", async (ctx) => {
|
||||
ctx.body = "[MCSManager Daemon] Status: OK | reference: https://mcsmanager.com/";
|
||||
ctx.status = 200;
|
||||
});
|
||||
|
||||
// file download route
|
||||
router.get("/download/:key/:fileName", async (ctx) => {
|
||||
const key = ctx.params.key;
|
||||
const paramsFileName = ctx.params.fileName;
|
||||
try {
|
||||
// Get the task from the task center
|
||||
const mission = missionPassport.getMission(key, "download");
|
||||
if (!mission) throw new Error((ctx.body = "Access denied: No task found"));
|
||||
const instance = InstanceSubsystem.getInstance(mission.parameter.instanceUuid);
|
||||
if (!instance) throw new Error($t("TXT_CODE_http_router.instanceNotExist"));
|
||||
if (!FileManager.checkFileName(paramsFileName))
|
||||
throw new Error($t("TXT_CODE_http_router.fileNameNotSpec"));
|
||||
|
||||
const cwd = instance.absoluteCwdPath();
|
||||
const fileRelativePath = mission.parameter.fileName;
|
||||
|
||||
// Check for file cross-directory security risks
|
||||
const fileManager = new FileManager(cwd);
|
||||
if (!fileManager.check(fileRelativePath))
|
||||
throw new Error((ctx.body = "Access denied: Invalid destination"));
|
||||
|
||||
// send File
|
||||
const fileAbsPath = fileManager.toAbsolutePath(fileRelativePath);
|
||||
const fileDir = path.dirname(fileAbsPath);
|
||||
const fileName = path.basename(fileAbsPath);
|
||||
ctx.set("Content-Type", "application/octet-stream");
|
||||
await send(ctx, fileName, { root: fileDir + "/", hidden: true });
|
||||
} catch (error: any) {
|
||||
ctx.body = $t("TXT_CODE_http_router.downloadErr", { error: error.message });
|
||||
ctx.status = 500;
|
||||
} finally {
|
||||
missionPassport.deleteMission(key);
|
||||
}
|
||||
});
|
||||
|
||||
// File upload route
|
||||
router.post("/upload/:key", async (ctx) => {
|
||||
const key = String(ctx.params.key);
|
||||
const unzip = Boolean(ctx.query.unzip);
|
||||
const zipCode = String(ctx.query.code);
|
||||
let tmpFiles: formidable.File | formidable.File[] | undefined;
|
||||
try {
|
||||
const mission = missionPassport.getMission(key, "upload");
|
||||
if (!mission) throw new Error("Access denied: No task found");
|
||||
const instance = InstanceSubsystem.getInstance(mission.parameter.instanceUuid);
|
||||
if (!instance) throw new Error("Access denied: No instance found");
|
||||
const uploadDir = mission.parameter.uploadDir;
|
||||
const cwd = instance.absoluteCwdPath();
|
||||
const tmpFiles = ctx.request.files?.file;
|
||||
if (tmpFiles) {
|
||||
let uploadedFile: formidable.File;
|
||||
if (tmpFiles instanceof Array) {
|
||||
uploadedFile = tmpFiles[0];
|
||||
} else {
|
||||
throw new Error("Access denied: Files must a array!");
|
||||
}
|
||||
|
||||
const originFileName = uploadedFile.originalFilename || "";
|
||||
if (!FileManager.checkFileName(path.basename(originFileName)))
|
||||
throw new Error("Access denied: Malformed file name");
|
||||
const fileManager = new FileManager(cwd);
|
||||
|
||||
const ext = path.extname(originFileName);
|
||||
const basename = path.basename(originFileName, ext);
|
||||
|
||||
let tempFileSaveName = basename + ext;
|
||||
let counter = 1;
|
||||
|
||||
while (
|
||||
fs.existsSync(
|
||||
fileManager.toAbsolutePath(path.normalize(path.join(uploadDir, tempFileSaveName)))
|
||||
) &&
|
||||
ctx.query.overwrite === "false"
|
||||
) {
|
||||
if (counter == 1) {
|
||||
tempFileSaveName = `${basename}-copy${ext}`;
|
||||
} else {
|
||||
tempFileSaveName = `${basename}-copy-${counter}${ext}`;
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
let fileSaveRelativePath = path.normalize(path.join(uploadDir, tempFileSaveName));
|
||||
|
||||
if (!fileManager.checkPath(fileSaveRelativePath))
|
||||
throw new Error("Access denied: Invalid destination");
|
||||
|
||||
const fileSaveAbsolutePath = fileManager.toAbsolutePath(fileSaveRelativePath);
|
||||
|
||||
logger.info(
|
||||
"Browser Upload File:",
|
||||
fileSaveAbsolutePath,
|
||||
"File size:",
|
||||
Number(uploadedFile.size / 1024 / 1024).toFixed(0),
|
||||
"MB"
|
||||
);
|
||||
|
||||
await fs.move(uploadedFile.filepath, fileSaveAbsolutePath, {
|
||||
overwrite: true
|
||||
});
|
||||
|
||||
if (unzip) {
|
||||
const instanceFiles = new FileManager(instance.absoluteCwdPath());
|
||||
instanceFiles.unzip(fileSaveAbsolutePath, ".", zipCode);
|
||||
}
|
||||
ctx.body = "OK";
|
||||
return;
|
||||
}
|
||||
ctx.body = "Access denied: No file found";
|
||||
ctx.status = 500;
|
||||
} catch (error: any) {
|
||||
ctx.body = error.message;
|
||||
ctx.status = 500;
|
||||
} finally {
|
||||
missionPassport.deleteMission(key);
|
||||
if (tmpFiles) clearUploadFiles(tmpFiles);
|
||||
}
|
||||
});
|
||||
export default router;
|
53
daemon/src/routers/info_router.ts
Executable file
53
daemon/src/routers/info_router.ts
Executable file
@ -0,0 +1,53 @@
|
||||
import * as protocol from "../service/protocol";
|
||||
import { routerApp } from "../service/router";
|
||||
import InstanceSubsystem from "../service/system_instance";
|
||||
import Instance from "../entity/instance/instance";
|
||||
|
||||
import { systemInfo } from "common";
|
||||
import { getVersion } from "../service/version";
|
||||
import { globalConfiguration } from "../entity/config";
|
||||
import i18next from "i18next";
|
||||
import logger from "../service/log";
|
||||
import fs from "fs-extra";
|
||||
import { LOCAL_PRESET_LANG_PATH } from "../const";
|
||||
import VisualDataSubsystem from "../service/system_visual_data";
|
||||
|
||||
// Get the basic information of the daemon system
|
||||
routerApp.on("info/overview", async (ctx) => {
|
||||
const daemonVersion = getVersion();
|
||||
let total = 0;
|
||||
let running = 0;
|
||||
InstanceSubsystem.getInstances().forEach((v) => {
|
||||
total++;
|
||||
if (v.status() == Instance.STATUS_RUNNING) running++;
|
||||
});
|
||||
const info = {
|
||||
version: daemonVersion,
|
||||
process: {
|
||||
cpu: process.cpuUsage().system,
|
||||
memory: process.memoryUsage().heapUsed,
|
||||
cwd: process.cwd()
|
||||
},
|
||||
instance: {
|
||||
running,
|
||||
total
|
||||
},
|
||||
system: systemInfo(),
|
||||
cpuMemChart: VisualDataSubsystem.getSystemChartArray()
|
||||
};
|
||||
protocol.response(ctx, info);
|
||||
});
|
||||
|
||||
routerApp.on("info/setting", async (ctx, data) => {
|
||||
const language = String(data.language);
|
||||
try {
|
||||
logger.warn("Language change:", language);
|
||||
i18next.changeLanguage(language);
|
||||
fs.remove(LOCAL_PRESET_LANG_PATH, () => {});
|
||||
globalConfiguration.config.language = language;
|
||||
globalConfiguration.store();
|
||||
protocol.response(ctx, true);
|
||||
} catch (error: any) {
|
||||
protocol.responseError(ctx, error);
|
||||
}
|
||||
});
|
76
daemon/src/routers/instance_event_router.ts
Executable file
76
daemon/src/routers/instance_event_router.ts
Executable file
@ -0,0 +1,76 @@
|
||||
import path from "path";
|
||||
|
||||
import RouterContext from "../entity/ctx";
|
||||
import * as protocol from "../service/protocol";
|
||||
import InstanceSubsystem from "../service/system_instance";
|
||||
import fs from "fs-extra";
|
||||
const MAX_LOG_SIZE = 512;
|
||||
|
||||
// buffer
|
||||
const buffer = new Map<string, string>();
|
||||
setInterval(() => {
|
||||
buffer.forEach((buf, instanceUuid) => {
|
||||
if (!buf || !instanceUuid) return;
|
||||
const logFilePath = path.join(InstanceSubsystem.LOG_DIR, `${instanceUuid}.log`);
|
||||
if (!fs.existsSync(InstanceSubsystem.LOG_DIR)) fs.mkdirsSync(InstanceSubsystem.LOG_DIR);
|
||||
try {
|
||||
const fileInfo = fs.statSync(logFilePath);
|
||||
if (fileInfo && fileInfo.size > 1024 * MAX_LOG_SIZE) fs.removeSync(logFilePath);
|
||||
} catch (err: any) {}
|
||||
fs.writeFile(logFilePath, buf, { encoding: "utf-8", flag: "a" }, () => {
|
||||
buffer.set(instanceUuid, "");
|
||||
});
|
||||
});
|
||||
}, 500);
|
||||
|
||||
// output stream record to buffer
|
||||
async function outputLog(instanceUuid: string, text: string) {
|
||||
const buf = (buffer.get(instanceUuid) ?? "") + text;
|
||||
if (buf.length > 1024 * 1024) buffer.set(instanceUuid, "");
|
||||
buffer.set(instanceUuid, buf ?? null);
|
||||
}
|
||||
|
||||
// instance output stream event
|
||||
// By default, it is added to the data cache to control the sending rate to ensure its stability
|
||||
InstanceSubsystem.on("data", (instanceUuid: string, text: string) => {
|
||||
InstanceSubsystem.forEachForward(instanceUuid, (socket) => {
|
||||
protocol.msg(new RouterContext(null, socket), "instance/stdout", {
|
||||
instanceUuid: instanceUuid,
|
||||
text: text
|
||||
});
|
||||
});
|
||||
// Append the output to the log file
|
||||
outputLog(instanceUuid, text)
|
||||
.then(() => {})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// instance exit event
|
||||
InstanceSubsystem.on("exit", (obj: any) => {
|
||||
InstanceSubsystem.forEachForward(obj.instanceUuid, (socket) => {
|
||||
protocol.msg(new RouterContext(null, socket), "instance/stopped", {
|
||||
instanceUuid: obj.instanceUuid,
|
||||
instanceName: obj.instanceName
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// instance start event
|
||||
InstanceSubsystem.on("open", (obj: any) => {
|
||||
InstanceSubsystem.forEachForward(obj.instanceUuid, (socket) => {
|
||||
protocol.msg(new RouterContext(null, socket), "instance/opened", {
|
||||
instanceUuid: obj.instanceUuid,
|
||||
instanceName: obj.instanceName
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Instance failure event (usually used for startup failure, or other operation failures)
|
||||
InstanceSubsystem.on("failure", (obj: any) => {
|
||||
InstanceSubsystem.forEachForward(obj.instanceUuid, (socket) => {
|
||||
protocol.msg(new RouterContext(null, socket), "instance/failure", {
|
||||
instanceUuid: obj.instanceUuid,
|
||||
instanceName: obj.instanceName
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user