Compare commits
844 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6a84e41e6 | ||
|
|
a7bce7cfcc | ||
|
|
ab2c52b72c | ||
|
|
05e73be7b2 | ||
|
|
ae07469442 | ||
|
|
c51a5fcff1 | ||
|
|
c04e061505 | ||
|
|
d7cb16f1b5 | ||
|
|
50f1d314b8 | ||
|
|
c6174b8315 | ||
|
|
ac1085ff9b | ||
|
|
66cb9a93b8 | ||
|
|
515d37147b | ||
|
|
acdade473c | ||
|
|
18b659653d | ||
|
|
7e888b825c | ||
|
|
2c05e6129c | ||
|
|
e0995a63ca | ||
|
|
056a9ff9b6 | ||
|
|
01400cb9ce | ||
|
|
58d9f5ef6a | ||
|
|
69d0391d12 | ||
|
|
304f8c3a97 | ||
|
|
6c11102c09 | ||
|
|
9e714f34dd | ||
|
|
5852bcadf8 | ||
|
|
5ae9a555ce | ||
|
|
1ea525feaa | ||
|
|
afc69b13a0 | ||
|
|
1a46dde11b | ||
|
|
0e84970ae5 | ||
|
|
57c4a7527e | ||
|
|
3dcdca18a3 | ||
|
|
6ff329c897 | ||
|
|
02637e81e3 | ||
|
|
df27c0c629 | ||
|
|
c631311e96 | ||
|
|
5340c421e1 | ||
|
|
0bd79b28b4 | ||
|
|
5580ff6f01 | ||
|
|
124ed625d9 | ||
|
|
33b5f189e2 | ||
|
|
82a3a435f5 | ||
|
|
2056d4def1 | ||
|
|
40b00bae75 | ||
|
|
716bf920f5 | ||
|
|
cd88977a78 | ||
|
|
a630ef9a54 | ||
|
|
98f31d4891 | ||
|
|
5aa9c045e1 | ||
|
|
24521e3cac | ||
|
|
ad5d3ad01d | ||
|
|
6f1900f3bb | ||
|
|
bc62de795e | ||
|
|
c62ca4b183 | ||
|
|
876e5bc683 | ||
|
|
b99f3b73cd | ||
|
|
7eecf29449 | ||
|
|
1d331d7810 | ||
|
|
68414678d8 | ||
|
|
2f6b9dac26 | ||
|
|
d1812d875b | ||
|
|
723dea100f | ||
|
|
c4419ed31f | ||
|
|
754ab86e51 | ||
|
|
04dab532cd | ||
|
|
add01ebc68 | ||
|
|
1cc9a1a30b | ||
|
|
92a1de7500 | ||
|
|
a6fedcff80 | ||
|
|
55eb999305 | ||
|
|
377b7b12ce | ||
|
|
ba2906a42e | ||
|
|
ee27f14be0 | ||
|
|
46c8be63a7 | ||
|
|
7ba66c419a | ||
|
|
340775a593 | ||
|
|
35d2ec8a44 | ||
|
|
2983b9950f | ||
|
|
dbf08a6cf8 | ||
|
|
28f31be36f | ||
|
|
3ec4db0225 | ||
|
|
f5688e077a | ||
|
|
2464d255d5 | ||
|
|
586d950b8c | ||
|
|
e7469388cc | ||
|
|
ab6ca8e16a | ||
|
|
02413a4fac | ||
|
|
05b8dd9ad8 | ||
|
|
29c9419a6e | ||
|
|
90e61989a4 | ||
|
|
b1f9f90fec | ||
|
|
b40849f672 | ||
|
|
44560c8da8 | ||
|
|
46fd01c264 | ||
|
|
100695c262 | ||
|
|
54b5a4ae55 | ||
|
|
ffb252962b | ||
|
|
ae31270e63 | ||
|
|
9b2b54d585 | ||
|
|
e1ccc583a3 | ||
|
|
7750e33f82 | ||
|
|
d2c4741f0b | ||
|
|
c79c4f6bde | ||
|
|
3849d0d1a9 | ||
|
|
8bd71ccd5e | ||
|
|
b731f7fb64 | ||
|
|
cd554f77f3 | ||
|
|
8c977c51ca | ||
|
|
a3252f9671 | ||
|
|
9bc945f76f | ||
|
|
f6b4dfffb6 | ||
|
|
68955c29cb | ||
|
|
97e4d036dc | ||
|
|
0f49f54c29 | ||
|
|
828e13adbb | ||
|
|
e6f0067728 | ||
|
|
5c473eb9cc | ||
|
|
2adf34fbaf | ||
|
|
05dd760388 | ||
|
|
2cf4864078 | ||
|
|
df4c92672f | ||
|
|
5b173315f9 | ||
|
|
c85ea7d8fa | ||
|
|
113154702f | ||
|
|
33ae46f76a | ||
|
|
27272680a2 | ||
|
|
b1621f6b34 | ||
|
|
2c65033c0a | ||
|
|
dcfbaa9243 | ||
|
|
accef65ede | ||
|
|
50755d8ba3 | ||
|
|
47b6509f70 | ||
|
|
89f3fdc05f | ||
|
|
03f8b73627 | ||
|
|
2e6e9635c3 | ||
|
|
6a312e3fdd | ||
|
|
79dbbdf6b4 | ||
|
|
0e8961efe3 | ||
|
|
fc2be42418 | ||
|
|
ab4336cfd7 | ||
|
|
20d3b5288c | ||
|
|
63a29d3a4a | ||
|
|
31856d9895 | ||
|
|
f51dcf23d6 | ||
|
|
6ecaeb4fde | ||
|
|
1883c9666e | ||
|
|
4b4cf76641 | ||
|
|
0016b4bd72 | ||
|
|
495bbecc01 | ||
|
|
e6af7e9885 | ||
|
|
182b8c2283 | ||
|
|
5318cccc5f | ||
|
|
99739575d4 | ||
|
|
b8ff331ccc | ||
|
|
9e63f3f7c6 | ||
|
|
6f9069a4fb | ||
|
|
a18ab7f1e9 | ||
|
|
05162ca350 | ||
|
|
be0371fb11 | ||
|
|
fa3329abf2 | ||
|
|
e830fade06 | ||
|
|
ac392dcb96 | ||
|
|
e662b2f393 | ||
|
|
00a5fdf491 | ||
|
|
63bc71da13 | ||
|
|
7fff9579c0 | ||
|
|
737beb11f6 | ||
|
|
f55af7da4c | ||
|
|
80461a78b0 | ||
|
|
40d194672b | ||
|
|
d63341ea06 | ||
|
|
df8c8dc93b | ||
|
|
dd3a140cb1 | ||
|
|
1b006599cf | ||
|
|
44aa3cc9b5 | ||
|
|
b88b24e231 | ||
|
|
890c31ba74 | ||
|
|
6dc9a11a89 | ||
|
|
ce2842d365 | ||
|
|
7d1096dbd8 | ||
|
|
95722802dc | ||
|
|
3047dae703 | ||
|
|
95cad7bdd9 | ||
|
|
4e22f13007 | ||
|
|
04611b0ae2 | ||
|
|
a00f1ab549 | ||
|
|
446b37793b | ||
|
|
b2b98643d8 | ||
|
|
b83eeeb131 | ||
|
|
e8d727c07a | ||
|
|
bb8109f67d | ||
|
|
e28fa26c43 | ||
|
|
639fc3793a | ||
|
|
2aaae5265a | ||
|
|
baa4c1fd25 | ||
|
|
479797361e | ||
|
|
0a9f1d2a27 | ||
|
|
5e103770fd | ||
|
|
e012a29b5e | ||
|
|
5d759f810c | ||
|
|
eb1f3a0ced | ||
|
|
29e8210782 | ||
|
|
45ca9405d3 | ||
|
|
e6f02bf8f7 | ||
|
|
57e75e3614 | ||
|
|
89ab67e067 | ||
|
|
e9d851e4d3 | ||
|
|
c675d0feee | ||
|
|
1859c0505e | ||
|
|
f15251096c | ||
|
|
115c599fd8 | ||
|
|
3121c08ee8 | ||
|
|
ef28b01286 | ||
|
|
a5bac39196 | ||
|
|
9f640b24b3 | ||
|
|
f48750c22c | ||
|
|
7a96e94491 | ||
|
|
22a32af750 | ||
|
|
dd423f2e7b | ||
|
|
12dec676db | ||
|
|
75e7556bfa | ||
|
|
504f1a8e97 | ||
|
|
e4a2af6ae7 | ||
|
|
fefa88fc2a | ||
|
|
ed8a7ee8a5 | ||
|
|
1771797453 | ||
|
|
46179f5c83 | ||
|
|
db6fc661a6 | ||
|
|
beb3a9f60a | ||
|
|
c088ab7a79 | ||
|
|
aab2b8fdbc | ||
|
|
b1e7a717af | ||
|
|
25e38bfc98 | ||
|
|
279c7324c4 | ||
|
|
1c90303914 | ||
|
|
6ab6502742 | ||
|
|
b79c029f21 | ||
|
|
020268fe67 | ||
|
|
176b1c9d20 | ||
|
|
5ab2efa0c0 | ||
|
|
88320488a7 | ||
|
|
2091abeea2 | ||
|
|
480f5c1a9a | ||
|
|
8e0db2705f | ||
|
|
1be9cdae67 | ||
|
|
e1a91a7e53 | ||
|
|
b952e3183f | ||
|
|
26ae0bf207 | ||
|
|
42cfd69463 | ||
|
|
7694b68e06 | ||
|
|
28e39c57bd | ||
|
|
2fa0a57d2b | ||
|
|
c9f3e1bdab | ||
|
|
2ba56b8c59 | ||
|
|
fb074c8c32 | ||
|
|
9fc082d1e6 | ||
|
|
dfda2f7d5d | ||
|
|
0c04802560 | ||
|
|
5146689158 | ||
|
|
e7fa94c3d3 | ||
|
|
a77ebd3b55 | ||
|
|
00114287e5 | ||
|
|
db0695126f | ||
|
|
eec5cf6b65 | ||
|
|
a9569d0ed9 | ||
|
|
88d9388be2 | ||
|
|
93c72ecea5 | ||
|
|
b5b0ac50bd | ||
|
|
4d2afdb1a9 | ||
|
|
39a177bd70 | ||
|
|
34fb6ac837 | ||
|
|
f868a454d9 | ||
|
|
24c6cd235b | ||
|
|
47855dc78b | ||
|
|
751ceab04e | ||
|
|
dbbc42c5fd | ||
|
|
27416efb6d | ||
|
|
21dd08544b | ||
|
|
ae88f7d181 | ||
|
|
9981ee7601 | ||
|
|
66b018a355 | ||
|
|
b6c48d0f98 | ||
|
|
097d77f7b3 | ||
|
|
ed1bc6c215 | ||
|
|
c552fdfc0f | ||
|
|
4006dba9f1 | ||
|
|
7a0586684b | ||
|
|
8f34d1c555 | ||
|
|
571db5c0ee | ||
|
|
9059855f2b | ||
|
|
e423678995 | ||
|
|
ece5577f26 | ||
|
|
f373abdd14 | ||
|
|
4defec194f | ||
|
|
5270a6781f | ||
|
|
fa93e195cb | ||
|
|
72898d897c | ||
|
|
c6ee65b654 | ||
|
|
4d7694de24 | ||
|
|
a083f25b6c | ||
|
|
befa9eb16d | ||
|
|
a278c630bb | ||
|
|
6a8d8babce | ||
|
|
76eb0f1775 | ||
|
|
0abe08f243 | ||
|
|
f692ebbbb9 | ||
|
|
c174b65465 | ||
|
|
015131f198 | ||
|
|
a730543c76 | ||
|
|
c704626a39 | ||
|
|
7ef25a3816 | ||
|
|
b43ad93c54 | ||
|
|
7850681ce1 | ||
|
|
846189b15b | ||
|
|
46a893a8b6 | ||
|
|
657aac0d68 | ||
|
|
30885cee01 | ||
|
|
9237984782 | ||
|
|
c289629a28 | ||
|
|
806196f572 | ||
|
|
0e598660b4 | ||
|
|
058bfe0737 | ||
|
|
81932c8cff | ||
|
|
bd7adafee0 | ||
|
|
faf0c2b816 | ||
|
|
419d4986f6 | ||
|
|
9f1a9a7d9c | ||
|
|
a3e7e7c6c9 | ||
|
|
94a5075b6d | ||
|
|
7c32404b69 | ||
|
|
d0c2dc53fe | ||
|
|
0e8530172c | ||
|
|
4427aeac54 | ||
|
|
93640bb08e | ||
|
|
512ed71fc3 | ||
|
|
0cfc43c444 | ||
|
|
ecd0edc29e | ||
|
|
6168a006f4 | ||
|
|
82ba5dad1b | ||
|
|
972ee8e42e | ||
|
|
7cd3f285ad | ||
|
|
89e327383e | ||
|
|
290a15bbd9 | ||
|
|
1dd21f1f76 | ||
|
|
46b3f83ce2 | ||
|
|
5c153c9e21 | ||
|
|
bca75a3ea4 | ||
|
|
0bc6f972b2 | ||
|
|
36cc9cc1ec | ||
|
|
20f6a5e797 | ||
|
|
ccbb68aa0c | ||
|
|
08003c59b6 | ||
|
|
dafa638558 | ||
|
|
75e5250509 | ||
|
|
0ed6eb7029 | ||
|
|
63e26b6050 | ||
|
|
949f1c648a | ||
|
|
3e7578d670 | ||
|
|
6f07ec2597 | ||
|
|
e65c0a0d1d | ||
|
|
be217b5354 | ||
|
|
bfe3029d31 | ||
|
|
6abdc39fe5 | ||
|
|
bf55367f4d | ||
|
|
9480758310 | ||
|
|
25b33fb031 | ||
|
|
10ede0d21c | ||
|
|
698bdd619f | ||
|
|
5cef6874f6 | ||
|
|
6d42ae2629 | ||
|
|
a3b94816f9 | ||
|
|
e0b47feb8b | ||
|
|
8aecec0b9a | ||
|
|
078bf41029 | ||
|
|
2754302fb7 | ||
|
|
dfb7658c3e | ||
|
|
a743785faf | ||
|
|
e4782dee68 | ||
|
|
64315df85f | ||
|
|
2a1fd16849 | ||
|
|
21e31d540e | ||
|
|
370c38ec76 | ||
|
|
854044229c | ||
|
|
69baa44a3a | ||
|
|
419e3f7f2b | ||
|
|
a9373d9779 | ||
|
|
1a0536d212 | ||
|
|
099b77cf9b | ||
|
|
c3d17bf847 | ||
|
|
e04b93a51a | ||
|
|
b36b62c68e | ||
|
|
ab465a755e | ||
|
|
c6f19db1ec | ||
|
|
019142efc9 | ||
|
|
a535fc17c3 | ||
|
|
0fbb18b315 | ||
|
|
3eb0093d2a | ||
|
|
d159dde2ca | ||
|
|
729a510c5b | ||
|
|
196561fed2 | ||
|
|
8f0bdcd172 | ||
|
|
fffc7f4098 | ||
|
|
c7a2e7ada1 | ||
|
|
95611e9c4b | ||
|
|
62fc6afd8a | ||
|
|
0f5cec0a60 | ||
|
|
d235ebaac9 | ||
|
|
6def083b4f | ||
|
|
87322744d4 | ||
|
|
f2a02b392e | ||
|
|
e6cedc257e | ||
|
|
1b5cf2d272 | ||
|
|
f76e822381 | ||
|
|
a2b1968d6e | ||
|
|
398eb13a7f | ||
|
|
956c8a8e03 | ||
|
|
6aba166c82 | ||
|
|
fd7c7ea6b7 | ||
|
|
d85e621bb3 | ||
|
|
822dd5e100 | ||
|
|
25801f374c | ||
|
|
8fd2d0b35c | ||
|
|
c16d8a1da1 | ||
|
|
ab1fdf69c8 | ||
|
|
dd196c0e11 | ||
|
|
0e506f5716 | ||
|
|
0a98ccff0c | ||
|
|
0c188f6d10 | ||
|
|
8009dd691b | ||
|
|
13d0e9914b | ||
|
|
9da49be44d | ||
|
|
00f7fa507b | ||
|
|
2c255b6dfe | ||
|
|
6e2cf8bb3f | ||
|
|
68ed1c80ce | ||
|
|
e0d23f4436 | ||
|
|
509f8a5353 | ||
|
|
b0c0cd7fda | ||
|
|
133dfd5063 | ||
|
|
e6abf4e33b | ||
|
|
07104b18f5 | ||
|
|
f39b85abf2 | ||
|
|
c6c97491ac | ||
|
|
355452cdb3 | ||
|
|
da3720c7a9 | ||
|
|
e92d4ff147 | ||
|
|
bb514d6216 | ||
|
|
3f380fa0da | ||
|
|
5aefb707fa | ||
|
|
4afd3c2322 | ||
|
|
b8eb8a90a5 | ||
|
|
4d6cb091cc | ||
|
|
fc8b1193de | ||
|
|
2c12af5af8 | ||
|
|
bd4d89fc21 | ||
|
|
9487529992 | ||
|
|
fa347fd49d | ||
|
|
8f7072d7e9 | ||
|
|
412c5d68cc | ||
|
|
e06b068033 | ||
|
|
6234391229 | ||
|
|
2568bfde5e | ||
|
|
fd7c2fbe93 | ||
|
|
206c185a3b | ||
|
|
7689cbbe0d | ||
|
|
c832b5d29e | ||
|
|
b57a9351b3 | ||
|
|
f0ae9e21ae | ||
|
|
9510c92288 | ||
|
|
755f3f05d8 | ||
|
|
5d8114b475 | ||
|
|
0ccbb52c1f | ||
|
|
85b39ecf99 | ||
|
|
230838c22b | ||
|
|
a7bfcdcb01 | ||
|
|
47ff630c55 | ||
|
|
70dc53bda7 | ||
|
|
0b8a142de0 | ||
|
|
7e1b433c17 | ||
|
|
800b0763e4 | ||
|
|
30aabe255b | ||
|
|
9b14d714ca | ||
|
|
8a38666105 | ||
|
|
ec878defab | ||
|
|
1786b70e14 | ||
|
|
7f525fa7dc | ||
|
|
e08d93b2aa | ||
|
|
df777c63fe | ||
|
|
3a5ee4a296 | ||
|
|
7b8a0114f5 | ||
|
|
003d110948 | ||
|
|
e9c9a67365 | ||
|
|
8b89e03999 | ||
|
|
9eff920989 | ||
|
|
711c82472c | ||
|
|
156bf02d21 | ||
|
|
932b53d92d | ||
|
|
2693b9a42d | ||
|
|
e9166c4a7d | ||
|
|
2bc64920dd | ||
|
|
aee5500833 | ||
|
|
6b336b7b2f | ||
|
|
f07992c091 | ||
|
|
3c0e77241d | ||
|
|
87461c7f72 | ||
|
|
a67f2b4976 | ||
|
|
8594781780 | ||
|
|
313e415ee9 | ||
|
|
c13d8f3699 | ||
|
|
e41f8f1d0f | ||
|
|
b2c8907635 | ||
|
|
05f4df1a30 | ||
|
|
35fe06a892 | ||
|
|
75ff541aec | ||
|
|
cd933ce6e4 | ||
|
|
0b93988450 | ||
|
|
056cab23e0 | ||
|
|
6bc8027644 | ||
|
|
3b9298ed2b | ||
|
|
12a323f691 | ||
|
|
9c4c211233 | ||
|
|
74ba68ff2c | ||
|
|
7273b37c16 | ||
|
|
0d4ebffc0e | ||
|
|
352b2fb4e7 | ||
|
|
6e6ef57303 | ||
|
|
cc1f14e5e9 | ||
|
|
1c419d5c65 | ||
|
|
71b83245b4 | ||
|
|
2b88555028 | ||
|
|
f021ad9b0a | ||
|
|
8884f64b4e | ||
|
|
dd790dceb5 | ||
|
|
b80e41503f | ||
|
|
8dfc5052e9 | ||
|
|
7f28fc17ca | ||
|
|
2c308ccd35 | ||
|
|
4d6dd44e10 | ||
|
|
b6992e32a5 | ||
|
|
ac080edb02 | ||
|
|
231859303d | ||
|
|
1acdd67fd9 | ||
|
|
bec63a9471 | ||
|
|
44e856e8dc | ||
|
|
3bab7678b7 | ||
|
|
61f68d9e1b | ||
|
|
94f1562ec5 | ||
|
|
46412acd13 | ||
|
|
e7426ea365 | ||
|
|
665eef68b9 | ||
|
|
7c63d4012f | ||
|
|
92be4e774e | ||
|
|
2395502e60 | ||
|
|
9f3902b48d | ||
|
|
6e76bcb77e | ||
|
|
e05a95dc2d | ||
|
|
86d61d698a | ||
|
|
8ce6535a7e | ||
|
|
65ca038eee | ||
|
|
f41f5ebebd | ||
|
|
9cf62f03fa | ||
|
|
f770d5072e | ||
|
|
5698b830ed | ||
|
|
bcc76dd60a | ||
|
|
70d4a0c022 | ||
|
|
8cfd994170 | ||
|
|
22d8d08355 | ||
|
|
641e829e3f | ||
|
|
f9edff8bf4 | ||
|
|
33e6be1ca6 | ||
|
|
e25c50a467 | ||
|
|
f8441ab42e | ||
|
|
4589d4b3f5 | ||
|
|
9cf720e040 | ||
|
|
cf793f7f49 | ||
|
|
2b3fddfe89 | ||
|
|
e148f143ea | ||
|
|
d202cb731d | ||
|
|
299d9998ad | ||
|
|
fba1484e2e | ||
|
|
4ab7300376 | ||
|
|
18cc5e0ee8 | ||
|
|
af0cda5dbf | ||
|
|
a730a3719b | ||
|
|
3b669193f6 | ||
|
|
c782bab296 | ||
|
|
b14646ebd9 | ||
|
|
7441de5fd9 | ||
|
|
f5360cb8d4 | ||
|
|
22cd2e3337 | ||
|
|
7e9d453a2c | ||
|
|
a4338b0d03 | ||
|
|
a35baca580 | ||
|
|
66b0108c51 | ||
|
|
2021431e2f | ||
|
|
ab836c6922 | ||
|
|
405b3be496 | ||
|
|
4a27128a1c | ||
|
|
c74bdc97ca | ||
|
|
ddd5e4c76d | ||
|
|
5e6a7e134f | ||
|
|
41bc519855 | ||
|
|
53d82618d9 | ||
|
|
57f548c6c0 | ||
|
|
8d83f64aba | ||
|
|
9162697117 | ||
|
|
47b19e3211 | ||
|
|
590f6d4c19 | ||
|
|
53108e816f | ||
|
|
3ac71e2f7f | ||
|
|
f4fadd366e | ||
|
|
cc38dab76f | ||
|
|
c8be701f0e | ||
|
|
417befb2be | ||
|
|
a0ce7f38e7 | ||
|
|
962e3d8e56 | ||
|
|
3a3df96996 | ||
|
|
2ffa632796 | ||
|
|
3c6c0b253d | ||
|
|
5f40fd6038 | ||
|
|
8e2dc8b3ee | ||
|
|
a02b531e47 | ||
|
|
a4cb2708cc | ||
|
|
973284607d | ||
|
|
28fd2f0314 | ||
|
|
9715873007 | ||
|
|
18a20407f6 | ||
|
|
1a396cfc7b | ||
|
|
e604c914d1 | ||
|
|
a310c160a5 | ||
|
|
45d50b12fd | ||
|
|
e87182264a | ||
|
|
a089d544a5 | ||
|
|
b6fe0be1b2 | ||
|
|
ba325b1581 | ||
|
|
1f47abf195 | ||
|
|
750f35bc36 | ||
|
|
c99d9d95c5 | ||
|
|
4d402b2600 | ||
|
|
64fb002168 | ||
|
|
1308b5bcf3 | ||
|
|
dc3dc4a1f0 | ||
|
|
99bb55af73 | ||
|
|
4a285225db | ||
|
|
d986bd2a6c | ||
|
|
8665342edf | ||
|
|
2e7c3bf789 | ||
|
|
31ea0fe3fe | ||
|
|
e0c9f8a5aa | ||
|
|
a17ec4221b | ||
|
|
328beaba35 | ||
|
|
efbbaa5741 | ||
|
|
14be2fa344 | ||
|
|
f3ccad192c | ||
|
|
5e580f9372 | ||
|
|
8410929e86 | ||
|
|
093a5d4ddf | ||
|
|
88028412bd | ||
|
|
11c93231aa | ||
|
|
5366b4c873 | ||
|
|
171e0ed312 | ||
|
|
a5b1b4e103 | ||
|
|
f50ddb436f | ||
|
|
0b4b091580 | ||
|
|
2f6d7ac128 | ||
|
|
6b990e1cee | ||
|
|
ddeed65994 | ||
|
|
d87748fda1 | ||
|
|
50f0ead113 | ||
|
|
4e3075aaba | ||
|
|
87d6684ca7 | ||
|
|
3bd7596873 | ||
|
|
39964bf077 | ||
|
|
089199e7c2 | ||
|
|
7b41b295b7 | ||
|
|
d7bc7a2d38 | ||
|
|
eae75c13bb | ||
|
|
fab13db4b4 | ||
|
|
69d5f521a5 | ||
|
|
c0a55142b5 | ||
|
|
513fb3428a | ||
|
|
9a0ae549f6 | ||
|
|
4410d7f195 | ||
|
|
92aa70182d | ||
|
|
90f5864f1e | ||
|
|
d44de670cd | ||
|
|
cb63025078 | ||
|
|
685e865b42 | ||
|
|
e47f126bd5 | ||
|
|
ea6f70e3c5 | ||
|
|
0469aab433 | ||
|
|
ad13b5eb4e | ||
|
|
7324a4973f | ||
|
|
8bc93d23b2 | ||
|
|
c708b685e1 | ||
|
|
65009e2f69 | ||
|
|
cbde91744f | ||
|
|
4c8a92bb0c | ||
|
|
11a2e96d06 | ||
|
|
095c5e4f95 | ||
|
|
069db28fb6 | ||
|
|
2e747d3ece | ||
|
|
147e24204b | ||
|
|
6580153f29 | ||
|
|
13c50e428f | ||
|
|
8403ccd3da | ||
|
|
c988bca958 | ||
|
|
e92bd61545 | ||
|
|
e84e8edb29 | ||
|
|
8215e0221a | ||
|
|
a4ef7205ca | ||
|
|
4b44d6fb83 | ||
|
|
ba8df96e41 | ||
|
|
0e2fc07881 | ||
|
|
0ae3e83ce4 | ||
|
|
f4b573379d | ||
|
|
862ca375ee | ||
|
|
5c578c0328 | ||
|
|
530de6741b | ||
|
|
5f7ff460fb | ||
|
|
3b3e1e37b9 | ||
|
|
5f40d9400c | ||
|
|
fcdc642acb | ||
|
|
46f594ab71 | ||
|
|
e8684cbb9d | ||
|
|
a36ab71600 | ||
|
|
35c1ff9014 | ||
|
|
e4ce05f94d | ||
|
|
38a624fecf | ||
|
|
fd96859883 | ||
|
|
94d22ed1aa | ||
|
|
3f4caed922 | ||
|
|
09303ab2fb | ||
|
|
df1ac8e1e2 | ||
|
|
7a55c91349 | ||
|
|
c491dfdd3a | ||
|
|
b5da076e2c | ||
|
|
18cd6c81a3 | ||
|
|
d9cc21f761 | ||
|
|
40b19c5e67 | ||
|
|
06207145af | ||
|
|
b195e3435f | ||
|
|
34b4577c0b | ||
|
|
8034e5bbcb | ||
|
|
df7a30bd14 | ||
|
|
d9dfacaaf4 | ||
|
|
d43767b945 | ||
|
|
cb36754c46 | ||
|
|
7e18aafe20 | ||
|
|
7a31d09356 | ||
|
|
f7b079b1b4 | ||
|
|
72ffedead7 | ||
|
|
cf3a501562 | ||
|
|
7becdc3034 | ||
|
|
f0d599781d | ||
|
|
3386105048 | ||
|
|
3b8fb70db1 | ||
|
|
c3ae146580 | ||
|
|
0d079f0d89 | ||
|
|
9f5a90ee9c | ||
|
|
a5307fd8cc | ||
|
|
180589144a | ||
|
|
d9c1867bd7 | ||
|
|
da37d649ec | ||
|
|
4204b4af90 | ||
|
|
941650f668 | ||
|
|
9c0c6c1bd6 | ||
|
|
bd0ddafcd0 | ||
|
|
19f5e92a74 | ||
|
|
3202c38061 | ||
|
|
e35a8c942b | ||
|
|
31811eb91e | ||
|
|
b9316a4112 | ||
|
|
b7abd878ac | ||
|
|
38c2c47789 | ||
|
|
c03778ec8b | ||
|
|
29b0850a94 | ||
|
|
712fde46eb | ||
|
|
c2e79ca5a7 | ||
|
|
c3a52b3989 | ||
|
|
7213d82f1b | ||
|
|
5bcad69cf7 | ||
|
|
c9a487fa4d | ||
|
|
3804a46f3b | ||
|
|
52c0bb5302 | ||
|
|
8aa19e6420 | ||
|
|
4d1c7a3884 | ||
|
|
25f2c057b7 | ||
|
|
010be05920 | ||
|
|
4c465850a2 | ||
|
|
8313dfaeb9 | ||
|
|
873f2b2814 | ||
|
|
e53c90f8f0 | ||
|
|
9499ea8ca9 | ||
|
|
f6c09109ba | ||
|
|
273b5768c4 | ||
|
|
ee13cf7dd9 | ||
|
|
fecbae761e | ||
|
|
e0ee89bdd9 | ||
|
|
833c1f22a3 | ||
|
|
6fed6c8d30 | ||
|
|
94cdaf5314 | ||
|
|
f83ae27352 | ||
|
|
6badf047c3 | ||
|
|
47de9ad15f | ||
|
|
09b91cc663 | ||
|
|
ded16549f7 | ||
|
|
c89e47577b | ||
|
|
bb50beb7ab | ||
|
|
e4cd4d64d7 | ||
|
|
5675fc51a0 | ||
|
|
c7438c4aff | ||
|
|
4a6a3da36c | ||
|
|
a657c332b1 | ||
|
|
cc9cd3fc14 | ||
|
|
234258a077 | ||
|
|
13cda80ee6 | ||
|
|
f6e142baf5 | ||
|
|
ddf1f9bcd5 | ||
|
|
aa950669f6 | ||
|
|
dacd5d3e6b | ||
|
|
e76ccba2f7 | ||
|
|
3933819d53 | ||
|
|
99019c2b1f | ||
|
|
4bf5eb398b | ||
|
|
dbfbac62c0 | ||
|
|
7685293da4 | ||
|
|
ee9c328606 | ||
|
|
cb7790ccba | ||
|
|
6556fcc531 | ||
|
|
178391e7b2 | ||
|
|
18922a1c6d | ||
|
|
5e9e26fa67 | ||
|
|
f5430f9151 | ||
|
|
4dfdf2f92f | ||
|
|
e4d283cc99 | ||
|
|
8ee64d22b3 | ||
|
|
10e3e80042 | ||
|
|
f77a208e2c | ||
|
|
9366dbb96e | ||
|
|
550b17552b | ||
|
|
bec307d0e9 | ||
|
|
93c751f6eb |
84
.github/workflows/startos-iso.yaml
vendored
@@ -12,9 +12,6 @@ on:
|
||||
- dev
|
||||
- unstable
|
||||
- dev-unstable
|
||||
- docker
|
||||
- dev-docker
|
||||
- dev-unstable-docker
|
||||
runner:
|
||||
type: choice
|
||||
description: Runner
|
||||
@@ -31,6 +28,7 @@ on:
|
||||
- aarch64
|
||||
- aarch64-nonfree
|
||||
- raspberrypi
|
||||
- riscv64
|
||||
deploy:
|
||||
type: choice
|
||||
description: Deploy
|
||||
@@ -48,7 +46,7 @@ on:
|
||||
- next/*
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "18.15.0"
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||
|
||||
jobs:
|
||||
@@ -65,6 +63,7 @@ jobs:
|
||||
"aarch64": ["aarch64"],
|
||||
"aarch64-nonfree": ["aarch64"],
|
||||
"raspberrypi": ["aarch64"],
|
||||
"riscv64": ["riscv64"],
|
||||
"ALL": ["x86_64", "aarch64"]
|
||||
}')[github.event.inputs.platform || 'ALL']
|
||||
}}
|
||||
@@ -74,24 +73,48 @@ jobs:
|
||||
sudo mount -t tmpfs tmpfs .
|
||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up system dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y qemu-user-static systemd-container squashfuse
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Configure sccache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Use Beta Toolchain
|
||||
run: rustup default beta
|
||||
|
||||
- name: Setup Cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Make
|
||||
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
|
||||
env:
|
||||
SCCACHE_GHA_ENABLED: on
|
||||
SCCACHE_GHA_VERSION: 0
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: compiled-${{ matrix.arch }}.tar
|
||||
path: compiled-${{ matrix.arch }}.tar
|
||||
@@ -124,6 +147,7 @@ jobs:
|
||||
"aarch64": "buildjet-8vcpu-ubuntu-2204-arm",
|
||||
"aarch64-nonfree": "buildjet-8vcpu-ubuntu-2204-arm",
|
||||
"raspberrypi": "buildjet-8vcpu-ubuntu-2204-arm",
|
||||
"riscv64": "buildjet-8vcpu-ubuntu-2204",
|
||||
}')[matrix.platform]
|
||||
)
|
||||
)[github.event.inputs.runner == 'fast']
|
||||
@@ -137,13 +161,23 @@ jobs:
|
||||
"aarch64": "aarch64",
|
||||
"aarch64-nonfree": "aarch64",
|
||||
"raspberrypi": "aarch64",
|
||||
"riscv64": "riscv64",
|
||||
}')[matrix.platform]
|
||||
}}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Free space
|
||||
run: rm -rf /opt/hostedtoolcache*
|
||||
if: ${{ github.event.inputs.runner != 'fast' }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -162,7 +196,7 @@ jobs:
|
||||
if: ${{ github.event.inputs.runner == 'fast' && (matrix.platform == 'x86_64' || matrix.platform == 'x86_64-nonfree') }}
|
||||
|
||||
- name: Download compiled artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: compiled-${{ env.ARCH }}.tar
|
||||
|
||||
@@ -171,9 +205,29 @@ jobs:
|
||||
|
||||
- name: Prevent rebuild of compiled artifacts
|
||||
run: |
|
||||
mkdir -p web/node_modules
|
||||
mkdir -p web/dist/raw
|
||||
mkdir -p core/startos/bindings
|
||||
mkdir -p sdk/base/lib/osBindings
|
||||
mkdir -p container-runtime/node_modules
|
||||
mkdir -p container-runtime/dist
|
||||
mkdir -p container-runtime/dist/node_modules
|
||||
mkdir -p core/startos/bindings
|
||||
mkdir -p sdk/dist
|
||||
mkdir -p sdk/baseDist
|
||||
mkdir -p patch-db/client/node_modules
|
||||
mkdir -p patch-db/client/dist
|
||||
mkdir -p web/.angular
|
||||
mkdir -p web/dist/raw/ui
|
||||
mkdir -p web/dist/raw/install-wizard
|
||||
mkdir -p web/dist/raw/setup-wizard
|
||||
mkdir -p web/dist/static/ui
|
||||
mkdir -p web/dist/static/install-wizard
|
||||
mkdir -p web/dist/static/setup-wizard
|
||||
PLATFORM=${{ matrix.platform }} make -t compiled-${{ env.ARCH }}.tar
|
||||
|
||||
- run: git status
|
||||
|
||||
- name: Run iso build
|
||||
run: PLATFORM=${{ matrix.platform }} make iso
|
||||
if: ${{ matrix.platform != 'raspberrypi' }}
|
||||
@@ -182,18 +236,18 @@ jobs:
|
||||
run: PLATFORM=${{ matrix.platform }} make img
|
||||
if: ${{ matrix.platform == 'raspberrypi' }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.platform }}.squashfs
|
||||
path: results/*.squashfs
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.platform }}.iso
|
||||
path: results/*.iso
|
||||
if: ${{ matrix.platform != 'raspberrypi' }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.platform }}.img
|
||||
path: results/*.img
|
||||
|
||||
12
.github/workflows/test.yaml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- next/*
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "18.15.0"
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: dev-unstable
|
||||
|
||||
jobs:
|
||||
@@ -19,13 +19,19 @@ jobs:
|
||||
name: Run Automated Tests
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Use Beta Toolchain
|
||||
run: rustup default beta
|
||||
|
||||
- name: Setup Cross
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
|
||||
- name: Build And Run Tests
|
||||
run: make test
|
||||
|
||||
7
.gitignore
vendored
@@ -1,8 +1,5 @@
|
||||
.DS_Store
|
||||
.idea
|
||||
system-images/binfmt/binfmt.tar
|
||||
system-images/compat/compat.tar
|
||||
system-images/util/util.tar
|
||||
/*.img
|
||||
/*.img.gz
|
||||
/*.img.xz
|
||||
@@ -20,7 +17,6 @@ secrets.db
|
||||
/ENVIRONMENT.txt
|
||||
/GIT_HASH.txt
|
||||
/VERSION.txt
|
||||
/eos-*.tar.gz
|
||||
/*.deb
|
||||
/target
|
||||
/*.squashfs
|
||||
@@ -28,4 +24,5 @@ secrets.db
|
||||
/dpkg-workdir
|
||||
/compiled.tar
|
||||
/compiled-*.tar
|
||||
/firmware
|
||||
/firmware
|
||||
/tmp
|
||||
@@ -1,7 +1,6 @@
|
||||
# Contributing to StartOS
|
||||
|
||||
This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/developer-docs/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute).
|
||||
|
||||
This guide is for contributing to the StartOS. If you are interested in packaging a service for StartOS, visit the [service packaging guide](https://docs.start9.com/latest/packaging-guide/). If you are interested in promoting, providing technical support, creating tutorials, or helping in other ways, please visit the [Start9 website](https://start9.com/contribute).
|
||||
|
||||
## Collaboration
|
||||
|
||||
@@ -13,64 +12,77 @@ This guide is for contributing to the StartOS. If you are interested in packagin
|
||||
```bash
|
||||
/
|
||||
├── assets/
|
||||
├── container-runtime/
|
||||
├── core/
|
||||
├── build/
|
||||
├── debian/
|
||||
├── web/
|
||||
├── image-recipe/
|
||||
├── patch-db
|
||||
└── system-images/
|
||||
└── sdk/
|
||||
```
|
||||
|
||||
#### assets
|
||||
|
||||
screenshots for the StartOS README
|
||||
|
||||
#### container-runtime
|
||||
|
||||
A NodeJS program that dynamically loads maintainer scripts and communicates with the OS to manage packages
|
||||
|
||||
#### core
|
||||
An API, daemon (startd), CLI (start-cli), and SDK (start-sdk) that together provide the core functionality of StartOS.
|
||||
|
||||
An API, daemon (startd), and CLI (start-cli) that together provide the core functionality of StartOS.
|
||||
|
||||
#### build
|
||||
|
||||
Auxiliary files and scripts to include in deployed StartOS images
|
||||
|
||||
#### debian
|
||||
|
||||
Maintainer scripts for the StartOS Debian package
|
||||
|
||||
#### web
|
||||
|
||||
Web UIs served under various conditions and used to interact with StartOS APIs.
|
||||
|
||||
#### image-recipe
|
||||
|
||||
Scripts for building StartOS images
|
||||
|
||||
#### patch-db (submodule)
|
||||
|
||||
A diff based data store used to synchronize data between the web interfaces and server.
|
||||
|
||||
#### system-images
|
||||
Docker images that assist with creating backups.
|
||||
#### sdk
|
||||
|
||||
A typescript sdk for building start-os packages
|
||||
|
||||
## Environment Setup
|
||||
|
||||
#### Clone the StartOS repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Start9Labs/start-os.git
|
||||
git clone https://github.com/Start9Labs/start-os.git --recurse-submodules
|
||||
cd start-os
|
||||
```
|
||||
|
||||
#### Load the PatchDB submodule
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
#### Continue to your project of interest for additional instructions:
|
||||
|
||||
- [`core`](core/README.md)
|
||||
- [`web-interfaces`](web-interfaces/README.md)
|
||||
- [`build`](build/README.md)
|
||||
- [`patch-db`](https://github.com/Start9Labs/patch-db)
|
||||
|
||||
## Building
|
||||
|
||||
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components. To build any specific component, simply run `make <TARGET>` replacing `<TARGET>` with the name of the target you'd like to build
|
||||
|
||||
### Requirements
|
||||
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
- [NodeJS v18.15.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||
- [NodeJS v20.16.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||
- [sed](https://www.gnu.org/software/sed/)
|
||||
- [grep](https://www.gnu.org/software/grep/)
|
||||
- [awk](https://www.gnu.org/software/gawk/)
|
||||
@@ -79,41 +91,43 @@ This project uses [GNU Make](https://www.gnu.org/software/make/) to build its co
|
||||
- [brotli](https://github.com/google/brotli)
|
||||
|
||||
### Environment variables
|
||||
|
||||
- `PLATFORM`: which platform you would like to build for. Must be one of `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `raspberrypi`
|
||||
- NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO
|
||||
- NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO
|
||||
- `ENVIRONMENT`: a hyphen separated set of feature flags to enable
|
||||
- `dev`: enables password ssh (INSECURE!) and does not compress frontends
|
||||
- `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons
|
||||
- `docker`: use `docker` instead of `podman`
|
||||
- `dev`: enables password ssh (INSECURE!) and does not compress frontends
|
||||
- `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons
|
||||
- `docker`: use `docker` instead of `podman`
|
||||
- `GIT_BRANCH_AS_HASH`: set to `1` to use the current git branch name as the git hash so that the project does not need to be rebuilt on each commit
|
||||
|
||||
### Useful Make Targets
|
||||
|
||||
- `iso`: Create a full `.iso` image
|
||||
- Only possible from Debian
|
||||
- Not available for `PLATFORM=raspberrypi`
|
||||
- Additional Requirements:
|
||||
- [debspawn](https://github.com/lkhq/debspawn)
|
||||
- Only possible from Debian
|
||||
- Not available for `PLATFORM=raspberrypi`
|
||||
- Additional Requirements:
|
||||
- [debspawn](https://github.com/lkhq/debspawn)
|
||||
- `img`: Create a full `.img` image
|
||||
- Only possible from Debian
|
||||
- Only available for `PLATFORM=raspberrypi`
|
||||
- Additional Requirements:
|
||||
- [debspawn](https://github.com/lkhq/debspawn)
|
||||
- Only possible from Debian
|
||||
- Only available for `PLATFORM=raspberrypi`
|
||||
- Additional Requirements:
|
||||
- [debspawn](https://github.com/lkhq/debspawn)
|
||||
- `format`: Run automatic code formatting for the project
|
||||
- Additional Requirements:
|
||||
- [rust](https://rustup.rs/)
|
||||
- Additional Requirements:
|
||||
- [rust](https://rustup.rs/)
|
||||
- `test`: Run automated tests for the project
|
||||
- Additional Requirements:
|
||||
- [rust](https://rustup.rs/)
|
||||
- Additional Requirements:
|
||||
- [rust](https://rustup.rs/)
|
||||
- `update`: Deploy the current working project to a device over ssh as if through an over-the-air update
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- `reflash`: Deploy the current working project to a device over ssh as if using a live `iso` image to reflash it
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- `update-overlay`: Deploy the current working project to a device over ssh to the in-memory overlay without restarting it
|
||||
- WARNING: changes will be reverted after the device is rebooted
|
||||
- WARNING: changes to `init` will not take effect as the device is already initialized
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- WARNING: changes will be reverted after the device is rebooted
|
||||
- WARNING: changes to `init` will not take effect as the device is already initialized
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- `wormhole`: Deploy the `startbox` to a device using [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
||||
- When the build it complete will emit a command to paste into the shell of the device to upgrade it
|
||||
- Additional Requirements:
|
||||
- [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
||||
- `clean`: Delete all compiled artifacts
|
||||
- When the build it complete will emit a command to paste into the shell of the device to upgrade it
|
||||
- Additional Requirements:
|
||||
- [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
||||
- `clean`: Delete all compiled artifacts
|
||||
|
||||
134
DEVELOPMENT.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Setting up your development environment on Debian/Ubuntu
|
||||
|
||||
A step-by-step guide
|
||||
|
||||
> This is the only officially supported build environment.
|
||||
> MacOS has limited build capabilities and Windows requires [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install)
|
||||
|
||||
## Installing dependencies
|
||||
|
||||
Run the following commands one at a time
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install -y ca-certificates curl gpg build-essential
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg-architecture -q DEB_HOST_ARCH) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bookworm stable" | sudo tee /etc/apt/sources.list.d/docker.list
|
||||
sudo apt update
|
||||
sudo apt install -y sed grep gawk jq gzip brotli containerd.io docker-ce docker-ce-cli docker-compose-plugin qemu-user-static binfmt-support squashfs-tools git debspawn rsync b3sum
|
||||
sudo mkdir -p /etc/debspawn/
|
||||
echo "AllowUnsafePermissions=true" | sudo tee /etc/debspawn/global.toml
|
||||
sudo usermod -aG docker $USER
|
||||
sudo su $USER
|
||||
docker run --privileged --rm tonistiigi/binfmt --install all
|
||||
docker buildx create --use
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # proceed with default installation
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
source ~/.bashrc
|
||||
nvm install 24
|
||||
nvm use 24
|
||||
nvm alias default 24 # this prevents your machine from reverting back to another version
|
||||
```
|
||||
|
||||
## Cloning the repository
|
||||
|
||||
```sh
|
||||
git clone --recursive https://github.com/Start9Labs/start-os.git --branch next/major
|
||||
cd start-os
|
||||
```
|
||||
|
||||
## Building an ISO
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make iso
|
||||
```
|
||||
|
||||
This will build an ISO for your current architecture. If you are building to run on an architecture other than the one you are currently on, replace `$(uname -m)` with the correct platform for the device (one of `aarch64`, `aarch64-nonfree`, `x86_64`, `x86_64-nonfree`, `raspberrypi`)
|
||||
|
||||
## Creating a VM
|
||||
|
||||
### Install virt-manager
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install -y virt-manager
|
||||
sudo usermod -aG libvirt $USER
|
||||
sudo su $USER
|
||||
```
|
||||
|
||||
### Launch virt-manager
|
||||
|
||||
```sh
|
||||
virt-manager
|
||||
```
|
||||
|
||||
### Create new virtual machine
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### make sure to set "Target Path" to the path to your results directory in start-os
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Updating a VM
|
||||
|
||||
The fastest way to update a VM to your latest code depends on what you changed:
|
||||
|
||||
### UI or startd:
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make update-startbox REMOTE=start9@<VM IP>
|
||||
```
|
||||
|
||||
### Container runtime or debian dependencies:
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make update-deb REMOTE=start9@<VM IP>
|
||||
```
|
||||
|
||||
### Image recipe:
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make update-squashfs REMOTE=start9@<VM IP>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If the device you are building for is not available via ssh, it is also possible to use `magic-wormhole` to send the relevant files.
|
||||
|
||||
### Prerequisites:
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install -y magic-wormhole
|
||||
```
|
||||
|
||||
As before, the fastest way to update a VM to your latest code depends on what you changed. Each of the following commands will return a command to paste into the shell of the device you would like to upgrade.
|
||||
|
||||
### UI or startd:
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole
|
||||
```
|
||||
|
||||
### Container runtime or debian dependencies:
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-deb
|
||||
```
|
||||
|
||||
### Image recipe:
|
||||
|
||||
```sh
|
||||
PLATFORM=$(uname -m) ENVIRONMENT=dev make wormhole-squashfs
|
||||
```
|
||||
342
Makefile
@@ -1,32 +1,46 @@
|
||||
ls-files = $(shell git ls-files --cached --others --exclude-standard $1)
|
||||
PROFILE = release
|
||||
|
||||
PLATFORM_FILE := $(shell ./check-platform.sh)
|
||||
ENVIRONMENT_FILE := $(shell ./check-environment.sh)
|
||||
GIT_HASH_FILE := $(shell ./check-git-hash.sh)
|
||||
VERSION_FILE := $(shell ./check-version.sh)
|
||||
BASENAME := $(shell ./basename.sh)
|
||||
BASENAME := $(shell PROJECT=startos ./basename.sh)
|
||||
PLATFORM := $(shell if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)
|
||||
ARCH := $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo aarch64; else echo $(PLATFORM) | sed 's/-nonfree$$//g'; fi)
|
||||
RUST_ARCH := $(shell if [ "$(ARCH)" = "riscv64" ]; then echo riscv64gc; else echo $(ARCH); fi)
|
||||
REGISTRY_BASENAME := $(shell PROJECT=start-registry PLATFORM=$(ARCH) ./basename.sh)
|
||||
TUNNEL_BASENAME := $(shell PROJECT=start-tunnel PLATFORM=$(ARCH) ./basename.sh)
|
||||
IMAGE_TYPE=$(shell if [ "$(PLATFORM)" = raspberrypi ]; then echo img; else echo iso; fi)
|
||||
BINS := core/target/$(ARCH)-unknown-linux-gnu/release/startbox core/target/aarch64-unknown-linux-musl/release/container-init core/target/x86_64-unknown-linux-musl/release/container-init
|
||||
WEB_UIS := web/dist/raw/ui web/dist/raw/setup-wizard web/dist/raw/diagnostic-ui web/dist/raw/install-wizard
|
||||
WEB_UIS := web/dist/raw/ui/index.html web/dist/raw/setup-wizard/index.html web/dist/raw/install-wizard/index.html
|
||||
COMPRESSED_WEB_UIS := web/dist/static/ui/index.html web/dist/static/setup-wizard/index.html web/dist/static/install-wizard/index.html
|
||||
FIRMWARE_ROMS := ./firmware/$(PLATFORM) $(shell jq --raw-output '.[] | select(.platform[] | contains("$(PLATFORM)")) | "./firmware/$(PLATFORM)/" + .id + ".rom.gz"' build/lib/firmware.json)
|
||||
BUILD_SRC := $(shell git ls-files build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
||||
DEBIAN_SRC := $(shell git ls-files debian/)
|
||||
IMAGE_RECIPE_SRC := $(shell git ls-files image-recipe/)
|
||||
BUILD_SRC := $(call ls-files, build) build/lib/depends build/lib/conflicts $(FIRMWARE_ROMS)
|
||||
IMAGE_RECIPE_SRC := $(call ls-files, image-recipe/)
|
||||
STARTD_SRC := core/startos/startd.service $(BUILD_SRC)
|
||||
COMPAT_SRC := $(shell git ls-files system-images/compat/)
|
||||
UTILS_SRC := $(shell git ls-files system-images/utils/)
|
||||
BINFMT_SRC := $(shell git ls-files system-images/binfmt/)
|
||||
CORE_SRC := $(shell git ls-files core) $(shell git ls-files --recurse-submodules patch-db) web/dist/static web/patchdb-ui-seed.json $(GIT_HASH_FILE)
|
||||
WEB_SHARED_SRC := $(shell git ls-files web/projects/shared) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules web/config.json patch-db/client/dist web/patchdb-ui-seed.json
|
||||
WEB_UI_SRC := $(shell git ls-files web/projects/ui)
|
||||
WEB_SETUP_WIZARD_SRC := $(shell git ls-files web/projects/setup-wizard)
|
||||
WEB_DIAGNOSTIC_UI_SRC := $(shell git ls-files web/projects/diagnostic-ui)
|
||||
WEB_INSTALL_WIZARD_SRC := $(shell git ls-files web/projects/install-wizard)
|
||||
CORE_SRC := $(call ls-files, core) $(shell git ls-files --recurse-submodules patch-db) $(GIT_HASH_FILE)
|
||||
WEB_SHARED_SRC := $(call ls-files, web/projects/shared) $(call ls-files, web/projects/marketplace) $(shell ls -p web/ | grep -v / | sed 's/^/web\//g') web/node_modules/.package-lock.json web/config.json patch-db/client/dist/index.js sdk/baseDist/package.json web/patchdb-ui-seed.json sdk/dist/package.json
|
||||
WEB_UI_SRC := $(call ls-files, web/projects/ui)
|
||||
WEB_SETUP_WIZARD_SRC := $(call ls-files, web/projects/setup-wizard)
|
||||
WEB_INSTALL_WIZARD_SRC := $(call ls-files, web/projects/install-wizard)
|
||||
WEB_START_TUNNEL_SRC := $(call ls-files, web/projects/start-tunnel)
|
||||
PATCH_DB_CLIENT_SRC := $(shell git ls-files --recurse-submodules patch-db/client)
|
||||
GZIP_BIN := $(shell which pigz || which gzip)
|
||||
TAR_BIN := $(shell which gtar || which tar)
|
||||
COMPILED_TARGETS := $(BINS) system-images/compat/docker-images/$(ARCH).tar system-images/utils/docker-images/$(ARCH).tar system-images/binfmt/docker-images/$(ARCH).tar
|
||||
ALL_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) $(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then echo cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep; fi) $(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then echo cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console; fi') $(PLATFORM_FILE)
|
||||
COMPILED_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox container-runtime/rootfs.$(ARCH).squashfs
|
||||
STARTOS_TARGETS := $(STARTD_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE) $(COMPILED_TARGETS) cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs $(PLATFORM_FILE) \
|
||||
$(shell if [ "$(PLATFORM)" = "raspberrypi" ]; then \
|
||||
echo cargo-deps/aarch64-unknown-linux-musl/release/pi-beep; \
|
||||
fi) \
|
||||
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]; then \
|
||||
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph; \
|
||||
fi') \
|
||||
$(shell /bin/bash -c 'if [[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]; then \
|
||||
echo cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console; \
|
||||
fi')
|
||||
REGISTRY_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox core/startos/start-registryd.service
|
||||
TUNNEL_TARGETS := core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
|
||||
REBUILD_TYPES = 1
|
||||
|
||||
ifeq ($(REMOTE),)
|
||||
mkdir = mkdir -p $1
|
||||
@@ -49,19 +63,18 @@ endif
|
||||
|
||||
.DELETE_ON_ERROR:
|
||||
|
||||
.PHONY: all metadata install clean format sdk snapshots uis ui reflash deb $(IMAGE_TYPE) squashfs sudo wormhole test
|
||||
.PHONY: all metadata install clean format cli uis ui reflash deb $(IMAGE_TYPE) squashfs wormhole wormhole-deb test test-core test-sdk test-container-runtime registry install-registry tunnel install-tunnel
|
||||
|
||||
all: $(ALL_TARGETS)
|
||||
all: $(STARTOS_TARGETS)
|
||||
|
||||
touch:
|
||||
touch $(STARTOS_TARGETS)
|
||||
|
||||
metadata: $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
||||
|
||||
sudo:
|
||||
sudo true
|
||||
|
||||
clean:
|
||||
rm -f system-images/**/*.tar
|
||||
rm -rf system-images/compat/target
|
||||
rm -rf core/target
|
||||
rm -rf core/startos/bindings
|
||||
rm -rf web/.angular
|
||||
rm -f web/config.json
|
||||
rm -rf web/node_modules
|
||||
@@ -74,6 +87,13 @@ clean:
|
||||
rm -rf image-recipe/deb
|
||||
rm -rf results
|
||||
rm -rf build/lib/firmware
|
||||
rm -rf container-runtime/dist
|
||||
rm -rf container-runtime/node_modules
|
||||
rm -f container-runtime/*.squashfs
|
||||
if [ -d container-runtime/tmp/combined ] && mountpoint container-runtime/tmp/combined; then sudo umount container-runtime/tmp/combined; fi
|
||||
if [ -d container-runtime/tmp/lower ] && mountpoint container-runtime/tmp/lower; then sudo umount container-runtime/tmp/lower; fi
|
||||
rm -rf container-runtime/tmp
|
||||
(cd sdk && make clean)
|
||||
rm -f ENVIRONMENT.txt
|
||||
rm -f PLATFORM.txt
|
||||
rm -f GIT_HASH.txt
|
||||
@@ -82,19 +102,65 @@ clean:
|
||||
format:
|
||||
cd core && cargo +nightly fmt
|
||||
|
||||
test: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
cd core && cargo build && cargo test
|
||||
test: | test-core test-sdk test-container-runtime
|
||||
|
||||
sdk:
|
||||
cd core && ./install-sdk.sh
|
||||
test-core: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
./core/run-tests.sh
|
||||
|
||||
test-sdk: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
||||
cd sdk && make test
|
||||
|
||||
test-container-runtime: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||
cd container-runtime && npm test
|
||||
|
||||
cli:
|
||||
./core/install-cli.sh
|
||||
|
||||
registry: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox
|
||||
|
||||
install-registry: $(REGISTRY_TARGETS)
|
||||
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox,$(DESTDIR)/usr/bin/start-registrybox)
|
||||
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registryd)
|
||||
$(call ln,/usr/bin/start-registrybox,$(DESTDIR)/usr/bin/start-registry)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||
$(call cp,core/startos/start-registryd.service,$(DESTDIR)/lib/systemd/system/start-registryd.service)
|
||||
|
||||
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/registrybox: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-registrybox.sh
|
||||
|
||||
tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox
|
||||
|
||||
install-tunnel: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox core/startos/start-tunneld.service
|
||||
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox,$(DESTDIR)/usr/bin/start-tunnelbox)
|
||||
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunneld)
|
||||
$(call ln,/usr/bin/start-tunnelbox,$(DESTDIR)/usr/bin/start-tunnel)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||
$(call cp,core/startos/start-tunneld.service,$(DESTDIR)/lib/systemd/system/start-tunneld.service)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/scripts)
|
||||
$(call cp,build/lib/scripts/forward-port,$(DESTDIR)/usr/lib/startos/scripts/forward-port)
|
||||
|
||||
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/tunnelbox: $(CORE_SRC) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) web/dist/static/start-tunnel/index.html
|
||||
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-tunnelbox.sh
|
||||
|
||||
deb: results/$(BASENAME).deb
|
||||
|
||||
debian/control: build/lib/depends build/lib/conflicts
|
||||
./debuild/control.sh
|
||||
results/$(BASENAME).deb: dpkg-build.sh $(call ls-files,debian/startos) $(STARTOS_TARGETS)
|
||||
PLATFORM=$(PLATFORM) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
|
||||
|
||||
results/$(BASENAME).deb: dpkg-build.sh $(DEBIAN_SRC) $(VERSION_FILE) $(PLATFORM_FILE) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE)
|
||||
PLATFORM=$(PLATFORM) ./dpkg-build.sh
|
||||
registry-deb: results/$(REGISTRY_BASENAME).deb
|
||||
|
||||
results/$(REGISTRY_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-registry) $(REGISTRY_TARGETS)
|
||||
PROJECT=start-registry PLATFORM=$(ARCH) REQUIRES=debian ./build/os-compat/run-compat.sh ./dpkg-build.sh
|
||||
|
||||
tunnel-deb: results/$(TUNNEL_BASENAME).deb
|
||||
|
||||
results/$(TUNNEL_BASENAME).deb: dpkg-build.sh $(call ls-files,debian/start-tunnel) $(TUNNEL_TARGETS)
|
||||
PROJECT=start-tunnel PLATFORM=$(ARCH) REQUIRES=debian DEPENDS=wireguard-tools,iptables ./build/os-compat/run-compat.sh ./dpkg-build.sh
|
||||
|
||||
$(IMAGE_TYPE): results/$(BASENAME).$(IMAGE_TYPE)
|
||||
|
||||
@@ -104,17 +170,21 @@ results/$(BASENAME).$(IMAGE_TYPE) results/$(BASENAME).squashfs: $(IMAGE_RECIPE_S
|
||||
./image-recipe/run-local-build.sh "results/$(BASENAME).deb"
|
||||
|
||||
# For creating os images. DO NOT USE
|
||||
install: $(ALL_TARGETS)
|
||||
install: $(STARTOS_TARGETS)
|
||||
$(call mkdir,$(DESTDIR)/usr/bin)
|
||||
$(call cp,core/target/$(ARCH)-unknown-linux-gnu/release/startbox,$(DESTDIR)/usr/bin/startbox)
|
||||
$(call mkdir,$(DESTDIR)/usr/sbin)
|
||||
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,$(DESTDIR)/usr/bin/startbox)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/startd)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-cli)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-sdk)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/start-deno)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/avahi-alias)
|
||||
$(call ln,/usr/bin/startbox,$(DESTDIR)/usr/bin/embassy-cli)
|
||||
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
|
||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then $(call cp,cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); fi
|
||||
if [ "$(PLATFORM)" = "raspberrypi" ]; then $(call cp,cargo-deps/aarch64-unknown-linux-musl/release/pi-beep,$(DESTDIR)/usr/bin/pi-beep); fi
|
||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)unstable($$|-) ]]'; then \
|
||||
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph,$(DESTDIR)/usr/bin/flamegraph); \
|
||||
fi
|
||||
if /bin/bash -c '[[ "${ENVIRONMENT}" =~ (^|-)console($$|-) ]]'; then \
|
||||
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console,$(DESTDIR)/usr/bin/tokio-console); \
|
||||
fi
|
||||
$(call cp,cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs,$(DESTDIR)/usr/bin/startos-backup-fs)
|
||||
$(call ln,/usr/bin/startos-backup-fs,$(DESTDIR)/usr/sbin/mount.backup-fs)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/lib/systemd/system)
|
||||
$(call cp,core/startos/startd.service,$(DESTDIR)/lib/systemd/system/startd.service)
|
||||
@@ -122,24 +192,17 @@ install: $(ALL_TARGETS)
|
||||
$(call mkdir,$(DESTDIR)/usr/lib)
|
||||
$(call rm,$(DESTDIR)/usr/lib/startos)
|
||||
$(call cp,build/lib,$(DESTDIR)/usr/lib/startos)
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/container-runtime)
|
||||
$(call cp,container-runtime/rootfs.$(ARCH).squashfs,$(DESTDIR)/usr/lib/startos/container-runtime/rootfs.squashfs)
|
||||
|
||||
$(call cp,PLATFORM.txt,$(DESTDIR)/usr/lib/startos/PLATFORM.txt)
|
||||
$(call cp,ENVIRONMENT.txt,$(DESTDIR)/usr/lib/startos/ENVIRONMENT.txt)
|
||||
$(call cp,GIT_HASH.txt,$(DESTDIR)/usr/lib/startos/GIT_HASH.txt)
|
||||
$(call cp,VERSION.txt,$(DESTDIR)/usr/lib/startos/VERSION.txt)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/container)
|
||||
$(call cp,core/target/aarch64-unknown-linux-musl/release/container-init,$(DESTDIR)/usr/lib/startos/container/container-init.arm64)
|
||||
$(call cp,core/target/x86_64-unknown-linux-musl/release/container-init,$(DESTDIR)/usr/lib/startos/container/container-init.amd64)
|
||||
|
||||
$(call mkdir,$(DESTDIR)/usr/lib/startos/system-images)
|
||||
$(call cp,system-images/compat/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/compat.tar)
|
||||
$(call cp,system-images/utils/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/utils.tar)
|
||||
$(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/startos/system-images/binfmt.tar)
|
||||
|
||||
$(call cp,firmware/$(PLATFORM),$(DESTDIR)/usr/lib/startos/firmware)
|
||||
|
||||
update-overlay: $(ALL_TARGETS)
|
||||
update-overlay: $(STARTOS_TARGETS)
|
||||
@echo "\033[33m!!! THIS WILL ONLY REFLASH YOUR DEVICE IN MEMORY !!!\033[0m"
|
||||
@echo "\033[33mALL CHANGES WILL BE REVERTED IF YOU RESTART THE DEVICE\033[0m"
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
@@ -148,78 +211,157 @@ update-overlay: $(ALL_TARGETS)
|
||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) PLATFORM=$(PLATFORM)
|
||||
$(call ssh,"sudo systemctl start startd")
|
||||
|
||||
wormhole: core/target/$(ARCH)-unknown-linux-gnu/release/startbox
|
||||
@wormhole send core/target/$(ARCH)-unknown-linux-gnu/release/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
|
||||
wormhole: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
|
||||
@echo "Paste the following command into the shell of your StartOS server:"
|
||||
@echo
|
||||
@wormhole send core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade \"cd /usr/bin && rm startbox && wormhole receive --accept-file %s && chmod +x startbox\"\n", $$3 }'
|
||||
|
||||
update: $(ALL_TARGETS)
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
$(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/")
|
||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/embassy/next PLATFORM=$(PLATFORM)
|
||||
$(call ssh,'sudo NO_SYNC=1 /media/embassy/next/usr/lib/startos/scripts/chroot-and-upgrade "apt-get install -y $(shell cat ./build/lib/depends)"')
|
||||
wormhole-deb: results/$(BASENAME).deb
|
||||
@echo "Paste the following command into the shell of your StartOS server:"
|
||||
@echo
|
||||
@wormhole send results/$(BASENAME).deb 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo /usr/lib/startos/scripts/chroot-and-upgrade '"'"'cd $$(mktemp -d) && wormhole receive --accept-file %s && apt-get install -y --reinstall ./$(BASENAME).deb'"'"'\n", $$3 }'
|
||||
|
||||
emulate-reflash: $(ALL_TARGETS)
|
||||
wormhole-squashfs: results/$(BASENAME).squashfs
|
||||
$(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs | head -c 32))
|
||||
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
|
||||
@echo "Paste the following command into the shell of your StartOS server:"
|
||||
@echo
|
||||
@wormhole send results/$(BASENAME).squashfs 2>&1 | awk -Winteractive '/wormhole receive/ { printf "sudo sh -c '"'"'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE) && /usr/lib/startos/scripts/prune-boot && cd /media/startos/images && wormhole receive --accept-file %s && CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img ./$(BASENAME).squashfs'"'"'\n", $$3 }'
|
||||
|
||||
update: $(STARTOS_TARGETS)
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
$(call ssh,"sudo rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next/")
|
||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/embassy/next PLATFORM=$(PLATFORM)
|
||||
$(call ssh,"sudo touch /media/embassy/config/upgrade && sudo rm -f /media/embassy/config/disk.guid && sudo sync && sudo reboot")
|
||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
|
||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
|
||||
|
||||
update-startbox: core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox # only update binary (faster than full update)
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||
$(call cp,core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox,/media/startos/next/usr/bin/startbox)
|
||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync true')
|
||||
|
||||
update-deb: results/$(BASENAME).deb # better than update, but only available from debian
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||
$(call mkdir,/media/startos/next/tmp/startos-deb)
|
||||
$(call cp,results/$(BASENAME).deb,/media/startos/next/tmp/startos-deb/$(BASENAME).deb)
|
||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y --reinstall /tmp/startos-deb/$(BASENAME).deb"')
|
||||
|
||||
update-squashfs: results/$(BASENAME).squashfs
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
$(eval SQFS_SUM := $(shell b3sum results/$(BASENAME).squashfs))
|
||||
$(eval SQFS_SIZE := $(shell du -s --bytes results/$(BASENAME).squashfs | awk '{print $$1}'))
|
||||
$(call ssh,'/usr/lib/startos/scripts/prune-images $(SQFS_SIZE)')
|
||||
$(call ssh,'/usr/lib/startos/scripts/prune-boot')
|
||||
$(call cp,results/$(BASENAME).squashfs,/media/startos/images/next.rootfs)
|
||||
$(call ssh,'sudo CHECKSUM=$(SQFS_SUM) /usr/lib/startos/scripts/use-img /media/startos/images/next.rootfs')
|
||||
|
||||
emulate-reflash: $(STARTOS_TARGETS)
|
||||
@if [ -z "$(REMOTE)" ]; then >&2 echo "Must specify REMOTE" && false; fi
|
||||
$(call ssh,'sudo /usr/lib/startos/scripts/chroot-and-upgrade --create')
|
||||
$(MAKE) install REMOTE=$(REMOTE) SSHPASS=$(SSHPASS) DESTDIR=/media/startos/next PLATFORM=$(PLATFORM)
|
||||
$(call ssh,'sudo rm -f /media/startos/config/disk.guid /media/startos/config/overlay/etc/hostname')
|
||||
$(call ssh,'sudo /media/startos/next/usr/lib/startos/scripts/chroot-and-upgrade --no-sync "apt-get install -y $(shell cat ./build/lib/depends)"')
|
||||
|
||||
upload-ota: results/$(BASENAME).squashfs
|
||||
TARGET=$(TARGET) KEY=$(KEY) ./upload-ota.sh
|
||||
|
||||
build/lib/depends build/lib/conflicts: build/dpkg-deps/*
|
||||
build/dpkg-deps/generate.sh
|
||||
container-runtime/debian.$(ARCH).squashfs: ./container-runtime/download-base-image.sh
|
||||
ARCH=$(ARCH) ./container-runtime/download-base-image.sh
|
||||
|
||||
container-runtime/package-lock.json: sdk/dist/package.json
|
||||
npm --prefix container-runtime i
|
||||
touch container-runtime/package-lock.json
|
||||
|
||||
container-runtime/node_modules/.package-lock.json: container-runtime/package-lock.json
|
||||
npm --prefix container-runtime ci
|
||||
touch container-runtime/node_modules/.package-lock.json
|
||||
|
||||
sdk/base/lib/osBindings/index.ts: $(shell if [ "$(REBUILD_TYPES)" -ne 0 ]; then echo core/startos/bindings/index.ts; fi)
|
||||
mkdir -p sdk/base/lib/osBindings
|
||||
rsync -ac --delete core/startos/bindings/ sdk/base/lib/osBindings/
|
||||
touch sdk/base/lib/osBindings/index.ts
|
||||
|
||||
core/startos/bindings/index.ts: $(call ls-files, core) $(ENVIRONMENT_FILE)
|
||||
rm -rf core/startos/bindings
|
||||
./core/build-ts.sh
|
||||
ls core/startos/bindings/*.ts | sed 's/core\/startos\/bindings\/\([^.]*\)\.ts/export { \1 } from ".\/\1";/g' | grep -v '"./index"' | tee core/startos/bindings/index.ts
|
||||
npm --prefix sdk exec -- prettier --config ./sdk/base/package.json -w ./core/startos/bindings/*.ts
|
||||
touch core/startos/bindings/index.ts
|
||||
|
||||
sdk/dist/package.json sdk/baseDist/package.json: $(call ls-files, sdk) sdk/base/lib/osBindings/index.ts
|
||||
(cd sdk && make bundle)
|
||||
touch sdk/dist/package.json
|
||||
touch sdk/baseDist/package.json
|
||||
|
||||
# TODO: make container-runtime its own makefile?
|
||||
container-runtime/dist/index.js: container-runtime/node_modules/.package-lock.json $(call ls-files, container-runtime/src) container-runtime/package.json container-runtime/tsconfig.json
|
||||
npm --prefix container-runtime run build
|
||||
|
||||
container-runtime/dist/node_modules/.package-lock.json container-runtime/dist/package.json container-runtime/dist/package-lock.json: container-runtime/package.json container-runtime/package-lock.json sdk/dist/package.json container-runtime/install-dist-deps.sh
|
||||
./container-runtime/install-dist-deps.sh
|
||||
touch container-runtime/dist/node_modules/.package-lock.json
|
||||
|
||||
container-runtime/rootfs.$(ARCH).squashfs: container-runtime/debian.$(ARCH).squashfs container-runtime/container-runtime.service container-runtime/update-image.sh container-runtime/deb-install.sh container-runtime/dist/index.js container-runtime/dist/node_modules/.package-lock.json core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox
|
||||
ARCH=$(ARCH) REQUIRES=linux ./build/os-compat/run-compat.sh ./container-runtime/update-image.sh
|
||||
|
||||
build/lib/depends build/lib/conflicts: $(ENVIRONMENT_FILE) $(PLATFORM_FILE) $(shell ls build/dpkg-deps/*)
|
||||
PLATFORM=$(PLATFORM) ARCH=$(ARCH) build/dpkg-deps/generate.sh
|
||||
|
||||
$(FIRMWARE_ROMS): build/lib/firmware.json download-firmware.sh $(PLATFORM_FILE)
|
||||
./download-firmware.sh $(PLATFORM)
|
||||
|
||||
system-images/compat/docker-images/$(ARCH).tar: $(COMPAT_SRC) core/Cargo.lock
|
||||
cd system-images/compat && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
||||
core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox: $(CORE_SRC) $(COMPRESSED_WEB_UIS) web/patchdb-ui-seed.json $(ENVIRONMENT_FILE)
|
||||
ARCH=$(ARCH) PROFILE=$(PROFILE) ./core/build-startbox.sh
|
||||
touch core/target/$(RUST_ARCH)-unknown-linux-musl/$(PROFILE)/startbox
|
||||
|
||||
system-images/utils/docker-images/$(ARCH).tar: $(UTILS_SRC)
|
||||
cd system-images/utils && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
||||
core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox: $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
ARCH=$(ARCH) ./core/build-containerbox.sh
|
||||
touch core/target/$(RUST_ARCH)-unknown-linux-musl/release/containerbox
|
||||
|
||||
system-images/binfmt/docker-images/$(ARCH).tar: $(BINFMT_SRC)
|
||||
cd system-images/binfmt && make docker-images/$(ARCH).tar && touch docker-images/$(ARCH).tar
|
||||
web/package-lock.json: web/package.json sdk/baseDist/package.json
|
||||
npm --prefix web i
|
||||
touch web/package-lock.json
|
||||
|
||||
snapshots: core/snapshot-creator/Cargo.toml
|
||||
cd core/ && ARCH=aarch64 ./build-v8-snapshot.sh
|
||||
cd core/ && ARCH=x86_64 ./build-v8-snapshot.sh
|
||||
|
||||
$(BINS): $(CORE_SRC) $(ENVIRONMENT_FILE)
|
||||
cd core && ARCH=$(ARCH) ./build-prod.sh
|
||||
touch $(BINS)
|
||||
|
||||
web/node_modules: web/package.json
|
||||
web/node_modules/.package-lock.json: web/package-lock.json
|
||||
npm --prefix web ci
|
||||
touch web/node_modules/.package-lock.json
|
||||
|
||||
web/dist/raw/ui: $(WEB_UI_SRC) $(WEB_SHARED_SRC)
|
||||
web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json web/node_modules/.package-lock.json
|
||||
rm -rf web/.angular
|
||||
mkdir -p web/.angular
|
||||
touch web/.angular/.updated
|
||||
|
||||
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:ui
|
||||
touch web/dist/raw/ui/index.html
|
||||
|
||||
web/dist/raw/setup-wizard: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC)
|
||||
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:setup
|
||||
touch web/dist/raw/setup-wizard/index.html
|
||||
|
||||
web/dist/raw/diagnostic-ui: $(WEB_DIAGNOSTIC_UI_SRC) $(WEB_SHARED_SRC)
|
||||
npm --prefix web run build:dui
|
||||
web/dist/raw/install-wizard/index.html: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:install
|
||||
touch web/dist/raw/install-wizard/index.html
|
||||
|
||||
web/dist/raw/install-wizard: $(WEB_INSTALL_WIZARD_SRC) $(WEB_SHARED_SRC)
|
||||
npm --prefix web run build:install-wiz
|
||||
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:tunnel
|
||||
touch web/dist/raw/start-tunnel/index.html
|
||||
|
||||
web/dist/static: $(WEB_UIS) $(ENVIRONMENT_FILE)
|
||||
./compress-uis.sh
|
||||
web/dist/static/%/index.html: web/dist/raw/%/index.html
|
||||
./compress-uis.sh $*
|
||||
|
||||
web/config.json: $(GIT_HASH_FILE) web/config-sample.json
|
||||
jq '.useMocks = false' web/config-sample.json | jq '.gitHash = "$(shell cat GIT_HASH.txt)"' > web/config.json
|
||||
|
||||
web/patchdb-ui-seed.json: web/package.json
|
||||
jq '."ack-welcome" = $(shell jq '.version' web/package.json)' web/patchdb-ui-seed.json > ui-seed.tmp
|
||||
mv ui-seed.tmp web/patchdb-ui-seed.json
|
||||
|
||||
patch-db/client/node_modules: patch-db/client/package.json
|
||||
patch-db/client/node_modules/.package-lock.json: patch-db/client/package.json
|
||||
npm --prefix patch-db/client ci
|
||||
touch patch-db/client/node_modules/.package-lock.json
|
||||
|
||||
patch-db/client/dist: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules
|
||||
! test -d patch-db/client/dist || rm -rf patch-db/client/dist
|
||||
npm --prefix web run build:deps
|
||||
patch-db/client/dist/index.js: $(PATCH_DB_CLIENT_SRC) patch-db/client/node_modules/.package-lock.json
|
||||
rm -rf patch-db/client/dist
|
||||
npm --prefix patch-db/client run build
|
||||
touch patch-db/client/dist/index.js
|
||||
|
||||
# used by github actions
|
||||
compiled-$(ARCH).tar: $(COMPILED_TARGETS) $(ENVIRONMENT_FILE) $(GIT_HASH_FILE) $(VERSION_FILE)
|
||||
@@ -231,8 +373,14 @@ uis: $(WEB_UIS)
|
||||
# this is a convenience step to build the UI
|
||||
ui: web/dist/raw/ui
|
||||
|
||||
cargo-deps/aarch64-unknown-linux-gnu/release/pi-beep:
|
||||
cargo-deps/aarch64-unknown-linux-musl/release/pi-beep:
|
||||
ARCH=aarch64 ./build-cargo-dep.sh pi-beep
|
||||
|
||||
cargo-deps/$(ARCH)-unknown-linux-gnu/release/tokio-console:
|
||||
ARCH=$(ARCH) ./build-cargo-dep.sh tokio-console
|
||||
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/tokio-console:
|
||||
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh tokio-console
|
||||
|
||||
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/startos-backup-fs:
|
||||
ARCH=$(ARCH) PREINSTALL="apk add fuse3 fuse3-dev fuse3-static musl-dev pkgconfig" ./build-cargo-dep.sh --git https://github.com/Start9Labs/start-fs.git startos-backup-fs
|
||||
|
||||
cargo-deps/$(RUST_ARCH)-unknown-linux-musl/release/flamegraph:
|
||||
ARCH=$(ARCH) PREINSTALL="apk add musl-dev pkgconfig" ./build-cargo-dep.sh flamegraph
|
||||
@@ -13,9 +13,6 @@
|
||||
<a href="https://twitter.com/start9labs">
|
||||
<img alt="X (formerly Twitter) Follow" src="https://img.shields.io/twitter/follow/start9labs">
|
||||
</a>
|
||||
<a href="https://mastodon.start9labs.com">
|
||||
<img src="https://img.shields.io/mastodon/follow/000000001?domain=https%3A%2F%2Fmastodon.start9labs.com&label=Follow&style=social">
|
||||
</a>
|
||||
<a href="https://matrix.to/#/#community:matrix.start9labs.com">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/community-matrix-yellow?logo=matrix">
|
||||
</a>
|
||||
@@ -48,7 +45,8 @@
|
||||
<br />
|
||||
|
||||
## Running StartOS
|
||||
There are multiple ways to get started with StartOS:
|
||||
> [!WARNING]
|
||||
> StartOS is in beta. It lacks features. It doesn't always work perfectly. Start9 servers are not plug and play. Using them properly requires some effort and patience. Please do not use StartOS or purchase a server if you are unable or unwilling to follow instructions and learn new concepts.
|
||||
|
||||
### 💰 Buy a Start9 server
|
||||
This is the most convenient option. Simply [buy a server](https://store.start9.com) from Start9 and plug it in.
|
||||
|
||||
59
START-TUNNEL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# StartTunnel
|
||||
|
||||
A self-hosted Wiregaurd VPN optimized for creating VLANs and reverse tunneling to personal servers.
|
||||
|
||||
You can think of StartTunnel as "virtual router in the cloud"
|
||||
|
||||
Use it for private, remote access, to self-hosted services running on a personal server, or to expose self-hosted services to the public Internet without revealing the host server's IP address.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Rent a low cost VPS. For most use cases, the cheapest option should be enough.
|
||||
|
||||
- It must have a dedicated public IP address.
|
||||
- For (CPU), memory (RAM), and storage (disk), choose the minimum spec.
|
||||
- For transfer (bandwidth), it depends on (1) your use case and (2) your home Internet's _upload_ speed. Even if you intend to serve large files or stream content from your server, there is no reason to pay for speeds that exceed your home Internet's upload speed.
|
||||
|
||||
1. Provision the VPS with the latest version of Debian.
|
||||
|
||||
1. Access the VPS via SSH.
|
||||
|
||||
1. Install StartTunnel:
|
||||
|
||||
@TODO
|
||||
|
||||
## Features
|
||||
|
||||
- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router.
|
||||
|
||||
- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique Wireguard config that must be copied, downloaded, or scanned into the device.
|
||||
|
||||
- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet.
|
||||
|
||||
## CLI
|
||||
|
||||
By default, StartTunnel is managed via the `start-tunnel` command line interface, which is self-documented.
|
||||
|
||||
```
|
||||
start-tunnel --help
|
||||
```
|
||||
|
||||
## Web Interface
|
||||
|
||||
If you choose to enable the web interface (recommended in most cases), StartTunnel can be accessed as a website from the browser, or programmatically via API.
|
||||
|
||||
1. Initialize the web interface.
|
||||
|
||||
start-tunnel web init
|
||||
|
||||
1. When prompted, select the IP address at which to host the web interface. In many cases, there will be only one IP address.
|
||||
|
||||
1. When prompted, enter the port at which to host the web interface. The default is 8443, and we recommend using it. If you change the default, choose an uncommon port to avoid conflicts.
|
||||
|
||||
1. Select whether to autogenerate a self-signed certificate or provide your own certificate and key. If you choose to autogenerate, you will be asked to list all IP addresses and domains for which to sign the certificate. For example, if you intend to access your StartTunnel web UI at a domain, include the domain in the list.
|
||||
|
||||
1. You will receive a success message that the webserver is running at the chosen IP:port, as well as your SSL certificate and an autogenerated UI password.
|
||||
|
||||
1. If not already, trust the certificate in your system keychain and/or browser.
|
||||
|
||||
1. If you lose/forget your password, you can reset it using the CLI.
|
||||
BIN
assets/create-vm/step-1.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/create-vm/step-10.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
assets/create-vm/step-11.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/create-vm/step-12.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
assets/create-vm/step-2.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/create-vm/step-3.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
assets/create-vm/step-4.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/create-vm/step-5.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/create-vm/step-6.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
assets/create-vm/step-7.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
assets/create-vm/step-8.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
assets/create-vm/step-9.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
@@ -1,5 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT=${PROJECT:-"startos"}
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
PLATFORM="$(if [ -f ./PLATFORM.txt ]; then cat ./PLATFORM.txt; else echo unknown; fi)"
|
||||
@@ -16,4 +18,4 @@ if [ -n "$STARTOS_ENV" ]; then
|
||||
VERSION_FULL="$VERSION_FULL~${STARTOS_ENV}"
|
||||
fi
|
||||
|
||||
echo -n "startos-${VERSION_FULL}_${PLATFORM}"
|
||||
echo -n "${PROJECT}-${VERSION_FULL}_${PLATFORM}"
|
||||
@@ -17,9 +17,18 @@ if [ -z "$ARCH" ]; then
|
||||
ARCH=$(uname -m)
|
||||
fi
|
||||
|
||||
mkdir -p cargo-deps
|
||||
alias 'rust-arm64-builder'='docker run $USE_TTY --rm -v "$HOME/.cargo/registry":/usr/local/cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -P start9/rust-arm-cross:aarch64'
|
||||
DOCKER_PLATFORM="linux/${ARCH}"
|
||||
if [ "$ARCH" = aarch64 ] || [ "$ARCH" = arm64 ]; then
|
||||
DOCKER_PLATFORM="linux/arm64"
|
||||
elif [ "$ARCH" = x86_64 ]; then
|
||||
DOCKER_PLATFORM="linux/amd64"
|
||||
fi
|
||||
|
||||
rust-arm64-builder cargo install "$1" --target-dir /home/rust/src --target=$ARCH-unknown-linux-gnu
|
||||
mkdir -p cargo-deps
|
||||
alias 'rust-musl-builder'='docker run $USE_TTY --platform=${DOCKER_PLATFORM} --rm -e "RUSTFLAGS=$RUSTFLAGS" -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)"/cargo-deps:/home/rust/src -w /home/rust/src -P rust:alpine'
|
||||
|
||||
PREINSTALL=${PREINSTALL:-true}
|
||||
|
||||
rust-musl-builder sh -c "$PREINSTALL && cargo install $* --target-dir /home/rust/src --target=$ARCH-unknown-linux-musl"
|
||||
sudo chown -R $USER cargo-deps
|
||||
sudo chown -R $USER ~/.cargo
|
||||
4
build/.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
lib/depends
|
||||
lib/conflicts
|
||||
/lib/depends
|
||||
/lib/conflicts
|
||||
107
build/README.md
@@ -1,107 +0,0 @@
|
||||
# Building StartOS
|
||||
|
||||
⚠️ The commands given assume a Debian or Ubuntu-based environment. _Building in
|
||||
a VM is NOT yet supported_ ⚠️
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
- Avahi
|
||||
- `sudo apt install -y avahi-daemon`
|
||||
- Installed by default on most Debian systems - https://avahi.org
|
||||
- Build Essentials (needed to run `make`)
|
||||
- `sudo apt install -y build-essential`
|
||||
- Docker
|
||||
- `curl -fsSL https://get.docker.com | sh`
|
||||
- https://docs.docker.com/get-docker
|
||||
- Add your user to the docker group: `sudo usermod -a -G docker $USER`
|
||||
- Reload user environment `exec sudo su -l $USER`
|
||||
- Prepare Docker environment
|
||||
- Setup buildx (https://docs.docker.com/buildx/working-with-buildx/)
|
||||
- Create a builder: `docker buildx create --use`
|
||||
- Add multi-arch build ability:
|
||||
`docker run --rm --privileged linuxkit/binfmt:v0.8`
|
||||
- Node Version 12+
|
||||
- snap: `sudo snap install node`
|
||||
- [nvm](https://github.com/nvm-sh/nvm#installing-and-updating):
|
||||
`nvm install --lts`
|
||||
- https://nodejs.org/en/docs
|
||||
- NPM Version 7+
|
||||
- apt: `sudo apt install -y npm`
|
||||
- [nvm](https://github.com/nvm-sh/nvm#installing-and-updating):
|
||||
`nvm install --lts`
|
||||
- https://docs.npmjs.com/downloading-and-installing-node-js-and-npm
|
||||
- jq
|
||||
- `sudo apt install -y jq`
|
||||
- https://stedolan.github.io/jq
|
||||
- yq
|
||||
- snap: `sudo snap install yq`
|
||||
- binaries: https://github.com/mikefarah/yq/releases/
|
||||
- https://mikefarah.gitbook.io/yq
|
||||
|
||||
2. Clone the latest repo with required submodules
|
||||
> :information_source: You chan check latest available version
|
||||
> [here](https://github.com/Start9Labs/start-os/releases)
|
||||
```
|
||||
git clone --recursive https://github.com/Start9Labs/start-os.git --branch latest
|
||||
```
|
||||
|
||||
## Build Raspberry Pi Image
|
||||
|
||||
```
|
||||
cd start-os
|
||||
make embassyos-raspi.img ARCH=aarch64
|
||||
```
|
||||
|
||||
## Flash
|
||||
|
||||
Flash the resulting `embassyos-raspi.img` to your SD Card
|
||||
|
||||
We recommend [Balena Etcher](https://www.balena.io/etcher/)
|
||||
|
||||
## Setup
|
||||
|
||||
Visit http://start.local from any web browser - We recommend
|
||||
[Firefox](https://www.mozilla.org/firefox/browsers)
|
||||
|
||||
Enter your product key. This is generated during the build process and can be
|
||||
found in `product_key.txt`, located in the root directory.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. I just flashed my SD card, fired up StartOS, bootup sounds and all, but my
|
||||
browser is saying "Unable to connect" with start.local.
|
||||
|
||||
- Try doing a hard refresh on your browser, or opening the url in a
|
||||
private/incognito window. If you've ran an instance of StartOS before,
|
||||
sometimes you can have a stale cache that will block you from navigating to
|
||||
the page.
|
||||
|
||||
2. Flashing the image isn't working with balenaEtcher. I'm getting
|
||||
`Cannot read property 'message' of null` when I try.
|
||||
|
||||
- The latest versions of Balena may not flash properly. This version here:
|
||||
https://github.com/balena-io/etcher/releases/tag/v1.5.122 should work
|
||||
properly.
|
||||
|
||||
3. Startup isn't working properly and I'm curious as to why. How can I view logs
|
||||
regarding startup for debugging?
|
||||
|
||||
- Find the IP of your device
|
||||
- Run `nc <ip> 8080` and it will print the logs
|
||||
|
||||
4. I need to ssh into my server to fix something, but I cannot get to the
|
||||
console to add ssh keys normally.
|
||||
|
||||
- During the Build step, instead of running just
|
||||
`make embassyos-raspi.img ARCH=aarch64` run
|
||||
`ENVIRONMENT=dev make embassyos-raspi.img ARCH=aarch64`. Flash like normal,
|
||||
and insert into your server. Boot up StartOS, then on another computer on
|
||||
the same network, ssh into the the server with the username `start9` password
|
||||
`embassy`.
|
||||
|
||||
4. I need to reset my password, how can I do that?
|
||||
|
||||
- You will need to reflash your device. Select "Use Existing Drive" once you are
|
||||
in setup, and it will prompt you to set a new password.
|
||||
@@ -1,76 +0,0 @@
|
||||
# Release Process
|
||||
|
||||
## `embassyos_0.3.x-1_amd64.deb`
|
||||
|
||||
- Description: debian package for x86_64 - intended to be installed on pureos
|
||||
- Destination: GitHub Release Tag
|
||||
- Requires: N/A
|
||||
- Build steps:
|
||||
- Clone `https://github.com/Start9Labs/embassy-os-deb` at `master`
|
||||
- Run `make TAG=master` from that folder
|
||||
- Artifact: `./embassyos_0.3.x-1_amd64.deb`
|
||||
|
||||
## `eos-<version>-<git hash>-<date>_amd64.iso`
|
||||
|
||||
- Description: live usb image for x86_64
|
||||
- Destination: GitHub Release Tag
|
||||
- Requires: `embassyos_0.3.x-1_amd64.deb`
|
||||
- Build steps:
|
||||
- Clone `https://github.com/Start9Labs/eos-image-recipes` at `master`
|
||||
- Copy `embassyos_0.3.x-1_amd64.deb` to
|
||||
`overlays/vendor/root/embassyos_0.3.x-1_amd64.deb`
|
||||
- Run `./run-local-build.sh byzantium` from that folder
|
||||
- Artifact: `./results/eos-<version>-<git hash>-<date>_amd64.iso`
|
||||
|
||||
## `eos.x86_64.squashfs`
|
||||
|
||||
- Description: compressed embassyOS x86_64 filesystem image
|
||||
- Destination: GitHub Release Tag, Registry @
|
||||
`resources/eos/<version>/eos.x86_64.squashfs`
|
||||
- Requires: `eos-<version>-<git hash>-<date>_amd64.iso`
|
||||
- Build steps:
|
||||
- From `https://github.com/Start9Labs/eos-image-recipes` at `master`
|
||||
- `./extract-squashfs.sh results/eos-<version>-<git hash>-<date>_amd64.iso`
|
||||
- Artifact: `./results/eos.x86_64.squashfs`
|
||||
|
||||
## `eos.raspberrypi.squashfs`
|
||||
|
||||
- Description: compressed embassyOS raspberrypi filesystem image
|
||||
- Destination: GitHub Release Tag, Registry @
|
||||
`resources/eos/<version>/eos.raspberrypi.squashfs`
|
||||
- Requires: N/A
|
||||
- Build steps:
|
||||
- Clone `https://github.com/Start9Labs/embassy-os` at `master`
|
||||
- `make embassyos-raspi.img`
|
||||
- flash `embassyos-raspi.img` to raspberry pi
|
||||
- boot raspberry pi with ethernet
|
||||
- wait for chime
|
||||
- you can watch logs using `nc <ip> 8080`
|
||||
- unplug raspberry pi, put sd card back in build machine
|
||||
- `./build/raspberry-pi/rip-image.sh`
|
||||
- Artifact: `./eos.raspberrypi.squashfs`
|
||||
|
||||
## `lite-upgrade.img`
|
||||
|
||||
- Description: update image for users coming from 0.3.2.1 and before
|
||||
- Destination: Registry @ `resources/eos/<version>/eos.img`
|
||||
- Requires: `eos.raspberrypi.squashfs`
|
||||
- Build steps:
|
||||
- From `https://github.com/Start9Labs/embassy-os` at `master`
|
||||
- `make lite-upgrade.img`
|
||||
- Artifact `./lite-upgrade.img`
|
||||
|
||||
## `eos-<version>-<git hash>-<date>_raspberrypi.tar.gz`
|
||||
|
||||
- Description: pre-initialized raspberrypi image
|
||||
- Destination: GitHub Release Tag (as tar.gz)
|
||||
- Requires: `eos.raspberrypi.squashfs`
|
||||
- Build steps:
|
||||
- From `https://github.com/Start9Labs/embassy-os` at `master`
|
||||
- `make eos_raspberrypi.img`
|
||||
- `tar --format=posix -cS -f- eos-<version>-<git hash>-<date>_raspberrypi.img | gzip > eos-<version>-<git hash>-<date>_raspberrypi.tar.gz`
|
||||
- Artifact `./eos-<version>-<git hash>-<date>_raspberrypi.tar.gz`
|
||||
|
||||
## `embassy-sdk`
|
||||
|
||||
- Build and deploy to all registries
|
||||
@@ -1,5 +1,6 @@
|
||||
avahi-daemon
|
||||
avahi-utils
|
||||
b3sum
|
||||
bash-completion
|
||||
beep
|
||||
bmon
|
||||
@@ -9,40 +10,48 @@ cifs-utils
|
||||
cryptsetup
|
||||
curl
|
||||
dmidecode
|
||||
dnsutils
|
||||
dosfstools
|
||||
e2fsprogs
|
||||
ecryptfs-utils
|
||||
exfatprogs
|
||||
flashrom
|
||||
fuse3
|
||||
grub-common
|
||||
grub-efi
|
||||
grub2-common
|
||||
htop
|
||||
httpdirfs
|
||||
iotop
|
||||
iptables
|
||||
iw
|
||||
jq
|
||||
libavahi-client3
|
||||
libyajl2
|
||||
linux-cpupower
|
||||
lm-sensors
|
||||
lshw
|
||||
lvm2
|
||||
lxc
|
||||
magic-wormhole
|
||||
man-db
|
||||
ncdu
|
||||
net-tools
|
||||
network-manager
|
||||
nfs-common
|
||||
nvme-cli
|
||||
nyx
|
||||
openssh-server
|
||||
podman
|
||||
postgresql
|
||||
psmisc
|
||||
qemu-guest-agent
|
||||
rfkill
|
||||
rsync
|
||||
samba-common-bin
|
||||
smartmontools
|
||||
socat
|
||||
sqlite3
|
||||
squashfs-tools
|
||||
squashfs-tools-ng
|
||||
sudo
|
||||
systemd
|
||||
systemd-resolved
|
||||
@@ -51,4 +60,5 @@ systemd-timesyncd
|
||||
tor
|
||||
util-linux
|
||||
vim
|
||||
wireguard-tools
|
||||
wireless-tools
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
+ containerd.io
|
||||
+ docker-ce
|
||||
+ docker-ce-cli
|
||||
+ docker-compose-plugin
|
||||
- podman
|
||||
@@ -5,11 +5,15 @@ set -e
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
IFS="-" read -ra FEATURES <<< "$ENVIRONMENT"
|
||||
FEATURES+=("${ARCH}")
|
||||
if [ "$ARCH" != "$PLATFORM" ]; then
|
||||
FEATURES+=("${PLATFORM}")
|
||||
fi
|
||||
|
||||
feature_file_checker='
|
||||
/^#/ { next }
|
||||
/^\+ [a-z0-9]+$/ { next }
|
||||
/^- [a-z0-9]+$/ { next }
|
||||
/^\+ [a-z0-9.-]+$/ { next }
|
||||
/^- [a-z0-9.-]+$/ { next }
|
||||
{ exit 1 }
|
||||
'
|
||||
|
||||
@@ -30,8 +34,8 @@ for type in conflicts depends; do
|
||||
for feature in ${FEATURES[@]}; do
|
||||
file="$feature.$type"
|
||||
if [ -f $file ]; then
|
||||
if grep "^- $pkg$" $file; then
|
||||
SKIP=1
|
||||
if grep "^- $pkg$" $file > /dev/null; then
|
||||
SKIP=yes
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
12
build/dpkg-deps/raspberrypi.depends
Normal file
@@ -0,0 +1,12 @@
|
||||
- grub-common
|
||||
- grub-efi
|
||||
- grub2-common
|
||||
+ parted
|
||||
+ raspberrypi-net-mods
|
||||
+ raspberrypi-sys-mods
|
||||
+ raspi-config
|
||||
+ raspi-firmware
|
||||
+ raspi-utils
|
||||
+ rpi-eeprom
|
||||
+ rpi-update
|
||||
+ rpi.gpio-common
|
||||
@@ -1,2 +1,3 @@
|
||||
+ gdb
|
||||
+ heaptrack
|
||||
+ heaptrack
|
||||
+ linux-perf
|
||||
1
build/dpkg-deps/x86_64.depends
Normal file
@@ -0,0 +1 @@
|
||||
+ grub-pc-bin
|
||||
@@ -1,13 +1,13 @@
|
||||
[
|
||||
{
|
||||
"id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3",
|
||||
"id": "pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-29",
|
||||
"platform": ["x86_64"],
|
||||
"system-product-name": "librem_mini_v2",
|
||||
"bios-version": {
|
||||
"semver-prefix": "PureBoot-Release-",
|
||||
"semver-range": "<28.3"
|
||||
"semver-range": "<29"
|
||||
},
|
||||
"url": "https://source.puri.sm/firmware/releases/-/raw/master/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-28.3.rom.gz",
|
||||
"shasum": "5019bcf53f7493c7aa74f8ef680d18b5fc26ec156c705a841433aaa2fdef8f35"
|
||||
"url": "https://source.puri.sm/firmware/releases/-/raw/75631ad6dcf7e6ee73e06a517ac7dc4e017518b7/librem_mini_v2/custom/pureboot-librem_mini_v2-basic_usb_autoboot_blob_jail-Release-29.rom.gz",
|
||||
"shasum": "96ec04f21b1cfe8e28d9a2418f1ff533efe21f9bbbbf16e162f7c814761b068b"
|
||||
}
|
||||
]
|
||||
|
||||
147
build/lib/motd
@@ -1,34 +1,123 @@
|
||||
#!/bin/sh
|
||||
printf "\n"
|
||||
printf "Welcome to\n"
|
||||
cat << "ASCII"
|
||||
|
||||
███████
|
||||
█ █ █
|
||||
█ █ █ █
|
||||
█ █ █ █
|
||||
█ █ █ █
|
||||
█ █ █ █
|
||||
█ █
|
||||
███████
|
||||
parse_essential_db_info() {
|
||||
DB_DUMP="/tmp/startos_db.json"
|
||||
|
||||
_____ __ ___ __ __
|
||||
(_ | /\ |__) | / \(_
|
||||
__) | / \| \ | \__/__)
|
||||
ASCII
|
||||
printf " v$(cat /usr/lib/startos/VERSION.txt)\n\n"
|
||||
printf " %s (%s %s)\n" "$(uname -o)" "$(uname -r)" "$(uname -m)"
|
||||
printf " Git Hash: $(cat /usr/lib/startos/GIT_HASH.txt)"
|
||||
if [ -n "$(cat /usr/lib/startos/ENVIRONMENT.txt)" ]; then
|
||||
printf " ~ $(cat /usr/lib/startos/ENVIRONMENT.txt)\n"
|
||||
else
|
||||
printf "\n"
|
||||
if command -v start-cli >/dev/null 2>&1; then
|
||||
start-cli db dump > "$DB_DUMP" 2>/dev/null || return 1
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
if command -v jq >/dev/null 2>&1 && [ -f "$DB_DUMP" ]; then
|
||||
HOSTNAME=$(jq -r '.value.serverInfo.hostname // "unknown"' "$DB_DUMP" 2>/dev/null)
|
||||
VERSION=$(jq -r '.value.serverInfo.version // "unknown"' "$DB_DUMP" 2>/dev/null)
|
||||
RAM_BYTES=$(jq -r '.value.serverInfo.ram // 0' "$DB_DUMP" 2>/dev/null)
|
||||
WAN_IP=$(jq -r '.value.serverInfo.network.gateways[].ipInfo.wanIp // "unknown"' "$DB_DUMP" 2>/dev/null | head -1)
|
||||
NTP_SYNCED=$(jq -r '.value.serverInfo.ntpSynced // false' "$DB_DUMP" 2>/dev/null)
|
||||
|
||||
if [ "$RAM_BYTES" != "0" ] && [ "$RAM_BYTES" != "null" ]; then
|
||||
RAM_GB=$(echo "scale=1; $RAM_BYTES / 1073741824" | bc 2>/dev/null || echo "unknown")
|
||||
else
|
||||
RAM_GB="unknown"
|
||||
fi
|
||||
|
||||
RUNNING_SERVICES=$(jq -r '[.value.packageData[] | select(.status.main == "running")] | length' "$DB_DUMP" 2>/dev/null)
|
||||
TOTAL_SERVICES=$(jq -r '.value.packageData | length' "$DB_DUMP" 2>/dev/null)
|
||||
|
||||
rm -f "$DB_DUMP"
|
||||
return 0
|
||||
else
|
||||
rm -f "$DB_DUMP" 2>/dev/null
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
DB_INFO_AVAILABLE=0
|
||||
if parse_essential_db_info; then
|
||||
DB_INFO_AVAILABLE=1
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
printf " * Documentation: https://docs.start9.com\n"
|
||||
printf " * Management: https://%s.local\n" "$(hostname)"
|
||||
printf " * Support: https://start9.com/contact\n"
|
||||
printf " * Source Code: https://github.com/Start9Labs/start-os\n"
|
||||
printf " * License: MIT\n"
|
||||
printf "\n"
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$VERSION" != "unknown" ]; then
|
||||
version_display="v$VERSION"
|
||||
else
|
||||
version_display="v$(cat /usr/lib/startos/VERSION.txt 2>/dev/null || echo 'unknown')"
|
||||
fi
|
||||
|
||||
printf "\n\033[1;37m ▄▄▀▀▀▀▀▄▄\033[0m\n"
|
||||
printf "\033[1;37m ▄▀ ▄ ▀▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄ ▄▄▄▄▄ ▄▄▄▄▄▄▄ \033[1;31m▄██████▄ ▄██████\033[0m\n"
|
||||
printf "\033[1;37m █ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██ \033[0m\n"
|
||||
printf "\033[1;37m█ █ █ █ ▀▄▄▄▄ █ █ █ █ ▄▄▄▀ █ \033[1;31m██ ██ ▀█████▄\033[0m\n"
|
||||
printf "\033[1;37m█ █ █ █ █ █ █ █ █ ▀▄ █ \033[1;31m██ ██ ██\033[0m\n"
|
||||
printf "\033[1;37m █ █ █ █ ▄▄▄▄▄▀ █ █ █ █ ▀▄ █ \033[1;31m▀██████▀ ██████▀\033[0m\n"
|
||||
printf "\033[1;37m █ █\033[0m\n"
|
||||
printf "\033[1;37m ▀▀▄▄▄▀▀ $version_display\033[0m\n\n"
|
||||
|
||||
uptime_str=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}' | sed 's/^ *//')
|
||||
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$RAM_GB" != "unknown" ]; then
|
||||
memory_used=$(free -m | awk 'NR==2{printf "%.0fMB", $3}')
|
||||
memory_display="$memory_used / ${RAM_GB}GB"
|
||||
else
|
||||
memory_display=$(free -m | awk 'NR==2{printf "%.0fMB / %.0fMB", $3, $2}')
|
||||
fi
|
||||
|
||||
root_usage=$(df -h / | awk 'NR==2{printf "%s (%s free)", $5, $4}')
|
||||
|
||||
if [ -d "/media/startos/data/package-data" ]; then
|
||||
data_usage=$(df -h /media/startos/data/package-data | awk 'NR==2{printf "%s (%s free)", $5, $4}')
|
||||
else
|
||||
data_usage="N/A"
|
||||
fi
|
||||
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
|
||||
services_text="$RUNNING_SERVICES/$TOTAL_SERVICES running"
|
||||
else
|
||||
services_text="Unknown"
|
||||
fi
|
||||
|
||||
local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
|
||||
if [ -z "$local_ip" ]; then local_ip="N/A"; fi
|
||||
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$WAN_IP" != "unknown" ]; then
|
||||
wan_ip="$WAN_IP"
|
||||
else
|
||||
wan_ip="N/A"
|
||||
fi
|
||||
|
||||
printf " \033[1;37m┌─ SYSTEM STATUS ───────────────────────────────────────────────────┐\033[0m\n"
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Uptime:" "$uptime_str" "Memory:" "$memory_display"
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Root:" "$root_usage" "Data:" "$data_usage"
|
||||
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ]; then
|
||||
if [ "$RUNNING_SERVICES" -eq "$TOTAL_SERVICES" ] && [ "$TOTAL_SERVICES" -gt 0 ]; then
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;32m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||
elif [ "$RUNNING_SERVICES" -gt 0 ]; then
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||
else
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;31m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||
fi
|
||||
else
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;37m%-22s\033[0m %-8s \033[0;33m%-23s\033[0m \033[1;37m│\033[0m\n" "Services:" "$services_text" "WAN:" "$wan_ip"
|
||||
fi
|
||||
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "true" ]; then
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;32m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Synced"
|
||||
elif [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$NTP_SYNCED" = "false" ]; then
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;31m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Not Synced"
|
||||
else
|
||||
printf " \033[1;37m│\033[0m %-8s \033[0;33m%-22s\033[0m %-8s \033[0;37m%-23s\033[0m \033[1;37m│\033[0m\n" "Local:" "$local_ip" "NTP:" "Unknown"
|
||||
fi
|
||||
|
||||
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m"
|
||||
|
||||
if [ "$DB_INFO_AVAILABLE" -eq 1 ] && [ "$HOSTNAME" != "unknown" ]; then
|
||||
web_url="https://$HOSTNAME.local"
|
||||
else
|
||||
web_url="https://$(hostname).local"
|
||||
fi
|
||||
printf "\n \033[1;37m┌──────────────────────────────────────────────────── QUICK ACCESS ─┐\033[0m\n"
|
||||
printf " \033[1;37m│\033[0m Web Interface: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "$web_url"
|
||||
printf " \033[1;37m│\033[0m Documentation: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://staging.docs.start9.com"
|
||||
printf " \033[1;37m│\033[0m Support: \033[0;36m%-50s\033[0m \033[1;37m│\033[0m\n" "https://start9.com/contact"
|
||||
printf " \033[1;37m└───────────────────────────────────────────────────────────────────┘\033[0m\n\n"
|
||||
|
||||
@@ -4,6 +4,3 @@ set -e
|
||||
|
||||
curl -fsSL https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor -o- > /usr/share/keyrings/tor-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org bullseye main" > /etc/apt/sources.list.d/tor.list
|
||||
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o- > /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" > /etc/apt/sources.list.d/docker.list
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
if cat /sys/class/drm/*/status | grep -qw connected; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,46 +1,107 @@
|
||||
#!/bin/bash
|
||||
|
||||
SOURCE_DIR="$(dirname $(realpath "${BASH_SOURCE[0]}"))"
|
||||
|
||||
if [ "$UID" -ne 0 ]; then
|
||||
>&2 echo 'Must be run as root'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--no-sync)
|
||||
NO_SYNC=1
|
||||
shift
|
||||
;;
|
||||
--create)
|
||||
ONLY_CREATE=1
|
||||
shift
|
||||
;;
|
||||
-*|--*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
if [ -z "$NO_SYNC" ]; then
|
||||
echo 'Syncing...'
|
||||
rsync -a --delete --force --info=progress2 /media/embassy/embassyfs/current/ /media/embassy/next
|
||||
umount -R /media/startos/next 2> /dev/null
|
||||
umount -R /media/startos/upper 2> /dev/null
|
||||
rm -rf /media/startos/upper /media/startos/next
|
||||
mkdir /media/startos/upper
|
||||
mount -t tmpfs tmpfs /media/startos/upper
|
||||
mkdir -p /media/startos/upper/data /media/startos/upper/work /media/startos/next
|
||||
mount -t overlay \
|
||||
-olowerdir=/media/startos/current,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
|
||||
overlay /media/startos/next
|
||||
mkdir -p /media/startos/next/media/startos/root
|
||||
mount --bind /media/startos/root /media/startos/next/media/startos/root
|
||||
fi
|
||||
|
||||
mkdir -p /media/embassy/next/run
|
||||
mkdir -p /media/embassy/next/dev
|
||||
mkdir -p /media/embassy/next/sys
|
||||
mkdir -p /media/embassy/next/proc
|
||||
mkdir -p /media/embassy/next/boot
|
||||
mount --bind /run /media/embassy/next/run
|
||||
mount --bind /dev /media/embassy/next/dev
|
||||
mount --bind /sys /media/embassy/next/sys
|
||||
mount --bind /proc /media/embassy/next/proc
|
||||
mount --bind /boot /media/embassy/next/boot
|
||||
if [ -n "$ONLY_CREATE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p /media/startos/next/run
|
||||
mkdir -p /media/startos/next/dev
|
||||
mkdir -p /media/startos/next/sys
|
||||
mkdir -p /media/startos/next/proc
|
||||
mkdir -p /media/startos/next/boot
|
||||
mount --bind /run /media/startos/next/run
|
||||
mount --bind /tmp /media/startos/next/tmp
|
||||
mount --bind /dev /media/startos/next/dev
|
||||
mount --bind /sys /media/startos/next/sys
|
||||
mount --bind /proc /media/startos/next/proc
|
||||
mount --bind /boot /media/startos/next/boot
|
||||
|
||||
if [ -z "$*" ]; then
|
||||
chroot /media/embassy/next
|
||||
chroot /media/startos/next
|
||||
CHROOT_RES=$?
|
||||
else
|
||||
chroot /media/embassy/next "$SHELL" -c "$*"
|
||||
chroot /media/startos/next "$SHELL" -c "$*"
|
||||
CHROOT_RES=$?
|
||||
fi
|
||||
|
||||
umount /media/embassy/next/run
|
||||
umount /media/embassy/next/dev
|
||||
umount /media/embassy/next/sys
|
||||
umount /media/embassy/next/proc
|
||||
umount /media/embassy/next/boot
|
||||
umount /media/startos/next/run
|
||||
umount /media/startos/next/tmp
|
||||
umount /media/startos/next/dev
|
||||
umount /media/startos/next/sys
|
||||
umount /media/startos/next/proc
|
||||
umount /media/startos/next/boot
|
||||
umount /media/startos/next/media/startos/root
|
||||
|
||||
if [ "$CHROOT_RES" -eq 0 ]; then
|
||||
|
||||
if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then
|
||||
${SOURCE_DIR}/prune-images $(du -s --bytes /media/startos/next | awk '{print $1}')
|
||||
fi
|
||||
|
||||
echo 'Upgrading...'
|
||||
|
||||
touch /media/embassy/config/upgrade
|
||||
if ! time mksquashfs /media/startos/next /media/startos/images/next.squashfs -b 4096 -comp gzip; then
|
||||
umount -R /media/startos/next
|
||||
umount -R /media/startos/upper
|
||||
rm -rf /media/startos/upper /media/startos/next
|
||||
exit 1
|
||||
fi
|
||||
hash=$(b3sum /media/startos/images/next.squashfs | head -c 32)
|
||||
mv /media/startos/images/next.squashfs /media/startos/images/${hash}.rootfs
|
||||
ln -rsf /media/startos/images/${hash}.rootfs /media/startos/config/current.rootfs
|
||||
|
||||
sync
|
||||
|
||||
reboot
|
||||
fi
|
||||
fi
|
||||
|
||||
umount -R /media/startos/next
|
||||
umount -R /media/startos/upper
|
||||
rm -rf /media/startos/upper /media/startos/next
|
||||
@@ -1 +0,0 @@
|
||||
start-cli net dhcp update $interface
|
||||
@@ -1,98 +0,0 @@
|
||||
# Local filesystem mounting -*- shell-script -*-
|
||||
|
||||
#
|
||||
# This script overrides local_mount_root() in /scripts/local
|
||||
# and mounts root as a read-only filesystem with a temporary (rw)
|
||||
# overlay filesystem.
|
||||
#
|
||||
|
||||
. /scripts/local
|
||||
|
||||
local_mount_root()
|
||||
{
|
||||
echo 'using embassy initramfs module'
|
||||
|
||||
local_top
|
||||
local_device_setup "${ROOT}" "root file system"
|
||||
ROOT="${DEV}"
|
||||
|
||||
# Get the root filesystem type if not set
|
||||
if [ -z "${ROOTFSTYPE}" ]; then
|
||||
FSTYPE=$(get_fstype "${ROOT}")
|
||||
else
|
||||
FSTYPE=${ROOTFSTYPE}
|
||||
fi
|
||||
|
||||
local_premount
|
||||
|
||||
# CHANGES TO THE ORIGINAL FUNCTION BEGIN HERE
|
||||
# N.B. this code still lacks error checking
|
||||
|
||||
modprobe ${FSTYPE}
|
||||
checkfs ${ROOT} root "${FSTYPE}"
|
||||
|
||||
ROOTFLAGS="$(echo "${ROOTFLAGS}" | sed 's/subvol=\(next\|current\)//' | sed 's/^-o *$//')"
|
||||
|
||||
if [ "${FSTYPE}" != "unknown" ]; then
|
||||
mount -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} ${rootmnt}
|
||||
else
|
||||
mount ${ROOTFLAGS} ${ROOT} ${rootmnt}
|
||||
fi
|
||||
|
||||
echo 'mounting embassyfs'
|
||||
|
||||
mkdir /embassyfs
|
||||
|
||||
mount --move ${rootmnt} /embassyfs
|
||||
|
||||
if ! [ -d /embassyfs/current ] && [ -d /embassyfs/prev ]; then
|
||||
mv /embassyfs/prev /embassyfs/current
|
||||
fi
|
||||
|
||||
if ! [ -d /embassyfs/current ]; then
|
||||
mkdir /embassyfs/current
|
||||
for FILE in $(ls /embassyfs); do
|
||||
if [ "$FILE" != current ]; then
|
||||
mv /embassyfs/$FILE /embassyfs/current/
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
mkdir -p /embassyfs/config
|
||||
|
||||
if [ -f /embassyfs/config/upgrade ] && [ -d /embassyfs/next ]; then
|
||||
mv /embassyfs/current /embassyfs/prev
|
||||
mv /embassyfs/next /embassyfs/current
|
||||
rm /embassyfs/config/upgrade
|
||||
fi
|
||||
|
||||
if ! [ -d /embassyfs/next ]; then
|
||||
if [ -d /embassyfs/prev ]; then
|
||||
mv /embassyfs/prev /embassyfs/next
|
||||
else
|
||||
mkdir /embassyfs/next
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir /lower /upper
|
||||
|
||||
mount -r --bind /embassyfs/current /lower
|
||||
|
||||
modprobe overlay || insmod "/lower/lib/modules/$(uname -r)/kernel/fs/overlayfs/overlay.ko"
|
||||
|
||||
# Mount a tmpfs for the overlay in /upper
|
||||
mount -t tmpfs tmpfs /upper
|
||||
mkdir /upper/data /upper/work
|
||||
|
||||
# Mount the final overlay-root in $rootmnt
|
||||
mount -t overlay \
|
||||
-olowerdir=/lower,upperdir=/upper/data,workdir=/upper/work \
|
||||
overlay ${rootmnt}
|
||||
|
||||
mkdir -p ${rootmnt}/media/embassy/config
|
||||
mount --bind /embassyfs/config ${rootmnt}/media/embassy/config
|
||||
mkdir -p ${rootmnt}/media/embassy/next
|
||||
mount --bind /embassyfs/next ${rootmnt}/media/embassy/next
|
||||
mkdir -p ${rootmnt}/media/embassy/embassyfs
|
||||
mount -r --bind /embassyfs ${rootmnt}/media/embassy/embassyfs
|
||||
}
|
||||
@@ -4,7 +4,7 @@ set -e
|
||||
|
||||
# install dependencies
|
||||
/usr/bin/apt update
|
||||
/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools
|
||||
/usr/bin/apt install --no-install-recommends -y xserver-xorg x11-xserver-utils xinit firefox-esr matchbox-window-manager libnss3-tools p11-kit-modules
|
||||
|
||||
#Change a default preference set by stock debian firefox-esr
|
||||
sed -i 's|^pref("extensions.update.enabled", true);$|pref("extensions.update.enabled", false);|' /etc/firefox-esr/firefox-esr.js
|
||||
@@ -14,14 +14,8 @@ if ! id kiosk; then
|
||||
useradd -s /bin/bash --create-home kiosk
|
||||
fi
|
||||
|
||||
# create kiosk script
|
||||
cat > /home/kiosk/kiosk.sh << 'EOF'
|
||||
#!/bin/sh
|
||||
PROFILE=$(mktemp -d)
|
||||
if [ -f /usr/local/share/ca-certificates/startos-root-ca.crt ]; then
|
||||
certutil -A -n "StartOS Local Root CA" -t "TCu,Cuw,Tuw" -i /usr/local/share/ca-certificates/startos-root-ca.crt -d $PROFILE
|
||||
fi
|
||||
cat >> $PROFILE/prefs.js << EOT
|
||||
mkdir /home/kiosk/fx-profile
|
||||
cat >> /home/kiosk/fx-profile/prefs.js << EOF
|
||||
user_pref("app.normandy.api_url", "");
|
||||
user_pref("app.normandy.enabled", false);
|
||||
user_pref("app.shield.optoutstudies.enabled", false);
|
||||
@@ -33,7 +27,6 @@ user_pref("browser.crashReports.unsubmittedCheck.autoSubmit2", false);
|
||||
user_pref("browser.newtabpage.activity-stream.feeds.asrouterfeed", false);
|
||||
user_pref("browser.newtabpage.activity-stream.feeds.topsites", false);
|
||||
user_pref("browser.newtabpage.activity-stream.showSponsoredTopSites", false);
|
||||
user_pref("browser.onboarding.enabled", false);
|
||||
user_pref("browser.ping-centre.telemetry", false);
|
||||
user_pref("browser.pocket.enabled", false);
|
||||
user_pref("browser.safebrowsing.blockedURIs.enabled", false);
|
||||
@@ -49,7 +42,7 @@ user_pref("browser.startup.homepage_override.mstone", "ignore");
|
||||
user_pref("browser.theme.content-theme", 0);
|
||||
user_pref("browser.theme.toolbar-theme", 0);
|
||||
user_pref("browser.urlbar.groupLabels.enabled", false);
|
||||
user_pref("browser.urlbar.suggest.searches" false);
|
||||
user_pref("browser.urlbar.suggest.searches", false);
|
||||
user_pref("datareporting.policy.firstRunURL", "");
|
||||
user_pref("datareporting.healthreport.service.enabled", false);
|
||||
user_pref("datareporting.healthreport.uploadEnabled", false);
|
||||
@@ -58,10 +51,9 @@ user_pref("dom.securecontext.allowlist_onions", true);
|
||||
user_pref("dom.securecontext.whitelist_onions", true);
|
||||
user_pref("experiments.enabled", false);
|
||||
user_pref("experiments.activeExperiment", false);
|
||||
user_pref("experiments.supported", false);
|
||||
user_pref("extensions.activeThemeID", "firefox-compact-dark@mozilla.org");
|
||||
user_pref("extensions.blocklist.enabled", false);
|
||||
user_pref("extensions.getAddons.cache.enabled", false);
|
||||
user_pref("extensions.htmlaboutaddons.recommendations.enabled", false);
|
||||
user_pref("extensions.pocket.enabled", false);
|
||||
user_pref("extensions.update.enabled", false);
|
||||
user_pref("extensions.shield-recipe-client.enabled", false);
|
||||
@@ -72,9 +64,15 @@ user_pref("messaging-system.rsexperimentloader.enabled", false);
|
||||
user_pref("network.allow-experiments", false);
|
||||
user_pref("network.captive-portal-service.enabled", false);
|
||||
user_pref("network.connectivity-service.enabled", false);
|
||||
user_pref("network.proxy.autoconfig_url", "file:///usr/lib/startos/proxy.pac");
|
||||
user_pref("network.proxy.socks", "10.0.3.1");
|
||||
user_pref("network.proxy.socks_port", 9050);
|
||||
user_pref("network.proxy.socks_version", 5);
|
||||
user_pref("network.proxy.socks_remote_dns", true);
|
||||
user_pref("network.proxy.type", 2);
|
||||
user_pref("network.proxy.type", 1);
|
||||
user_pref("privacy.resistFingerprinting", true);
|
||||
//Enable letterboxing if we want the window size sent to the server to snap to common resolutions:
|
||||
//user_pref("privacy.resistFingerprinting.letterboxing", true);
|
||||
user_pref("privacy.trackingprotection.enabled", true);
|
||||
user_pref("signon.rememberSignons", false);
|
||||
user_pref("toolkit.telemetry.archive.enabled", false);
|
||||
user_pref("toolkit.telemetry.bhrPing.enabled", false);
|
||||
@@ -87,22 +85,31 @@ user_pref("toolkit.telemetry.shutdownPingSender.enabled", false);
|
||||
user_pref("toolkit.telemetry.unified", false);
|
||||
user_pref("toolkit.telemetry.updatePing.enabled", false);
|
||||
user_pref("toolkit.telemetry.cachedClientID", "");
|
||||
EOT
|
||||
//Blocking automatic Mozilla CDN server requests
|
||||
user_pref("extensions.getAddons.showPane", false);
|
||||
user_pref("extensions.getAddons.cache.enabled", false);
|
||||
//user_pref("services.settings.server", ""); // Remote settings server (HSTS preload updates and Cerfiticate Revocation Lists are fetched)
|
||||
user_pref("browser.aboutHomeSnippets.updateUrl", "");
|
||||
user_pref("browser.newtabpage.activity-stream.feeds.snippets", false);
|
||||
user_pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
|
||||
user_pref("browser.newtabpage.activity-stream.feeds.system.topstories", false);
|
||||
user_pref("browser.newtabpage.activity-stream.feeds.discoverystreamfeed", false);
|
||||
user_pref("browser.safebrowsing.provider.mozilla.updateURL", "");
|
||||
user_pref("browser.safebrowsing.provider.mozilla.gethashURL", "");
|
||||
EOF
|
||||
|
||||
ln -sf /usr/lib/$(uname -m)-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox-esr/libnssckbi.so
|
||||
|
||||
# create kiosk script
|
||||
cat > /home/kiosk/kiosk.sh << 'EOF'
|
||||
#!/bin/sh
|
||||
while ! curl "http://localhost" > /dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
while ! /usr/lib/startos/scripts/check-monitor; do
|
||||
sleep 15
|
||||
done
|
||||
(
|
||||
while /usr/lib/startos/scripts/check-monitor; do
|
||||
sleep 15
|
||||
done
|
||||
killall firefox-esr
|
||||
) &
|
||||
matchbox-window-manager -use_titlebar no &
|
||||
firefox-esr http://localhost --profile $PROFILE
|
||||
rm -rf $PROFILE
|
||||
cp -r /home/kiosk/fx-profile /home/kiosk/fx-profile-tmp
|
||||
firefox-esr http://localhost --profile /home/kiosk/fx-profile-tmp
|
||||
rm -rf /home/kiosk/fx-profile-tmp
|
||||
EOF
|
||||
chmod +x /home/kiosk/kiosk.sh
|
||||
|
||||
@@ -116,6 +123,8 @@ fi
|
||||
EOF
|
||||
fi
|
||||
|
||||
chown -R kiosk:kiosk /home/kiosk
|
||||
|
||||
# enable autologin
|
||||
mkdir -p /etc/systemd/system/getty@tty1.service.d
|
||||
cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << 'EOF'
|
||||
|
||||
38
build/lib/scripts/forward-port
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$sip" ] || [ -z "$dip" ] || [ -z "$sport" ] || [ -z "$dport" ]; then
|
||||
>&2 echo 'missing required env var'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Helper function to check if a rule exists
|
||||
nat_rule_exists() {
|
||||
iptables -t nat -C "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
# Helper function to add or delete a rule idempotently
|
||||
# Usage: apply_rule [add|del] <iptables args...>
|
||||
apply_nat_rule() {
|
||||
local action="$1"
|
||||
shift
|
||||
|
||||
if [ "$action" = "add" ]; then
|
||||
# Only add if rule doesn't exist
|
||||
if ! rule_exists "$@"; then
|
||||
iptables -t nat -A "$@"
|
||||
fi
|
||||
elif [ "$action" = "del" ]; then
|
||||
if rule_exists "$@"; then
|
||||
iptables -t nat -D "$@"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$UNDO" = 1 ]; then
|
||||
action="del"
|
||||
else
|
||||
action="add"
|
||||
fi
|
||||
|
||||
apply_nat_rule "$action" PREROUTING -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport
|
||||
apply_nat_rule "$action" OUTPUT -p tcp -d $sip --dport $sport -j DNAT --to-destination $dip:$dport
|
||||
105
build/lib/scripts/gather-debug-info
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Define the output file
|
||||
OUTPUT_FILE="system_debug_info.txt"
|
||||
|
||||
# Check if the script is run as root, if not, restart with sudo
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
exec sudo bash "$0" "$@"
|
||||
fi
|
||||
|
||||
# Create or clear the output file and add a header
|
||||
echo "===================================================================" > "$OUTPUT_FILE"
|
||||
echo " StartOS System Debug Information " >> "$OUTPUT_FILE"
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
echo "Generated on: $(date)" >> "$OUTPUT_FILE"
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to run a command if it exists and append its output to the file with headers
|
||||
run_command() {
|
||||
local CMD="$1"
|
||||
local DESC="$2"
|
||||
local CMD_NAME="${CMD%% *}" # Extract the command name (first word)
|
||||
|
||||
if command_exists "$CMD_NAME"; then
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
echo "COMMAND: $CMD" >> "$OUTPUT_FILE"
|
||||
echo "DESCRIPTION: $DESC" >> "$OUTPUT_FILE"
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
eval "$CMD" >> "$OUTPUT_FILE" 2>&1
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
else
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
echo "COMMAND: $CMD" >> "$OUTPUT_FILE"
|
||||
echo "DESCRIPTION: $DESC" >> "$OUTPUT_FILE"
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
echo "SKIPPED: Command not found" >> "$OUTPUT_FILE"
|
||||
echo "" >> "$OUTPUT_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Collecting basic system information
|
||||
run_command "start-cli --version; start-cli git-info" "StartOS CLI version and Git information"
|
||||
run_command "hostname" "Hostname of the system"
|
||||
run_command "uname -a" "Kernel version and system architecture"
|
||||
|
||||
# Services Info
|
||||
run_command "start-cli lxc stats" "All Running Services"
|
||||
|
||||
# Collecting CPU information
|
||||
run_command "lscpu" "CPU architecture information"
|
||||
run_command "cat /proc/cpuinfo" "Detailed CPU information"
|
||||
|
||||
# Collecting memory information
|
||||
run_command "free -h" "Available and used memory"
|
||||
run_command "cat /proc/meminfo" "Detailed memory information"
|
||||
|
||||
# Collecting storage information
|
||||
run_command "lsblk" "List of block devices"
|
||||
run_command "df -h" "Disk space usage"
|
||||
run_command "fdisk -l" "Detailed disk partition information"
|
||||
|
||||
# Collecting network information
|
||||
run_command "ip a" "Network interfaces and IP addresses"
|
||||
run_command "ip route" "Routing table"
|
||||
run_command "netstat -i" "Network interface statistics"
|
||||
|
||||
# Collecting RAID information (if applicable)
|
||||
run_command "cat /proc/mdstat" "List of RAID devices (if applicable)"
|
||||
|
||||
# Collecting virtualization information
|
||||
run_command "egrep -c '(vmx|svm)' /proc/cpuinfo" "Check if CPU supports virtualization"
|
||||
run_command "systemd-detect-virt" "Check if the system is running inside a virtual machine"
|
||||
|
||||
# Final message
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
echo " End of StartOS System Debug Information " >> "$OUTPUT_FILE"
|
||||
echo "===================================================================" >> "$OUTPUT_FILE"
|
||||
|
||||
# Prompt user to send the log file to a Start9 Technician
|
||||
echo "System debug information has been collected in $OUTPUT_FILE."
|
||||
echo ""
|
||||
echo "Would you like to send this log file to a Start9 Technician? (yes/no)"
|
||||
read SEND_LOG
|
||||
|
||||
if [[ "$SEND_LOG" == "yes" || "$SEND_LOG" == "y" ]]; then
|
||||
if command -v wormhole >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "==================================================================="
|
||||
echo " Running wormhole to send the file. Please follow the "
|
||||
echo " instructions and provide the code to the Start9 support team. "
|
||||
echo "==================================================================="
|
||||
wormhole send "$OUTPUT_FILE"
|
||||
echo "==================================================================="
|
||||
else
|
||||
echo "Error: wormhole command not found."
|
||||
fi
|
||||
else
|
||||
echo "Log file not sent. You can manually share $OUTPUT_FILE with the Start9 support team if needed."
|
||||
fi
|
||||
@@ -3,8 +3,8 @@
|
||||
ARGS=
|
||||
|
||||
for ARG in $@; do
|
||||
if [ -d "/media/embassy/embassyfs" ] && [ "$ARG" = "/" ]; then
|
||||
ARG=/media/embassy/embassyfs
|
||||
if [ -d "/media/startos/root" ] && [ "$ARG" = "/" ]; then
|
||||
ARG=/media/startos/root
|
||||
fi
|
||||
ARGS="$ARGS $ARG"
|
||||
done
|
||||
|
||||
35
build/lib/scripts/prune-boot
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$UID" -ne 0 ]; then
|
||||
>&2 echo 'Must be run as root'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current kernel version
|
||||
current_kernel=$(uname -r)
|
||||
|
||||
echo "Current kernel: $current_kernel"
|
||||
echo "Searching for old kernel files in /boot..."
|
||||
|
||||
# Extract base kernel version (without possible suffixes)
|
||||
current_base=$(echo "$current_kernel" | sed 's/-.*//')
|
||||
|
||||
cd /boot || { echo "/boot directory not found!"; exit 1; }
|
||||
|
||||
for file in vmlinuz-* initrd.img-* System.map-* config-*; do
|
||||
# Extract version from filename
|
||||
version=$(echo "$file" | sed -E 's/^[^0-9]*([0-9][^ ]*).*/\1/')
|
||||
# Skip if file matches current kernel version
|
||||
if [[ "$file" == *"$current_kernel"* ]]; then
|
||||
continue
|
||||
fi
|
||||
# Compare versions, delete if less than current
|
||||
if dpkg --compare-versions "$version" lt "$current_kernel"; then
|
||||
echo "Deleting $file (version $version is older than $current_kernel)"
|
||||
sudo rm -f "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Old kernel files deleted."
|
||||
50
build/lib/scripts/prune-images
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$UID" -ne 0 ]; then
|
||||
>&2 echo 'Must be run as root'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-*|--*)
|
||||
echo "Unknown option $1"
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") # save positional arg
|
||||
shift # past argument
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
needed=$1
|
||||
|
||||
if [ -z "$needed" ]; then
|
||||
>&2 echo "usage: $0 <SPACE NEEDED>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -h /media/startos/config/current.rootfs ] && [ -e /media/startos/config/current.rootfs ]; then
|
||||
echo 'Pruning...'
|
||||
current="$(readlink -f /media/startos/config/current.rootfs)"
|
||||
while [[ "$(df -B1 --output=avail --sync /media/startos/images | tail -n1)" -lt "$needed" ]]; do
|
||||
to_prune="$(ls -t1 /media/startos/images/*.rootfs /media/startos/images/*.squashfs 2> /dev/null | grep -v "$current" | tail -n1)"
|
||||
if [ -e "$to_prune" ]; then
|
||||
echo " Pruning $to_prune"
|
||||
rm -rf "$to_prune"
|
||||
sync
|
||||
else
|
||||
>&2 echo "Not enough space and nothing to prune!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo 'done.'
|
||||
else
|
||||
>&2 echo 'No current.rootfs, not safe to prune'
|
||||
exit 1
|
||||
fi
|
||||
115
build/lib/scripts/startos-initramfs-module
Executable file
@@ -0,0 +1,115 @@
|
||||
# Local filesystem mounting -*- shell-script -*-
|
||||
|
||||
#
|
||||
# This script overrides local_mount_root() in /scripts/local
|
||||
# and mounts root as a read-only filesystem with a temporary (rw)
|
||||
# overlay filesystem.
|
||||
#
|
||||
|
||||
. /scripts/local
|
||||
|
||||
local_mount_root()
|
||||
{
|
||||
echo 'using startos initramfs module'
|
||||
|
||||
local_top
|
||||
local_device_setup "${ROOT}" "root file system"
|
||||
ROOT="${DEV}"
|
||||
|
||||
# Get the root filesystem type if not set
|
||||
if [ -z "${ROOTFSTYPE}" ]; then
|
||||
FSTYPE=$(get_fstype "${ROOT}")
|
||||
else
|
||||
FSTYPE=${ROOTFSTYPE}
|
||||
fi
|
||||
|
||||
local_premount
|
||||
|
||||
# CHANGES TO THE ORIGINAL FUNCTION BEGIN HERE
|
||||
# N.B. this code still lacks error checking
|
||||
|
||||
modprobe ${FSTYPE}
|
||||
checkfs ${ROOT} root "${FSTYPE}"
|
||||
|
||||
echo 'mounting startos'
|
||||
mkdir /startos
|
||||
|
||||
ROOTFLAGS="$(echo "${ROOTFLAGS}" | sed 's/subvol=\(next\|current\)//' | sed 's/^-o *$//')"
|
||||
|
||||
if [ "${FSTYPE}" != "unknown" ]; then
|
||||
mount -t ${FSTYPE} ${ROOTFLAGS} ${ROOT} /startos
|
||||
else
|
||||
mount ${ROOTFLAGS} ${ROOT} /startos
|
||||
fi
|
||||
|
||||
if [ -d /startos/images ]; then
|
||||
if [ -h /startos/config/current.rootfs ] && [ -e /startos/config/current.rootfs ]; then
|
||||
image=$(readlink -f /startos/config/current.rootfs)
|
||||
else
|
||||
image="$(ls -t1 /startos/images/*.rootfs | head -n1)"
|
||||
fi
|
||||
if ! [ -f "$image" ]; then
|
||||
>&2 echo "image $image not available to boot"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ -f /startos/config/upgrade ] && [ -d /startos/next ]; then
|
||||
oldroot=/startos/next
|
||||
elif [ -d /startos/current ]; then
|
||||
oldroot=/startos/current
|
||||
elif [ -d /startos/prev ]; then
|
||||
oldroot=/startos/prev
|
||||
else
|
||||
>&2 echo no StartOS filesystem found
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p /startos/config/overlay/etc
|
||||
mv $oldroot/etc/fstab /startos/config/overlay/etc/fstab
|
||||
mv $oldroot/etc/machine-id /startos/config/overlay/etc/machine-id
|
||||
mv $oldroot/etc/ssh /startos/config/overlay/etc/ssh
|
||||
|
||||
mkdir -p /startos/images
|
||||
mv $oldroot /startos/images/legacy.rootfs
|
||||
|
||||
rm -rf /startos/next /startos/current /startos/prev
|
||||
|
||||
ln -rsf /startos/images/old.squashfs /startos/config/current.rootfs
|
||||
image=$(readlink -f /startos/config/current.rootfs)
|
||||
fi
|
||||
|
||||
mkdir /lower /upper
|
||||
|
||||
if [ -d "$image" ]; then
|
||||
mount -r --bind $image /lower
|
||||
elif [ -f "$image" ]; then
|
||||
modprobe loop
|
||||
modprobe squashfs
|
||||
mount -r $image /lower
|
||||
else
|
||||
>&2 echo "not a regular file or directory: $image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
modprobe overlay || insmod "/lower/lib/modules/$(uname -r)/kernel/fs/overlayfs/overlay.ko"
|
||||
|
||||
# Mount a tmpfs for the overlay in /upper
|
||||
mount -t tmpfs tmpfs /upper
|
||||
mkdir /upper/data /upper/work
|
||||
|
||||
mkdir -p /startos/config/overlay
|
||||
|
||||
# Mount the final overlay-root in $rootmnt
|
||||
mount -t overlay \
|
||||
-olowerdir=/startos/config/overlay:/lower,upperdir=/upper/data,workdir=/upper/work \
|
||||
overlay ${rootmnt}
|
||||
|
||||
mkdir -p ${rootmnt}/media/startos/config
|
||||
mount --bind /startos/config ${rootmnt}/media/startos/config
|
||||
mkdir -p ${rootmnt}/media/startos/images
|
||||
mount --bind /startos/images ${rootmnt}/media/startos/images
|
||||
mkdir -p ${rootmnt}/media/startos/root
|
||||
mount -r --bind /startos ${rootmnt}/media/startos/root
|
||||
mkdir -p ${rootmnt}/media/startos/current
|
||||
mount -r --bind /lower ${rootmnt}/media/startos/current
|
||||
}
|
||||
61
build/lib/scripts/use-img
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$UID" -ne 0 ]; then
|
||||
>&2 echo 'Must be run as root'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
>&2 echo "usage: $0 <SQUASHFS>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$(unsquashfs -cat $1 /usr/lib/startos/VERSION.txt)
|
||||
GIT_HASH=$(unsquashfs -cat $1 /usr/lib/startos/GIT_HASH.txt)
|
||||
B3SUM=$(b3sum $1 | head -c 32)
|
||||
|
||||
if [ -n "$CHECKSUM" ] && [ "$CHECKSUM" != "$B3SUM" ]; then
|
||||
>&2 echo "CHECKSUM MISMATCH"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mv $1 /media/startos/images/${B3SUM}.rootfs
|
||||
ln -rsf /media/startos/images/${B3SUM}.rootfs /media/startos/config/current.rootfs
|
||||
|
||||
unsquashfs -n -f -d / /media/startos/images/${B3SUM}.rootfs boot
|
||||
|
||||
umount -R /media/startos/next 2> /dev/null || true
|
||||
umount -R /media/startos/lower 2> /dev/null || true
|
||||
umount -R /media/startos/upper 2> /dev/null || true
|
||||
|
||||
rm -rf /media/startos/lower /media/startos/upper /media/startos/next
|
||||
mkdir /media/startos/upper
|
||||
mount -t tmpfs tmpfs /media/startos/upper
|
||||
mkdir -p /media/startos/lower /media/startos/upper/data /media/startos/upper/work /media/startos/next
|
||||
mount /media/startos/images/${B3SUM}.rootfs /media/startos/lower
|
||||
mount -t overlay \
|
||||
-olowerdir=/media/startos/lower,upperdir=/media/startos/upper/data,workdir=/media/startos/upper/work \
|
||||
overlay /media/startos/next
|
||||
mkdir -p /media/startos/next/media/startos/root
|
||||
mount --bind /media/startos/root /media/startos/next/media/startos/root
|
||||
mkdir -p /media/startos/next/dev
|
||||
mkdir -p /media/startos/next/sys
|
||||
mkdir -p /media/startos/next/proc
|
||||
mkdir -p /media/startos/next/boot
|
||||
mount --bind /dev /media/startos/next/dev
|
||||
mount --bind /sys /media/startos/next/sys
|
||||
mount --bind /proc /media/startos/next/proc
|
||||
mount --bind /boot /media/startos/next/boot
|
||||
|
||||
chroot /media/startos/next update-grub2
|
||||
|
||||
umount -R /media/startos/next
|
||||
umount -R /media/startos/upper
|
||||
umount -R /media/startos/lower
|
||||
rm -rf /media/startos/lower /media/startos/upper /media/startos/next
|
||||
|
||||
sync
|
||||
|
||||
reboot
|
||||
555
build/lib/scripts/wireguard-vps-proxy-setup
Executable file
@@ -0,0 +1,555 @@
|
||||
#!/bin/bash
|
||||
|
||||
# =============================================================================
|
||||
# Wireguard VPS Proxy Setup
|
||||
# =============================================================================
|
||||
#
|
||||
# This script automates the setup of a WireGuard VPN server on a remote VPS
|
||||
# for StartOS Clearnet functionality. It handles:
|
||||
#
|
||||
# 1. SSH key-based authentication setup
|
||||
# 2. Root access configuration (if needed)
|
||||
# 3. WireGuard server installation
|
||||
# 4. Configuration file generation and import
|
||||
#
|
||||
# Usage:
|
||||
# wireguard-vps-proxy-setup [-h] [-i IP] [-u USERNAME] [-p PORT] [-k SSH_KEY]
|
||||
#
|
||||
# Options:
|
||||
# -h Show help message
|
||||
# -i VPS IP address
|
||||
# -u SSH username (default: root)
|
||||
# -p SSH port (default: 22)
|
||||
# -k Path to custom SSH private key
|
||||
#
|
||||
# Example:
|
||||
# wireguard-vps-proxy-setup -i 110.18.1.1 -u debian
|
||||
#
|
||||
# Note: This script requires root privileges and will auto-elevate if needed.
|
||||
# =============================================================================
|
||||
|
||||
# Colors for better output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[1;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0;37m' # No Color
|
||||
|
||||
# --- Constants ---
|
||||
readonly WIREGUARD_INSTALL_URL="https://raw.githubusercontent.com/start9labs/wireguard-vps-proxy-setup/master/wireguard-install.sh"
|
||||
readonly SSH_KEY_DIR="/home/start9/.ssh"
|
||||
readonly SSH_KEY_NAME="id_ed25519"
|
||||
readonly SSH_PRIVATE_KEY="$SSH_KEY_DIR/$SSH_KEY_NAME"
|
||||
readonly SSH_PUBLIC_KEY="$SSH_PRIVATE_KEY.pub"
|
||||
|
||||
# Store original arguments
|
||||
SCRIPT_ARGS=("$@")
|
||||
|
||||
# --- Functions ---
|
||||
|
||||
# Function to ensure script runs with root privileges by auto-elevating if needed
|
||||
check_root() {
|
||||
if [[ "$EUID" -ne 0 ]]; then
|
||||
exec sudo "$0" "${SCRIPT_ARGS[@]}"
|
||||
fi
|
||||
sudo chown -R start9:startos "$SSH_KEY_DIR"
|
||||
}
|
||||
|
||||
# Function to print banner
|
||||
print_banner() {
|
||||
echo -e "${BLUE}"
|
||||
echo "================================================"
|
||||
echo -e " ${NC}Wireguard VPS Proxy Setup${BLUE} "
|
||||
echo "================================================"
|
||||
echo -e "${NC}"
|
||||
}
|
||||
|
||||
# Function to print usage
|
||||
print_usage() {
|
||||
echo -e "Usage: $0 [-h] [-i IP] [-u USERNAME] [-p PORT] [-k SSH_KEY]"
|
||||
echo "Options:"
|
||||
echo " -h Show this help message"
|
||||
echo " -i VPS IP address"
|
||||
echo " -u SSH username (default: root)"
|
||||
echo " -p SSH port (default: 22)"
|
||||
echo " -k Path to the custom SSH private key (optional)"
|
||||
echo " If no key is provided, the default key '$SSH_PRIVATE_KEY' will be used."
|
||||
}
|
||||
|
||||
# Function to display end message
|
||||
display_end_message() {
|
||||
echo -e "\n${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "${GREEN}Wireguard VPS Proxy server setup complete!${NC}"
|
||||
echo -e "${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "\n${GREEN}Clearnet functionality has been enabled via VPS (${VPS_IP})${NC}"
|
||||
echo -e "\n${YELLOW}Next steps:${NC}"
|
||||
echo -e "Visit https://docs.start9.com to complete the Clearnet setup"
|
||||
echo -e "\n${BLUE}------------------------------------------------------------------${NC}"
|
||||
}
|
||||
|
||||
# Function to validate IP address
|
||||
validate_ip() {
|
||||
local ip=$1
|
||||
# IPv4 validation
|
||||
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
# Additional IPv4 validation to ensure each octet is <= 255
|
||||
local IFS='.'
|
||||
read -ra ADDR <<< "$ip"
|
||||
for i in "${ADDR[@]}"; do
|
||||
if [ "$i" -gt 255 ]; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
# IPv6 validation
|
||||
elif [[ $ip =~ ^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}$ ]] || \
|
||||
[[ $ip =~ ^::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^[0-9a-fA-F]{1,4}::([0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,3}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,2}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,1}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,7}:$ ]] || \
|
||||
[[ $ip =~ ^::([0-9a-fA-F]{1,4}:){0,7}[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^[0-9a-fA-F]{1,4}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,6}(:[0-9a-fA-F]{1,4}){1,1}$ ]] || \
|
||||
[[ $ip =~ ^([0-9a-fA-F]{1,4}:){1,7}:$ ]] || \
|
||||
[[ $ip =~ ^::$ ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function for configuring SSH key authentication on remote server
|
||||
configure_ssh_key_auth() {
|
||||
echo -e "${BLUE}Configuring SSH key authentication on remote server...${NC}"
|
||||
|
||||
ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" '
|
||||
# Check if PubkeyAuthentication is commented out
|
||||
if grep -q "^#PubkeyAuthentication" /etc/ssh/sshd_config; then
|
||||
sed -i "s/^#PubkeyAuthentication.*/PubkeyAuthentication yes/" /etc/ssh/sshd_config
|
||||
# Check if PubkeyAuthentication exists but is not enabled
|
||||
elif grep -q "^PubkeyAuthentication" /etc/ssh/sshd_config; then
|
||||
sed -i "s/^PubkeyAuthentication.*/PubkeyAuthentication yes/" /etc/ssh/sshd_config
|
||||
# Add PubkeyAuthentication if it doesnt exist
|
||||
else
|
||||
echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
|
||||
fi
|
||||
|
||||
# Enable root login
|
||||
if grep -q "^#PermitRootLogin" /etc/ssh/sshd_config; then
|
||||
sed -i "s/^#PermitRootLogin.*/PermitRootLogin yes/" /etc/ssh/sshd_config
|
||||
elif grep -q "^PermitRootLogin" /etc/ssh/sshd_config; then
|
||||
sed -i "s/^PermitRootLogin.*/PermitRootLogin yes/" /etc/ssh/sshd_config
|
||||
else
|
||||
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
||||
fi
|
||||
|
||||
# Configure AuthorizedKeysFile if needed
|
||||
if grep -q "^#AuthorizedKeysFile" /etc/ssh/sshd_config; then
|
||||
sed -i "s/^#AuthorizedKeysFile.*/AuthorizedKeysFile .ssh\/authorized_keys .ssh\/authorized_keys2/" /etc/ssh/sshd_config
|
||||
elif ! grep -q "^AuthorizedKeysFile" /etc/ssh/sshd_config; then
|
||||
echo "AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2" >> /etc/ssh/sshd_config
|
||||
fi
|
||||
|
||||
# Reload SSH service
|
||||
systemctl reload sshd
|
||||
'
|
||||
}
|
||||
|
||||
# Function to handle StartOS connection (download only)
|
||||
handle_startos_connection() {
|
||||
echo -e "${BLUE}Fetching the WireGuard configuration file...${NC}"
|
||||
|
||||
# Fetch the client configuration file
|
||||
config_file=$(ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" 'ls -t ~/*.conf 2>/dev/null | head -n 1')
|
||||
if [ -z "$config_file" ]; then
|
||||
echo -e "${RED}Error: No WireGuard configuration file found on the remote server.${NC}"
|
||||
return 1 # Exit with error
|
||||
fi
|
||||
CONFIG_NAME=$(basename "$config_file")
|
||||
|
||||
# Download the configuration file
|
||||
if ! scp -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -P "$SSH_PORT" "$SSH_USER@$VPS_IP":~/"$CONFIG_NAME" ./; then
|
||||
echo -e "${RED}Error: Failed to download the WireGuard configuration file.${NC}"
|
||||
return 1 # Exit with error
|
||||
fi
|
||||
echo -e "${GREEN}WireGuard configuration file '$CONFIG_NAME' downloaded successfully.${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to import WireGuard configuration
|
||||
import_wireguard_config() {
|
||||
local config_name="$1"
|
||||
if [ -z "$config_name" ]; then
|
||||
echo -e "${RED}Error: Configuration file name is missing.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local connection_name=$(basename "$config_name" .conf) #Extract base name without extension
|
||||
|
||||
# Check if the connection with same name already exists
|
||||
if nmcli connection show --active | grep -q "^${connection_name}\s"; then
|
||||
read -r -p "A connection with the name '$connection_name' already exists. Do you want to override it? (y/N): " answer
|
||||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||||
nmcli connection delete "$connection_name"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Error: Failed to delete existing connection '$connection_name'.${NC}"
|
||||
return 1
|
||||
fi
|
||||
# Import if user chose to override or if connection did not exist
|
||||
if ! nmcli connection import type wireguard file "$config_name"; then
|
||||
echo -e "${RED}Error: Failed to import the WireGuard configuration using NetworkManager.${NC}"
|
||||
rm -f "$config_name"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}WireGuard configuration '$config_name' has been imported to NetworkManager.${NC}"
|
||||
rm -f "$config_name"
|
||||
display_end_message
|
||||
else
|
||||
echo -e "${BLUE}Skipping import of the WireGuard configuration.${NC}"
|
||||
rm -f "$config_name"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
# Import if connection did not exist
|
||||
if command -v nmcli &>/dev/null; then
|
||||
if ! nmcli connection import type wireguard file "$config_name"; then
|
||||
echo -e "${RED}Error: Failed to import the WireGuard configuration using NetworkManager.${NC}"
|
||||
rm -f "$config_name"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}WireGuard configuration '$config_name' has been imported to NetworkManager.${NC}"
|
||||
rm -f "$config_name"
|
||||
display_end_message
|
||||
else
|
||||
echo -e "${YELLOW}Warning: NetworkManager 'nmcli' not found. Configuration file '$config_name' saved in current directory.${NC}"
|
||||
echo -e "${YELLOW}Import the configuration to your StartOS manually by going to NetworkManager or using wg-quick up <config> command${NC}"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to download the install script
|
||||
download_install_script() {
|
||||
echo -e "${BLUE}Downloading latest WireGuard install script...${NC}"
|
||||
# Download the script
|
||||
if ! curl -sSf "$WIREGUARD_INSTALL_URL" -o wireguard-install.sh; then
|
||||
echo -e "${RED}Failed to download WireGuard installation script.${NC}"
|
||||
return 1
|
||||
fi
|
||||
chmod +x wireguard-install.sh
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Failed to chmod +x wireguard install script.${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -e "${GREEN}WireGuard install script downloaded successfully!${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to install WireGuard
|
||||
install_wireguard() {
|
||||
echo -e "\n${BLUE}Installing WireGuard...${NC}"
|
||||
|
||||
# Check if install script exist
|
||||
if [ ! -f "wireguard-install.sh" ]; then
|
||||
echo -e "${RED}WireGuard install script is missing. Did it failed to download?${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Run the remote install script and let it complete
|
||||
if ! ssh -o ConnectTimeout=60 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" -t "$SSH_USER@$VPS_IP" "bash -c 'export TERM=xterm-256color; export STARTOS_HOSTNAME=clearnet; bash ~/wireguard-install.sh'"; then
|
||||
echo -e "${RED}WireGuard installation failed on remote server.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test if wireguard installed
|
||||
if ! ssh -q -o BatchMode=yes -o ConnectTimeout=5 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" "test -f /etc/wireguard/wg0.conf"; then
|
||||
echo -e "\n${RED}WireGuard installation failed because /etc/wireguard/wg0.conf is missing, which means the script removed it.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "\n${GREEN}WireGuard installation completed successfully!${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to enable root login via SSH
|
||||
enable_root_login() {
|
||||
echo -e "${BLUE}Checking and configuring root SSH access...${NC}"
|
||||
|
||||
# Try to modify sshd config using sudo
|
||||
if ! ssh -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" '
|
||||
# Check if we can use sudo without password
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
echo -e "\033[1;33mNOTE: You may be prompted for your sudo password.\033[0m"
|
||||
fi
|
||||
|
||||
# Check if user is in sudo group
|
||||
if ! groups | grep -q sudo; then
|
||||
echo -e "\033[1;31mError: Your user is not in the sudo group. Root access cannot be configured.\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup sshd config
|
||||
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
|
||||
|
||||
# Enable root login with SSH keys only
|
||||
if sudo grep -q "^PermitRootLogin" /etc/ssh/sshd_config; then
|
||||
sudo sed -i "s/^PermitRootLogin.*/PermitRootLogin prohibit-password/" /etc/ssh/sshd_config
|
||||
else
|
||||
echo "PermitRootLogin prohibit-password" | sudo tee -a /etc/ssh/sshd_config
|
||||
fi
|
||||
|
||||
# Ensure password authentication is disabled
|
||||
if sudo grep -q "^PasswordAuthentication" /etc/ssh/sshd_config; then
|
||||
sudo sed -i "s/^PasswordAuthentication.*/PasswordAuthentication no/" /etc/ssh/sshd_config
|
||||
else
|
||||
echo "PasswordAuthentication no" | sudo tee -a /etc/ssh/sshd_config
|
||||
fi
|
||||
|
||||
# Set up root SSH directory and keys
|
||||
echo -e "\033[1;33mSetting up root SSH access...\033[0m"
|
||||
sudo mkdir -p /root/.ssh
|
||||
sudo cp ~/.ssh/authorized_keys /root/.ssh/
|
||||
sudo chown -R root:root /root/.ssh
|
||||
sudo chmod 700 /root/.ssh
|
||||
sudo chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
# Reload SSH service
|
||||
sudo systemctl reload sshd
|
||||
|
||||
# Verify the changes
|
||||
if ! sudo grep -q "^PermitRootLogin prohibit-password" /etc/ssh/sshd_config; then
|
||||
echo -e "\033[1;31mError: Failed to verify root login configuration.\033[0m"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test root SSH access
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
echo -e "\033[1;33mNOTE: Please try to log in as root now using your SSH key.\033[0m"
|
||||
echo -e "\033[1;33mIf successful, run this script again without the -u parameter.\033[0m"
|
||||
else
|
||||
echo -e "\033[1;32mRoot SSH access has been configured successfully!\033[0m"
|
||||
fi
|
||||
'; then
|
||||
echo -e "${RED}Failed to configure root SSH access.${NC}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Root SSH access has been configured successfully!${NC}"
|
||||
echo -e "${YELLOW}Please try to log in as root now using your SSH key. If successful, run this script again without the -u parameter.${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Main Script ---
|
||||
# Initialize variables
|
||||
VPS_IP=""
|
||||
SSH_USER="root"
|
||||
SSH_PORT="22"
|
||||
CUSTOM_SSH_KEY=""
|
||||
CONFIG_NAME=""
|
||||
|
||||
# Check if the script is run as root before anything else
|
||||
check_root
|
||||
|
||||
# Print banner
|
||||
print_banner
|
||||
|
||||
# Parse command line arguments
|
||||
while getopts "hi:u:p:k:" opt; do
|
||||
case $opt in
|
||||
h)
|
||||
print_usage
|
||||
exit 0
|
||||
;;
|
||||
i)
|
||||
VPS_IP=$OPTARG
|
||||
;;
|
||||
u)
|
||||
SSH_USER=$OPTARG
|
||||
;;
|
||||
p)
|
||||
SSH_PORT=$OPTARG
|
||||
;;
|
||||
k)
|
||||
CUSTOM_SSH_KEY=$OPTARG
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
print_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check if custom SSH key is passed and update the private key variable
|
||||
if [ -n "$CUSTOM_SSH_KEY" ]; then
|
||||
if [ ! -f "$CUSTOM_SSH_KEY" ]; then
|
||||
echo -e "${RED}Custom SSH key '$CUSTOM_SSH_KEY' not found.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
SSH_PRIVATE_KEY="$CUSTOM_SSH_KEY"
|
||||
SSH_PUBLIC_KEY="$CUSTOM_SSH_KEY.pub"
|
||||
else
|
||||
# Use default StartOS SSH key
|
||||
if [ ! -f "$SSH_PRIVATE_KEY" ]; then
|
||||
echo -e "${RED}No SSH key found at default location '$SSH_PRIVATE_KEY'. Please ensure StartOS SSH keys are properly configured.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -f "$SSH_PUBLIC_KEY" ]; then
|
||||
echo -e "${RED}Public key '$SSH_PUBLIC_KEY' not found. Please ensure both private and public keys exist.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If VPS_IP is not provided via command line, ask for it
|
||||
if [ -z "$VPS_IP" ]; then
|
||||
while true; do
|
||||
echo -n "Please enter your VPS IP address: "
|
||||
read VPS_IP
|
||||
if validate_ip "$VPS_IP"; then
|
||||
break
|
||||
else
|
||||
echo -e "${RED}Invalid IP address format. Please try again.${NC}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Confirm SSH connection details
|
||||
echo -e "\n${GREEN}Connection details:${NC}"
|
||||
echo "VPS IP: $VPS_IP"
|
||||
echo "SSH User: $SSH_USER"
|
||||
echo "SSH Port: $SSH_PORT"
|
||||
|
||||
echo -e "\n${GREEN}Proceeding with SSH key-based authentication...${NC}\n"
|
||||
|
||||
# Copy SSH public key to the remote server
|
||||
if ! ssh-copy-id -i "$SSH_PUBLIC_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP"; then
|
||||
echo -e "${RED}Failed to copy SSH key to the remote server. Please ensure you have correct credentials.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}SSH key-based authentication configured successfully!${NC}"
|
||||
|
||||
# Test SSH connection using key-based authentication
|
||||
echo -e "\nTesting SSH connection with key-based authentication..."
|
||||
if ! ssh -q -o BatchMode=yes -o ConnectTimeout=5 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" 'exit'; then
|
||||
echo -e "${RED}SSH connection test failed. Please check your credentials and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If we're connecting as a non-root user, set up root access first
|
||||
if [ "$SSH_USER" != "root" ]; then
|
||||
echo -e "\n${YELLOW}You are connecting as a non-root user. This script needs to enable root SSH access.${NC}"
|
||||
echo -e "${YELLOW}This is a one-time setup that will allow direct root login for WireGuard installation.${NC}"
|
||||
echo -n -e "${YELLOW}Would you like to proceed? (y/N): ${NC}"
|
||||
read -r answer
|
||||
|
||||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||||
if enable_root_login; then
|
||||
echo -e "\n${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "${GREEN}Root SSH access has been configured successfully!${NC}"
|
||||
echo -e "${YELLOW}Please run this script again without the -u parameter to continue setup.${NC}"
|
||||
echo -e "${BLUE}------------------------------------------------------------------${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}Failed to configure root SSH access. Please check your sudo privileges and try again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "\n${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "${YELLOW}To manually configure SSH for root access:${NC}"
|
||||
echo -e "\n ${YELLOW}1. Connect to your VPS and edit sshd_config:${NC}"
|
||||
echo " sudo nano /etc/ssh/sshd_config"
|
||||
echo -e "\n ${YELLOW}2. Find and uncomment or add these lines:${NC}"
|
||||
echo " PubkeyAuthentication yes"
|
||||
echo " PermitRootLogin yes"
|
||||
echo " AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2"
|
||||
echo -e "\n ${YELLOW}3. Restart the SSH service:${NC}"
|
||||
echo " sudo systemctl restart sshd"
|
||||
echo -e "\n ${YELLOW}4. Copy your SSH key to root user:${NC}"
|
||||
echo " sudo mkdir -p /root/.ssh"
|
||||
echo " sudo cp ~/.ssh/authorized_keys /root/.ssh/"
|
||||
echo " sudo chown -R root:root /root/.ssh"
|
||||
echo " sudo chmod 700 /root/.ssh"
|
||||
echo " sudo chmod 600 /root/.ssh/authorized_keys"
|
||||
echo -e "${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "\n${YELLOW}After completing these steps, run this script again without the -u parameter.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if root login is permitted when connecting as root
|
||||
if [ "$SSH_USER" = "root" ]; then
|
||||
# Check for both "yes" and "prohibit-password" as valid root login settings
|
||||
if ! ssh -q -o BatchMode=yes -o ConnectTimeout=5 -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -p "$SSH_PORT" "$SSH_USER@$VPS_IP" 'grep -q "^PermitRootLogin.*\(yes\|prohibit-password\)" /etc/ssh/sshd_config'; then
|
||||
echo -e "\n${RED}Root SSH login is not enabled on your VPS.${NC}"
|
||||
echo -e "\n${YELLOW}Would you like this script to automatically enable root SSH access? (y/N):${NC} "
|
||||
read -r answer
|
||||
|
||||
if [[ "$answer" =~ ^[Yy]$ ]]; then
|
||||
configure_ssh_key_auth
|
||||
else
|
||||
echo -e "\n${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "${YELLOW}To manually configure SSH for root access:${NC}"
|
||||
echo -e "\n ${YELLOW}1. Connect to your VPS and edit sshd_config:${NC}"
|
||||
echo " sudo nano /etc/ssh/sshd_config"
|
||||
echo -e "\n ${YELLOW}2. Find and uncomment or add these lines:${NC}"
|
||||
echo " PubkeyAuthentication yes"
|
||||
echo " PermitRootLogin prohibit-password"
|
||||
echo " AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2"
|
||||
echo -e "\n ${YELLOW}3. Restart the SSH service:${NC}"
|
||||
echo " sudo systemctl restart sshd"
|
||||
echo -e "${BLUE}------------------------------------------------------------------${NC}"
|
||||
echo -e "\n${YELLOW}Please enable root SSH access and run this script again.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}SSH connection successful with key-based authentication!${NC}"
|
||||
|
||||
# Download the WireGuard install script locally
|
||||
if ! download_install_script; then
|
||||
echo -e "${RED}Failed to download the latest install script. Exiting...${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Upload the install script to the remote server
|
||||
if ! scp -i "$SSH_PRIVATE_KEY" -o StrictHostKeyChecking=no -P "$SSH_PORT" wireguard-install.sh "$SSH_USER@$VPS_IP":~/; then
|
||||
echo -e "${RED}Failed to upload WireGuard install script to the remote server.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install WireGuard on remote server using the downloaded script
|
||||
if ! install_wireguard; then
|
||||
echo -e "${RED}WireGuard installation failed.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove the local install script
|
||||
rm wireguard-install.sh >/dev/null 2>&1
|
||||
|
||||
# Handle the StartOS config (download)
|
||||
if ! handle_startos_connection; then
|
||||
echo -e "${RED}StartOS configuration download failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Import the configuration
|
||||
if ! import_wireguard_config "$CONFIG_NAME"; then
|
||||
echo -e "${RED}StartOS configuration import failed or skipped!${NC}"
|
||||
fi
|
||||
56
build/os-compat/buildenv.Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
||||
FROM debian:bookworm
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
ca-certificates \
|
||||
curl \
|
||||
gpg \
|
||||
build-essential \
|
||||
sed \
|
||||
grep \
|
||||
gawk \
|
||||
jq \
|
||||
gzip \
|
||||
brotli \
|
||||
qemu-user-static \
|
||||
binfmt-support \
|
||||
squashfs-tools \
|
||||
git \
|
||||
debspawn \
|
||||
rsync \
|
||||
b3sum \
|
||||
fuse-overlayfs \
|
||||
sudo \
|
||||
systemd \
|
||||
systemd-container \
|
||||
systemd-sysv \
|
||||
dbus \
|
||||
dbus-user-session
|
||||
|
||||
RUN systemctl mask \
|
||||
systemd-firstboot.service \
|
||||
systemd-udevd.service \
|
||||
getty@tty1.service \
|
||||
console-getty.service
|
||||
|
||||
RUN git config --global --add safe.directory /root/start-os
|
||||
|
||||
RUN mkdir -p /etc/debspawn && \
|
||||
echo "AllowUnsafePermissions=true" > /etc/debspawn/global.toml
|
||||
|
||||
ENV NVM_DIR=~/.nvm
|
||||
ENV NODE_VERSION=22
|
||||
RUN mkdir -p $NVM_DIR && \
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash && \
|
||||
. $NVM_DIR/nvm.sh \
|
||||
nvm install $NODE_VERSION && \
|
||||
nvm use $NODE_VERSION && \
|
||||
nvm alias default $NODE_VERSION && \
|
||||
ln -s $(which node) /usr/bin/node && \
|
||||
ln -s $(which npm) /usr/bin/npm
|
||||
|
||||
RUN mkdir -p /root/start-os
|
||||
WORKDIR /root/start-os
|
||||
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
ENTRYPOINT [ "/docker-entrypoint.sh" ]
|
||||
3
build/os-compat/docker-entrypoint.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
exec /lib/systemd/systemd --unit=multi-user.target --show-status=false --log-target=journal
|
||||
27
build/os-compat/run-compat.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$FORCE_COMPAT" = 1 ] || ( [ "$REQUIRES" = "linux" ] && [ "$(uname -s)" != "Linux" ] ) || ( [ "$REQUIRES" = "debian" ] && ! which dpkg > /dev/null ); then
|
||||
project_pwd="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)/"
|
||||
pwd="$(pwd)/"
|
||||
if ! [[ "$pwd" = "$project_pwd"* ]]; then
|
||||
>&2 echo "Must be run from start-os project dir"
|
||||
exit 1
|
||||
fi
|
||||
rel_pwd="${pwd#"$project_pwd"}"
|
||||
|
||||
SYSTEMD_TTY="-P"
|
||||
USE_TTY=
|
||||
if tty -s; then
|
||||
USE_TTY="-it"
|
||||
SYSTEMD_TTY="-t"
|
||||
fi
|
||||
|
||||
docker run -d --rm --name os-compat --privileged --security-opt apparmor=unconfined -v "${project_pwd}:/root/start-os" -v /lib/modules:/lib/modules:ro start9/build-env
|
||||
while ! docker exec os-compat systemctl is-active --quiet multi-user.target 2> /dev/null; do sleep .5; done
|
||||
docker exec -eARCH -eENVIRONMENT -ePLATFORM -eGIT_BRANCH_AS_HASH -ePROJECT -eDEPENDS -eCONFLICTS $USE_TTY -w "/root/start-os${rel_pwd}" os-compat $@
|
||||
code=$?
|
||||
docker stop os-compat
|
||||
exit $code
|
||||
else
|
||||
exec $@
|
||||
fi
|
||||
@@ -63,7 +63,7 @@ sudo unsquashfs -f -d $TMPDIR startos.raspberrypi.squashfs
|
||||
REAL_GIT_HASH=$(cat $TMPDIR/usr/lib/startos/GIT_HASH.txt)
|
||||
REAL_VERSION=$(cat $TMPDIR/usr/lib/startos/VERSION.txt)
|
||||
REAL_ENVIRONMENT=$(cat $TMPDIR/usr/lib/startos/ENVIRONMENT.txt)
|
||||
sudo sed -i 's| boot=embassy| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
|
||||
sudo sed -i 's| boot=startos| init=/usr/lib/startos/scripts/init_resize\.sh|' $TMPDIR/boot/cmdline.txt
|
||||
sudo cp ./build/raspberrypi/fstab $TMPDIR/etc/
|
||||
sudo cp ./build/raspberrypi/init_resize.sh $TMPDIR/usr/lib/startos/scripts/init_resize.sh
|
||||
sudo umount $TMPDIR/boot
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "$GIT_BRANCH_AS_HASH" != 1 ]; then
|
||||
GIT_HASH="$(git describe --always --abbrev=40 --dirty=-modified)"
|
||||
GIT_HASH="$(git rev-parse HEAD)$(if ! git diff-index --quiet HEAD --; then echo '-modified'; fi)"
|
||||
else
|
||||
GIT_HASH="@$(git rev-parse --abbrev-ref HEAD)"
|
||||
fi
|
||||
|
||||
if ! [ -f ./GIT_HASH.txt ] || [ "$(cat ./GIT_HASH.txt)" != "$GIT_HASH" ]; then
|
||||
>&2 echo Git hash changed from "$([ -f ./GIT_HASH.txt ] && cat ./GIT_HASH.txt)" to "$GIT_HASH"
|
||||
echo -n "$GIT_HASH" > ./GIT_HASH.txt
|
||||
fi
|
||||
|
||||
|
||||
@@ -4,13 +4,17 @@ cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
rm -rf web/dist/static
|
||||
STATIC_DIR=web/dist/static/$1
|
||||
RAW_DIR=web/dist/raw/$1
|
||||
|
||||
mkdir -p $STATIC_DIR
|
||||
rm -rf $STATIC_DIR
|
||||
|
||||
if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
|
||||
find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf
|
||||
find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf
|
||||
find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 gzip -kf
|
||||
find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br' | xargs -n 1 -P 0 brotli -kf
|
||||
|
||||
for file in $(find web/dist/raw -type f -not -name '*.gz' -and -not -name '*.br'); do
|
||||
for file in $(find $RAW_DIR -type f -not -name '*.gz' -and -not -name '*.br'); do
|
||||
raw_size=$(du $file | awk '{print $1 * 512}')
|
||||
gz_size=$(du $file.gz | awk '{print $1 * 512}')
|
||||
br_size=$(du $file.br | awk '{print $1 * 512}')
|
||||
@@ -23,4 +27,5 @@ if ! [[ "$ENVIRONMENT" =~ (^|-)dev($|-) ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
cp -r web/dist/raw web/dist/static
|
||||
|
||||
cp -r $RAW_DIR $STATIC_DIR
|
||||
8
container-runtime/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
bundle.js
|
||||
startInit.js
|
||||
service/
|
||||
service.js
|
||||
*.squashfs
|
||||
/tmp
|
||||
89
container-runtime/RPCSpec.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Container RPC SERVER Specification
|
||||
|
||||
## Methods
|
||||
|
||||
### init
|
||||
|
||||
initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`)
|
||||
|
||||
called after os has mounted js and images to the container
|
||||
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### exit
|
||||
|
||||
shutdown runtime
|
||||
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### start
|
||||
|
||||
run main method if not already running
|
||||
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### stop
|
||||
|
||||
stop main method by sending SIGTERM to child processes, and SIGKILL after timeout
|
||||
|
||||
#### args
|
||||
|
||||
`{ timeout: millis }`
|
||||
|
||||
#### response
|
||||
|
||||
`null`
|
||||
|
||||
### execute
|
||||
|
||||
run a specific package procedure
|
||||
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
procedure: JsonPath,
|
||||
input: any,
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`any`
|
||||
|
||||
### sandbox
|
||||
|
||||
run a specific package procedure in sandbox mode
|
||||
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
procedure: JsonPath,
|
||||
input: any,
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`any`
|
||||
6
container-runtime/container-runtime-failure.service
Normal file
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=StartOS Container Runtime Failure Handler
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/start-container rebuild
|
||||
11
container-runtime/container-runtime.service
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=StartOS Container Runtime
|
||||
OnFailure=container-runtime-failure.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/node --experimental-detect-module --trace-warnings --unhandled-rejections=warn /usr/lib/startos/init/index.js
|
||||
Restart=no
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
22
container-runtime/deb-install.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
mkdir -p /run/systemd/resolve
|
||||
echo "nameserver 8.8.8.8" > /run/systemd/resolve/stub-resolv.conf
|
||||
|
||||
apt-get update
|
||||
apt-get install -y curl rsync qemu-user-static nodejs
|
||||
|
||||
sed -i '/\(^\|#\)DNSStubListener=/c\DNSStubListener=no' /etc/systemd/resolved.conf
|
||||
sed -i '/\(^\|#\)Storage=/c\Storage=persistent' /etc/systemd/journald.conf
|
||||
sed -i '/\(^\|#\)Compress=/c\Compress=yes' /etc/systemd/journald.conf
|
||||
sed -i '/\(^\|#\)SystemMaxUse=/c\SystemMaxUse=1G' /etc/systemd/journald.conf
|
||||
sed -i '/\(^\|#\)ForwardToSyslog=/c\ForwardToSyslog=no' /etc/systemd/journald.conf
|
||||
|
||||
systemctl enable container-runtime.service
|
||||
|
||||
rm -rf /run/systemd
|
||||
|
||||
rm -f /etc/resolv.conf
|
||||
echo "nameserver 10.0.3.1" > /etc/resolv.conf
|
||||
22
container-runtime/download-base-image.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
set -e
|
||||
|
||||
DISTRO=debian
|
||||
VERSION=trixie
|
||||
ARCH=${ARCH:-$(uname -m)}
|
||||
FLAVOR=default
|
||||
|
||||
_ARCH=$ARCH
|
||||
if [ "$_ARCH" = "x86_64" ]; then
|
||||
_ARCH=amd64
|
||||
elif [ "$_ARCH" = "aarch64" ]; then
|
||||
_ARCH=arm64
|
||||
fi
|
||||
|
||||
BASE_URL="https://images.linuxcontainers.org$(curl -fsSL https://images.linuxcontainers.org/meta/1.0/index-system | grep "^$DISTRO;$VERSION;$_ARCH;$FLAVOR;" | head -n1 | sed 's/^.*;//g')"
|
||||
OUTPUT_FILE="debian.${ARCH}.squashfs"
|
||||
|
||||
echo "Downloading ${BASE_URL}/rootfs.squashfs to $OUTPUT_FILE"
|
||||
curl -fsSL "${BASE_URL}/rootfs.squashfs" > "$OUTPUT_FILE"
|
||||
curl -fsSL "$BASE_URL/SHA256SUMS" | grep 'rootfs\.squashfs' | awk '{print $1" '"$OUTPUT_FILE"'"}' | shasum -a 256 -c
|
||||
10
container-runtime/install-dist-deps.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
set -e
|
||||
|
||||
cat ./package.json | sed 's/file:\.\([.\/]\)/file:..\/.\1/g' > ./dist/package.json
|
||||
cat ./package-lock.json | sed 's/"\.\([.\/]\)/"..\/.\1/g' > ./dist/package-lock.json
|
||||
|
||||
npm --prefix dist ci --omit=dev
|
||||
8
container-runtime/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
automock: false,
|
||||
testEnvironment: "node",
|
||||
rootDir: "./src/",
|
||||
modulePathIgnorePatterns: ["./dist/"],
|
||||
}
|
||||
28
container-runtime/mkcontainer.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
IMAGE=$1
|
||||
|
||||
if [ -z "$IMAGE" ]; then
|
||||
>&2 echo "usage: $0 <image id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ -d "/media/images/$IMAGE" ]; then
|
||||
>&2 echo "image does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
container=$(mktemp -d)
|
||||
mkdir -p $container/rootfs $container/upper $container/work
|
||||
mount -t overlay -olowerdir=/media/images/$IMAGE,upperdir=$container/upper,workdir=$container/work overlay $container/rootfs
|
||||
|
||||
rootfs=$container/rootfs
|
||||
|
||||
for special in dev sys proc run; do
|
||||
mkdir -p $rootfs/$special
|
||||
mount --bind /$special $rootfs/$special
|
||||
done
|
||||
|
||||
echo $rootfs
|
||||
6832
container-runtime/package-lock.json
generated
Normal file
47
container-runtime/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "container-runtime",
|
||||
"version": "0.0.0",
|
||||
"description": "We want to be the sdk intermitent for the system",
|
||||
"module": "./index.js",
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"build": "prettier . '!tmp/**' --write && rm -rf dist && tsc",
|
||||
"tsc": "rm -rf dist; tsc",
|
||||
"test": "jest -c ./jest.config.js"
|
||||
},
|
||||
"author": "",
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": false
|
||||
},
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/start-sdk": "file:../sdk/dist",
|
||||
"esbuild-plugin-resolve": "^2.0.0",
|
||||
"filebrowser": "^1.0.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jsonpath": "^1.1.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime": "^4.0.7",
|
||||
"node-fetch": "^3.1.0",
|
||||
"ts-matches": "^6.3.2",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.1.3",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.65",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jsonpath": "^0.2.4",
|
||||
"@types/node": "^20.11.13",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.2.3",
|
||||
"typescript": ">5.2"
|
||||
}
|
||||
}
|
||||
12
container-runtime/rmcontainer.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
rootfs=$1
|
||||
if [ -z "$rootfs" ]; then
|
||||
>&2 echo "usage: $0 <container rootfs path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umount --recursive $rootfs
|
||||
rm -rf $rootfs/..
|
||||
323
container-runtime/src/Adapters/EffectCreator.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
import {
|
||||
ExtendedVersion,
|
||||
types as T,
|
||||
utils,
|
||||
VersionRange,
|
||||
} from "@start9labs/start-sdk"
|
||||
import * as net from "net"
|
||||
import { object, string, number, literals, some, unknown } from "ts-matches"
|
||||
import { Effects } from "../Models/Effects"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { asError } from "@start9labs/start-sdk/base/lib/util"
|
||||
const matchRpcError = object({
|
||||
error: object({
|
||||
code: number,
|
||||
message: string,
|
||||
data: some(
|
||||
string,
|
||||
object({
|
||||
details: string,
|
||||
debug: string.nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.nullable()
|
||||
.optional(),
|
||||
}),
|
||||
})
|
||||
const testRpcError = matchRpcError.test
|
||||
const testRpcResult = object({
|
||||
result: unknown,
|
||||
}).test
|
||||
type RpcError = typeof matchRpcError._TYPE
|
||||
|
||||
const SOCKET_PATH = "/media/startos/rpc/host.sock"
|
||||
let hostSystemId = 0
|
||||
|
||||
export type EffectContext = {
|
||||
eventId: string | null
|
||||
callbacks?: CallbackHolder
|
||||
constRetry?: () => void
|
||||
}
|
||||
|
||||
const rpcRoundFor =
|
||||
(eventId: string | null) =>
|
||||
<K extends T.EffectMethod | "clearCallbacks">(
|
||||
method: K,
|
||||
params: Record<string, unknown>,
|
||||
) => {
|
||||
const id = hostSystemId++
|
||||
const client = net.createConnection({ path: SOCKET_PATH }, () => {
|
||||
client.write(
|
||||
JSON.stringify({
|
||||
id,
|
||||
method,
|
||||
params: { ...params, eventId: eventId ?? undefined },
|
||||
}) + "\n",
|
||||
)
|
||||
})
|
||||
let bufs: Buffer[] = []
|
||||
return new Promise((resolve, reject) => {
|
||||
client.on("data", (data) => {
|
||||
try {
|
||||
bufs.push(data)
|
||||
if (data.reduce((acc, x) => acc || x == 10, false)) {
|
||||
const res: unknown = JSON.parse(
|
||||
Buffer.concat(bufs).toString().split("\n")[0],
|
||||
)
|
||||
if (testRpcError(res)) {
|
||||
let message = res.error.message
|
||||
console.error(
|
||||
"Error in host RPC:",
|
||||
utils.asError({ method, params, error: res.error }),
|
||||
)
|
||||
if (string.test(res.error.data)) {
|
||||
message += ": " + res.error.data
|
||||
console.error(`Details: ${res.error.data}`)
|
||||
} else {
|
||||
if (res.error.data?.details) {
|
||||
message += ": " + res.error.data.details
|
||||
console.error(`Details: ${res.error.data.details}`)
|
||||
}
|
||||
if (res.error.data?.debug) {
|
||||
message += "\n" + res.error.data.debug
|
||||
console.error(`Debug: ${res.error.data.debug}`)
|
||||
}
|
||||
}
|
||||
reject(new Error(`${message}@${method}`))
|
||||
} else if (testRpcResult(res)) {
|
||||
resolve(res.result)
|
||||
} else {
|
||||
reject(new Error(`malformed response ${JSON.stringify(res)}`))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
client.end()
|
||||
})
|
||||
client.on("error", (error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function makeEffects(context: EffectContext): Effects {
|
||||
const rpcRound = rpcRoundFor(context.eventId)
|
||||
const self: Effects = {
|
||||
eventId: context.eventId,
|
||||
child: (name) =>
|
||||
makeEffects({ ...context, callbacks: context.callbacks?.child(name) }),
|
||||
constRetry: context.constRetry,
|
||||
isInContext: !!context.callbacks,
|
||||
onLeaveContext:
|
||||
context.callbacks?.onLeaveContext?.bind(context.callbacks) ||
|
||||
(() => {
|
||||
console.warn(
|
||||
"no context for this effects object",
|
||||
new Error().stack?.replace(/^Error/, ""),
|
||||
)
|
||||
}),
|
||||
clearCallbacks(...[options]: Parameters<T.Effects["clearCallbacks"]>) {
|
||||
return rpcRound("clear-callbacks", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["clearCallbacks"]>
|
||||
},
|
||||
action: {
|
||||
clear(...[options]: Parameters<T.Effects["action"]["clear"]>) {
|
||||
return rpcRound("action.clear", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["clear"]>
|
||||
},
|
||||
export(...[options]: Parameters<T.Effects["action"]["export"]>) {
|
||||
return rpcRound("action.export", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["export"]>
|
||||
},
|
||||
getInput(...[options]: Parameters<T.Effects["action"]["getInput"]>) {
|
||||
return rpcRound("action.get-input", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["getInput"]>
|
||||
},
|
||||
createTask(...[options]: Parameters<T.Effects["action"]["createTask"]>) {
|
||||
return rpcRound("action.create-task", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["createTask"]>
|
||||
},
|
||||
run(...[options]: Parameters<T.Effects["action"]["run"]>) {
|
||||
return rpcRound("action.run", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["run"]>
|
||||
},
|
||||
clearTasks(...[options]: Parameters<T.Effects["action"]["clearTasks"]>) {
|
||||
return rpcRound("action.clear-tasks", {
|
||||
...options,
|
||||
}) as ReturnType<T.Effects["action"]["clearTasks"]>
|
||||
},
|
||||
},
|
||||
bind(...[options]: Parameters<T.Effects["bind"]>) {
|
||||
return rpcRound("bind", {
|
||||
...options,
|
||||
stack: new Error().stack,
|
||||
}) as ReturnType<T.Effects["bind"]>
|
||||
},
|
||||
clearBindings(...[options]: Parameters<T.Effects["clearBindings"]>) {
|
||||
return rpcRound("clear-bindings", { ...options }) as ReturnType<
|
||||
T.Effects["clearBindings"]
|
||||
>
|
||||
},
|
||||
clearServiceInterfaces(
|
||||
...[options]: Parameters<T.Effects["clearServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("clear-service-interfaces", { ...options }) as ReturnType<
|
||||
T.Effects["clearServiceInterfaces"]
|
||||
>
|
||||
},
|
||||
getInstalledPackages(...[]: Parameters<T.Effects["getInstalledPackages"]>) {
|
||||
return rpcRound("get-installed-packages", {}) as ReturnType<
|
||||
T.Effects["getInstalledPackages"]
|
||||
>
|
||||
},
|
||||
subcontainer: {
|
||||
createFs(options: { imageId: string; name: string }) {
|
||||
return rpcRound("subcontainer.create-fs", options) as ReturnType<
|
||||
T.Effects["subcontainer"]["createFs"]
|
||||
>
|
||||
},
|
||||
destroyFs(options: { guid: string }): Promise<null> {
|
||||
return rpcRound("subcontainer.destroy-fs", options) as ReturnType<
|
||||
T.Effects["subcontainer"]["destroyFs"]
|
||||
>
|
||||
},
|
||||
},
|
||||
exportServiceInterface: ((
|
||||
...[options]: Parameters<Effects["exportServiceInterface"]>
|
||||
) => {
|
||||
return rpcRound("export-service-interface", options) as ReturnType<
|
||||
T.Effects["exportServiceInterface"]
|
||||
>
|
||||
}) as Effects["exportServiceInterface"],
|
||||
getContainerIp(...[options]: Parameters<T.Effects["getContainerIp"]>) {
|
||||
return rpcRound("get-container-ip", options) as ReturnType<
|
||||
T.Effects["getContainerIp"]
|
||||
>
|
||||
},
|
||||
getOsIp(...[]: Parameters<T.Effects["getOsIp"]>) {
|
||||
return rpcRound("get-os-ip", {}) as ReturnType<T.Effects["getOsIp"]>
|
||||
},
|
||||
getHostInfo: ((...[allOptions]: Parameters<T.Effects["getHostInfo"]>) => {
|
||||
const options = {
|
||||
...allOptions,
|
||||
callback: context.callbacks?.addCallback(allOptions.callback) || null,
|
||||
}
|
||||
return rpcRound("get-host-info", options) as ReturnType<
|
||||
T.Effects["getHostInfo"]
|
||||
> as any
|
||||
}) as Effects["getHostInfo"],
|
||||
getServiceInterface(
|
||||
...[options]: Parameters<T.Effects["getServiceInterface"]>
|
||||
) {
|
||||
return rpcRound("get-service-interface", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getServiceInterface"]>
|
||||
},
|
||||
|
||||
getServicePortForward(
|
||||
...[options]: Parameters<T.Effects["getServicePortForward"]>
|
||||
) {
|
||||
return rpcRound("get-service-port-forward", options) as ReturnType<
|
||||
T.Effects["getServicePortForward"]
|
||||
>
|
||||
},
|
||||
getSslCertificate(options: Parameters<T.Effects["getSslCertificate"]>[0]) {
|
||||
return rpcRound("get-ssl-certificate", options) as ReturnType<
|
||||
T.Effects["getSslCertificate"]
|
||||
>
|
||||
},
|
||||
getSslKey(options: Parameters<T.Effects["getSslKey"]>[0]) {
|
||||
return rpcRound("get-ssl-key", options) as ReturnType<
|
||||
T.Effects["getSslKey"]
|
||||
>
|
||||
},
|
||||
getSystemSmtp(...[options]: Parameters<T.Effects["getSystemSmtp"]>) {
|
||||
return rpcRound("get-system-smtp", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["getSystemSmtp"]>
|
||||
},
|
||||
listServiceInterfaces(
|
||||
...[options]: Parameters<T.Effects["listServiceInterfaces"]>
|
||||
) {
|
||||
return rpcRound("list-service-interfaces", {
|
||||
...options,
|
||||
callback: context.callbacks?.addCallback(options.callback) || null,
|
||||
}) as ReturnType<T.Effects["listServiceInterfaces"]>
|
||||
},
|
||||
mount(...[options]: Parameters<T.Effects["mount"]>) {
|
||||
return rpcRound("mount", options) as ReturnType<T.Effects["mount"]>
|
||||
},
|
||||
restart(...[]: Parameters<T.Effects["restart"]>) {
|
||||
console.log("Restarting service...")
|
||||
return rpcRound("restart", {}) as ReturnType<T.Effects["restart"]>
|
||||
},
|
||||
setDependencies(
|
||||
dependencies: Parameters<T.Effects["setDependencies"]>[0],
|
||||
): ReturnType<T.Effects["setDependencies"]> {
|
||||
return rpcRound("set-dependencies", dependencies) as ReturnType<
|
||||
T.Effects["setDependencies"]
|
||||
>
|
||||
},
|
||||
checkDependencies(
|
||||
options: Parameters<T.Effects["checkDependencies"]>[0],
|
||||
): ReturnType<T.Effects["checkDependencies"]> {
|
||||
return rpcRound("check-dependencies", options) as ReturnType<
|
||||
T.Effects["checkDependencies"]
|
||||
>
|
||||
},
|
||||
getDependencies(): ReturnType<T.Effects["getDependencies"]> {
|
||||
return rpcRound("get-dependencies", {}) as ReturnType<
|
||||
T.Effects["getDependencies"]
|
||||
>
|
||||
},
|
||||
setHealth(...[options]: Parameters<T.Effects["setHealth"]>) {
|
||||
return rpcRound("set-health", options) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
},
|
||||
|
||||
getStatus(...[o]: Parameters<T.Effects["getStatus"]>) {
|
||||
return rpcRound("get-status", o) as ReturnType<T.Effects["getStatus"]>
|
||||
},
|
||||
setMainStatus(o: { status: "running" | "stopped" }): Promise<null> {
|
||||
return rpcRound("set-main-status", o) as ReturnType<
|
||||
T.Effects["setHealth"]
|
||||
>
|
||||
},
|
||||
|
||||
shutdown(...[]: Parameters<T.Effects["shutdown"]>) {
|
||||
return rpcRound("shutdown", {}) as ReturnType<T.Effects["shutdown"]>
|
||||
},
|
||||
getDataVersion() {
|
||||
return rpcRound("get-data-version", {}) as ReturnType<
|
||||
T.Effects["getDataVersion"]
|
||||
>
|
||||
},
|
||||
setDataVersion(...[options]: Parameters<T.Effects["setDataVersion"]>) {
|
||||
return rpcRound("set-data-version", options) as ReturnType<
|
||||
T.Effects["setDataVersion"]
|
||||
>
|
||||
},
|
||||
}
|
||||
if (context.callbacks?.onLeaveContext)
|
||||
self.onLeaveContext(() => {
|
||||
self.isInContext = false
|
||||
self.onLeaveContext = () => {
|
||||
console.warn(
|
||||
"this effects object is already out of context",
|
||||
new Error().stack?.replace(/^Error/, ""),
|
||||
)
|
||||
}
|
||||
})
|
||||
return self
|
||||
}
|
||||
461
container-runtime/src/Adapters/RpcListener.ts
Normal file
@@ -0,0 +1,461 @@
|
||||
// @ts-check
|
||||
|
||||
import * as net from "net"
|
||||
import {
|
||||
object,
|
||||
some,
|
||||
string,
|
||||
literal,
|
||||
array,
|
||||
number,
|
||||
matches,
|
||||
any,
|
||||
shape,
|
||||
anyOf,
|
||||
literals,
|
||||
} from "ts-matches"
|
||||
|
||||
import {
|
||||
ExtendedVersion,
|
||||
types as T,
|
||||
utils,
|
||||
VersionRange,
|
||||
} from "@start9labs/start-sdk"
|
||||
import * as fs from "fs"
|
||||
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
import { AllGetDependencies } from "../Interfaces/AllGetDependencies"
|
||||
import { jsonPath, unNestPath } from "../Models/JsonPath"
|
||||
import { System } from "../Interfaces/System"
|
||||
import { makeEffects } from "./EffectCreator"
|
||||
type MaybePromise<T> = T | Promise<T>
|
||||
export const matchRpcResult = anyOf(
|
||||
object({ result: any }),
|
||||
object({
|
||||
error: object({
|
||||
code: number,
|
||||
message: string,
|
||||
data: object({
|
||||
details: string.optional(),
|
||||
debug: any.optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export type RpcResult = typeof matchRpcResult._TYPE
|
||||
type SocketResponse = ({ jsonrpc: "2.0"; id: IdType } & RpcResult) | null
|
||||
|
||||
const SOCKET_PARENT = "/media/startos/rpc"
|
||||
const SOCKET_PATH = "/media/startos/rpc/service.sock"
|
||||
const jsonrpc = "2.0" as const
|
||||
|
||||
const isResult = object({ result: any }).test
|
||||
|
||||
const idType = some(string, number, literal(null))
|
||||
type IdType = null | string | number | undefined
|
||||
const runType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("execute"),
|
||||
params: object({
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number.nullable().optional(),
|
||||
}),
|
||||
})
|
||||
const sandboxRunType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("sandbox"),
|
||||
params: object({
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number.nullable().optional(),
|
||||
}),
|
||||
})
|
||||
const callbackType = object({
|
||||
method: literal("callback"),
|
||||
params: object({
|
||||
id: number,
|
||||
args: array,
|
||||
}),
|
||||
})
|
||||
const initType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("init"),
|
||||
params: object({
|
||||
id: string,
|
||||
kind: literals("install", "update", "restore").nullable(),
|
||||
}),
|
||||
})
|
||||
const startType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("start"),
|
||||
})
|
||||
const stopType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("stop"),
|
||||
})
|
||||
const exitType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("exit"),
|
||||
params: object({
|
||||
id: string,
|
||||
target: string.nullable(),
|
||||
}),
|
||||
})
|
||||
const evalType = object({
|
||||
id: idType.optional(),
|
||||
method: literal("eval"),
|
||||
params: object({
|
||||
script: string,
|
||||
}),
|
||||
})
|
||||
|
||||
const jsonParse = (x: string) => JSON.parse(x)
|
||||
|
||||
const handleRpc = (id: IdType, result: Promise<RpcResult>) =>
|
||||
result
|
||||
.then((result) => {
|
||||
return {
|
||||
jsonrpc,
|
||||
id,
|
||||
...result,
|
||||
}
|
||||
})
|
||||
.then((x) => {
|
||||
if (
|
||||
("result" in x && x.result === undefined) ||
|
||||
!("error" in x || "result" in x)
|
||||
)
|
||||
(x as any).result = null
|
||||
return x
|
||||
})
|
||||
.catch((error) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: 0,
|
||||
message: typeof error,
|
||||
data: { details: "" + error, debug: error?.stack },
|
||||
},
|
||||
}))
|
||||
|
||||
const hasId = object({ id: idType }).test
|
||||
export class RpcListener {
|
||||
unixSocketServer = net.createServer(async (server) => {})
|
||||
private _system: System | undefined
|
||||
private callbacks: CallbackHolder | undefined
|
||||
|
||||
constructor(readonly getDependencies: AllGetDependencies) {
|
||||
if (!fs.existsSync(SOCKET_PARENT)) {
|
||||
fs.mkdirSync(SOCKET_PARENT, { recursive: true })
|
||||
}
|
||||
if (fs.existsSync(SOCKET_PATH)) fs.rmSync(SOCKET_PATH, { force: true })
|
||||
|
||||
this.unixSocketServer.listen(SOCKET_PATH)
|
||||
|
||||
this.unixSocketServer.on("connection", (s) => {
|
||||
let id: IdType = null
|
||||
const captureId = <X>(x: X) => {
|
||||
if (hasId(x)) id = x.id
|
||||
return x
|
||||
}
|
||||
const logData =
|
||||
(location: string) =>
|
||||
<X>(x: X) => {
|
||||
console.log({
|
||||
location,
|
||||
stringified: JSON.stringify(x),
|
||||
type: typeof x,
|
||||
id,
|
||||
})
|
||||
return x
|
||||
}
|
||||
const mapError = (error: any): SocketResponse => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
message: typeof error,
|
||||
data: {
|
||||
details: error?.message ?? String(error),
|
||||
debug: error?.stack,
|
||||
},
|
||||
code: 1,
|
||||
},
|
||||
})
|
||||
const writeDataToSocket = (x: SocketResponse) => {
|
||||
if (x != null) {
|
||||
return new Promise((resolve) =>
|
||||
s.write(JSON.stringify(x) + "\n", resolve),
|
||||
)
|
||||
}
|
||||
}
|
||||
s.on("data", (a) =>
|
||||
Promise.resolve(a)
|
||||
.then((b) => b.toString())
|
||||
.then((buf) => {
|
||||
for (let s of buf.split("\n")) {
|
||||
if (s)
|
||||
Promise.resolve(s)
|
||||
.then(logData("dataIn"))
|
||||
.then(jsonParse)
|
||||
.then(captureId)
|
||||
.then((x) => this.dealWithInput(x))
|
||||
.catch(mapError)
|
||||
.then(logData("response"))
|
||||
.then(writeDataToSocket)
|
||||
.catch((e) => {
|
||||
console.error(`Major error in socket handling: ${e}`)
|
||||
console.debug(`Data in: ${a.toString()}`)
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get system() {
|
||||
if (!this._system) throw new Error("System not initialized")
|
||||
return this._system
|
||||
}
|
||||
|
||||
callCallback(callback: number, args: any[]): void {
|
||||
if (this.callbacks) {
|
||||
this.callbacks
|
||||
.callCallback(callback, args)
|
||||
.catch((error) =>
|
||||
console.error(`callback ${callback} failed`, utils.asError(error)),
|
||||
)
|
||||
} else {
|
||||
console.warn(
|
||||
`callback ${callback} ignored because system is not initialized`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private dealWithInput(input: unknown): MaybePromise<SocketResponse> {
|
||||
return matches(input)
|
||||
.when(runType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const { input, timeout, id: eventId } = params
|
||||
const result = this.getResult(
|
||||
procedure,
|
||||
system,
|
||||
eventId,
|
||||
timeout,
|
||||
input,
|
||||
)
|
||||
|
||||
return handleRpc(id, result)
|
||||
})
|
||||
.when(sandboxRunType, async ({ id, params }) => {
|
||||
const system = this.system
|
||||
const procedure = jsonPath.unsafeCast(params.procedure)
|
||||
const { input, timeout, id: eventId } = params
|
||||
const result = this.getResult(
|
||||
procedure,
|
||||
system,
|
||||
eventId,
|
||||
timeout,
|
||||
input,
|
||||
)
|
||||
|
||||
return handleRpc(id, result)
|
||||
})
|
||||
.when(callbackType, async ({ params: { id, args } }) => {
|
||||
this.callCallback(id, args)
|
||||
return null
|
||||
})
|
||||
.when(startType, async ({ id }) => {
|
||||
const callbacks =
|
||||
this.callbacks?.getChild("main") || this.callbacks?.child("main")
|
||||
const effects = makeEffects({
|
||||
eventId: null,
|
||||
callbacks,
|
||||
})
|
||||
return handleRpc(
|
||||
id,
|
||||
this.system.start(effects).then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(stopType, async ({ id }) => {
|
||||
this.callbacks?.removeChild("main")
|
||||
return handleRpc(
|
||||
id,
|
||||
this.system.stop().then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(exitType, async ({ id, params }) => {
|
||||
return handleRpc(
|
||||
id,
|
||||
(async () => {
|
||||
if (this._system) {
|
||||
let target = null
|
||||
if (params.target)
|
||||
try {
|
||||
target = ExtendedVersion.parse(params.target)
|
||||
} catch (_) {
|
||||
target = VersionRange.parse(params.target).normalize()
|
||||
}
|
||||
await this._system.exit(
|
||||
makeEffects({
|
||||
eventId: params.id,
|
||||
}),
|
||||
target,
|
||||
)
|
||||
}
|
||||
})().then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(initType, async ({ id, params }) => {
|
||||
return handleRpc(
|
||||
id,
|
||||
(async () => {
|
||||
if (!this._system) {
|
||||
const system = await this.getDependencies.system()
|
||||
this.callbacks = new CallbackHolder(
|
||||
makeEffects({
|
||||
eventId: params.id,
|
||||
}),
|
||||
)
|
||||
const callbacks = this.callbacks.child("init")
|
||||
console.error("Initializing...")
|
||||
await system.init(
|
||||
makeEffects({
|
||||
eventId: params.id,
|
||||
callbacks,
|
||||
}),
|
||||
params.kind,
|
||||
)
|
||||
console.error("Initialization complete.")
|
||||
this._system = system
|
||||
}
|
||||
})().then((result) => ({ result })),
|
||||
)
|
||||
})
|
||||
.when(evalType, async ({ id, params }) => {
|
||||
return handleRpc(
|
||||
id,
|
||||
(async () => {
|
||||
const result = await new Function(
|
||||
`return (async () => { return (${params.script}) }).call(this)`,
|
||||
).call({
|
||||
listener: this,
|
||||
require: require,
|
||||
})
|
||||
return {
|
||||
jsonrpc,
|
||||
id,
|
||||
result: ![
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"null",
|
||||
"object",
|
||||
].includes(typeof result)
|
||||
? null
|
||||
: result,
|
||||
}
|
||||
})(),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
shape({ id: idType.optional(), method: string }),
|
||||
({ id, method }) => ({
|
||||
jsonrpc,
|
||||
id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Method not found`,
|
||||
data: {
|
||||
details: method,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
.defaultToLazy(() => {
|
||||
console.warn(
|
||||
`Couldn't parse the following input ${JSON.stringify(input)}`,
|
||||
)
|
||||
return {
|
||||
jsonrpc,
|
||||
id: (input as any)?.id,
|
||||
error: {
|
||||
code: -32602,
|
||||
message: "invalid params",
|
||||
data: {
|
||||
details: JSON.stringify(input),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
private getResult(
|
||||
procedure: typeof jsonPath._TYPE,
|
||||
system: System,
|
||||
eventId: string,
|
||||
timeout: number | null | undefined,
|
||||
input: any,
|
||||
) {
|
||||
const ensureResultTypeShape = (
|
||||
result: void | T.ActionInput | T.ActionResult | null,
|
||||
): { result: any } => {
|
||||
return { result }
|
||||
}
|
||||
const callbacks = this.callbacks?.child(procedure)
|
||||
const effects = makeEffects({
|
||||
eventId,
|
||||
callbacks,
|
||||
})
|
||||
|
||||
return (async () => {
|
||||
switch (procedure) {
|
||||
case "/backup/create":
|
||||
return system.createBackup(effects, timeout || null)
|
||||
default:
|
||||
const procedures = unNestPath(procedure)
|
||||
switch (true) {
|
||||
case procedures[1] === "actions" && procedures[3] === "getInput":
|
||||
return system.getActionInput(
|
||||
effects,
|
||||
procedures[2],
|
||||
timeout || null,
|
||||
)
|
||||
case procedures[1] === "actions" && procedures[3] === "run":
|
||||
return system.runAction(
|
||||
effects,
|
||||
procedures[2],
|
||||
input.input,
|
||||
timeout || null,
|
||||
)
|
||||
}
|
||||
}
|
||||
})().then(ensureResultTypeShape, (error) =>
|
||||
matches(error)
|
||||
.when(
|
||||
object({
|
||||
error: string,
|
||||
code: number.defaultTo(0),
|
||||
}),
|
||||
(error) => ({
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.error,
|
||||
},
|
||||
}),
|
||||
)
|
||||
.defaultToLazy(() => ({
|
||||
error: {
|
||||
code: 0,
|
||||
message: String(error),
|
||||
},
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as cp from "child_process"
|
||||
import { SubContainer, types as T } from "@start9labs/start-sdk"
|
||||
import { promisify } from "util"
|
||||
import { DockerProcedure, VolumeId } from "../../../Models/DockerProcedure"
|
||||
import { Volume } from "./matchVolume"
|
||||
import {
|
||||
CommandOptions,
|
||||
ExecOptions,
|
||||
SubContainerOwned,
|
||||
} from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||
import { Mounts } from "@start9labs/start-sdk/package/lib/mainFn/Mounts"
|
||||
import { Manifest } from "@start9labs/start-sdk/base/lib/osBindings"
|
||||
import { BackupEffects } from "@start9labs/start-sdk/package/lib/backup/Backups"
|
||||
import { Drop } from "@start9labs/start-sdk/package/lib/util"
|
||||
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
||||
export const exec = promisify(cp.exec)
|
||||
export const execFile = promisify(cp.execFile)
|
||||
|
||||
export class DockerProcedureContainer extends Drop {
|
||||
private constructor(
|
||||
private readonly subcontainer: SubContainer<SDKManifest>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
static async of(
|
||||
effects: T.Effects,
|
||||
packageId: string,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
name: string,
|
||||
options: { subcontainer?: SubContainer<SDKManifest> } = {},
|
||||
) {
|
||||
const subcontainer =
|
||||
options?.subcontainer ??
|
||||
(await DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
packageId,
|
||||
data,
|
||||
volumes,
|
||||
name,
|
||||
))
|
||||
return new DockerProcedureContainer(subcontainer)
|
||||
}
|
||||
static async createSubContainer(
|
||||
effects: T.Effects,
|
||||
packageId: string,
|
||||
data: DockerProcedure,
|
||||
volumes: { [id: VolumeId]: Volume },
|
||||
name: string,
|
||||
) {
|
||||
const subcontainer = await SubContainerOwned.of(
|
||||
effects as BackupEffects,
|
||||
{ imageId: data.image },
|
||||
null,
|
||||
name,
|
||||
)
|
||||
|
||||
if (data.mounts) {
|
||||
const mounts = data.mounts
|
||||
for (const mount in mounts) {
|
||||
const path = mounts[mount].startsWith("/")
|
||||
? `${subcontainer.rootfs}${mounts[mount]}`
|
||||
: `${subcontainer.rootfs}/${mounts[mount]}`
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
const volumeMount = volumes[mount]
|
||||
if (volumeMount.type === "data") {
|
||||
await subcontainer.mount(
|
||||
Mounts.of().mountVolume({
|
||||
volumeId: mount,
|
||||
subpath: null,
|
||||
mountpoint: mounts[mount],
|
||||
readonly: false,
|
||||
}),
|
||||
)
|
||||
} else if (volumeMount.type === "assets") {
|
||||
await subcontainer.mount(
|
||||
Mounts.of().mountAssets({
|
||||
subpath: mount,
|
||||
mountpoint: mounts[mount],
|
||||
}),
|
||||
)
|
||||
} else if (volumeMount.type === "certificate") {
|
||||
const hostnames = [
|
||||
`${packageId}.embassy`,
|
||||
...new Set(
|
||||
Object.values(
|
||||
(
|
||||
await effects.getHostInfo({
|
||||
hostId: volumeMount["interface-id"],
|
||||
})
|
||||
)?.hostnameInfo || {},
|
||||
)
|
||||
.flatMap((h) => h)
|
||||
.flatMap((h) => (h.kind === "onion" ? [h.hostname.value] : [])),
|
||||
).values(),
|
||||
]
|
||||
const certChain = await effects.getSslCertificate({
|
||||
hostnames,
|
||||
})
|
||||
const key = await effects.getSslKey({
|
||||
hostnames,
|
||||
})
|
||||
await fs.writeFile(
|
||||
`${path}/${volumeMount["interface-id"]}.cert.pem`,
|
||||
certChain.join("\n"),
|
||||
)
|
||||
await fs.writeFile(
|
||||
`${path}/${volumeMount["interface-id"]}.key.pem`,
|
||||
key,
|
||||
)
|
||||
} else if (volumeMount.type === "pointer") {
|
||||
await effects.mount({
|
||||
location: path,
|
||||
target: {
|
||||
packageId: volumeMount["package-id"],
|
||||
subpath: volumeMount.path,
|
||||
readonly: volumeMount.readonly,
|
||||
volumeId: volumeMount["volume-id"],
|
||||
filetype: "directory",
|
||||
},
|
||||
})
|
||||
} else if (volumeMount.type === "backup") {
|
||||
await subcontainer.mount(
|
||||
Mounts.of().mountBackups({
|
||||
subpath: null,
|
||||
mountpoint: mounts[mount],
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return subcontainer
|
||||
}
|
||||
|
||||
async exec(
|
||||
commands: string[],
|
||||
options?: CommandOptions & ExecOptions,
|
||||
timeoutMs?: number | null,
|
||||
) {
|
||||
try {
|
||||
return await this.subcontainer.exec(commands, options, timeoutMs)
|
||||
} finally {
|
||||
await this.subcontainer.destroy?.()
|
||||
}
|
||||
}
|
||||
|
||||
async execFail(
|
||||
commands: string[],
|
||||
timeoutMs: number | null,
|
||||
options?: CommandOptions & ExecOptions,
|
||||
) {
|
||||
try {
|
||||
const res = await this.subcontainer.exec(commands, options, timeoutMs)
|
||||
if (res.exitCode !== 0) {
|
||||
const codeOrSignal =
|
||||
res.exitCode !== null
|
||||
? `code ${res.exitCode}`
|
||||
: `signal ${res.exitSignal}`
|
||||
throw new Error(
|
||||
`Process exited with ${codeOrSignal}: ${res.stderr.toString()}`,
|
||||
)
|
||||
}
|
||||
return res
|
||||
} finally {
|
||||
await this.subcontainer.destroy?.()
|
||||
}
|
||||
}
|
||||
|
||||
// async spawn(commands: string[]): Promise<cp.ChildProcess> {
|
||||
// return await this.subcontainer.spawn(commands)
|
||||
// }
|
||||
|
||||
onDrop(): void {
|
||||
this.subcontainer.destroy?.()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import { polyfillEffects } from "./polyfillEffects"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import { SystemForEmbassy } from "."
|
||||
import { T, utils } from "@start9labs/start-sdk"
|
||||
import { Daemon } from "@start9labs/start-sdk/package/lib/mainFn/Daemon"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
import { off } from "node:process"
|
||||
import { CommandController } from "@start9labs/start-sdk/package/lib/mainFn/CommandController"
|
||||
import { SDKManifest } from "@start9labs/start-sdk/base/lib/types"
|
||||
import { SubContainerRc } from "@start9labs/start-sdk/package/lib/util/SubContainer"
|
||||
|
||||
const EMBASSY_HEALTH_INTERVAL = 15 * 1000
|
||||
const EMBASSY_PROPERTIES_LOOP = 30 * 1000
|
||||
/**
|
||||
* We wanted something to represent what the main loop is doing, and
|
||||
* in this case it used to run the properties, health, and the docker/ js main.
|
||||
* Also, this has an ability to clean itself up too if need be.
|
||||
*/
|
||||
export class MainLoop {
|
||||
private subcontainerRc?: SubContainerRc<SDKManifest>
|
||||
get mainSubContainerHandle() {
|
||||
this.subcontainerRc =
|
||||
this.subcontainerRc ??
|
||||
this.mainEvent?.daemon?.subcontainerRc() ??
|
||||
undefined
|
||||
return this.subcontainerRc
|
||||
}
|
||||
private healthLoops?: {
|
||||
name: string
|
||||
interval: NodeJS.Timeout
|
||||
}[]
|
||||
|
||||
private mainEvent?: {
|
||||
daemon: Daemon<SDKManifest>
|
||||
}
|
||||
|
||||
private constructor(
|
||||
readonly system: SystemForEmbassy,
|
||||
readonly effects: Effects,
|
||||
) {}
|
||||
|
||||
static async of(
|
||||
system: SystemForEmbassy,
|
||||
effects: Effects,
|
||||
): Promise<MainLoop> {
|
||||
const res = new MainLoop(system, effects)
|
||||
res.healthLoops = res.constructHealthLoops()
|
||||
res.mainEvent = await res.constructMainEvent()
|
||||
return res
|
||||
}
|
||||
|
||||
private async constructMainEvent() {
|
||||
const { system, effects } = this
|
||||
const currentCommand: [string, ...string[]] = [
|
||||
system.manifest.main.entrypoint,
|
||||
...system.manifest.main.args,
|
||||
]
|
||||
|
||||
await this.setupInterfaces(effects)
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
const jsMain = (this.system.moduleCode as any)?.jsMain
|
||||
if (jsMain) {
|
||||
throw new Error("Unreachable")
|
||||
}
|
||||
const subcontainer = await DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
this.system.manifest.id,
|
||||
this.system.manifest.main,
|
||||
this.system.manifest.volumes,
|
||||
`Main - ${currentCommand.join(" ")}`,
|
||||
)
|
||||
const daemon = await Daemon.of()(this.effects, subcontainer, {
|
||||
command: currentCommand,
|
||||
runAsInit: true,
|
||||
env: {
|
||||
TINI_SUBREAPER: "true",
|
||||
},
|
||||
sigtermTimeout: utils.inMs(this.system.manifest.main["sigterm-timeout"]),
|
||||
})
|
||||
|
||||
daemon.start()
|
||||
return {
|
||||
daemon,
|
||||
}
|
||||
}
|
||||
|
||||
private async setupInterfaces(effects: T.Effects) {
|
||||
for (const interfaceId in this.system.manifest.interfaces) {
|
||||
const iface = this.system.manifest.interfaces[interfaceId]
|
||||
const internalPorts = new Set<number>()
|
||||
for (const port of Object.values(
|
||||
iface["tor-config"]?.["port-mapping"] || {},
|
||||
)) {
|
||||
internalPorts.add(parseInt(port))
|
||||
}
|
||||
for (const port of Object.values(iface["lan-config"] || {})) {
|
||||
internalPorts.add(port.internal)
|
||||
}
|
||||
for (const internalPort of internalPorts) {
|
||||
const torConf = Object.entries(
|
||||
iface["tor-config"]?.["port-mapping"] || {},
|
||||
)
|
||||
.map(([external, internal]) => ({
|
||||
internal: parseInt(internal),
|
||||
external: parseInt(external),
|
||||
}))
|
||||
.find((conf) => conf.internal == internalPort)
|
||||
const lanConf = Object.entries(iface["lan-config"] || {})
|
||||
.map(([external, conf]) => ({
|
||||
external: parseInt(external),
|
||||
...conf,
|
||||
}))
|
||||
.find((conf) => conf.internal == internalPort)
|
||||
await effects.bind({
|
||||
id: interfaceId,
|
||||
internalPort,
|
||||
preferredExternalPort: torConf?.external || internalPort,
|
||||
secure: null,
|
||||
addSsl: lanConf?.ssl
|
||||
? {
|
||||
preferredExternalPort: lanConf.external,
|
||||
alpn: { specified: ["http/1.1"] },
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async clean(options?: { timeout?: number }) {
|
||||
const { mainEvent, healthLoops } = this
|
||||
const main = await mainEvent
|
||||
delete this.mainEvent
|
||||
delete this.healthLoops
|
||||
await main?.daemon
|
||||
.stop()
|
||||
.catch((e: unknown) => console.error(`Main loop error`, utils.asError(e)))
|
||||
this.effects.setMainStatus({ status: "stopped" })
|
||||
if (healthLoops) healthLoops.forEach((x) => clearInterval(x.interval))
|
||||
}
|
||||
|
||||
private constructHealthLoops() {
|
||||
const { manifest } = this.system
|
||||
const effects = this.effects
|
||||
const start = Date.now()
|
||||
return Object.entries(manifest["health-checks"]).map(
|
||||
([healthId, value]) => {
|
||||
effects
|
||||
.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "starting",
|
||||
message: null,
|
||||
})
|
||||
.catch((e) => console.error(utils.asError(e)))
|
||||
const interval = setInterval(async () => {
|
||||
const actionProcedure = value
|
||||
const timeChanged = Date.now() - start
|
||||
if (actionProcedure.type === "docker") {
|
||||
const subcontainer = actionProcedure.inject
|
||||
? this.mainSubContainerHandle
|
||||
: undefined
|
||||
const commands = [
|
||||
actionProcedure.entrypoint,
|
||||
...actionProcedure.args,
|
||||
]
|
||||
const container = await DockerProcedureContainer.of(
|
||||
effects,
|
||||
manifest.id,
|
||||
actionProcedure,
|
||||
manifest.volumes,
|
||||
`Health Check - ${commands.join(" ")}`,
|
||||
{
|
||||
subcontainer,
|
||||
},
|
||||
)
|
||||
const env: Record<string, string> = actionProcedure.inject
|
||||
? {
|
||||
HOME: "/root",
|
||||
}
|
||||
: {}
|
||||
const executed = await container.exec(commands, {
|
||||
input: JSON.stringify(timeChanged),
|
||||
env,
|
||||
})
|
||||
|
||||
if (executed.exitCode === 0) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message: actionProcedure["success-message"] ?? null,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode === 59) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "disabled",
|
||||
message:
|
||||
executed.stderr.toString() || executed.stdout.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode === 60) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "starting",
|
||||
message:
|
||||
executed.stderr.toString() || executed.stdout.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode === 61) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "loading",
|
||||
message:
|
||||
executed.stderr.toString() || executed.stdout.toString(),
|
||||
})
|
||||
return
|
||||
}
|
||||
const errorMessage = executed.stderr.toString()
|
||||
const message = executed.stdout.toString()
|
||||
if (!!errorMessage) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: errorMessage,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (executed.exitCode && executed.exitCode > 0) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message:
|
||||
executed.stderr.toString() ||
|
||||
executed.stdout.toString() ||
|
||||
`Program exited with code ${executed.exitCode}:`,
|
||||
})
|
||||
return
|
||||
}
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
actionProcedure
|
||||
const moduleCode = await this.system.moduleCode
|
||||
const method = moduleCode.health?.[healthId]
|
||||
if (!method) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: `Expecting that the js health check ${healthId} exists`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const result = await method(
|
||||
polyfillEffects(effects, this.system.manifest),
|
||||
timeChanged,
|
||||
)
|
||||
|
||||
if ("result" in result) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "success",
|
||||
message: null,
|
||||
})
|
||||
return
|
||||
}
|
||||
if ("error" in result) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: result.error,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!("error-code" in result)) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: `Unknown error type ${JSON.stringify(result)}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const [code, message] = result["error-code"]
|
||||
if (code === 59) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "disabled",
|
||||
message,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (code === 60) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "starting",
|
||||
message,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (code === 61) {
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "loading",
|
||||
message,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await effects.setHealth({
|
||||
id: healthId,
|
||||
name: value.name,
|
||||
result: "failure",
|
||||
message: `${result["error-code"][0]}: ${result["error-code"][1]}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
}, EMBASSY_HEALTH_INTERVAL)
|
||||
|
||||
return { name: healthId, interval }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
export default {
|
||||
"peer-tor-address": {
|
||||
name: "Peer Tor Address",
|
||||
description: "The Tor address of the peer interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "bitcoind",
|
||||
target: "tor-address",
|
||||
interface: "peer",
|
||||
},
|
||||
"rpc-tor-address": {
|
||||
name: "RPC Tor Address",
|
||||
description: "The Tor address of the RPC interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "bitcoind",
|
||||
target: "tor-address",
|
||||
interface: "rpc",
|
||||
},
|
||||
rpc: {
|
||||
type: "object",
|
||||
name: "RPC Settings",
|
||||
description: "RPC configuration options.",
|
||||
spec: {
|
||||
enable: {
|
||||
type: "boolean",
|
||||
name: "Enable",
|
||||
description: "Allow remote RPC requests.",
|
||||
default: true,
|
||||
},
|
||||
username: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "Username",
|
||||
description: "The username for connecting to Bitcoin over RPC.",
|
||||
warning:
|
||||
"You will need to restart all services that depend on Bitcoin.",
|
||||
default: "bitcoin",
|
||||
masked: true,
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
"pattern-description": "Must be alphanumeric (can contain underscore).",
|
||||
},
|
||||
password: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "RPC Password",
|
||||
description: "The password for connecting to Bitcoin over RPC.",
|
||||
warning:
|
||||
"You will need to restart all services that depend on Bitcoin.",
|
||||
default: {
|
||||
charset: "a-z,2-7",
|
||||
len: 20,
|
||||
},
|
||||
pattern: "^[a-zA-Z0-9_]+$",
|
||||
"pattern-description": "Must be alphanumeric (can contain underscore).",
|
||||
copyable: true,
|
||||
masked: true,
|
||||
},
|
||||
advanced: {
|
||||
type: "object",
|
||||
name: "Advanced",
|
||||
description: "Advanced RPC Settings",
|
||||
spec: {
|
||||
auth: {
|
||||
name: "Authorization",
|
||||
description:
|
||||
"Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.",
|
||||
type: "list",
|
||||
subtype: "string",
|
||||
default: [],
|
||||
spec: {
|
||||
pattern: "^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$",
|
||||
"pattern-description":
|
||||
'Each item must be of the form "<USERNAME>:<SALT>$<HASH>".',
|
||||
},
|
||||
range: "[0,*)",
|
||||
},
|
||||
servertimeout: {
|
||||
name: "Rpc Server Timeout",
|
||||
description:
|
||||
"Number of seconds after which an uncompleted RPC call will time out.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[5,300]",
|
||||
integral: true,
|
||||
units: "seconds",
|
||||
default: 30,
|
||||
},
|
||||
threads: {
|
||||
name: "Threads",
|
||||
description:
|
||||
"Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
default: 16,
|
||||
range: "[1,64]",
|
||||
integral: true,
|
||||
units: undefined,
|
||||
},
|
||||
workqueue: {
|
||||
name: "Work Queue",
|
||||
description:
|
||||
"Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
default: 128,
|
||||
range: "[8,256]",
|
||||
integral: true,
|
||||
units: "requests",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"zmq-enabled": {
|
||||
type: "boolean",
|
||||
name: "ZeroMQ Enabled",
|
||||
description:
|
||||
"The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data",
|
||||
default: true,
|
||||
},
|
||||
txindex: {
|
||||
type: "boolean",
|
||||
name: "Transaction Index",
|
||||
description:
|
||||
"By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `gettransaction`.",
|
||||
default: true,
|
||||
},
|
||||
coinstatsindex: {
|
||||
type: "boolean",
|
||||
name: "Coinstats Index",
|
||||
description:
|
||||
"Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space",
|
||||
default: false,
|
||||
},
|
||||
wallet: {
|
||||
type: "object",
|
||||
name: "Wallet",
|
||||
description: "Wallet Settings",
|
||||
spec: {
|
||||
enable: {
|
||||
name: "Enable Wallet",
|
||||
description: "Load the wallet and enable wallet RPC calls.",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
avoidpartialspends: {
|
||||
name: "Avoid Partial Spends",
|
||||
description:
|
||||
"Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
discardfee: {
|
||||
name: "Discard Change Tolerance",
|
||||
description:
|
||||
"The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
default: 0.0001,
|
||||
range: "[0,.01]",
|
||||
integral: false,
|
||||
units: "BTC/kB",
|
||||
},
|
||||
},
|
||||
},
|
||||
advanced: {
|
||||
type: "object",
|
||||
name: "Advanced",
|
||||
description: "Advanced Settings",
|
||||
spec: {
|
||||
mempool: {
|
||||
type: "object",
|
||||
name: "Mempool",
|
||||
description: "Mempool Settings",
|
||||
spec: {
|
||||
persistmempool: {
|
||||
type: "boolean",
|
||||
name: "Persist Mempool",
|
||||
description: "Save the mempool on shutdown and load on restart.",
|
||||
default: true,
|
||||
},
|
||||
maxmempool: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Max Mempool Size",
|
||||
description:
|
||||
"Keep the transaction memory pool below <n> megabytes.",
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
default: 300,
|
||||
},
|
||||
mempoolexpiry: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Mempool Expiration",
|
||||
description:
|
||||
"Do not keep transactions in the mempool longer than <n> hours.",
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
units: "Hr",
|
||||
default: 336,
|
||||
},
|
||||
mempoolfullrbf: {
|
||||
name: "Enable Full RBF",
|
||||
description:
|
||||
"Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
},
|
||||
permitbaremultisig: {
|
||||
type: "boolean",
|
||||
name: "Permit Bare Multisig",
|
||||
description: "Relay non-P2SH multisig transactions",
|
||||
default: true,
|
||||
},
|
||||
datacarrier: {
|
||||
type: "boolean",
|
||||
name: "Relay OP_RETURN Transactions",
|
||||
description: "Relay transactions with OP_RETURN outputs",
|
||||
default: true,
|
||||
},
|
||||
datacarriersize: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Max OP_RETURN Size",
|
||||
description: "Maximum size of data in OP_RETURN outputs to relay",
|
||||
range: "[0,10000]",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 83,
|
||||
},
|
||||
},
|
||||
},
|
||||
peers: {
|
||||
type: "object",
|
||||
name: "Peers",
|
||||
description: "Peer Connection Settings",
|
||||
spec: {
|
||||
listen: {
|
||||
type: "boolean",
|
||||
name: "Make Public",
|
||||
description:
|
||||
"Allow other nodes to find your server on the network.",
|
||||
default: true,
|
||||
},
|
||||
onlyconnect: {
|
||||
type: "boolean",
|
||||
name: "Disable Peer Discovery",
|
||||
description: "Only connect to specified peers.",
|
||||
default: false,
|
||||
},
|
||||
onlyonion: {
|
||||
type: "boolean",
|
||||
name: "Disable Clearnet",
|
||||
description: "Only connect to peers over Tor.",
|
||||
default: false,
|
||||
},
|
||||
v2transport: {
|
||||
type: "boolean",
|
||||
name: "Use V2 P2P Transport Protocol",
|
||||
description:
|
||||
"Enable or disable the use of BIP324 V2 P2P transport protocol.",
|
||||
default: false,
|
||||
},
|
||||
addnode: {
|
||||
name: "Add Nodes",
|
||||
description: "Add addresses of nodes to connect to.",
|
||||
type: "list",
|
||||
subtype: "object",
|
||||
range: "[0,*)",
|
||||
default: [],
|
||||
spec: {
|
||||
spec: {
|
||||
hostname: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "Hostname",
|
||||
description: "Domain or IP address of bitcoin peer",
|
||||
pattern:
|
||||
"(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))",
|
||||
"pattern-description":
|
||||
"Must be either a domain name, or an IPv4 or IPv6 address. Do not include protocol scheme (eg 'http://') or port.",
|
||||
},
|
||||
port: {
|
||||
type: "number",
|
||||
nullable: true,
|
||||
name: "Port",
|
||||
description:
|
||||
"Port that peer is listening on for inbound p2p connections",
|
||||
range: "[0,65535]",
|
||||
integral: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pruning: {
|
||||
type: "union",
|
||||
name: "Pruning Settings",
|
||||
description:
|
||||
"Blockchain Pruning Options\nReduce the blockchain size on disk\n",
|
||||
warning:
|
||||
"Disabling pruning will convert your node into a full archival node. This requires a resync of the entire blockchain, a process that may take several days.\n",
|
||||
tag: {
|
||||
id: "mode",
|
||||
name: "Pruning Mode",
|
||||
description:
|
||||
"- Disabled: Disable pruning\n- Automatic: Limit blockchain size on disk to a certain number of megabytes\n",
|
||||
"variant-names": {
|
||||
disabled: "Disabled",
|
||||
automatic: "Automatic",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
disabled: {},
|
||||
automatic: {
|
||||
size: {
|
||||
type: "number",
|
||||
nullable: false,
|
||||
name: "Max Chain Size",
|
||||
description: "Limit of blockchain size on disk.",
|
||||
warning:
|
||||
"Increasing this value will require re-syncing your node.",
|
||||
default: 550,
|
||||
range: "[550,1000000)",
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
},
|
||||
},
|
||||
},
|
||||
default: "disabled",
|
||||
},
|
||||
dbcache: {
|
||||
type: "number",
|
||||
nullable: true,
|
||||
name: "Database Cache",
|
||||
description:
|
||||
"How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.",
|
||||
warning:
|
||||
"WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.",
|
||||
range: "(0,*)",
|
||||
integral: true,
|
||||
units: "MiB",
|
||||
},
|
||||
blockfilters: {
|
||||
type: "object",
|
||||
name: "Block Filters",
|
||||
description: "Settings for storing and serving compact block filters",
|
||||
spec: {
|
||||
blockfilterindex: {
|
||||
type: "boolean",
|
||||
name: "Compute Compact Block Filters (BIP158)",
|
||||
description:
|
||||
"Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.",
|
||||
default: true,
|
||||
},
|
||||
peerblockfilters: {
|
||||
type: "boolean",
|
||||
name: "Serve Compact Block Filters to Peers (BIP157)",
|
||||
description:
|
||||
"Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
bloomfilters: {
|
||||
type: "object",
|
||||
name: "Bloom Filters (BIP37)",
|
||||
description: "Setting for serving Bloom Filters",
|
||||
spec: {
|
||||
peerbloomfilters: {
|
||||
type: "boolean",
|
||||
name: "Serve Bloom Filters to Peers",
|
||||
description:
|
||||
"Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.",
|
||||
warning:
|
||||
"This is ONLY for use with Bisq integration, please use Block Filters for all other applications.",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
export default {
|
||||
homepage: {
|
||||
name: "Homepage",
|
||||
description:
|
||||
"The page that will be displayed when your Start9 Pages .onion address is visited. Since this page is technically publicly accessible, you can choose to which type of page to display.",
|
||||
type: "union",
|
||||
default: "welcome",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
"variant-names": {
|
||||
welcome: "Welcome",
|
||||
index: "Table of Contents",
|
||||
"web-page": "Web Page",
|
||||
redirect: "Redirect",
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
welcome: {},
|
||||
index: {},
|
||||
"web-page": {
|
||||
source: {
|
||||
name: "Folder Location",
|
||||
description: "The service that contains your website files.",
|
||||
type: "enum",
|
||||
values: ["filebrowser", "nextcloud"],
|
||||
"value-names": {},
|
||||
default: "nextcloud",
|
||||
},
|
||||
folder: {
|
||||
type: "string",
|
||||
name: "Folder Path",
|
||||
placeholder: "e.g. websites/resume",
|
||||
description:
|
||||
'The path to the folder that contains the static files of your website. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.',
|
||||
pattern:
|
||||
"^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$",
|
||||
"pattern-description": "Must be a valid relative file path",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
redirect: {
|
||||
target: {
|
||||
type: "string",
|
||||
name: "Target Subdomain",
|
||||
description:
|
||||
"The name of the subdomain to redirect users to. This must be a valid subdomain site within your Start9 Pages.",
|
||||
pattern: "^[a-z-]+$",
|
||||
"pattern-description":
|
||||
"May contain only lowercase characters and hyphens.",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
subdomains: {
|
||||
type: "list",
|
||||
name: "Subdomains",
|
||||
description: "The websites you want to serve.",
|
||||
default: [],
|
||||
range: "[0, *)",
|
||||
subtype: "object",
|
||||
spec: {
|
||||
"unique-by": "name",
|
||||
"display-as": "{{name}}",
|
||||
spec: {
|
||||
name: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
name: "Subdomain name",
|
||||
description:
|
||||
'The subdomain of your Start9 Pages .onion address to host the website on. For example, a value of "me" would produce a website hosted at http://me.xxxxxx.onion.',
|
||||
pattern: "^[a-z-]+$",
|
||||
"pattern-description":
|
||||
"May contain only lowercase characters and hyphens",
|
||||
},
|
||||
settings: {
|
||||
type: "union",
|
||||
name: "Settings",
|
||||
description:
|
||||
"The desired behavior you want to occur when the subdomain is visited. You can either redirect to another subdomain, or load a stored web page.",
|
||||
default: "web-page",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
"variant-names": { "web-page": "Web Page", redirect: "Redirect" },
|
||||
},
|
||||
variants: {
|
||||
"web-page": {
|
||||
source: {
|
||||
name: "Folder Location",
|
||||
description: "The service that contains your website files.",
|
||||
type: "enum",
|
||||
values: ["filebrowser", "nextcloud"],
|
||||
"value-names": {},
|
||||
default: "nextcloud",
|
||||
},
|
||||
folder: {
|
||||
type: "string",
|
||||
name: "Folder Path",
|
||||
placeholder: "e.g. websites/resume",
|
||||
description:
|
||||
'The path to the folder that contains the website files. For example, a value of "projects/resume" would tell Start9 Pages to look for that folder path in the selected service.',
|
||||
pattern:
|
||||
"^(\\.|[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)(/[a-zA-Z0-9_ -][a-zA-Z0-9_ .-]*|/([a-zA-Z0-9_ .-][a-zA-Z0-9_ -]+\\.*)+)*/?$",
|
||||
"pattern-description": "Must be a valid relative file path",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
redirect: {
|
||||
target: {
|
||||
type: "string",
|
||||
name: "Target Subdomain",
|
||||
description:
|
||||
"The subdomain of your Start9 Pages .onion address to redirect to. This should be the name of another subdomain on Start9 Pages. Leave empty to redirect to the homepage.",
|
||||
pattern: "^[a-z-]+$",
|
||||
"pattern-description":
|
||||
"May contain only lowercase characters and hyphens.",
|
||||
nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
export default {
|
||||
"eos-version": "0.3.5.1",
|
||||
id: "gitea",
|
||||
"git-hash": "91fada3edf30357a2e75c281d32f8888c87fcc2d\n",
|
||||
title: "Gitea",
|
||||
version: "1.22.0",
|
||||
description: {
|
||||
short: "A painless self-hosted Git service.",
|
||||
long: "Gitea is a community managed lightweight code hosting solution written in Go. It is published under the MIT license.\n",
|
||||
},
|
||||
assets: {
|
||||
license: "LICENSE",
|
||||
instructions: "instructions.md",
|
||||
icon: "icon.png",
|
||||
"docker-images": null,
|
||||
assets: null,
|
||||
scripts: null,
|
||||
},
|
||||
build: ["make"],
|
||||
"release-notes":
|
||||
"* Upstream code update\n* Fix deprecated config options\n* Full list of upstream changes available [here](https://github.com/go-gitea/gitea/compare/v1.21.8...v1.22.0)\n",
|
||||
license: "MIT",
|
||||
"wrapper-repo": "https://github.com/Start9Labs/gitea-startos",
|
||||
"upstream-repo": "https://github.com/go-gitea/gitea",
|
||||
"support-site": "https://docs.gitea.io/en-us/",
|
||||
"marketing-site": "https://gitea.io/en-us/",
|
||||
"donation-url": null,
|
||||
alerts: {
|
||||
install: null,
|
||||
uninstall: null,
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: null,
|
||||
},
|
||||
main: {
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "/usr/local/bin/docker_entrypoint.sh",
|
||||
args: [],
|
||||
inject: false,
|
||||
mounts: { main: "/data" },
|
||||
"io-format": null,
|
||||
"sigterm-timeout": null,
|
||||
"shm-size-mb": null,
|
||||
"gpu-acceleration": false,
|
||||
},
|
||||
"health-checks": {
|
||||
"user-signups-off": {
|
||||
name: "User Signups Off",
|
||||
"success-message": null,
|
||||
type: "script",
|
||||
args: [],
|
||||
timeout: null,
|
||||
},
|
||||
web: {
|
||||
name: "Web & Git HTTP Tor Interfaces",
|
||||
"success-message":
|
||||
"Gitea is ready to be visited in a web browser and git can be used with SSH over TOR.",
|
||||
type: "script",
|
||||
args: [],
|
||||
timeout: null,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
get: { type: "script", args: [] },
|
||||
set: { type: "script", args: [] },
|
||||
},
|
||||
properties: { type: "script", args: [] },
|
||||
volumes: { main: { type: "data" } },
|
||||
interfaces: {
|
||||
main: {
|
||||
name: "Web UI / Git HTTPS/SSH",
|
||||
description:
|
||||
"Port 80: Browser Interface and HTTP Git Interface / Port 22: Git SSH Interface",
|
||||
"tor-config": { "port-mapping": { "22": "22", "80": "3000" } },
|
||||
"lan-config": { "443": { ssl: true, internal: 3000 } },
|
||||
ui: true,
|
||||
protocols: ["tcp", "http", "ssh", "git"],
|
||||
},
|
||||
},
|
||||
backup: {
|
||||
create: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "create", "/mnt/backup", "/root/data"],
|
||||
inject: false,
|
||||
mounts: { BACKUP: "/mnt/backup", main: "/root/data" },
|
||||
"io-format": "yaml",
|
||||
"sigterm-timeout": null,
|
||||
"shm-size-mb": null,
|
||||
"gpu-acceleration": false,
|
||||
},
|
||||
restore: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "restore", "/mnt/backup", "/root/data"],
|
||||
inject: false,
|
||||
mounts: { BACKUP: "/mnt/backup", main: "/root/data" },
|
||||
"io-format": "yaml",
|
||||
"sigterm-timeout": null,
|
||||
"shm-size-mb": null,
|
||||
"gpu-acceleration": false,
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
from: { "*": { type: "script", args: ["from"] } },
|
||||
to: { "*": { type: "script", args: ["to"] } },
|
||||
},
|
||||
actions: {},
|
||||
dependencies: {},
|
||||
containers: null,
|
||||
replaces: [],
|
||||
"hardware-requirements": {
|
||||
device: {},
|
||||
ram: null,
|
||||
arch: ["x86_64", "aarch64"],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
"tor-address": {
|
||||
name: "Tor Address",
|
||||
description: "The Tor address of the network interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr-wallet-connect",
|
||||
target: "tor-address",
|
||||
interface: "main",
|
||||
},
|
||||
"lan-address": {
|
||||
name: "LAN Address",
|
||||
description: "The LAN address of the network interface",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr-wallet-connect",
|
||||
target: "lan-address",
|
||||
interface: "main",
|
||||
},
|
||||
"nostr-relay": {
|
||||
type: "string",
|
||||
name: "Nostr Relay",
|
||||
default: "wss://relay.getalby.com/v1",
|
||||
description: "The Nostr Relay to use for Nostr Wallet Connect connections",
|
||||
copyable: true,
|
||||
nullable: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
export default {
|
||||
"tor-address": {
|
||||
name: "Tor Address",
|
||||
description: "The Tor address for the websocket server.",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr",
|
||||
target: "tor-address",
|
||||
interface: "websocket",
|
||||
},
|
||||
"lan-address": {
|
||||
name: "Tor Address",
|
||||
description: "The LAN address for the websocket server.",
|
||||
type: "pointer",
|
||||
subtype: "package",
|
||||
"package-id": "nostr",
|
||||
target: "lan-address",
|
||||
interface: "websocket",
|
||||
},
|
||||
"relay-type": {
|
||||
type: "union",
|
||||
name: "Relay Type",
|
||||
warning:
|
||||
"Running a public relay carries risk. Your relay can be spammed, resulting in large amounts of disk usage.",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Relay Type",
|
||||
description:
|
||||
"Private or public. A private relay (highly recommended) restricts write access to specific pubkeys. Anyone can write to a public relay.",
|
||||
"variant-names": { private: "Private", public: "Public" },
|
||||
},
|
||||
default: "private",
|
||||
variants: {
|
||||
private: {
|
||||
pubkey_whitelist: {
|
||||
name: "Pubkey Whitelist (hex)",
|
||||
description:
|
||||
"A list of pubkeys that are permitted to publish through your relay. A minimum, you need to enter your own Nostr hex (not npub) pubkey. Go to https://damus.io/key/ to convert from npub to hex.",
|
||||
type: "list",
|
||||
range: "[1,*)",
|
||||
subtype: "string",
|
||||
spec: {
|
||||
placeholder: "hex (not npub) pubkey",
|
||||
pattern: "[0-9a-fA-F]{64}",
|
||||
"pattern-description":
|
||||
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||
},
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
public: {
|
||||
info: {
|
||||
name: "Relay Info",
|
||||
description: "General public info about your relay",
|
||||
type: "object",
|
||||
spec: {
|
||||
name: {
|
||||
name: "Relay Name",
|
||||
description: "Your relay's human-readable identifier",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
placeholder: "Bob's Public Relay",
|
||||
pattern: ".{3,32}",
|
||||
"pattern-description":
|
||||
"Must be at least 3 character and no more than 32 characters",
|
||||
masked: false,
|
||||
},
|
||||
description: {
|
||||
name: "Relay Description",
|
||||
description: "A more detailed description for your relay",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
placeholder: "The best relay in town",
|
||||
pattern: ".{6,256}",
|
||||
"pattern-description":
|
||||
"Must be at least 6 character and no more than 256 characters",
|
||||
masked: false,
|
||||
},
|
||||
pubkey: {
|
||||
name: "Admin contact pubkey (hex)",
|
||||
description:
|
||||
"The Nostr hex (not npub) pubkey of the relay administrator",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
placeholder: "hex (not npub) pubkey",
|
||||
pattern: "[0-9a-fA-F]{64}",
|
||||
"pattern-description":
|
||||
"Must be a valid 64-digit hexadecimal value (ie a Nostr hex pubkey, not an npub). Go to https://damus.io/key/ to convert npub to hex.",
|
||||
masked: false,
|
||||
},
|
||||
contact: {
|
||||
name: "Admin contact email",
|
||||
description: "The email address of the relay administrator",
|
||||
type: "string",
|
||||
nullable: true,
|
||||
pattern: "[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+",
|
||||
"pattern-description": "Must be a valid email address.",
|
||||
masked: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
limits: {
|
||||
name: "Limits",
|
||||
description:
|
||||
"Data limits to protect your relay from using too many resources",
|
||||
type: "object",
|
||||
spec: {
|
||||
messages_per_sec: {
|
||||
name: "Messages Per Second Limit",
|
||||
description:
|
||||
"Limit events created per second, averaged over one minute. Note: this is for the server as a whole, not per connection.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
default: 2,
|
||||
units: "messages/sec",
|
||||
},
|
||||
subscriptions_per_min: {
|
||||
name: "Subscriptions Per Minute Limit",
|
||||
description:
|
||||
"Limit client subscriptions created per second, averaged over one minute. Strongly recommended to set this to a low value such as 10 to ensure fair service.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[1,*)",
|
||||
integral: true,
|
||||
default: 10,
|
||||
units: "subscriptions",
|
||||
},
|
||||
max_blocking_threads: {
|
||||
name: "Max Blocking Threads",
|
||||
description:
|
||||
"Maximum number of blocking threads used for database connections.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "threads",
|
||||
default: 16,
|
||||
},
|
||||
max_event_bytes: {
|
||||
name: "Max Event Size",
|
||||
description:
|
||||
"Limit the maximum size of an EVENT message. Set to 0 for unlimited",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 131072,
|
||||
},
|
||||
max_ws_message_bytes: {
|
||||
name: "Max Websocket Message Size",
|
||||
description: "Maximum WebSocket message in bytes.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 131072,
|
||||
},
|
||||
max_ws_frame_bytes: {
|
||||
name: "Max Websocket Frame Size",
|
||||
description: "Maximum WebSocket frame size in bytes.",
|
||||
type: "number",
|
||||
nullable: false,
|
||||
range: "[0,*)",
|
||||
integral: true,
|
||||
units: "bytes",
|
||||
default: 131072,
|
||||
},
|
||||
event_kind_blacklist: {
|
||||
name: "Event Kind Blacklist",
|
||||
description:
|
||||
"Events with these kinds will be discarded. For a list of event kinds, see here: https://github.com/nostr-protocol/nips#event-kinds",
|
||||
type: "list",
|
||||
range: "[0,*)",
|
||||
subtype: "number",
|
||||
spec: { integral: true, placeholder: 30023, range: "(0,100000]" },
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
export default {
|
||||
nodes: {
|
||||
type: "list",
|
||||
subtype: "union",
|
||||
name: "Lightning Nodes",
|
||||
description: "List of Lightning Network node instances to manage",
|
||||
range: "[1,*)",
|
||||
default: ["lnd"],
|
||||
spec: {
|
||||
type: "string",
|
||||
"display-as": "{{name}}",
|
||||
"unique-by": "name",
|
||||
name: "Node Implementation",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
description:
|
||||
"- LND: Lightning Network Daemon from Lightning Labs\n- CLN: Core Lightning from Blockstream\n",
|
||||
"variant-names": {
|
||||
lnd: "Lightning Network Daemon (LND)",
|
||||
"c-lightning": "Core Lightning (CLN)",
|
||||
},
|
||||
},
|
||||
default: "lnd",
|
||||
variants: {
|
||||
lnd: {
|
||||
name: {
|
||||
type: "string",
|
||||
name: "Node Name",
|
||||
description: "Name of this node in the list",
|
||||
default: "StartOS LND",
|
||||
nullable: false,
|
||||
},
|
||||
"connection-settings": {
|
||||
type: "union",
|
||||
name: "Connection Settings",
|
||||
description: "The Lightning Network Daemon node to connect to.",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
description:
|
||||
"- Internal: The Lightning Network Daemon service installed to your StartOS server.\n- External: A Lightning Network Daemon instance running on a remote device (advanced).\n",
|
||||
"variant-names": {
|
||||
internal: "Internal",
|
||||
external: "External",
|
||||
},
|
||||
},
|
||||
default: "internal",
|
||||
variants: {
|
||||
internal: {},
|
||||
external: {
|
||||
address: {
|
||||
type: "string",
|
||||
name: "Public Address",
|
||||
description:
|
||||
"The public address of your LND REST server\nNOTE: RTL does not support a .onion URL here\n",
|
||||
nullable: false,
|
||||
},
|
||||
"rest-port": {
|
||||
type: "number",
|
||||
name: "REST Port",
|
||||
description:
|
||||
"The port that your Lightning Network Daemon REST server is bound to",
|
||||
nullable: false,
|
||||
range: "[0,65535]",
|
||||
integral: true,
|
||||
default: 8080,
|
||||
},
|
||||
macaroon: {
|
||||
type: "string",
|
||||
name: "Macaroon",
|
||||
description:
|
||||
'Your admin.macaroon file, Base64URL encoded. This is the same as the value after "macaroon=" in your lndconnect URL.',
|
||||
nullable: false,
|
||||
masked: true,
|
||||
pattern: "[=A-Za-z0-9_-]+",
|
||||
"pattern-description":
|
||||
"Macaroon must be encoded in Base64URL format (only A-Z, a-z, 0-9, _, - and = allowed)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"c-lightning": {
|
||||
name: {
|
||||
type: "string",
|
||||
name: "Node Name",
|
||||
description: "Name of this node in the list",
|
||||
default: "StartOS CLN",
|
||||
nullable: false,
|
||||
},
|
||||
"connection-settings": {
|
||||
type: "union",
|
||||
name: "Connection Settings",
|
||||
description: "The Core Lightning (CLN) node to connect to.",
|
||||
tag: {
|
||||
id: "type",
|
||||
name: "Type",
|
||||
description:
|
||||
"- Internal: The Core Lightning (CLN) service installed to your StartOS server.\n- External: A Core Lightning (CLN) instance running on a remote device (advanced).\n",
|
||||
"variant-names": {
|
||||
internal: "Internal",
|
||||
external: "External",
|
||||
},
|
||||
},
|
||||
default: "internal",
|
||||
variants: {
|
||||
internal: {},
|
||||
external: {
|
||||
address: {
|
||||
type: "string",
|
||||
name: "Public Address",
|
||||
description:
|
||||
"The public address of your CLNRest server\nNOTE: RTL does not support a .onion URL here\n",
|
||||
nullable: false,
|
||||
},
|
||||
"rest-port": {
|
||||
type: "number",
|
||||
name: "CLNRest Port",
|
||||
description: "The port that your CLNRest server is bound to",
|
||||
nullable: false,
|
||||
range: "[0,65535]",
|
||||
integral: true,
|
||||
default: 3010,
|
||||
},
|
||||
macaroon: {
|
||||
type: "string",
|
||||
name: "Rune",
|
||||
description:
|
||||
"Your CLNRest unrestricted Rune, Base64URL encoded.",
|
||||
nullable: false,
|
||||
masked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
password: {
|
||||
type: "string",
|
||||
name: "Password",
|
||||
description: "The password for your Ride the Lightning dashboard",
|
||||
nullable: false,
|
||||
copyable: true,
|
||||
masked: true,
|
||||
default: {
|
||||
charset: "a-z,A-Z,0-9",
|
||||
len: 22,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
"instance-name": {
|
||||
type: "string",
|
||||
name: "SearXNG Instance Name",
|
||||
description:
|
||||
"Enter a name for your SearXNG instance. This is the name that will be listed if you want to share your SearXNG engine publicly.",
|
||||
nullable: false,
|
||||
default: "My SearXNG Engine",
|
||||
placeholder: "Uncle Jim SearXNG Engine",
|
||||
},
|
||||
"tor-url": {
|
||||
name: "Enable Tor address as the base URL",
|
||||
description:
|
||||
"Activates the utilization of a .onion address as the primary URL, particularly beneficial for publicly hosted instances over the Tor network.",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
"enable-metrics": {
|
||||
name: "Enable Stats",
|
||||
description:
|
||||
"Your SearXNG instance will collect anonymous stats about its own usage and performance. You can view these metrics by appending `/stats` or `/stats/errors` to your SearXNG URL.",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
}, //,
|
||||
// "email-address": {
|
||||
// "type": "string",
|
||||
// "name": "Email Address",
|
||||
// "description": "Your Email address - required to create an SSL certificate.",
|
||||
// "nullable": false,
|
||||
// "default": "youremail@domain.com",
|
||||
// },
|
||||
// "public-host": {
|
||||
// "type": "string",
|
||||
// "name": "Public Domain Name",
|
||||
// "description": "Enter a domain name here if you want to share your SearXNG engine publicly. You will also need to modify your domain name's DNS settings to point to your Start9 server.",
|
||||
// "nullable": true,
|
||||
// "placeholder": "https://search.mydomain.com"
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
export default {
|
||||
id: "synapse",
|
||||
title: "Synapse",
|
||||
version: "1.98.0",
|
||||
"release-notes":
|
||||
"* Upstream code update\n* Synapse Admin updated to the latest version - ([full changelog](https://github.com/Awesome-Technologies/synapse-admin/compare/0.8.7...0.9.1))\n* Instructions update\n* Updated package and upstream repositories links\n* Full list of upstream changes available [here](https://github.com/element-hq/synapse/compare/v1.95.1...v1.98.0)\n",
|
||||
license: "Apache-2.0",
|
||||
"wrapper-repo": "https://github.com/Start9Labs/synapse-startos",
|
||||
"upstream-repo": "https://github.com/element-hq/synapse",
|
||||
"support-site": "https://github.com/element-hq/synapse/issues",
|
||||
"marketing-site": "https://matrix.org/",
|
||||
build: ["make"],
|
||||
description: {
|
||||
short:
|
||||
"Synapse is a battle-tested implementation of the Matrix protocol, the killer of all messaging apps.",
|
||||
long: "Synapse is the battle-tested, reference implementation of the Matrix protocol. Matrix is a next-generation, federated, full-featured, encrypted, independent messaging system. There are no trusted third parties involved. (see matrix.org for details).",
|
||||
},
|
||||
assets: {
|
||||
license: "LICENSE",
|
||||
icon: "icon.png",
|
||||
instructions: "instructions.md",
|
||||
},
|
||||
main: {
|
||||
type: "docker",
|
||||
image: "main",
|
||||
entrypoint: "docker_entrypoint.sh",
|
||||
args: [],
|
||||
mounts: {
|
||||
main: "/data",
|
||||
cert: "/mnt/cert",
|
||||
"admin-cert": "/mnt/admin-cert",
|
||||
},
|
||||
},
|
||||
"health-checks": {
|
||||
federation: {
|
||||
name: "Federation",
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "check-federation.sh",
|
||||
args: [],
|
||||
mounts: {},
|
||||
"io-format": "json",
|
||||
inject: true,
|
||||
},
|
||||
"synapse-admin": {
|
||||
name: "Admin interface",
|
||||
"success-message":
|
||||
"Synapse Admin is ready to be visited in a web browser.",
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "check-ui.sh",
|
||||
args: [],
|
||||
mounts: {},
|
||||
"io-format": "yaml",
|
||||
inject: true,
|
||||
},
|
||||
"user-signups-off": {
|
||||
name: "User Signups Off",
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "user-signups-off.sh",
|
||||
args: [],
|
||||
mounts: {},
|
||||
"io-format": "yaml",
|
||||
inject: true,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
get: {
|
||||
type: "script",
|
||||
},
|
||||
set: {
|
||||
type: "script",
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
type: "script",
|
||||
},
|
||||
volumes: {
|
||||
main: {
|
||||
type: "data",
|
||||
},
|
||||
cert: {
|
||||
type: "certificate",
|
||||
"interface-id": "main",
|
||||
},
|
||||
"admin-cert": {
|
||||
type: "certificate",
|
||||
"interface-id": "admin",
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
start:
|
||||
"After your first run, Synapse needs a little time to establish a stable TOR connection over federation. We kindly ask for your patience during this process. Remember, great things take time! 🕒",
|
||||
},
|
||||
interfaces: {
|
||||
main: {
|
||||
name: "Homeserver Address",
|
||||
description:
|
||||
"Used by clients and other servers to connect with your homeserver",
|
||||
"tor-config": {
|
||||
"port-mapping": {
|
||||
"80": "80",
|
||||
"443": "443",
|
||||
"8448": "8448",
|
||||
},
|
||||
},
|
||||
ui: false,
|
||||
protocols: ["tcp", "http", "matrix"],
|
||||
},
|
||||
admin: {
|
||||
name: "Admin Portal",
|
||||
description: "A web application for administering your Synapse server",
|
||||
"tor-config": {
|
||||
"port-mapping": {
|
||||
"80": "8080",
|
||||
"443": "4433",
|
||||
},
|
||||
},
|
||||
"lan-config": {
|
||||
"443": {
|
||||
ssl: true,
|
||||
internal: 8080,
|
||||
},
|
||||
},
|
||||
ui: true,
|
||||
protocols: ["tcp", "http"],
|
||||
},
|
||||
},
|
||||
dependencies: {},
|
||||
backup: {
|
||||
create: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "create", "/mnt/backup", "/data"],
|
||||
mounts: {
|
||||
BACKUP: "/mnt/backup",
|
||||
main: "/data",
|
||||
},
|
||||
},
|
||||
restore: {
|
||||
type: "docker",
|
||||
image: "compat",
|
||||
system: true,
|
||||
entrypoint: "compat",
|
||||
args: ["duplicity", "restore", "/mnt/backup", "/data"],
|
||||
mounts: {
|
||||
BACKUP: "/mnt/backup",
|
||||
main: "/data",
|
||||
},
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
"reset-first-user": {
|
||||
name: "Reset First User",
|
||||
description:
|
||||
"This action will reset the password of the first user in your database to a random value.",
|
||||
"allowed-statuses": ["stopped"],
|
||||
implementation: {
|
||||
type: "docker",
|
||||
image: "main",
|
||||
system: false,
|
||||
entrypoint: "docker_entrypoint.sh",
|
||||
args: ["reset-first-user"],
|
||||
mounts: {
|
||||
main: "/data",
|
||||
},
|
||||
"io-format": "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
from: {
|
||||
"*": {
|
||||
type: "script",
|
||||
args: ["from"],
|
||||
},
|
||||
},
|
||||
to: {
|
||||
"*": {
|
||||
type: "script",
|
||||
args: ["to"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
1280
container-runtime/src/Adapters/Systems/SystemForEmbassy/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { matchManifest } from "./matchManifest"
|
||||
import giteaManifest from "./__fixtures__/giteaManifest"
|
||||
import synapseManifest from "./__fixtures__/synapseManifest"
|
||||
|
||||
describe("matchManifest", () => {
|
||||
test("gittea", () => {
|
||||
matchManifest.unsafeCast(giteaManifest)
|
||||
})
|
||||
test("synapse", () => {
|
||||
matchManifest.unsafeCast(synapseManifest)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
object,
|
||||
literal,
|
||||
string,
|
||||
array,
|
||||
boolean,
|
||||
dictionary,
|
||||
literals,
|
||||
number,
|
||||
unknown,
|
||||
some,
|
||||
every,
|
||||
} from "ts-matches"
|
||||
import { matchVolume } from "./matchVolume"
|
||||
import { matchDockerProcedure } from "../../../Models/DockerProcedure"
|
||||
|
||||
const matchJsProcedure = object({
|
||||
type: literal("script"),
|
||||
args: array(unknown).nullable().optional().defaultTo([]),
|
||||
})
|
||||
|
||||
const matchProcedure = some(matchDockerProcedure, matchJsProcedure)
|
||||
export type Procedure = typeof matchProcedure._TYPE
|
||||
|
||||
const matchAction = object({
|
||||
name: string,
|
||||
description: string,
|
||||
warning: string.nullable().optional(),
|
||||
implementation: matchProcedure,
|
||||
"allowed-statuses": array(literals("running", "stopped")),
|
||||
"input-spec": unknown.nullable().optional(),
|
||||
})
|
||||
export const matchManifest = object({
|
||||
id: string,
|
||||
title: string,
|
||||
version: string,
|
||||
main: matchDockerProcedure,
|
||||
assets: object({
|
||||
assets: string.nullable().optional(),
|
||||
scripts: string.nullable().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
"health-checks": dictionary([
|
||||
string,
|
||||
every(
|
||||
matchProcedure,
|
||||
object({
|
||||
name: string,
|
||||
["success-message"]: string.nullable().optional(),
|
||||
}),
|
||||
),
|
||||
]),
|
||||
config: object({
|
||||
get: matchProcedure,
|
||||
set: matchProcedure,
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
properties: matchProcedure.nullable().optional(),
|
||||
volumes: dictionary([string, matchVolume]),
|
||||
interfaces: dictionary([
|
||||
string,
|
||||
object({
|
||||
name: string,
|
||||
description: string,
|
||||
"tor-config": object({
|
||||
"port-mapping": dictionary([string, string]),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
"lan-config": dictionary([
|
||||
string,
|
||||
object({
|
||||
ssl: boolean,
|
||||
internal: number,
|
||||
}),
|
||||
])
|
||||
.nullable()
|
||||
.optional(),
|
||||
ui: boolean,
|
||||
protocols: array(string),
|
||||
}),
|
||||
]),
|
||||
backup: object({
|
||||
create: matchProcedure,
|
||||
restore: matchProcedure,
|
||||
}),
|
||||
migrations: object({
|
||||
to: dictionary([string, matchProcedure]),
|
||||
from: dictionary([string, matchProcedure]),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
dependencies: dictionary([
|
||||
string,
|
||||
object({
|
||||
version: string,
|
||||
requirement: some(
|
||||
object({
|
||||
type: literal("opt-in"),
|
||||
how: string,
|
||||
}),
|
||||
object({
|
||||
type: literal("opt-out"),
|
||||
how: string,
|
||||
}),
|
||||
object({
|
||||
type: literal("required"),
|
||||
}),
|
||||
),
|
||||
description: string.nullable().optional(),
|
||||
config: object({
|
||||
check: matchProcedure,
|
||||
"auto-configure": matchProcedure,
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
]),
|
||||
|
||||
actions: dictionary([string, matchAction]),
|
||||
})
|
||||
export type Manifest = typeof matchManifest._TYPE
|
||||
@@ -0,0 +1,32 @@
|
||||
import { object, literal, string, boolean, some } from "ts-matches"
|
||||
|
||||
const matchDataVolume = object({
|
||||
type: literal("data"),
|
||||
readonly: boolean.optional(),
|
||||
})
|
||||
const matchAssetVolume = object({
|
||||
type: literal("assets"),
|
||||
})
|
||||
const matchPointerVolume = object({
|
||||
type: literal("pointer"),
|
||||
"package-id": string,
|
||||
"volume-id": string,
|
||||
path: string,
|
||||
readonly: boolean,
|
||||
})
|
||||
const matchCertificateVolume = object({
|
||||
type: literal("certificate"),
|
||||
"interface-id": string,
|
||||
})
|
||||
const matchBackupVolume = object({
|
||||
type: literal("backup"),
|
||||
readonly: boolean,
|
||||
})
|
||||
export const matchVolume = some(
|
||||
matchDataVolume,
|
||||
matchAssetVolume,
|
||||
matchPointerVolume,
|
||||
matchCertificateVolume,
|
||||
matchBackupVolume,
|
||||
)
|
||||
export type Volume = typeof matchVolume._TYPE
|
||||
@@ -0,0 +1,477 @@
|
||||
// deno-lint-ignore no-namespace
|
||||
export type ExpectedExports = {
|
||||
version: 2
|
||||
/** Set configuration is called after we have modified and saved the configuration in the embassy ui. Use this to make a file for the docker to read from for configuration. */
|
||||
setConfig: (effects: Effects, input: Config) => Promise<ResultType<SetResult>>
|
||||
/** Get configuration returns a shape that describes the format that the embassy ui will generate, and later send to the set config */
|
||||
getConfig: (effects: Effects) => Promise<ResultType<ConfigRes>>
|
||||
/** These are how we make sure the our dependency configurations are valid and if not how to fix them. */
|
||||
dependencies: Dependencies
|
||||
/** For backing up service data though the embassyOS UI */
|
||||
createBackup: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
/** For restoring service data that was previously backed up using the embassyOS UI create backup flow. Backup restores are also triggered via the embassyOS UI, or doing a system restore flow during setup. */
|
||||
restoreBackup: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
/** Properties are used to get values from the docker, like a username + password, what ports we are hosting from */
|
||||
properties: (effects: Effects) => Promise<ResultType<Properties>>
|
||||
health: {
|
||||
/** Should be the health check id */
|
||||
[id: string]: (
|
||||
effects: Effects,
|
||||
dateMs: number,
|
||||
) => Promise<ResultType<unknown>>
|
||||
}
|
||||
migration: (
|
||||
effects: Effects,
|
||||
version: string,
|
||||
...args: unknown[]
|
||||
) => Promise<ResultType<MigrationRes>>
|
||||
action: {
|
||||
[id: string]: (
|
||||
effects: Effects,
|
||||
config?: Config,
|
||||
) => Promise<ResultType<ActionResult>>
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the entrypoint for the main container. Used to start up something like the service that the
|
||||
* package represents, like running a bitcoind in a bitcoind-wrapper.
|
||||
*/
|
||||
main: (effects: Effects) => Promise<ResultType<unknown>>
|
||||
}
|
||||
|
||||
/** Used to reach out from the pure js runtime */
|
||||
export type Effects = {
|
||||
/** Usable when not sandboxed */
|
||||
writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void>
|
||||
readFile(input: { volumeId: string; path: string }): Promise<string>
|
||||
metadata(input: { volumeId: string; path: string }): Promise<Metadata>
|
||||
/** Create a directory. Usable when not sandboxed */
|
||||
createDir(input: { volumeId: string; path: string }): Promise<string>
|
||||
|
||||
readDir(input: { volumeId: string; path: string }): Promise<string[]>
|
||||
/** Remove a directory. Usable when not sandboxed */
|
||||
removeDir(input: { volumeId: string; path: string }): Promise<string>
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void>
|
||||
|
||||
/** Write a json file into an object. Usable when not sandboxed */
|
||||
writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void>
|
||||
|
||||
/** Read a json file into an object */
|
||||
readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>>
|
||||
|
||||
runCommand(input: {
|
||||
command: string
|
||||
args?: string[]
|
||||
timeoutMillis?: number
|
||||
}): Promise<ResultType<string>>
|
||||
runDaemon(input: { command: string; args?: string[] }): {
|
||||
wait(): Promise<ResultType<string>>
|
||||
term(): Promise<void>
|
||||
}
|
||||
|
||||
chown(input: { volumeId: string; path: string; uid: string }): Promise<null>
|
||||
chmod(input: { volumeId: string; path: string; mode: string }): Promise<null>
|
||||
|
||||
sleep(timeMs: number): Promise<null>
|
||||
|
||||
/** Log at the trace level */
|
||||
trace(whatToPrint: string): void
|
||||
/** Log at the warn level */
|
||||
warn(whatToPrint: string): void
|
||||
/** Log at the error level */
|
||||
error(whatToPrint: string): void
|
||||
/** Log at the debug level */
|
||||
debug(whatToPrint: string): void
|
||||
/** Log at the info level */
|
||||
info(whatToPrint: string): void
|
||||
|
||||
/** Sandbox mode lets us read but not write */
|
||||
is_sandboxed(): boolean
|
||||
|
||||
// Does a volume and path exist?
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean>
|
||||
|
||||
fetch(
|
||||
url: string,
|
||||
options?: {
|
||||
method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "PATCH"
|
||||
headers?: Record<string, string>
|
||||
body?: string
|
||||
},
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null
|
||||
/// Returns the body as a string
|
||||
text(): Promise<string>
|
||||
/// Returns the body as a json
|
||||
json(): Promise<unknown>
|
||||
}>
|
||||
diskUsage(options?: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<{ used: number; total: number }>
|
||||
|
||||
runRsync(options: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
// rsync options: https://linux.die.net/man/1/rsync
|
||||
options: BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
}
|
||||
}
|
||||
|
||||
// rsync options: https://linux.die.net/man/1/rsync
|
||||
export type BackupOptions = {
|
||||
delete: boolean
|
||||
force: boolean
|
||||
ignoreExisting: boolean
|
||||
exclude: string[]
|
||||
}
|
||||
export type Metadata = {
|
||||
fileType: string
|
||||
isDir: boolean
|
||||
isFile: boolean
|
||||
isSymlink: boolean
|
||||
len: number
|
||||
modified?: Date
|
||||
accessed?: Date
|
||||
created?: Date
|
||||
readonly: boolean
|
||||
uid: number
|
||||
gid: number
|
||||
mode: number
|
||||
}
|
||||
|
||||
export type MigrationRes = {
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
export type ActionResult = {
|
||||
version: "0"
|
||||
message: string
|
||||
value?: string
|
||||
copyable: boolean
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
export type ConfigRes = {
|
||||
/** This should be the previous config, that way during set config we start with the previous */
|
||||
config?: Config
|
||||
/** Shape that is describing the form in the ui */
|
||||
spec: ConfigSpec
|
||||
}
|
||||
export type Config = {
|
||||
[propertyName: string]: unknown
|
||||
}
|
||||
|
||||
export type ConfigSpec = {
|
||||
/** Given a config value, define what it should render with the following spec */
|
||||
[configValue: string]: ValueSpecAny
|
||||
}
|
||||
export type WithDefault<T, Default> = T & {
|
||||
default: Default
|
||||
}
|
||||
export type WithNullableDefault<T, Default> = T & {
|
||||
default?: Default
|
||||
}
|
||||
|
||||
export type WithDescription<T> = T & {
|
||||
description?: string
|
||||
name: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type WithOptionalDescription<T> = T & {
|
||||
/** @deprecated - optional only for backwards compatibility */
|
||||
description?: string
|
||||
/** @deprecated - optional only for backwards compatibility */
|
||||
name?: string
|
||||
warning?: string
|
||||
}
|
||||
|
||||
export type ListSpec<T> = {
|
||||
spec: T
|
||||
range: string
|
||||
}
|
||||
|
||||
export type Tag<T extends string, V> = V & {
|
||||
type: T
|
||||
}
|
||||
|
||||
export type Subtype<T extends string, V> = V & {
|
||||
subtype: T
|
||||
}
|
||||
|
||||
export type Target<T extends string, V> = V & {
|
||||
target: T
|
||||
}
|
||||
|
||||
export type UniqueBy =
|
||||
| {
|
||||
any: UniqueBy[]
|
||||
}
|
||||
| string
|
||||
| null
|
||||
|
||||
export type WithNullable<T> = T & {
|
||||
nullable: boolean
|
||||
}
|
||||
export type DefaultString =
|
||||
| string
|
||||
| {
|
||||
/** The chars available for the random generation */
|
||||
charset?: string
|
||||
/** Length that we generate to */
|
||||
len: number
|
||||
}
|
||||
|
||||
export type ValueSpecString = // deno-lint-ignore ban-types
|
||||
(
|
||||
| {}
|
||||
| {
|
||||
pattern: string
|
||||
"pattern-description": string
|
||||
}
|
||||
) & {
|
||||
copyable?: boolean
|
||||
masked?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
export type ValueSpecNumber = {
|
||||
/** Something like [3,6] or [0, *) */
|
||||
range?: string
|
||||
integral?: boolean
|
||||
/** Used a description of the units */
|
||||
units?: string
|
||||
placeholder?: number
|
||||
}
|
||||
export type ValueSpecBoolean = Record<string, unknown>
|
||||
export type ValueSpecAny =
|
||||
| Tag<"boolean", WithDescription<WithDefault<ValueSpecBoolean, boolean>>>
|
||||
| Tag<
|
||||
"string",
|
||||
WithDescription<
|
||||
WithNullableDefault<WithNullable<ValueSpecString>, DefaultString>
|
||||
>
|
||||
>
|
||||
| Tag<
|
||||
"number",
|
||||
WithDescription<
|
||||
WithNullableDefault<WithNullable<ValueSpecNumber>, number>
|
||||
>
|
||||
>
|
||||
| Tag<
|
||||
"enum",
|
||||
WithDescription<
|
||||
WithDefault<
|
||||
{
|
||||
values: readonly string[] | string[]
|
||||
"value-names": {
|
||||
[key: string]: string
|
||||
}
|
||||
},
|
||||
string
|
||||
>
|
||||
>
|
||||
>
|
||||
| Tag<"list", ValueSpecList>
|
||||
| Tag<"object", WithDescription<WithNullableDefault<ValueSpecObject, Config>>>
|
||||
| Tag<"union", WithOptionalDescription<WithDefault<ValueSpecUnion, string>>>
|
||||
| Tag<
|
||||
"pointer",
|
||||
WithDescription<
|
||||
| Subtype<
|
||||
"package",
|
||||
| Target<
|
||||
"tor-key",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"tor-address",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"lan-address",
|
||||
{
|
||||
"package-id": string
|
||||
interface: string
|
||||
}
|
||||
>
|
||||
| Target<
|
||||
"config",
|
||||
{
|
||||
"package-id": string
|
||||
selector: string
|
||||
multi: boolean
|
||||
}
|
||||
>
|
||||
>
|
||||
| Subtype<"system", Record<string, unknown>>
|
||||
>
|
||||
>
|
||||
export type ValueSpecUnion = {
|
||||
/** What tag for the specification, for tag unions */
|
||||
tag: {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
"variant-names": {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
/** The possible enum values */
|
||||
variants: {
|
||||
[key: string]: ConfigSpec
|
||||
}
|
||||
"display-as"?: string
|
||||
"unique-by"?: UniqueBy
|
||||
}
|
||||
export type ValueSpecObject = {
|
||||
spec: ConfigSpec
|
||||
"display-as"?: string
|
||||
"unique-by"?: UniqueBy
|
||||
}
|
||||
export type ValueSpecList =
|
||||
| Subtype<
|
||||
"boolean",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecBoolean>, boolean[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"string",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecString>, string[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"number",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecNumber>, number[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"enum",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecEnum>, string[]>>
|
||||
>
|
||||
| Subtype<
|
||||
"object",
|
||||
WithDescription<
|
||||
WithNullableDefault<
|
||||
ListSpec<ValueSpecObject>,
|
||||
Record<string, unknown>[]
|
||||
>
|
||||
>
|
||||
>
|
||||
| Subtype<
|
||||
"union",
|
||||
WithDescription<WithDefault<ListSpec<ValueSpecUnion>, string[]>>
|
||||
>
|
||||
export type ValueSpecEnum = {
|
||||
values: string[]
|
||||
"value-names": { [key: string]: string }
|
||||
}
|
||||
|
||||
export type SetResult = {
|
||||
/** These are the unix process signals */
|
||||
signal:
|
||||
| "SIGTERM"
|
||||
| "SIGHUP"
|
||||
| "SIGINT"
|
||||
| "SIGQUIT"
|
||||
| "SIGILL"
|
||||
| "SIGTRAP"
|
||||
| "SIGABRT"
|
||||
| "SIGBUS"
|
||||
| "SIGFPE"
|
||||
| "SIGKILL"
|
||||
| "SIGUSR1"
|
||||
| "SIGSEGV"
|
||||
| "SIGUSR2"
|
||||
| "SIGPIPE"
|
||||
| "SIGALRM"
|
||||
| "SIGSTKFLT"
|
||||
| "SIGCHLD"
|
||||
| "SIGCONT"
|
||||
| "SIGSTOP"
|
||||
| "SIGTSTP"
|
||||
| "SIGTTIN"
|
||||
| "SIGTTOU"
|
||||
| "SIGURG"
|
||||
| "SIGXCPU"
|
||||
| "SIGXFSZ"
|
||||
| "SIGVTALRM"
|
||||
| "SIGPROF"
|
||||
| "SIGWINCH"
|
||||
| "SIGIO"
|
||||
| "SIGPWR"
|
||||
| "SIGSYS"
|
||||
| "SIGEMT"
|
||||
| "SIGINFO"
|
||||
"depends-on": DependsOn
|
||||
}
|
||||
|
||||
export type DependsOn = {
|
||||
[packageId: string]: string[]
|
||||
}
|
||||
|
||||
export type KnownError =
|
||||
| { error: string }
|
||||
| {
|
||||
"error-code": [number, string] | readonly [number, string]
|
||||
}
|
||||
export type ResultType<T> = KnownError | { result: T }
|
||||
|
||||
export type PackagePropertiesV2 = {
|
||||
[name: string]: PackagePropertyObject | PackagePropertyString
|
||||
}
|
||||
export type PackagePropertyString = {
|
||||
type: "string"
|
||||
description?: string
|
||||
value: string
|
||||
/** Let's the ui make this copyable button */
|
||||
copyable?: boolean
|
||||
/** Let the ui create a qr for this field */
|
||||
qr?: boolean
|
||||
/** Hiding the value unless toggled off for field */
|
||||
masked?: boolean
|
||||
}
|
||||
export type PackagePropertyObject = {
|
||||
value: PackagePropertiesV2
|
||||
type: "object"
|
||||
description: string
|
||||
}
|
||||
|
||||
export type Properties = {
|
||||
version: 2
|
||||
data: PackagePropertiesV2
|
||||
}
|
||||
|
||||
export type Dependencies = {
|
||||
/** Id is the id of the package, should be the same as the manifest */
|
||||
[id: string]: {
|
||||
/** Checks are called to make sure that our dependency is in the correct shape. If a known error is returned we know that the dependency needs modification */
|
||||
check(effects: Effects, input: Config): Promise<ResultType<void | null>>
|
||||
/** This is called after we know that the dependency package needs a new configuration, this would be a transform for defaults */
|
||||
autoConfigure(effects: Effects, input: Config): Promise<ResultType<Config>>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as oet from "./oldEmbassyTypes"
|
||||
import { Volume } from "../../../Models/Volume"
|
||||
import * as child_process from "child_process"
|
||||
import { promisify } from "util"
|
||||
import { daemons, startSdk, T, utils } from "@start9labs/start-sdk"
|
||||
import "isomorphic-fetch"
|
||||
import { Manifest } from "./matchManifest"
|
||||
import { DockerProcedureContainer } from "./DockerProcedureContainer"
|
||||
import * as cp from "child_process"
|
||||
import { Effects } from "../../../Models/Effects"
|
||||
import { Mounts } from "@start9labs/start-sdk/package/lib/mainFn/Mounts"
|
||||
export const execFile = promisify(cp.execFile)
|
||||
export const polyfillEffects = (
|
||||
effects: Effects,
|
||||
manifest: Manifest,
|
||||
): oet.Effects => {
|
||||
const self = {
|
||||
effects,
|
||||
manifest,
|
||||
async writeFile(input: {
|
||||
path: string
|
||||
volumeId: string
|
||||
toWrite: string
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
input.toWrite,
|
||||
)
|
||||
},
|
||||
async readFile(input: { volumeId: string; path: string }): Promise<string> {
|
||||
return (
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString()
|
||||
},
|
||||
async metadata(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<oet.Metadata> {
|
||||
const stats = await fs.stat(new Volume(input.volumeId, input.path).path)
|
||||
return {
|
||||
fileType: stats.isFile() ? "file" : "directory",
|
||||
gid: stats.gid,
|
||||
uid: stats.uid,
|
||||
mode: stats.mode,
|
||||
isDir: stats.isDirectory(),
|
||||
isFile: stats.isFile(),
|
||||
isSymlink: stats.isSymbolicLink(),
|
||||
len: stats.size,
|
||||
readonly: (stats.mode & 0o200) > 0,
|
||||
}
|
||||
},
|
||||
async createDir(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.mkdir(path, { recursive: true })
|
||||
return path
|
||||
},
|
||||
async readDir(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<string[]> {
|
||||
return fs.readdir(new Volume(input.volumeId, input.path).path)
|
||||
},
|
||||
async removeDir(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<string> {
|
||||
const path = new Volume(input.volumeId, input.path).path
|
||||
await fs.rmdir(new Volume(input.volumeId, input.path).path, {
|
||||
recursive: true,
|
||||
})
|
||||
return path
|
||||
},
|
||||
removeFile(input: { volumeId: string; path: string }): Promise<void> {
|
||||
return fs.rm(new Volume(input.volumeId, input.path).path)
|
||||
},
|
||||
async writeJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
toWrite: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
await fs.writeFile(
|
||||
new Volume(input.volumeId, input.path).path,
|
||||
JSON.stringify(input.toWrite),
|
||||
)
|
||||
},
|
||||
async readJsonFile(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
}): Promise<Record<string, unknown>> {
|
||||
return JSON.parse(
|
||||
(
|
||||
await fs.readFile(new Volume(input.volumeId, input.path).path)
|
||||
).toString(),
|
||||
)
|
||||
},
|
||||
runCommand({
|
||||
command,
|
||||
args,
|
||||
timeoutMillis,
|
||||
}: {
|
||||
command: string
|
||||
args?: string[] | undefined
|
||||
timeoutMillis?: number | undefined
|
||||
}): Promise<oet.ResultType<string>> {
|
||||
const commands: [string, ...string[]] = [command, ...(args || [])]
|
||||
return startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ imageId: manifest.main.image },
|
||||
commands,
|
||||
{ mounts: Mounts.of() },
|
||||
commands.join(" "),
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) =>
|
||||
!!x.stderr ? { error: x.stderr } : { result: x.stdout },
|
||||
)
|
||||
},
|
||||
runDaemon(input: { command: string; args?: string[] | undefined }): {
|
||||
wait(): Promise<oet.ResultType<string>>
|
||||
term(): Promise<void>
|
||||
} {
|
||||
const promiseSubcontainer = DockerProcedureContainer.createSubContainer(
|
||||
effects,
|
||||
manifest.id,
|
||||
manifest.main,
|
||||
manifest.volumes,
|
||||
[input.command, ...(input.args || [])].join(" "),
|
||||
)
|
||||
const daemon = promiseSubcontainer.then((subcontainer) =>
|
||||
daemons.runCommand()(effects, subcontainer, {
|
||||
command: [input.command, ...(input.args || [])],
|
||||
}),
|
||||
)
|
||||
return {
|
||||
wait: () =>
|
||||
daemon.then((daemon) =>
|
||||
daemon.wait().then(() => {
|
||||
return { result: "" }
|
||||
}),
|
||||
),
|
||||
term: () => daemon.then((daemon) => daemon.term()),
|
||||
}
|
||||
},
|
||||
async chown(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
uid: string
|
||||
}): Promise<null> {
|
||||
const commands: [string, ...string[]] = [
|
||||
"chown",
|
||||
"--recursive",
|
||||
input.uid,
|
||||
`/drive/${input.path}`,
|
||||
]
|
||||
await startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ imageId: manifest.main.image },
|
||||
commands,
|
||||
{
|
||||
mounts: Mounts.of().mountVolume({
|
||||
volumeId: input.volumeId,
|
||||
subpath: null,
|
||||
mountpoint: "/drive",
|
||||
readonly: false,
|
||||
}),
|
||||
},
|
||||
commands.join(" "),
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
})
|
||||
return null
|
||||
},
|
||||
async chmod(input: {
|
||||
volumeId: string
|
||||
path: string
|
||||
mode: string
|
||||
}): Promise<null> {
|
||||
const commands: [string, ...string[]] = [
|
||||
"chmod",
|
||||
"--recursive",
|
||||
input.mode,
|
||||
`/drive/${input.path}`,
|
||||
]
|
||||
await startSdk
|
||||
.runCommand(
|
||||
effects,
|
||||
{ imageId: manifest.main.image },
|
||||
commands,
|
||||
{
|
||||
mounts: Mounts.of().mountVolume({
|
||||
volumeId: input.volumeId,
|
||||
subpath: null,
|
||||
mountpoint: "/drive",
|
||||
readonly: false,
|
||||
}),
|
||||
},
|
||||
commands.join(" "),
|
||||
)
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
})
|
||||
return null
|
||||
},
|
||||
sleep(timeMs: number): Promise<null> {
|
||||
return new Promise((resolve) => setTimeout(resolve, timeMs))
|
||||
},
|
||||
trace(whatToPrint: string): void {
|
||||
console.trace(utils.asError(whatToPrint))
|
||||
},
|
||||
warn(whatToPrint: string): void {
|
||||
console.warn(utils.asError(whatToPrint))
|
||||
},
|
||||
error(whatToPrint: string): void {
|
||||
console.error(utils.asError(whatToPrint))
|
||||
},
|
||||
debug(whatToPrint: string): void {
|
||||
console.debug(utils.asError(whatToPrint))
|
||||
},
|
||||
info(whatToPrint: string): void {
|
||||
console.log(false)
|
||||
},
|
||||
is_sandboxed(): boolean {
|
||||
return false
|
||||
},
|
||||
exists(input: { volumeId: string; path: string }): Promise<boolean> {
|
||||
return self
|
||||
.metadata(input)
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
},
|
||||
async fetch(
|
||||
url: string,
|
||||
options?:
|
||||
| {
|
||||
method?:
|
||||
| "GET"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "HEAD"
|
||||
| "PATCH"
|
||||
| undefined
|
||||
headers?: Record<string, string> | undefined
|
||||
body?: string | undefined
|
||||
}
|
||||
| undefined,
|
||||
): Promise<{
|
||||
method: string
|
||||
ok: boolean
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body?: string | null | undefined
|
||||
text(): Promise<string>
|
||||
json(): Promise<unknown>
|
||||
}> {
|
||||
const fetched = await fetch(url, options)
|
||||
return {
|
||||
method: fetched.type,
|
||||
ok: fetched.ok,
|
||||
status: fetched.status,
|
||||
headers: Object.fromEntries(fetched.headers.entries()),
|
||||
body: await fetched.text(),
|
||||
text: () => fetched.text(),
|
||||
json: () => fetched.json(),
|
||||
}
|
||||
},
|
||||
|
||||
runRsync(rsyncOptions: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
let secondRun: ReturnType<typeof self._runRsync> | undefined
|
||||
let firstRun = self._runRsync(rsyncOptions)
|
||||
let waitValue = firstRun.wait().then((x) => {
|
||||
secondRun = self._runRsync(rsyncOptions)
|
||||
return secondRun.wait()
|
||||
})
|
||||
const id = async () => {
|
||||
return secondRun?.id?.() ?? firstRun.id()
|
||||
}
|
||||
const wait = () => waitValue
|
||||
const progress = async () => {
|
||||
const secondProgress = secondRun?.progress?.()
|
||||
if (secondProgress) {
|
||||
return (await secondProgress) / 2.0 + 0.5
|
||||
}
|
||||
return (await firstRun.progress()) / 2.0
|
||||
}
|
||||
return { id, wait, progress }
|
||||
},
|
||||
_runRsync(rsyncOptions: {
|
||||
srcVolume: string
|
||||
dstVolume: string
|
||||
srcPath: string
|
||||
dstPath: string
|
||||
options: oet.BackupOptions
|
||||
}): {
|
||||
id: () => Promise<string>
|
||||
wait: () => Promise<null>
|
||||
progress: () => Promise<number>
|
||||
} {
|
||||
const { srcVolume, dstVolume, srcPath, dstPath, options } = rsyncOptions
|
||||
const command = "rsync"
|
||||
const args: string[] = []
|
||||
if (options.delete) {
|
||||
args.push("--delete")
|
||||
}
|
||||
if (options.force) {
|
||||
args.push("--force")
|
||||
}
|
||||
if (options.ignoreExisting) {
|
||||
args.push("--ignore-existing")
|
||||
}
|
||||
for (const exclude of options.exclude) {
|
||||
args.push(`--exclude=${exclude}`)
|
||||
}
|
||||
args.push("-actAXH")
|
||||
args.push("--info=progress2")
|
||||
args.push("--no-inc-recursive")
|
||||
args.push(new Volume(srcVolume, srcPath).path)
|
||||
args.push(new Volume(dstVolume, dstPath).path)
|
||||
const spawned = child_process.spawn(command, args, { detached: true })
|
||||
let percentage = 0.0
|
||||
spawned.stdout.on("data", (data: unknown) => {
|
||||
const lines = String(data).replace("\r", "\n").split("\n")
|
||||
for (const line of lines) {
|
||||
const parsed = /$([0-9.]+)%/.exec(line)?.[1]
|
||||
if (!parsed) continue
|
||||
percentage = Number.parseFloat(parsed)
|
||||
}
|
||||
})
|
||||
|
||||
spawned.stderr.on("data", (data: unknown) => {
|
||||
console.error(`polyfill.runAsync`, utils.asError(data))
|
||||
})
|
||||
|
||||
const id = async () => {
|
||||
const pid = spawned.pid
|
||||
if (pid === undefined) {
|
||||
throw new Error("rsync process has no pid")
|
||||
}
|
||||
return String(pid)
|
||||
}
|
||||
const waitPromise = new Promise<null>((resolve, reject) => {
|
||||
spawned.on("exit", (code: any) => {
|
||||
if (code === 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
reject(new Error(`rsync exited with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
const wait = () => waitPromise
|
||||
const progress = () => Promise.resolve(percentage)
|
||||
return { id, wait, progress }
|
||||
},
|
||||
async diskUsage(
|
||||
options?: { volumeId: string; path: string } | undefined,
|
||||
): Promise<{ used: number; total: number }> {
|
||||
const output = await execFile("df", ["--block-size=1", "-P", "/"])
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
return parseDfOutput(x.stdout)
|
||||
})
|
||||
if (!!options) {
|
||||
const used = await execFile("du", [
|
||||
"-s",
|
||||
"--block-size=1",
|
||||
"-P",
|
||||
new Volume(options.volumeId, options.path).path,
|
||||
])
|
||||
.then((x: any) => ({
|
||||
stderr: x.stderr.toString(),
|
||||
stdout: x.stdout.toString(),
|
||||
}))
|
||||
.then((x: any) => {
|
||||
if (!!x.stderr) {
|
||||
throw new Error(x.stderr)
|
||||
}
|
||||
return Number.parseInt(x.stdout.split(/\s+/)[0])
|
||||
})
|
||||
return {
|
||||
...output,
|
||||
used,
|
||||
}
|
||||
}
|
||||
return output
|
||||
},
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
function parseDfOutput(output: string): { used: number; total: number } {
|
||||
const lines = output
|
||||
.split("\n")
|
||||
.filter((x) => x.length)
|
||||
.map((x) => x.split(/\s+/))
|
||||
const index = lines.splice(0, 1)[0].map((x) => x.toLowerCase())
|
||||
const usedIndex = index.indexOf("used")
|
||||
const sizeIndex = index.indexOf("size")
|
||||
const used = lines.map((x) => Number.parseInt(x[usedIndex]))[0] || 0
|
||||
const total = lines.map((x) => Number.parseInt(x[sizeIndex]))[0] || 0
|
||||
return { used, total }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
matchOldConfigSpec,
|
||||
matchOldValueSpecList,
|
||||
transformConfigSpec,
|
||||
} from "./transformConfigSpec"
|
||||
import fixtureEmbassyPagesConfig from "./__fixtures__/embassyPagesConfig"
|
||||
import fixtureRTLConfig from "./__fixtures__/rtlConfig"
|
||||
import searNXG from "./__fixtures__/searNXG"
|
||||
import bitcoind from "./__fixtures__/bitcoind"
|
||||
import nostr from "./__fixtures__/nostr"
|
||||
import nostrConfig2 from "./__fixtures__/nostrConfig2"
|
||||
|
||||
describe("transformConfigSpec", () => {
|
||||
test("matchOldConfigSpec(embassyPages.homepage.variants[web-page])", () => {
|
||||
matchOldConfigSpec.unsafeCast(
|
||||
fixtureEmbassyPagesConfig.homepage.variants["web-page"],
|
||||
)
|
||||
})
|
||||
test("matchOldConfigSpec(embassyPages)", () => {
|
||||
matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
|
||||
})
|
||||
test("transformConfigSpec(embassyPages)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(fixtureEmbassyPagesConfig)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("matchOldConfigSpec(RTL.nodes)", () => {
|
||||
matchOldValueSpecList.unsafeCast(fixtureRTLConfig.nodes)
|
||||
})
|
||||
test("matchOldConfigSpec(RTL)", () => {
|
||||
matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
|
||||
})
|
||||
test("transformConfigSpec(RTL)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(fixtureRTLConfig)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test("transformConfigSpec(searNXG)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(searNXG)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
test("transformConfigSpec(bitcoind)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(bitcoind)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
test("transformConfigSpec(nostr)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(nostr)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
test("transformConfigSpec(nostr2)", () => {
|
||||
const spec = matchOldConfigSpec.unsafeCast(nostrConfig2)
|
||||
expect(transformConfigSpec(spec)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,617 @@
|
||||
import { IST } from "@start9labs/start-sdk"
|
||||
import {
|
||||
dictionary,
|
||||
object,
|
||||
anyOf,
|
||||
string,
|
||||
literals,
|
||||
array,
|
||||
number,
|
||||
boolean,
|
||||
Parser,
|
||||
deferred,
|
||||
every,
|
||||
nill,
|
||||
literal,
|
||||
} from "ts-matches"
|
||||
|
||||
export function transformConfigSpec(oldSpec: OldConfigSpec): IST.InputSpec {
|
||||
return Object.entries(oldSpec).reduce((inputSpec, [key, oldVal]) => {
|
||||
let newVal: IST.ValueSpec
|
||||
|
||||
if (oldVal.type === "boolean") {
|
||||
newVal = {
|
||||
type: "toggle",
|
||||
name: oldVal.name,
|
||||
default: oldVal.default,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
}
|
||||
} else if (oldVal.type === "enum") {
|
||||
newVal = {
|
||||
type: "select",
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
default: oldVal.default,
|
||||
values: oldVal.values.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr]: oldVal["value-names"][curr] || curr,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
}
|
||||
} else if (oldVal.type === "list") {
|
||||
if (isUnionList(oldVal)) return inputSpec
|
||||
newVal = getListSpec(oldVal)
|
||||
} else if (oldVal.type === "number") {
|
||||
const range = Range.from(oldVal.range)
|
||||
|
||||
newVal = {
|
||||
type: "number",
|
||||
name: oldVal.name,
|
||||
default: oldVal.default || null,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
required: !oldVal.nullable,
|
||||
min: range.min
|
||||
? range.minInclusive
|
||||
? range.min
|
||||
: range.min + 1
|
||||
: null,
|
||||
max: range.max
|
||||
? range.maxInclusive
|
||||
? range.max
|
||||
: range.max - 1
|
||||
: null,
|
||||
integer: oldVal.integral,
|
||||
step: null,
|
||||
units: oldVal.units || null,
|
||||
placeholder: oldVal.placeholder ? String(oldVal.placeholder) : null,
|
||||
}
|
||||
} else if (oldVal.type === "object") {
|
||||
newVal = {
|
||||
type: "object",
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(oldVal.spec)),
|
||||
}
|
||||
} else if (oldVal.type === "string") {
|
||||
newVal = {
|
||||
type: "text",
|
||||
name: oldVal.name,
|
||||
default: oldVal.default || null,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
required: !oldVal.nullable,
|
||||
patterns:
|
||||
oldVal.pattern && oldVal["pattern-description"]
|
||||
? [
|
||||
{
|
||||
regex: oldVal.pattern,
|
||||
description: oldVal["pattern-description"],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: oldVal.masked || false,
|
||||
generate: null,
|
||||
inputmode: "text",
|
||||
placeholder: oldVal.placeholder || null,
|
||||
}
|
||||
} else if (oldVal.type === "union") {
|
||||
newVal = {
|
||||
type: "union",
|
||||
name: oldVal.tag.name,
|
||||
description: oldVal.tag.description || null,
|
||||
warning: oldVal.tag.warning || null,
|
||||
variants: Object.entries(oldVal.variants).reduce(
|
||||
(obj, [id, spec]) => ({
|
||||
...obj,
|
||||
[id]: {
|
||||
name: oldVal.tag["variant-names"][id] || id,
|
||||
spec: transformConfigSpec(matchOldConfigSpec.unsafeCast(spec)),
|
||||
},
|
||||
}),
|
||||
{} as Record<string, { name: string; spec: IST.InputSpec }>,
|
||||
),
|
||||
disabled: false,
|
||||
default: oldVal.default,
|
||||
immutable: false,
|
||||
}
|
||||
} else if (oldVal.type === "pointer") {
|
||||
return inputSpec
|
||||
} else {
|
||||
throw new Error(`unknown spec ${JSON.stringify(oldVal)}`)
|
||||
}
|
||||
|
||||
return {
|
||||
...inputSpec,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {} as IST.InputSpec)
|
||||
}
|
||||
|
||||
export function transformOldConfigToNew(
|
||||
spec: OldConfigSpec,
|
||||
config: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
if (!config) return config
|
||||
return Object.entries(spec).reduce((obj, [key, val]) => {
|
||||
let newVal = config[key]
|
||||
|
||||
if (isObject(val)) {
|
||||
newVal = transformOldConfigToNew(
|
||||
matchOldConfigSpec.unsafeCast(val.spec),
|
||||
config[key],
|
||||
)
|
||||
}
|
||||
|
||||
if (isUnion(val)) {
|
||||
if (!config[key]) return obj
|
||||
|
||||
const selection = config[key]?.[val.tag.id]
|
||||
|
||||
if (!selection) return obj
|
||||
|
||||
delete config[key][val.tag.id]
|
||||
|
||||
if (!val.variants[selection]) return obj
|
||||
|
||||
newVal = {
|
||||
selection,
|
||||
value: transformOldConfigToNew(
|
||||
matchOldConfigSpec.unsafeCast(val.variants[selection]),
|
||||
config[key],
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (isList(val)) {
|
||||
if (!config[key]) return obj
|
||||
|
||||
if (isObjectList(val)) {
|
||||
newVal = (config[key] as object[]).map((obj) =>
|
||||
transformOldConfigToNew(
|
||||
matchOldConfigSpec.unsafeCast(val.spec.spec),
|
||||
obj,
|
||||
),
|
||||
)
|
||||
} else if (isUnionList(val)) return obj
|
||||
}
|
||||
|
||||
if (isPointer(val)) {
|
||||
return obj
|
||||
}
|
||||
|
||||
return {
|
||||
...obj,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
export function transformNewConfigToOld(
|
||||
spec: OldConfigSpec,
|
||||
config: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
if (!config) return config
|
||||
return Object.entries(spec).reduce((obj, [key, val]) => {
|
||||
let newVal = config[key]
|
||||
|
||||
if (isObject(val)) {
|
||||
newVal = transformNewConfigToOld(
|
||||
matchOldConfigSpec.unsafeCast(val.spec),
|
||||
config[key],
|
||||
)
|
||||
}
|
||||
|
||||
if (isUnion(val)) {
|
||||
newVal = {
|
||||
[val.tag.id]: config[key].selection,
|
||||
...transformNewConfigToOld(
|
||||
matchOldConfigSpec.unsafeCast(val.variants[config[key].selection]),
|
||||
config[key].value,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (isList(val)) {
|
||||
if (isObjectList(val)) {
|
||||
newVal = (config[key] as object[]).map((obj) =>
|
||||
transformNewConfigToOld(
|
||||
matchOldConfigSpec.unsafeCast(val.spec.spec),
|
||||
obj,
|
||||
),
|
||||
)
|
||||
} else if (isUnionList(val)) return obj
|
||||
}
|
||||
|
||||
return {
|
||||
...obj,
|
||||
[key]: newVal,
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
|
||||
function getListSpec(
|
||||
oldVal: OldValueSpecList,
|
||||
): IST.ValueSpecMultiselect | IST.ValueSpecList {
|
||||
const range = Range.from(oldVal.range)
|
||||
|
||||
let partial: Omit<IST.ValueSpecList, "type" | "spec" | "default"> = {
|
||||
name: oldVal.name,
|
||||
description: oldVal.description || null,
|
||||
warning: oldVal.warning || null,
|
||||
minLength: range.min
|
||||
? range.minInclusive
|
||||
? range.min
|
||||
: range.min + 1
|
||||
: null,
|
||||
maxLength: range.max
|
||||
? range.maxInclusive
|
||||
? range.max
|
||||
: range.max - 1
|
||||
: null,
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
if (isEnumList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "multiselect",
|
||||
default: oldVal.default as string[],
|
||||
immutable: false,
|
||||
values: oldVal.spec.values.reduce(
|
||||
(obj, curr) => ({
|
||||
...obj,
|
||||
[curr]: oldVal.spec["value-names"][curr],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
}
|
||||
} else if (isNumberList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "list",
|
||||
default: oldVal.default.map(String) as string[],
|
||||
spec: {
|
||||
type: "text",
|
||||
patterns: oldVal.spec.integral
|
||||
? [{ regex: "[0-9]+", description: "Integral number type" }]
|
||||
: [
|
||||
{
|
||||
regex: "[-+]?[0-9]*\\.?[0-9]+",
|
||||
description: "Number type",
|
||||
},
|
||||
],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: false,
|
||||
generate: null,
|
||||
inputmode: "text",
|
||||
placeholder: oldVal.spec.placeholder
|
||||
? String(oldVal.spec.placeholder)
|
||||
: null,
|
||||
},
|
||||
}
|
||||
} else if (isStringList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "list",
|
||||
default: oldVal.default as string[],
|
||||
spec: {
|
||||
type: "text",
|
||||
patterns:
|
||||
oldVal.spec.pattern && oldVal.spec["pattern-description"]
|
||||
? [
|
||||
{
|
||||
regex: oldVal.spec.pattern,
|
||||
description: oldVal.spec["pattern-description"],
|
||||
},
|
||||
]
|
||||
: [],
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: oldVal.spec.masked || false,
|
||||
generate: null,
|
||||
inputmode: "text",
|
||||
placeholder: oldVal.spec.placeholder || null,
|
||||
},
|
||||
}
|
||||
} else if (isObjectList(oldVal)) {
|
||||
return {
|
||||
...partial,
|
||||
type: "list",
|
||||
default: oldVal.default as Record<string, unknown>[],
|
||||
spec: {
|
||||
type: "object",
|
||||
spec: transformConfigSpec(
|
||||
matchOldConfigSpec.unsafeCast(oldVal.spec.spec),
|
||||
),
|
||||
uniqueBy: oldVal.spec["unique-by"] || null,
|
||||
displayAs: oldVal.spec["display-as"] || null,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid list subtype. enum, string, and object permitted.")
|
||||
}
|
||||
}
|
||||
|
||||
function isObject(val: OldValueSpec): val is OldValueSpecObject {
|
||||
return val.type === "object"
|
||||
}
|
||||
|
||||
function isUnion(val: OldValueSpec): val is OldValueSpecUnion {
|
||||
return val.type === "union"
|
||||
}
|
||||
|
||||
function isList(val: OldValueSpec): val is OldValueSpecList {
|
||||
return val.type === "list"
|
||||
}
|
||||
|
||||
function isPointer(val: OldValueSpec): val is OldValueSpecPointer {
|
||||
return val.type === "pointer"
|
||||
}
|
||||
|
||||
function isEnumList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "enum" } {
|
||||
return val.subtype === "enum"
|
||||
}
|
||||
|
||||
function isStringList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "string" } {
|
||||
return val.subtype === "string"
|
||||
}
|
||||
function isNumberList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "number" } {
|
||||
return val.subtype === "number"
|
||||
}
|
||||
function isObjectList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "object" } {
|
||||
return val.subtype === "object"
|
||||
}
|
||||
function isUnionList(
|
||||
val: OldValueSpecList,
|
||||
): val is OldValueSpecList & { subtype: "union" } {
|
||||
return val.subtype === "union"
|
||||
}
|
||||
|
||||
export type OldConfigSpec = Record<string, OldValueSpec>
|
||||
const [_matchOldConfigSpec, setMatchOldConfigSpec] = deferred<unknown>()
|
||||
export const matchOldConfigSpec = _matchOldConfigSpec as Parser<
|
||||
unknown,
|
||||
OldConfigSpec
|
||||
>
|
||||
export const matchOldDefaultString = anyOf(
|
||||
string,
|
||||
object({ charset: string, len: number }),
|
||||
)
|
||||
type OldDefaultString = typeof matchOldDefaultString._TYPE
|
||||
|
||||
export const matchOldValueSpecString = object({
|
||||
type: literals("string"),
|
||||
name: string,
|
||||
masked: boolean.nullable().optional(),
|
||||
copyable: boolean.nullable().optional(),
|
||||
nullable: boolean.nullable().optional(),
|
||||
placeholder: string.nullable().optional(),
|
||||
pattern: string.nullable().optional(),
|
||||
"pattern-description": string.nullable().optional(),
|
||||
default: matchOldDefaultString.nullable().optional(),
|
||||
textarea: boolean.nullable().optional(),
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
})
|
||||
|
||||
export const matchOldValueSpecNumber = object({
|
||||
type: literals("number"),
|
||||
nullable: boolean,
|
||||
name: string,
|
||||
range: string,
|
||||
integral: boolean,
|
||||
default: number.nullable().optional(),
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
units: string.nullable().optional(),
|
||||
placeholder: anyOf(number, string).nullable().optional(),
|
||||
})
|
||||
type OldValueSpecNumber = typeof matchOldValueSpecNumber._TYPE
|
||||
|
||||
export const matchOldValueSpecBoolean = object({
|
||||
type: literals("boolean"),
|
||||
default: boolean,
|
||||
name: string,
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
})
|
||||
type OldValueSpecBoolean = typeof matchOldValueSpecBoolean._TYPE
|
||||
|
||||
const matchOldValueSpecObject = object({
|
||||
type: literals("object"),
|
||||
spec: _matchOldConfigSpec,
|
||||
name: string,
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
})
|
||||
type OldValueSpecObject = typeof matchOldValueSpecObject._TYPE
|
||||
|
||||
const matchOldValueSpecEnum = object({
|
||||
values: array(string),
|
||||
"value-names": dictionary([string, string]),
|
||||
type: literals("enum"),
|
||||
default: string,
|
||||
name: string,
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
})
|
||||
type OldValueSpecEnum = typeof matchOldValueSpecEnum._TYPE
|
||||
|
||||
const matchOldUnionTagSpec = object({
|
||||
id: string, // The name of the field containing one of the union variants
|
||||
"variant-names": dictionary([string, string]), // The name of each variant
|
||||
name: string,
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
})
|
||||
const matchOldValueSpecUnion = object({
|
||||
type: literals("union"),
|
||||
tag: matchOldUnionTagSpec,
|
||||
variants: dictionary([string, _matchOldConfigSpec]),
|
||||
default: string,
|
||||
})
|
||||
type OldValueSpecUnion = typeof matchOldValueSpecUnion._TYPE
|
||||
|
||||
const [matchOldUniqueBy, setOldUniqueBy] = deferred<OldUniqueBy>()
|
||||
type OldUniqueBy =
|
||||
| null
|
||||
| string
|
||||
| { any: OldUniqueBy[] }
|
||||
| { all: OldUniqueBy[] }
|
||||
|
||||
setOldUniqueBy(
|
||||
anyOf(
|
||||
nill,
|
||||
string,
|
||||
object({ any: array(matchOldUniqueBy) }),
|
||||
object({ all: array(matchOldUniqueBy) }),
|
||||
),
|
||||
)
|
||||
|
||||
const matchOldListValueSpecObject = object({
|
||||
spec: _matchOldConfigSpec, // this is a mapped type of the config object at this level, replacing the object's values with specs on those values
|
||||
"unique-by": matchOldUniqueBy.nullable().optional(), // indicates whether duplicates can be permitted in the list
|
||||
"display-as": string.nullable().optional(), // this should be a handlebars template which can make use of the entire config which corresponds to 'spec'
|
||||
})
|
||||
const matchOldListValueSpecUnion = object({
|
||||
"unique-by": matchOldUniqueBy.nullable().optional(),
|
||||
"display-as": string.nullable().optional(),
|
||||
tag: matchOldUnionTagSpec,
|
||||
variants: dictionary([string, _matchOldConfigSpec]),
|
||||
})
|
||||
const matchOldListValueSpecString = object({
|
||||
masked: boolean.nullable().optional(),
|
||||
copyable: boolean.nullable().optional(),
|
||||
pattern: string.nullable().optional(),
|
||||
"pattern-description": string.nullable().optional(),
|
||||
placeholder: string.nullable().optional(),
|
||||
})
|
||||
|
||||
const matchOldListValueSpecEnum = object({
|
||||
values: array(string),
|
||||
"value-names": dictionary([string, string]),
|
||||
})
|
||||
const matchOldListValueSpecNumber = object({
|
||||
range: string,
|
||||
integral: boolean,
|
||||
units: string.nullable().optional(),
|
||||
placeholder: anyOf(number, string).nullable().optional(),
|
||||
})
|
||||
|
||||
// represents a spec for a list
|
||||
export const matchOldValueSpecList = every(
|
||||
object({
|
||||
type: literals("list"),
|
||||
range: string, // '[0,1]' (inclusive) OR '[0,*)' (right unbounded), normal math rules
|
||||
default: anyOf(
|
||||
array(string),
|
||||
array(number),
|
||||
array(matchOldDefaultString),
|
||||
array(object),
|
||||
),
|
||||
name: string,
|
||||
description: string.nullable().optional(),
|
||||
warning: string.nullable().optional(),
|
||||
}),
|
||||
anyOf(
|
||||
object({
|
||||
subtype: literals("string"),
|
||||
spec: matchOldListValueSpecString,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("enum"),
|
||||
spec: matchOldListValueSpecEnum,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("object"),
|
||||
spec: matchOldListValueSpecObject,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("number"),
|
||||
spec: matchOldListValueSpecNumber,
|
||||
}),
|
||||
object({
|
||||
subtype: literals("union"),
|
||||
spec: matchOldListValueSpecUnion,
|
||||
}),
|
||||
),
|
||||
)
|
||||
type OldValueSpecList = typeof matchOldValueSpecList._TYPE
|
||||
|
||||
const matchOldValueSpecPointer = every(
|
||||
object({
|
||||
type: literal("pointer"),
|
||||
}),
|
||||
anyOf(
|
||||
object({
|
||||
subtype: literal("package"),
|
||||
target: literals("tor-key", "tor-address", "lan-address"),
|
||||
"package-id": string,
|
||||
interface: string,
|
||||
}),
|
||||
object({
|
||||
subtype: literal("package"),
|
||||
target: literals("config"),
|
||||
"package-id": string,
|
||||
selector: string,
|
||||
multi: boolean,
|
||||
}),
|
||||
),
|
||||
)
|
||||
type OldValueSpecPointer = typeof matchOldValueSpecPointer._TYPE
|
||||
|
||||
export const matchOldValueSpec = anyOf(
|
||||
matchOldValueSpecString,
|
||||
matchOldValueSpecNumber,
|
||||
matchOldValueSpecBoolean,
|
||||
matchOldValueSpecObject,
|
||||
matchOldValueSpecEnum,
|
||||
matchOldValueSpecList,
|
||||
matchOldValueSpecUnion,
|
||||
matchOldValueSpecPointer,
|
||||
)
|
||||
type OldValueSpec = typeof matchOldValueSpec._TYPE
|
||||
|
||||
setMatchOldConfigSpec(dictionary([string, matchOldValueSpec]))
|
||||
|
||||
export class Range {
|
||||
min?: number
|
||||
max?: number
|
||||
minInclusive!: boolean
|
||||
maxInclusive!: boolean
|
||||
|
||||
static from(s: string = "(*,*)"): Range {
|
||||
const r = new Range()
|
||||
r.minInclusive = s.startsWith("[")
|
||||
r.maxInclusive = s.endsWith("]")
|
||||
const [minStr, maxStr] = s.split(",").map((a) => a.trim())
|
||||
r.min = minStr === "(*" ? undefined : Number(minStr.slice(1))
|
||||
r.max = maxStr === "*)" ? undefined : Number(maxStr.slice(0, -1))
|
||||
return r
|
||||
}
|
||||
}
|
||||
104
container-runtime/src/Adapters/Systems/SystemForStartOs.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { System } from "../../Interfaces/System"
|
||||
import { Effects } from "../../Models/Effects"
|
||||
import { ExtendedVersion, T, utils, VersionRange } from "@start9labs/start-sdk"
|
||||
|
||||
export const STARTOS_JS_LOCATION = "/usr/lib/startos/package/index.js"
|
||||
|
||||
type RunningMain = {
|
||||
stop: () => Promise<void>
|
||||
}
|
||||
|
||||
export class SystemForStartOs implements System {
|
||||
private runningMain: RunningMain | undefined
|
||||
private starting: boolean = false
|
||||
|
||||
static of() {
|
||||
return new SystemForStartOs(require(STARTOS_JS_LOCATION))
|
||||
}
|
||||
|
||||
constructor(readonly abi: T.ABI) {
|
||||
this
|
||||
}
|
||||
|
||||
async init(
|
||||
effects: Effects,
|
||||
kind: "install" | "update" | "restore" | null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.init({ effects, kind }))
|
||||
}
|
||||
|
||||
async exit(
|
||||
effects: Effects,
|
||||
target: ExtendedVersion | VersionRange | null,
|
||||
timeoutMs: number | null = null,
|
||||
): Promise<void> {
|
||||
await this.stop()
|
||||
return void (await this.abi.uninit({ effects, target }))
|
||||
}
|
||||
|
||||
async createBackup(
|
||||
effects: T.Effects,
|
||||
timeoutMs: number | null,
|
||||
): Promise<void> {
|
||||
return void (await this.abi.createBackup({
|
||||
effects,
|
||||
}))
|
||||
}
|
||||
getActionInput(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionInput | null> {
|
||||
const action = this.abi.actions.get(id)
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.getInput({ effects })
|
||||
}
|
||||
runAction(
|
||||
effects: Effects,
|
||||
id: string,
|
||||
input: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult | null> {
|
||||
const action = this.abi.actions.get(id)
|
||||
if (!action) throw new Error(`Action ${id} not found`)
|
||||
return action.run({ effects, input })
|
||||
}
|
||||
|
||||
async start(effects: Effects): Promise<void> {
|
||||
try {
|
||||
if (this.runningMain || this.starting) return
|
||||
this.starting = true
|
||||
effects.constRetry = utils.once(() => effects.restart())
|
||||
let mainOnTerm: () => Promise<void> | undefined
|
||||
const started = async (onTerm: () => Promise<void>) => {
|
||||
await effects.setMainStatus({ status: "running" })
|
||||
mainOnTerm = onTerm
|
||||
return null
|
||||
}
|
||||
const daemons = await (
|
||||
await this.abi.main({
|
||||
effects,
|
||||
started,
|
||||
})
|
||||
).build()
|
||||
this.runningMain = {
|
||||
stop: async () => {
|
||||
if (mainOnTerm) await mainOnTerm()
|
||||
await daemons.term()
|
||||
},
|
||||
}
|
||||
} finally {
|
||||
this.starting = false
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.runningMain) {
|
||||
try {
|
||||
await this.runningMain.stop()
|
||||
} finally {
|
||||
this.runningMain = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
container-runtime/src/Adapters/Systems/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
import { System } from "../../Interfaces/System"
|
||||
import { EMBASSY_JS_LOCATION, SystemForEmbassy } from "./SystemForEmbassy"
|
||||
import { STARTOS_JS_LOCATION, SystemForStartOs } from "./SystemForStartOs"
|
||||
export async function getSystem(): Promise<System> {
|
||||
if (
|
||||
await fs.access(STARTOS_JS_LOCATION).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
) {
|
||||
return SystemForStartOs.of()
|
||||
} else if (
|
||||
await fs.access(EMBASSY_JS_LOCATION).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
) {
|
||||
return SystemForEmbassy.of()
|
||||
}
|
||||
throw new Error(`${STARTOS_JS_LOCATION} not found`)
|
||||
}
|
||||
4
container-runtime/src/Interfaces/AllGetDependencies.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { GetDependency } from "./GetDependency"
|
||||
import { System } from "./System"
|
||||
|
||||
export type AllGetDependencies = GetDependency<"system", Promise<System>>
|
||||
3
container-runtime/src/Interfaces/GetDependency.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type GetDependency<K extends string, T> = {
|
||||
[OtherK in K]: () => T
|
||||
}
|
||||
48
container-runtime/src/Interfaces/System.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ExtendedVersion,
|
||||
types as T,
|
||||
VersionRange,
|
||||
} from "@start9labs/start-sdk"
|
||||
import { Effects } from "../Models/Effects"
|
||||
import { CallbackHolder } from "../Models/CallbackHolder"
|
||||
|
||||
export type Procedure =
|
||||
| "/backup/create"
|
||||
| `/actions/${string}/getInput`
|
||||
| `/actions/${string}/run`
|
||||
|
||||
export type ExecuteResult =
|
||||
| { ok: unknown }
|
||||
| { err: { code: number; message: string } }
|
||||
export type System = {
|
||||
init(
|
||||
effects: T.Effects,
|
||||
kind: "install" | "update" | "restore" | null,
|
||||
): Promise<void>
|
||||
|
||||
start(effects: T.Effects): Promise<void>
|
||||
stop(): Promise<void>
|
||||
|
||||
createBackup(effects: T.Effects, timeoutMs: number | null): Promise<void>
|
||||
runAction(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
input: unknown,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionResult | null>
|
||||
getActionInput(
|
||||
effects: Effects,
|
||||
actionId: string,
|
||||
timeoutMs: number | null,
|
||||
): Promise<T.ActionInput | null>
|
||||
|
||||
exit(
|
||||
effects: Effects,
|
||||
target: ExtendedVersion | VersionRange | null,
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
export type RunningMain = {
|
||||
callbacks: CallbackHolder
|
||||
stop(): Promise<void>
|
||||
}
|
||||
89
container-runtime/src/Models/CallbackHolder.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { T } from "@start9labs/start-sdk"
|
||||
|
||||
const CallbackIdCell = { inc: 1 }
|
||||
|
||||
const callbackRegistry = new FinalizationRegistry(
|
||||
async (options: { cbs: Map<number, Function>; effects: T.Effects }) => {
|
||||
await options.effects.clearCallbacks({
|
||||
only: Array.from(options.cbs.keys()),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export class CallbackHolder {
|
||||
constructor(private effects?: T.Effects) {}
|
||||
|
||||
private callbacks = new Map<number, Function>()
|
||||
private onLeaveContextCallbacks: Function[] = []
|
||||
private children: Map<string, CallbackHolder> = new Map()
|
||||
private newId() {
|
||||
return CallbackIdCell.inc++
|
||||
}
|
||||
addCallback(callback?: Function) {
|
||||
if (!callback) {
|
||||
return
|
||||
}
|
||||
const id = this.newId()
|
||||
console.error("adding callback", id)
|
||||
this.callbacks.set(id, callback)
|
||||
if (this.effects)
|
||||
callbackRegistry.register(this, {
|
||||
cbs: this.callbacks,
|
||||
effects: this.effects,
|
||||
})
|
||||
return id
|
||||
}
|
||||
child(name: string): CallbackHolder {
|
||||
this.removeChild(name)
|
||||
const child = new CallbackHolder(this.effects)
|
||||
this.children.set(name, child)
|
||||
return child
|
||||
}
|
||||
|
||||
getChild(name: string): CallbackHolder | null {
|
||||
return this.children.get(name) || null
|
||||
}
|
||||
|
||||
removeChild(name: string) {
|
||||
const child = this.children.get(name)
|
||||
if (child) {
|
||||
child.leaveContext()
|
||||
this.children.delete(name)
|
||||
}
|
||||
}
|
||||
private getCallback(index: number): Function | undefined {
|
||||
let callback = this.callbacks.get(index)
|
||||
if (callback) this.callbacks.delete(index)
|
||||
else {
|
||||
for (let [_, child] of this.children) {
|
||||
callback = child.getCallback(index)
|
||||
if (callback) return callback
|
||||
}
|
||||
}
|
||||
return callback
|
||||
}
|
||||
callCallback(index: number, args: any[]): Promise<unknown> {
|
||||
const callback = this.getCallback(index)
|
||||
if (!callback) return Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => callback(...args))
|
||||
.catch((e) => console.error("callback failed", e))
|
||||
}
|
||||
onLeaveContext(fn: Function) {
|
||||
this.onLeaveContextCallbacks.push(fn)
|
||||
}
|
||||
leaveContext() {
|
||||
for (let [_, child] of this.children) {
|
||||
child.leaveContext()
|
||||
}
|
||||
this.children = new Map()
|
||||
for (let fn of this.onLeaveContextCallbacks) {
|
||||
try {
|
||||
fn()
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
this.onLeaveContextCallbacks = []
|
||||
}
|
||||
}
|
||||
41
container-runtime/src/Models/DockerProcedure.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
object,
|
||||
literal,
|
||||
string,
|
||||
boolean,
|
||||
array,
|
||||
dictionary,
|
||||
literals,
|
||||
number,
|
||||
Parser,
|
||||
some,
|
||||
} from "ts-matches"
|
||||
import { matchDuration } from "./Duration"
|
||||
|
||||
const VolumeId = string
|
||||
const Path = string
|
||||
|
||||
export type VolumeId = string
|
||||
export type Path = string
|
||||
export const matchDockerProcedure = object({
|
||||
type: literal("docker"),
|
||||
image: string,
|
||||
system: boolean.optional(),
|
||||
entrypoint: string,
|
||||
args: array(string).defaultTo([]),
|
||||
mounts: dictionary([VolumeId, Path]).optional(),
|
||||
"io-format": literals(
|
||||
"json",
|
||||
"json-pretty",
|
||||
"yaml",
|
||||
"cbor",
|
||||
"toml",
|
||||
"toml-pretty",
|
||||
)
|
||||
.nullable()
|
||||
.optional(),
|
||||
"sigterm-timeout": some(number, matchDuration).onMismatch(30),
|
||||
inject: boolean.defaultTo(false),
|
||||
})
|
||||
|
||||
export type DockerProcedure = typeof matchDockerProcedure._TYPE
|
||||
30
container-runtime/src/Models/Duration.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { string } from "ts-matches"
|
||||
|
||||
export type TimeUnit = "d" | "h" | "s" | "ms" | "m" | "µs" | "ns"
|
||||
export type Duration = `${number}${TimeUnit}`
|
||||
|
||||
const durationRegex = /^([0-9]*(\.[0-9]+)?)(ns|µs|ms|s|m|d)$/
|
||||
|
||||
export const matchDuration = string.refine(isDuration)
|
||||
export function isDuration(value: string): value is Duration {
|
||||
return durationRegex.test(value)
|
||||
}
|
||||
|
||||
export function duration(timeValue: number, timeUnit: TimeUnit = "s") {
|
||||
return `${timeValue > 0 ? timeValue : 0}${timeUnit}` as Duration
|
||||
}
|
||||
const unitsToSeconds: Record<string, number> = {
|
||||
ns: 1e-9,
|
||||
µs: 1e-6,
|
||||
ms: 0.001,
|
||||
s: 1,
|
||||
m: 60,
|
||||
h: 3600,
|
||||
d: 86400,
|
||||
}
|
||||
|
||||
export function fromDuration(duration: Duration | number): number {
|
||||
if (typeof duration === "number") return duration
|
||||
const [, num, , unit] = duration.match(durationRegex) || []
|
||||
return Number(num) * unitsToSeconds[unit]
|
||||
}
|
||||
3
container-runtime/src/Models/Effects.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { types as T } from "@start9labs/start-sdk"
|
||||
|
||||
export type Effects = T.Effects
|
||||
30
container-runtime/src/Models/JsonPath.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { literals, some, string } from "ts-matches"
|
||||
|
||||
type NestedPath<A extends string, B extends string> = `/${A}/${string}/${B}`
|
||||
type NestedPaths = NestedPath<"actions", "run" | "getInput">
|
||||
// prettier-ignore
|
||||
type UnNestPaths<A> =
|
||||
A extends `${infer A}/${infer B}` ? [...UnNestPaths<A>, ... UnNestPaths<B>] :
|
||||
[A]
|
||||
|
||||
export function unNestPath<A extends string>(a: A): UnNestPaths<A> {
|
||||
return a.split("/") as UnNestPaths<A>
|
||||
}
|
||||
function isNestedPath(path: string): path is NestedPaths {
|
||||
const paths = path.split("/")
|
||||
if (paths.length !== 4) return false
|
||||
if (paths[1] === "actions" && (paths[3] === "run" || paths[3] === "getInput"))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
export const jsonPath = some(
|
||||
literals(
|
||||
"/packageInit",
|
||||
"/packageUninit",
|
||||
"/backup/create",
|
||||
"/backup/restore",
|
||||
),
|
||||
string.refine(isNestedPath, "isNestedPath"),
|
||||
)
|
||||
|
||||
export type JsonPath = typeof jsonPath._TYPE
|
||||
22
container-runtime/src/Models/Volume.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as fs from "node:fs/promises"
|
||||
|
||||
export const BACKUP = "backup"
|
||||
export class Volume {
|
||||
readonly path: string
|
||||
constructor(
|
||||
readonly volumeId: string,
|
||||
_path = "",
|
||||
) {
|
||||
if (volumeId.toLowerCase() === BACKUP) {
|
||||
this.path = `/media/startos/backup${!_path ? "" : `/${_path}`}`
|
||||
} else {
|
||||
this.path = `/media/startos/volumes/${volumeId}${!_path ? "" : `/${_path}`}`
|
||||
}
|
||||
}
|
||||
async exists() {
|
||||
return fs.stat(this.path).then(
|
||||
() => true,
|
||||
() => false,
|
||||
)
|
||||
}
|
||||
}
|
||||
42
container-runtime/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { RpcListener } from "./Adapters/RpcListener"
|
||||
import { SystemForEmbassy } from "./Adapters/Systems/SystemForEmbassy"
|
||||
import { AllGetDependencies } from "./Interfaces/AllGetDependencies"
|
||||
import { getSystem } from "./Adapters/Systems"
|
||||
|
||||
const getDependencies: AllGetDependencies = {
|
||||
system: getSystem,
|
||||
}
|
||||
|
||||
new RpcListener(getDependencies)
|
||||
|
||||
/**
|
||||
|
||||
So, this is going to be sent into a running container along with any of the other node modules that are going to be needed and used.
|
||||
|
||||
Once the container is started, we will go into a loading/ await state.
|
||||
This is the init system, and it will always be running, and it will be waiting for a command to be sent to it.
|
||||
|
||||
Each command will be a stopable promise. And an example is going to be something like an action/ main/ or just a query into the types.
|
||||
|
||||
A command will be sent an object which are the effects, and the effects will be things like the file system, the network, the process, and the os.
|
||||
|
||||
|
||||
*/
|
||||
// So OS Adapter
|
||||
// ==============
|
||||
|
||||
/**
|
||||
* Why: So when the we call from the os we enter or leave here?
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
Command: This is a command that the
|
||||
|
||||
There are
|
||||
*/
|
||||
|
||||
/**
|
||||
TODO:
|
||||
Should I separate those adapter in/out?
|
||||
*/
|
||||
26
container-runtime/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["dist"],
|
||||
"inputs": ["./src/index.ts"],
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"strict": true,
|
||||
"outDir": "dist",
|
||||
"preserveConstEnums": true,
|
||||
"sourceMap": true,
|
||||
"target": "ES2022",
|
||||
"pretty": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["node", "jest"],
|
||||
"moduleResolution": "Node16",
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
}
|
||||
}
|
||||
}
|
||||