forked from rosa/hakurei
Compare commits
1570 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
79342e3053
|
|||
|
825bf24731
|
|||
|
08112f0b90
|
|||
|
e1a1e1e399
|
|||
|
92b61889a6
|
|||
|
28e133c298
|
|||
|
323dcb2820
|
|||
|
f46a0370a7
|
|||
|
5ee66a86ee
|
|||
|
d75b8b6e32
|
|||
|
7c9481d38d
|
|||
|
983ab4378f
|
|||
|
5b5c096d31
|
|||
|
5f49127120
|
|||
|
164f8c873d
|
|||
|
9274b642ec
|
|||
|
5992be3dd3
|
|||
|
c661a3b63a
|
|||
|
df0bb877db
|
|||
|
f333b8fbd6
|
|||
|
928a9f61e9
|
|||
|
ce06539eca
|
|||
|
69908e5a41
|
|||
|
b5445573a8
|
|||
|
f869ff95a1
|
|||
|
725f2e0ef3
|
|||
|
4ffa20cd3f
|
|||
|
38450db74a
|
|||
|
9344f694c7
|
|||
|
e8bb5a622d
|
|||
|
22e508fe17
|
|||
|
b18ecf5832
|
|||
|
ec29e755fb
|
|||
|
9aaf160ff4
|
|||
|
e192fca762
|
|||
|
7eafc7b1e4
|
|||
|
282462c2f0
|
|||
|
d9f522d648
|
|||
|
a0b0c7ecc9
|
|||
|
2b8809da7a
|
|||
|
84cda19d63
|
|||
|
154ab953c1
|
|||
|
34e11dd312
|
|||
|
140cb3cc47
|
|||
|
82c974d656
|
|||
|
8c17f201e5
|
|||
|
63e0457538
|
|||
|
ca93264c1f
|
|||
|
94173eafff
|
|||
|
e28c4aa3c0
|
|||
|
8e8410ce38
|
|||
|
8fb6fdaa80
|
|||
|
db69dcf0be
|
|||
|
76c1fb84c8
|
|||
|
729be19af3
|
|||
|
4d04f86f4a
|
|||
|
42cea1e7c6
|
|||
|
83498b5a8a
|
|||
|
9e824452bd
|
|||
|
56937ac396
|
|||
|
6c2f7089b6
|
|||
|
74c18390b4
|
|||
|
1490b32387
|
|||
|
fbd2329d50
|
|||
|
f398f71fa9
|
|||
|
4d017b1309
|
|||
|
cd1d447664
|
|||
|
8b87e0eb76
|
|||
|
f4215ddda5
|
|||
|
652b4c2ba0
|
|||
|
75715f4590
|
|||
|
87c3b3663d
|
|||
|
6d792023b2
|
|||
|
3e62cf379f
|
|||
|
6d991f2644
|
|||
|
a2cc28f53c
|
|||
|
598c7aa30f
|
|||
|
b18f40d974
|
|||
|
12a6061051
|
|||
|
02c3823ed7
|
|||
|
cfbd8bd6f1
|
|||
|
fc7a339ed2
|
|||
|
97fdd5db8b
|
|||
|
a5d9f76f50
|
|||
|
b313dfefb0
|
|||
|
5b32297178
|
|||
|
32cc72821e
|
|||
|
958473e6f4
|
|||
|
cc52bb3035
|
|||
|
7816f8b523
|
|||
|
91e4229e32
|
|||
|
c346cb4c57
|
|||
|
0e0d5e8def
|
|||
|
ea875416d4
|
|||
|
8c7109a2d5
|
|||
|
caf3e0db4b
|
|||
|
12b9f51128
|
|||
|
d15f965d0c
|
|||
|
fadd1b14f7
|
|||
|
3458806685
|
|||
|
8ca70550ab
|
|||
|
7eebf49b98
|
|||
|
8c84883af6
|
|||
|
4e1359aa95
|
|||
|
a3867ad65f
|
|||
|
689f972976
|
|||
|
3f33b62dfd
|
|||
|
ac5488eef6
|
|||
|
77a15130c7
|
|||
|
4c1e823908
|
|||
|
5f5a398a5b
|
|||
|
d5e4a2e6a7
|
|||
|
57c6b84b60
|
|||
|
4269627b4b
|
|||
|
d50e3c3d5b
|
|||
|
eae2890d98
|
|||
|
f8ebfd71a7
|
|||
|
dce1a05f6c
|
|||
|
eff265837c
|
|||
|
7d809eb15f
|
|||
|
9accc2f961
|
|||
|
e5a4094298
|
|||
|
410c4f8bb0
|
|||
|
2e23c6d367
|
|||
|
f5e9a0c04e
|
|||
|
3bd4ef616c
|
|||
|
41402fd578
|
|||
|
b47fa1a214
|
|||
|
9e363cb2c9
|
|||
|
1389c77022
|
|||
|
e231341e48
|
|||
|
70f977627d
|
|||
|
f3a6f7ddf9
|
|||
|
2a9aa3b400
|
|||
|
68a91523b9
|
|||
|
5647321622
|
|||
|
cdd31dd27b
|
|||
|
0615899e56
|
|||
|
0914569e62
|
|||
|
25d9edfc64
|
|||
|
af4c3bbff2
|
|||
|
54aae9d72a
|
|||
|
58646f8ea5
|
|||
|
9d7a27d8ac
|
|||
|
6546ddc64b
|
|||
|
cbf18b302d
|
|||
|
1acb5b0105
|
|||
|
40b33f9fc7
|
|||
|
443a7a30f6
|
|||
|
497e4a5642
|
|||
|
c0e3841ddb
|
|||
|
9ce2c325db
|
|||
|
9836030c59
|
|||
|
b482fd4abf
|
|||
|
2e502ede6c
|
|||
|
4bec0b890c
|
|||
|
7770ccf0aa
|
|||
|
656059278d
|
|||
|
1a9974ffdc
|
|||
|
1a2699b486
|
|||
|
1d3d621e2f
|
|||
|
47f4e287fc
|
|||
|
2e710328a4
|
|||
|
2e7b52d701
|
|||
|
d728607505
|
|||
|
ef414ab01a
|
|||
|
96abf266dd
|
|||
|
fcba32e9c4
|
|||
|
a7f5a5802d
|
|||
|
bb230378e0
|
|||
|
f638c73933
|
|||
|
98d915af3d
|
|||
|
c0593e8325
|
|||
|
608d8303ec
|
|||
|
1c6f30379e
|
|||
|
009a4e0d58
|
|||
|
e7c8656691
|
|||
|
d6be116ff8
|
|||
|
962b02cf25
|
|||
|
6fd6d971ed
|
|||
|
548c96c7ec
|
|||
|
6e8bfa6c4c
|
|||
|
a770d62b9b
|
|||
|
ff44060763
|
|||
|
3010a209b5
|
|||
|
e65a3b435c
|
|||
|
23515f67c8
|
|||
|
4389df60ae
|
|||
|
8092492018
|
|||
|
a7877844bf
|
|||
|
1ed027846d
|
|||
|
2f376d4813
|
|||
|
dc3810b530
|
|||
|
6e9e8c74f3
|
|||
|
4d60fa5632
|
|||
|
8807cbc730
|
|||
|
0e95573f18
|
|||
|
eb2b53307a
|
|||
|
682b3a2ce5
|
|||
|
594221eb78
|
|||
|
34822925e1
|
|||
|
37df040d85
|
|||
|
0360e779f3
|
|||
|
3e236333a7
|
|||
|
f24ae21af1
|
|||
|
99b324fb17
|
|||
|
6f50811dc9
|
|||
|
6b87bac401
|
|||
|
a967aa3b6e
|
|||
|
38bc2c7508
|
|||
|
30eb0d6a61
|
|||
|
c2ff9c9fa5
|
|||
|
d38d306147
|
|||
|
c32c06b2e8
|
|||
|
61199f734c
|
|||
|
87cf0d4e6b
|
|||
|
cf0dffa0f5
|
|||
|
686d7ec63a
|
|||
|
4c653b1151
|
|||
|
0b0a63d151
|
|||
|
6231cfe2aa
|
|||
|
712e80890b
|
|||
|
3fe7d48014
|
|||
|
16f9d39427
|
|||
|
c1cd5ba07b
|
|||
|
7b0cd2e472
|
|||
|
e580307528
|
|||
|
ee1dffb676
|
|||
|
f095fcf181
|
|||
|
ca8a130130
|
|||
|
0ad6b00e41
|
|||
|
ad0f1cf36b
|
|||
|
b12d924fa2
|
|||
|
c31d8ae41a
|
|||
|
6dbbf15c0e
|
|||
|
be7de68a42
|
|||
|
a759cf3666
|
|||
|
8c2dd3e984
|
|||
|
67038d5af4
|
|||
|
53d8d12e7f
|
|||
|
7997d79e56
|
|||
|
f2f1726190
|
|||
|
f63203cb0a
|
|||
|
19555c7670
|
|||
|
a3beab8959
|
|||
|
2ea786d6a9
|
|||
|
747d4ec4b0
|
|||
|
b76e6f6519
|
|||
|
840d8f68bf
|
|||
|
4bede7ecdd
|
|||
|
487a03b5a3
|
|||
|
8f3c22896a
|
|||
|
a167c1aba5
|
|||
|
a6008ef68b
|
|||
|
5228b27362
|
|||
|
f00d3a07ad
|
|||
|
f9538bc21b
|
|||
|
6ae5efec56
|
|||
|
14f4c59c8c
|
|||
|
688d43417b
|
|||
|
9f8fafa39b
|
|||
|
6643cfbeee
|
|||
|
dcde38f2e9
|
|||
|
deebbf6b1a
|
|||
|
0c557798bc
|
|||
|
327e6ed5a2
|
|||
|
76c7a423a9
|
|||
|
6e113b8836
|
|||
|
ce9f4b5f71
|
|||
|
8f727273ef
|
|||
|
d0a63b942e
|
|||
|
7f2126df32
|
|||
|
0cf0e18e35
|
|||
|
ee5c0dd135
|
|||
|
92c48d82e2
|
|||
|
c79a4fe7f8
|
|||
|
0aeb2bccfb
|
|||
|
50e079b99f
|
|||
|
fb2cb5005a
|
|||
|
6e73c28a92
|
|||
|
2c08aa3674
|
|||
|
1af73ae7b4
|
|||
|
c9aa5e04b1
|
|||
|
70a38bd3b0
|
|||
|
533b15da89
|
|||
|
a890e1d0e5
|
|||
|
e3520835bb
|
|||
|
0e56847754
|
|||
|
145d03b366
|
|||
|
2886228d40
|
|||
|
e1e499b79e
|
|||
|
65b7dd8b37
|
|||
|
8d72b9e5bd
|
|||
|
8a3c3d145a
|
|||
|
575ef307ad
|
|||
|
d4144fcf7f
|
|||
|
bad66facbc
|
|||
|
4aba014eac
|
|||
|
779ba994ce
|
|||
|
917be2de93
|
|||
|
9aad98d409
|
|||
|
b0d06b67dc
|
|||
|
089100f29d
|
|||
|
dfd26abf6c
|
|||
|
617ee21647
|
|||
|
15cdb37ec2
|
|||
|
1f0bdc7aca
|
|||
|
e3ffe85670
|
|||
|
f4403ba5cd
|
|||
|
5a26895a22
|
|||
|
09d9f766a9
|
|||
|
6558169666
|
|||
|
cccf970c57
|
|||
|
57ffb21690
|
|||
|
9c560b455a
|
|||
|
4c7c0fbfc6
|
|||
|
18b3b7904e
|
|||
|
fefefdf734
|
|||
|
b84bb09a80
|
|||
|
337bf20f50
|
|||
|
1cb792cf6e
|
|||
|
b2b40b07e8
|
|||
|
da11b26ec1
|
|||
|
024489e800
|
|||
|
0f795712b0
|
|||
|
7e2210ff71
|
|||
|
a71a008f3c
|
|||
|
162265b47e
|
|||
|
3fa7ac04e4
|
|||
|
bf2867d653
|
|||
|
ec0f0f6507
|
|||
|
a77a802955
|
|||
|
4407e14dfc
|
|||
|
e024d3184a
|
|||
|
8e1bf00c2d
|
|||
|
b111e22050
|
|||
|
1fa458c0be
|
|||
|
2c7ae67a67
|
|||
|
3826621b21
|
|||
|
041b505c2e
|
|||
|
e6debce649
|
|||
|
aa26b86fce
|
|||
|
a57a8fd5d8
|
|||
|
1d5d063d6a
|
|||
|
e61628a34e
|
|||
|
5a18f14929
|
|||
|
f12880688d
|
|||
|
bb5bbfe16a
|
|||
|
427e1ca37c
|
|||
|
96fdd9ecc5
|
|||
|
02771b655b
|
|||
|
d1c8d2c39b
|
|||
|
0efd742e8a
|
|||
|
ae1fe638d5
|
|||
|
445d95023b
|
|||
|
fc66f0bb47
|
|||
|
2cd6b35bee
|
|||
|
09a216c6ec
|
|||
|
44d17325c2
|
|||
|
544ce77cbc
|
|||
|
63c3c30b23
|
|||
|
d23c4ecc7c
|
|||
|
a46656dff8
|
|||
|
77db153ff5
|
|||
|
520d95bc07
|
|||
|
451df3f4e7
|
|||
|
011fac15ed
|
|||
|
347682ad0b
|
|||
|
1a2b979add
|
|||
|
b1c90cc380
|
|||
|
3a66b8143a
|
|||
|
64bbd3aabd
|
|||
|
08799a13d0
|
|||
|
1aef9c3bbb
|
|||
|
1f38303747
|
|||
|
640777b00c
|
|||
|
1d657193cf
|
|||
|
bab5406295
|
|||
|
725ae7d64d
|
|||
|
37a0c3967e
|
|||
|
ea0692548f
|
|||
|
48ea23e648
|
|||
|
40320e4920
|
|||
|
3ca0f61632
|
|||
|
6ffaac96e3
|
|||
|
13c7713d0c
|
|||
|
42389f7ec5
|
|||
|
30f130c691
|
|||
|
ceb4d26087
|
|||
|
852f3a9b3d
|
|||
|
5e02dbdb0d
|
|||
|
6a3248d472
|
|||
|
67404c98d9
|
|||
|
b9bf69cfce
|
|||
|
4648f98272
|
|||
|
11d99439ac
|
|||
|
39e4c5b8ac
|
|||
|
e8f6db38b6
|
|||
|
20d5b71575
|
|||
|
e903e7f542
|
|||
|
1caa051f4d
|
|||
|
dcdc6f7f6d
|
|||
|
5ad6f26b46
|
|||
|
7ba75a79f4
|
|||
|
9ef84d3904
|
|||
|
3b7b6e51fb
|
|||
|
b1b4debb82
|
|||
|
021cbbc2a8
|
|||
|
a4a54a4a4d
|
|||
|
04a344aac6
|
|||
|
6b98156a3d
|
|||
|
753432cf09
|
|||
|
f8902e3679
|
|||
|
8ee53a5164
|
|||
|
3981d44757
|
|||
|
9fd67e47b4
|
|||
|
4dcec40156
|
|||
|
9a274c78a3
|
|||
|
5647c3a91f
|
|||
|
992139c75d
|
|||
|
57c69b533e
|
|||
|
6f0c2a80f2
|
|||
|
08dfefb28d
|
|||
|
b081629662
|
|||
|
fba541f301
|
|||
|
5f0da3d5c2
|
|||
|
4d5841dd62
|
|||
|
9e752b588a
|
|||
|
27b1aaae38
|
|||
|
9e18de1dc2
|
|||
|
b80ea91a42
|
|||
|
30a9dfa4b8
|
|||
|
8d657b6fdf
|
|||
|
ae9b9adfd2
|
|||
|
dd6a480a21
|
|||
|
3942272c30
|
|||
|
9036986156
|
|||
|
a394971dd7
|
|||
|
9daba60809
|
|||
|
bcd79a22ff
|
|||
|
0ff7ab915b
|
|||
|
823575acac
|
|||
|
136bc0917b
|
|||
|
d6b082dd0b
|
|||
|
89d6d9576b
|
|||
|
fafce04a5d
|
|||
|
5d760a1db9
|
|||
|
d197e40b2a
|
|||
|
2008902247
|
|||
|
30ac985fd2
|
|||
|
e9fec368f8
|
|||
|
46add42f58
|
|||
|
377b61e342
|
|||
|
520c36db6d
|
|||
|
3352bb975b
|
|||
|
f7f48d57e9
|
|||
|
5c2345128e
|
|||
|
78f9676b1f
|
|||
|
5b5b676132
|
|||
|
78383fb6e8
|
|||
|
e97f6a393f
|
|||
|
eeffefd22b
|
|||
|
ac825640ab
|
|||
|
a7f7ce1795
|
|||
|
38c639e35c
|
|||
|
b2cb13e94c
|
|||
|
46f98d12d6
|
|||
|
503c7f953c
|
|||
|
15c9f6545d
|
|||
|
83b0e32c55
|
|||
|
eeaf26e7a2
|
|||
|
b587caf2e8
|
|||
|
f1c2ca4928
|
|||
|
0ca301219f
|
|||
|
e2199e1276
|
|||
|
86eacb3208
|
|||
|
8541bdd858
|
|||
|
46be0b0dc8
|
|||
|
cbe37e87e7
|
|||
|
66d741fb07
|
|||
|
0d449011f6
|
|||
|
46428ed85d
|
|||
|
081d6b463c
|
|||
|
11b3171180
|
|||
|
adbb84c3dd
|
|||
|
1084e31d95
|
|||
|
27a1b8fe0a
|
|||
|
b2141a41d7
|
|||
|
c0dff5bc87
|
|||
|
04513c0510
|
|||
|
28ebf973d6
|
|||
|
41aeb404ec
|
|||
|
0b1009786f
|
|||
|
b390640376
|
|||
|
ad2c9f36cd
|
|||
|
67db3fbb8d
|
|||
|
560cb626a1
|
|||
|
c33a6a5b7e
|
|||
|
952082bd9b
|
|||
|
24a9b24823
|
|||
|
c2e61e7987
|
|||
|
86787b3bc5
|
|||
|
cdfcfe6ce0
|
|||
|
68a2f0c240
|
|||
|
7319c7adf9
|
|||
|
e9c890cbb2
|
|||
|
6f924336fc
|
|||
|
bd88f10524
|
|||
|
e34e3b917e
|
|||
|
b0ba165107
|
|||
|
351d6c5a35
|
|||
|
f23f73701c
|
|||
|
876917229a
|
|||
|
0558032c2d
|
|||
|
c61cdc505f
|
|||
|
062edb3487
|
|||
|
e4355279a1
|
|||
|
289fdebead
|
|||
|
9c9e190db9
|
|||
|
d7d42c69a1
|
|||
|
c758e762bd
|
|||
|
10f8b1c221
|
|||
|
6907700d67
|
|||
|
0243f3ffbd
|
|||
|
cd0beeaf8e
|
|||
|
a69273ab2a
|
|||
|
4cd0f57e48
|
|||
|
33a0e6c01b
|
|||
|
d58f5c7590
|
|||
|
1da992e342
|
|||
|
9641805ec2
|
|||
|
0738f4889a
|
|||
|
7de3cfe221
|
|||
|
8b0648dd5d
|
|||
|
4667fac76c
|
|||
|
52e5443b0e
|
|||
|
130e470b60
|
|||
|
ba5ee8e3ee
|
|||
|
d1cef30877
|
|||
|
0188a3f0c7
|
|||
|
04fe3b24ce
|
|||
|
93ad551054
|
|||
|
3d54d1f176
|
|||
|
9feac7738f
|
|||
|
591a60bac9
|
|||
|
5093a06026
|
|||
|
50c1d7f880
|
|||
|
9e63633fbc
|
|||
|
61f981a34a
|
|||
|
d717c41bbe
|
|||
|
b896eec9b7
|
|||
|
8ab99e5e40
|
|||
|
2b6160ef7d
|
|||
|
4dcac7f133
|
|||
|
966fd4df9e
|
|||
|
a2cf59b989
|
|||
|
e87f59c4e4
|
|||
|
3b221c3e77
|
|||
|
ff3b385b12
|
|||
|
c6920e6ab7
|
|||
|
59b25d45fe
|
|||
|
9b99650eb1
|
|||
|
15bff9e1a6
|
|||
|
b948525c07
|
|||
|
9acbd16e9a
|
|||
|
64e5a1068b
|
|||
|
b6cbd49d8c
|
|||
|
6913b9224a
|
|||
|
9584958ecc
|
|||
|
389844b1ea
|
|||
|
5b7ab35633
|
|||
|
52b1a5a725
|
|||
|
6b78df8714
|
|||
|
dadf170a46
|
|||
|
9594832302
|
|||
|
91a2d4d6e1
|
|||
|
a854719b9f
|
|||
|
f03c0fb249
|
|||
|
a6600be34a
|
|||
|
b5592633f5
|
|||
|
584e302168
|
|||
|
141958656f
|
|||
|
648079f42c
|
|||
|
19c76e0831
|
|||
|
71fcc972ba
|
|||
|
62002efd08
|
|||
|
e33294db9c
|
|||
|
b1ea3b4acf
|
|||
|
2c254c70b8
|
|||
|
ea014d6af2
|
|||
|
1b48484c16
|
|||
|
713bff3eb0
|
|||
|
30f459e690
|
|||
|
8766fddcb3
|
|||
|
2745602be3
|
|||
|
ee22847dde
|
|||
|
c61188649b
|
|||
|
6a87a96838
|
|||
|
2548a681e9
|
|||
|
d514d0679f
|
|||
|
4407892632
|
|||
|
e661260607
|
|||
|
044490e0a5
|
|||
|
af038c89ff
|
|||
|
d2f30173cd
|
|||
|
5319ea994c
|
|||
|
bbe178be3e
|
|||
|
ca28e9936b
|
|||
|
f61c6ade56
|
|||
|
fce3d63823
|
|||
|
722c3cc54f
|
|||
|
372d509e5c
|
|||
|
d62516ed1e
|
|||
|
d2b635eb55
|
|||
|
50403e9d60
|
|||
|
b98c5f2e21
|
|||
|
d972cffe5a
|
|||
|
d8648304bb
|
|||
|
f7bfa9a6c2
|
|||
|
7035b4b598
|
|||
|
094b8400dd
|
|||
|
4652d921d8
|
|||
|
066213c245
|
|||
|
98832c21ee
|
|||
|
6cdb6a652b
|
|||
|
7c932cbceb
|
|||
|
20ebddd9bf
|
|||
|
420c721c7d
|
|||
|
bac583f89e
|
|||
|
722989c682
|
|||
|
b852402f67
|
|||
|
6d015a949e
|
|||
|
e9a72490db
|
|||
|
0a12d456ce
|
|||
|
d1fc1a3db7
|
|||
|
1c2d5f6b57
|
|||
|
faea1f4bd6
|
|||
|
0cb1007daa
|
|||
|
e292031624
|
|||
|
cd5959fe5a
|
|||
|
08c35ca24f
|
|||
|
72bd3fb05e
|
|||
|
59c66747df
|
|||
|
9e6fe8db4b
|
|||
|
5168ee3e13
|
|||
|
c8313c2dc4
|
|||
|
3fcdadb669
|
|||
|
3966bc5152
|
|||
|
b208af8b85
|
|||
|
8d650c0c8f
|
|||
|
a720efc32d
|
|||
|
400540cd41
|
|||
|
1113efa5c2
|
|||
|
8b875f865c
|
|||
|
8905d653ba
|
|||
|
9c2fb6246f
|
|||
|
9c116acec6
|
|||
|
988239a2bc
|
|||
|
bc03118142
|
|||
|
74c213264a
|
|||
|
345cffddc2
|
|||
|
49163758c8
|
|||
|
ad22c15fb1
|
|||
|
9c774f7e0a
|
|||
|
707f0a349f
|
|||
|
7c35be066a
|
|||
|
f91d55fa5e
|
|||
|
5862cc1966
|
|||
|
b3f0360a05
|
|||
|
8938994036
|
|||
|
96d382f805
|
|||
|
5c785c135c
|
|||
|
0130f8ea6d
|
|||
|
faac5c4a83
|
|||
|
620062cca9
|
|||
|
196b200d0f
|
|||
|
04e6bc3c5c
|
|||
|
5c540f90aa
|
|||
|
1e8ac5f68e
|
|||
|
fd515badff
|
|||
|
330a344845
|
|||
|
48cdf8bf85
|
|||
|
7fb42ba49d
|
|||
|
19a2737148
|
|||
|
baf2def9cc
|
|||
|
242e042cb9
|
|||
|
6988c9c4db
|
|||
|
d6e0ed8c76
|
|||
|
53be3309c5
|
|||
|
644dd18a52
|
|||
|
27c6f976df
|
|||
|
279a973633
|
|||
|
9c1b522689
|
|||
|
5c8cd46c02
|
|||
|
2dba550a2b
|
|||
|
8c64812b34
|
|||
|
d1423d980d
|
|||
|
104da0f66a
|
|||
|
d996d9fbb7
|
|||
|
469f97ccc1
|
|||
|
af7a6180a1
|
|||
|
03b5c0e20a
|
|||
|
6a31fb4fa3
|
|||
|
bae45363bc
|
|||
|
2c17d1abe0
|
|||
|
0aa459d1a9
|
|||
|
00053e6287
|
|||
|
3a0c020150
|
|||
|
78655f159e
|
|||
|
30bb52e380
|
|||
|
66197ebdb2
|
|||
|
f7a2744025
|
|||
|
f16b7bfaf0
|
|||
|
6228cda7ad
|
|||
|
86c336de88
|
|||
|
ba5d882ef2
|
|||
|
1e0d68a29e
|
|||
|
80f2367c16
|
|||
|
5ea4dae4b8
|
|||
|
eb1a3918a8
|
|||
|
349011a5e6
|
|||
|
861249751a
|
|||
|
e3445c2a7e
|
|||
|
7315e64a8a
|
|||
|
7d74454f6d
|
|||
|
96956c849a
|
|||
|
aabdcbba1c
|
|||
|
38cc4a6429
|
|||
|
27ef7f81fa
|
|||
|
f7888074b9
|
|||
|
95ffe0429c
|
|||
|
16d0cf04c1
|
|||
|
6a2b32b48c
|
|||
|
c1472fc54d
|
|||
|
179cf07e48
|
|||
|
c2d2795e2b
|
|||
|
2c1d7edd7a
|
|||
|
1ee8d09223
|
|||
|
7f01cb3d59
|
|||
|
65ae4f57c2
|
|||
|
77110601cc
|
|||
|
c5b1949430
|
|||
|
17805cdfa8
|
|||
|
9c9befb4c9
|
|||
|
fcdf9ecee4
|
|||
|
fbd97b658f
|
|||
|
c93725ac58
|
|||
|
f14ab80253
|
|||
|
9989881dd9
|
|||
|
a36b3ece16
|
|||
|
75970a5650
|
|||
|
572c99825d
|
|||
|
ebdf9dcecc
|
|||
|
8ea2a56d5b
|
|||
|
159a45c027
|
|||
|
0eb2bfa12e
|
|||
|
e19a98244a
|
|||
|
7e2f13fa1b
|
|||
|
97448e2104
|
|||
|
a87ad28b8b
|
|||
|
883d4ee4af
|
|||
|
d2c6d486b0
|
|||
|
6fdd800b2b
|
|||
|
94e3debc63
|
|||
|
ea87664a75
|
|||
|
04d9984da0
|
|||
|
145ccd1c92
|
|||
|
c5089cad78
|
|||
|
c83905f311
|
|||
|
b7cc14f296
|
|||
|
57e1e5141d
|
|||
|
1440195c3f
|
|||
|
cc60e0d15d
|
|||
|
9deaf853f0
|
|||
|
2baa9df133
|
|||
|
51d3df2419
|
|||
|
1d0fcf3a75
|
|||
|
e92971e0c2
|
|||
|
6159c74e96
|
|||
|
2a34a269d0
|
|||
|
ef130adb27
|
|||
|
5694e528e6
|
|||
|
b4e82e68a7
|
|||
|
d041fee791
|
|||
|
cefd02e960
|
|||
|
ad8f799703
|
|||
|
c74c269b66
|
|||
|
4b0cce4db5
|
|||
|
cd9b534d6b
|
|||
|
84e6922f30
|
|||
|
c16725a679
|
|||
|
a6160cd410
|
|||
|
826347fe1f
|
|||
|
085eaed7ba
|
|||
|
37d368a7f9
|
|||
|
2aeac7f582
|
|||
|
2b93631f52
|
|||
|
b3749aaf0b
|
|||
|
c8bb88cced
|
|||
|
f7f80f95b9
|
|||
|
6ea6c794fb
|
|||
|
6c2da4c4b2
|
|||
|
90f915a708
|
|||
|
a5fea4686e
|
|||
|
ae8c365c0f
|
|||
|
485db515f7
|
|||
|
ec7ee0789e
|
|||
|
42c93a57a4
|
|||
|
b1b14810ac
|
|||
|
de117ef365
|
|||
|
5e4bf23e0c
|
|||
|
d4519e2075
|
|||
|
7f1e4cf43c
|
|||
|
d021621fba
|
|||
|
56567307ec
|
|||
|
0264a1ef09
|
|||
|
0123bbee3d
|
|||
|
771adad603
|
|||
|
178305cb22
|
|||
|
c2456e252c
|
|||
|
273068b90c
|
|||
|
16b20e1d34
|
|||
|
b983917a6e
|
|||
|
e1b8f40add
|
|||
|
6df0d37c5a
|
|||
|
1619b06541
|
|||
|
e335d99c6b
|
|||
|
d888d09b6d
|
|||
|
54176e7315
|
|||
|
3bfe99d3d8
|
|||
|
149dfbb6af
|
|||
|
58801b44d4
|
|||
|
e065bbf792
|
|||
|
a883e57e7d
|
|||
|
ef9bd8ecbf
|
|||
|
a40527dcb2
|
|||
|
88d9a6163e
|
|||
|
47860b0387
|
|||
|
50c9da8b6d
|
|||
|
16966043c7
|
|||
|
a3515a6ef5
|
|||
|
7f05baab28
|
|||
|
d4d5e631ae
|
|||
|
1df3bcc3b9
|
|||
|
1809b53e52
|
|||
|
67b2914c94
|
|||
|
74dee11822
|
|||
|
a58c9258cc
|
|||
|
710b164c91
|
|||
|
93911d6015
|
|||
|
bb097536d4
|
|||
|
49b6526a38
|
|||
|
f9c31df94d
|
|||
|
4f570cc5c9
|
|||
|
5828631e79
|
|||
|
4f9f4875d7
|
|||
|
d49e654482
|
|||
|
b746e352e5
|
|||
|
c620d88dce
|
|||
|
7cd14b8865
|
|||
|
3e18a4b397
|
|||
|
1791b604b5
|
|||
|
59ff6db7ec
|
|||
|
430e099556
|
|||
|
17b64bb42c
|
|||
|
dbb89dfb0f
|
|||
|
de06ea2be4
|
|||
|
1ef7bedfb5
|
|||
|
05a828c474
|
|||
|
0061d11f93
|
|||
|
fb101a02f2
|
|||
|
3dbd67d113
|
|||
|
f511f0a9e9
|
|||
|
47995137b3
|
|||
|
e1b8607101
|
|||
|
3d3bd45b95
|
|||
|
9fb0b2452e
|
|||
|
a3e87dd0ef
|
|||
|
90a38c0708
|
|||
|
39cc8caa93
|
|||
|
c4f64f7606
|
|||
|
a9e2a5e59f
|
|||
|
9fb0722cdf
|
|||
|
2f3e323c46
|
|||
|
1fc9c3200f
|
|||
|
096a25ad3a
|
|||
|
ffd2f979fb
|
|||
|
31a8cc9b5c
|
|||
|
bb3f60fc74
|
|||
|
697c91e04d
|
|||
|
3f7b8b4332
|
|||
|
fa94155f42
|
|||
|
233bd163fb
|
|||
|
f9b69c94bc
|
|||
|
68aefa6d59
|
|||
|
159fd55dbb
|
|||
|
ce6b3ff53b
|
|||
|
30afa0e2ab
|
|||
|
9b751de078
|
|||
|
d77ad3bb6e
|
|||
|
0142fc90b0
|
|||
|
3c9f7cfcd0
|
|||
|
a3526b3ceb
|
|||
|
6ad21e2288
|
|||
|
27e2e3f996
|
|||
|
e0c720681b
|
|||
|
f982b13a59
|
|||
|
443911ada1
|
|||
|
d7a3706db3
|
|||
|
3226dc44dc
|
|||
|
9f98d12ad8
|
|||
|
550e83dda9
|
|||
|
7877b4e627
|
|||
|
47ce6f5bd0
|
|||
|
48f4ccba33
|
|||
|
c31884bee4
|
|||
|
f8661ad479
|
|||
|
536f0cbae6
|
|||
|
8d872ff1cd
|
|||
|
bf14a412e4
|
|||
|
8b4576bc5f
|
|||
|
29ebc52e26
|
|||
|
5f81aac0e2
|
|||
|
47490823be
|
|||
|
1ac8ca7a80
|
|||
|
fd8b2fd522
|
|||
|
20a8519044
|
|||
|
8c4fd00c50
|
|||
|
bc3dd6fbb0
|
|||
|
616ed29edf
|
|||
|
9d9b7294a4
|
|||
|
6c1e2f10a7
|
|||
|
abf96d2283
|
|||
|
6c90e879da
|
|||
|
d1b404dc3a
|
|||
|
744e4e0632
|
|||
|
85eda49b2b
|
|||
|
b26bc05bb0
|
|||
|
2d63ea8fee
|
|||
|
dd4326418c
|
|||
|
79c0106ea0
|
|||
|
536db533de
|
|||
|
07927006a8
|
|||
|
77ea27b038
|
|||
|
e76bc6a13a
|
|||
|
cc403c96d8
|
|||
|
66118ba941
|
|||
|
823ba08dbc
|
|||
|
660835151e
|
|||
|
53e6df7e81
|
|||
|
bd80327a8f
|
|||
|
41f9aebbb7
|
|||
|
a2a0e36802
|
|||
|
fbe93fc771
|
|||
|
968d8dbaf1
|
|||
|
f1758a6fa8
|
|||
|
88aaa4497c
|
|||
|
b7ea68de35
|
|||
|
67e453f5c4
|
|||
|
67092c835a
|
|||
|
18918d9a0d
|
|||
|
380ca4e022
|
|||
|
887aef8514
|
|||
|
d61faa09eb
|
|||
|
50153788ef
|
|||
|
c84fe63217
|
|||
|
eb67e5e0a8
|
|||
|
948afe33e5
|
|||
|
76c657177d
|
|||
|
4356f978aa
|
|||
|
4f17dad645
|
|||
|
68b7d41c65
|
|||
|
e48f303e38
|
|||
|
f1fd406b82
|
|||
|
53b1de3395
|
|||
|
92dcadbf27
|
|||
|
0bd6a18326
|
|||
|
67d592c337
|
|||
|
fdc8a8419b
|
|||
|
122cfbf63a
|
|||
|
504f5d28fe
|
|||
|
3eadd5c580
|
|||
|
4d29333807
|
|||
|
e1533fa4c6
|
|||
|
9a74d5273d
|
|||
|
2abc8c454e
|
|||
|
fecb963e85
|
|||
|
cd9da57f20
|
|||
|
c6a95f5a6a
|
|||
|
228489371d
|
|||
|
490471d22b
|
|||
|
763d2572fe
|
|||
|
bb1b6beb87
|
|||
|
3224a7da63
|
|||
|
8a86cf74ee
|
|||
|
e34a59e332
|
|||
|
861801597d
|
|||
|
334578fdde
|
|||
|
20790af71e
|
|||
|
43b8a40fc0
|
|||
|
87c3059214
|
|||
|
6956dfc31a
|
|||
|
d9ebaf20f8
|
|||
|
acee0b3632
|
|||
|
5e55a796df
|
|||
|
f6eaf76ec9
|
|||
|
5c127a7035
|
|||
|
8a26521f5b
|
|||
|
0fd4556e38
|
|||
|
50b82dcf82
|
|||
|
20a8d30821
|
|||
|
cdf2e4a2fb
|
|||
|
dcb8a6ea06
|
|||
|
094a62ba9d
|
|||
|
6420b6e6e8
|
|||
|
d7d058fdc5
|
|||
|
84795b5d9f
|
|||
|
f84d30deed
|
|||
|
77821feb8b
|
|||
|
eb1060f395
|
|||
|
0e08254595
|
|||
|
349d8693bf
|
|||
|
e88ae87e50
|
|||
|
7cd4aa838c
|
|||
|
641942a4e3
|
|||
|
b6a66acfe4
|
|||
|
b72dc43bc3
|
|||
|
8e59ff98b5
|
|||
|
f06d7fd387
|
|||
|
ba75587132
|
|||
|
9a06ce2db0
|
|||
|
3ec15bcdf1
|
|||
|
d933234784
|
|||
|
1c49c75f95
|
|||
|
6a01a55d7e
|
|||
|
b14964a66d
|
|||
|
ff98c9ded9
|
|||
|
7f3d1d6375
|
|||
|
3a4f20b759
|
|||
|
21858ecfe4
|
|||
|
574a64aa85
|
|||
|
85d27229fd
|
|||
|
83fb80d710
|
|||
|
fe6dc62ebf
|
|||
|
823f9c76a7
|
|||
|
2df913999b
|
|||
|
52c959bd6a
|
|||
|
d258dea0bf
|
|||
|
dc96302111
|
|||
|
88e9a143d6
|
|||
|
8d06c0235b
|
|||
|
4155adc16a
|
|||
|
2a9525c77a
|
|||
|
efc90c3221
|
|||
|
610ee13ab3
|
|||
|
5936e6a4aa
|
|||
|
3499a82785
|
|||
|
088d35e4e6
|
|||
|
1667df9c43
|
|||
|
156dd767ef
|
|||
|
5fe166a4a7
|
|||
|
41a8d03dd2
|
|||
|
610572d0e6
|
|||
|
29951c5174
|
|||
|
91c3594dee
|
|||
|
7ccc2fc5ec
|
|||
|
63e137856e
|
|||
|
e1e46504a1
|
|||
|
ec9343ebd6
|
|||
|
423808ac76
|
|||
|
2494ede106
|
|||
|
da3848b92f
|
|||
|
34cb4ebd3b
|
|||
|
f712466714
|
|||
|
f2430b5f5e
|
|||
|
863e6f5db6
|
|||
|
23df2ab999
|
|||
|
7bd4d7d0e6
|
|||
|
b3c30bcc51
|
|||
|
38059db835
|
|||
|
409fd3149e
|
|||
|
4eea136308
|
|||
|
c86ff02d8d
|
|||
|
e8dda70c41
|
|||
|
7ea4e8b643
|
|||
|
5eefebcb48
|
|||
|
8e08e8f518
|
|||
|
54da6ce03d
|
|||
|
3a21ba1bca
|
|||
|
45301559bf
|
|||
|
0df87ab111
|
|||
|
aa0a949cef
|
|||
|
ce0064384d
|
|||
|
53d80f4b66
|
|||
|
156096ac98
|
|||
|
ceb75538cf
|
|||
|
0741a614ed
|
|||
|
e7e9b4caea
|
|||
|
f6d32e482a
|
|||
|
79adf217f4
|
|||
|
8efffd72f4
|
|||
|
86ad8b72aa
|
|||
|
e91049c3c5
|
|||
|
3d4d32932d
|
|||
|
0ab6c13c77
|
|||
|
834cb0d40b
|
|||
|
7548a627e5
|
|||
|
b98d27f773
|
|||
|
f3aa31e401
|
|||
|
4da26681b5
|
|||
|
4897b0259e
|
|||
|
d6e4f85864
|
|||
|
3eb927823f
|
|||
|
d76b9d04b8
|
|||
|
fa93476896
|
|||
|
bd0ef086b1
|
|||
|
05202cf994
|
|||
|
40081e7a06
|
|||
|
863d3dcf9f
|
|||
|
8ad9909065
|
|||
|
deda16da38
|
|||
|
55465c6e72
|
|||
|
ce249d23f1
|
|||
|
dd5d792d14
|
|||
|
d15d2ec2bd
|
|||
|
3078c41ce7
|
|||
|
e9de5d3aca
|
|||
|
993afde840
|
|||
|
c9cd16fd2a
|
|||
|
e42ea32dbe
|
|||
|
e7982b4ee9
|
|||
|
ef1ebf12d9
|
|||
|
775a9f57c9
|
|||
|
2f8ca83376
|
|||
|
3d720ada92
|
|||
|
2e5362e536
|
|||
|
6d3bd27220
|
|||
|
a27305cb4a
|
|||
|
0e476c5e5b
|
|||
|
54712e0426
|
|||
|
b77c1ecfdb
|
|||
|
dce5839a79
|
|||
|
d597592e1f
|
|||
|
056f5b12d4
|
|||
|
da2bb546ba
|
|||
|
7bfbd59810
|
|||
|
ea815a59e8
|
|||
|
28a8dc67d2
|
|||
|
ec49c63c5f
|
|||
|
5a50bf80ee
|
|||
|
ce06b7b663
|
|||
|
08bdc68f3a
|
|||
|
8cb0b433b2
|
|||
|
767f1844d2
|
|||
|
54610aaddc
|
|||
|
2e80660169
|
|||
|
d0a3c6a2f3
|
|||
|
0c0e3d6fc2
|
|||
|
fae910a1ad
|
|||
|
178c8bc28b
|
|||
|
30dcab0734
|
|||
|
0ea051062b
|
|||
|
b0f2ab6fff
|
|||
|
00a5bdf006
|
|||
|
a27dfdc058
|
|||
|
6d0d9cecd1
|
|||
|
17248d7d61
|
|||
|
41e5628c67
|
|||
|
ffbec828e1
|
|||
|
de0467a65e
|
|||
|
b5999b8814
|
|||
|
ebc67bb8ad
|
|||
|
e60ff660f6
|
|||
|
47db461546
|
|||
|
0a3fe5f907
|
|||
|
b72d502f1c
|
|||
|
f8b3db3f66
|
|||
|
0e2fb1788f
|
|||
|
d8417e2927
|
|||
|
ccc0d98bd7
|
|||
|
a3fd05765e
|
|||
|
c538df7daa
|
|||
|
44e5aa1a36
|
|||
|
cf0e7d8c27
|
|||
|
130add21e5
|
|||
|
5ec4045e24
|
|||
|
be2075f169
|
|||
|
e9fb1d7be5
|
|||
|
dafe9f8efc
|
|||
|
96dd7abd80
|
|||
|
d5fb179012
|
|||
|
462863e290
|
|||
|
2786611b88
|
|||
|
791a1dfa55
|
|||
|
564db6863b
|
|||
|
87781c7658
|
|||
|
0c38fb7b6a
|
|||
|
357cfcddee
|
|||
|
6bf245cf1b
|
|||
|
c8eeb4a4d1
|
|||
|
5785714b64
|
|||
|
422efcf258
|
|||
|
104eeecf65
|
|||
|
bf856f06e5
|
|||
|
1931b54600
|
|||
|
093e30c788
|
|||
|
1b17ccda91
|
|||
|
7c6fc1128b
|
|||
|
8cdd659239
|
|||
|
15c2839a09
|
|||
|
b9b9705b52
|
|||
|
246e04214a
|
|||
|
503bfc6468
|
|||
|
d837628b4c
|
|||
|
3cb58b4b72
|
|||
|
bb1fc4c7bc
|
|||
|
f44923da29
|
|||
|
5e7861bb00
|
|||
|
7cb3308a53
|
|||
|
490093a659
|
|||
|
2b22efcdf1
|
|||
|
8a2f9edcf9
|
|||
|
0d3f332d45
|
|||
|
d5509cc6e5
|
|||
|
0d3ae6cb23
|
|||
|
69b1131d66
|
|||
|
2c0b92771a
|
|||
|
054c91879f
|
|||
|
c34439fc5f
|
|||
|
32fb137bb2
|
|||
|
e7a665e043
|
|||
|
af741f20a0
|
|||
|
39c6716fb0
|
|||
|
7bc73afadd
|
|||
|
647aa9d02f
|
|||
|
91aaabaa1b
|
|||
|
3d4c7cdd9e
|
|||
|
4fd6d6c037
|
|||
|
de3fc7ba38
|
|||
|
5a5c4705dd
|
|||
|
f703aa20a5
|
|||
|
5c12425d48
|
|||
|
cbe86dc4f0
|
|||
|
d08a1081bd
|
|||
|
72a2601d74
|
|||
|
1dab87aaf0
|
|||
|
2bafde99e3
|
|||
|
91efeb101a
|
|||
|
dcb22a61c0
|
|||
|
e028a61fc1
|
|||
|
73987be7d4
|
|||
|
563b5e66fc
|
|||
|
2edcfe1e68
|
|||
|
2698ca00e8
|
|||
|
1d0143386d
|
|||
|
a55c209099
|
|||
|
10ff276da1
|
|||
|
fd4d379b67
|
|||
|
77f5b89a41
|
|||
|
14e33f17e5
|
|||
|
cfeb7818eb
|
|||
|
05391da556
|
|||
|
463f8836e6
|
|||
|
2e465c94da
|
|||
|
26009fd3f7
|
|||
|
2d7b896a8c
|
|||
|
a0eb010aab
|
|||
|
b1b27ac1df
|
|||
|
fc3d78fe01
|
|||
|
591637264a
|
|||
|
e77652bf89
|
|||
|
88d3e46413
|
|||
|
e51e81bb22
|
|||
|
8f4a3bcf9f
|
|||
|
827dc9e1ba
|
|||
|
d92de1c709
|
|||
|
5bcafcf734
|
|||
|
9f7b0c2f46
|
|||
|
3e87187c4c
|
|||
|
b651d95e77
|
|||
|
aab92ce3c1
|
|||
|
a495e09a8f
|
|||
|
3afca2bd5b
|
|||
|
b73a789dfe
|
|||
|
38b5ff0cec
|
|||
|
3c204b9b40
|
|||
|
00771efeb4
|
|||
|
61972d61f6
|
|||
|
fe40af7b7e
|
|||
|
12751932d1
|
|||
|
41b49137a8
|
|||
|
c761e1de4d
|
|||
|
a91920310d
|
|||
|
16e674782a
|
|||
|
47244daefb
|
|||
|
46fa104419
|
|||
|
45953b3d9c
|
|||
|
42759e7a9f
|
|||
|
8e2d2c8246
|
|||
|
299685775a
|
|||
|
b7406cc4c4
|
|||
|
690a0ed0d6
|
|||
|
a9d72a5eb1
|
|||
|
6d14bb814f
|
|||
|
be0e387ab0
|
|||
|
abeb67964f
|
|||
|
bf5d10743f
|
|||
|
4e7aab07d5
|
|||
|
15a66a2b31
|
|||
|
f347d44c22
|
|||
|
b5630f6883
|
|||
|
17ffdb2dcf
|
|||
|
ac34635890
|
|||
|
9dec9dbc4b
|
|||
|
2f74adc8bd
|
|||
|
d7e0104ae4
|
|||
|
bb92e3ada9
|
|||
|
fad419c2a2
|
|||
|
b1a1e73238
|
|||
|
38e9128a8c
|
|||
|
7ee702a44e
|
|||
|
3d188ef884
|
|||
|
34ccda84b2
|
|||
|
042013bb04
|
|||
|
5c2b63a7f1
|
|||
|
9fd97e71d0
|
|||
|
fba201c995
|
|||
|
7f27a6dc51
|
|||
|
b65aba9446
|
|||
|
becaf8b6d7
|
|||
|
54c0d6bf48
|
|||
|
c1399f5030
|
|||
|
9ac63aac0c
|
|||
|
cb9ebf0e15
|
|||
|
9a2a7b749f
|
|||
|
ec5cb9400c
|
|||
|
ae66b3d2fb
|
|||
|
149bc3671a
|
|||
|
24435694a5
|
|||
|
1c168babf2
|
|||
|
0edcb7c1d3
|
|||
|
0e5ca74b98
|
|||
|
23ae7822bf
|
|||
|
898b5aed3d
|
|||
|
7c3c3135d8
|
|||
|
f33aea9ff9
|
|||
|
e7fc311d0b
|
|||
|
f5274067f6
|
|||
|
e7161f8e61
|
|||
|
6931ad95c3
|
|||
|
2ba599b399
|
|||
|
d3d3417125
|
|||
|
651cdf9ccb
|
|||
|
68ff0a2ba6
|
|||
|
6a0ecced90
|
|||
|
b667fea1cb
|
|||
|
b25ade5f3d
|
|||
|
ebdcff1049
|
|||
|
46c5ce4936
|
|||
|
36f8064905
|
|||
|
eeb9f98e5b
|
|||
|
3f9f331501
|
|||
|
2563391086
|
|||
|
a0b4e47acc
|
|||
|
a52f7038e5
|
|||
|
274686d10d
|
|||
|
65342d588f
|
|||
|
5e5826459e
|
|||
|
4a463b7f03
|
|||
|
dacd9550e0
|
|||
|
546b00429f
|
|||
|
86f4219062
|
|||
|
fe2929d5f7
|
|||
|
470e545d27
|
|||
|
8d3381821f
|
|||
|
e9d00b9071
|
|||
|
4f41afee0f
|
|||
|
7de593e816
|
|||
|
2442eda8d9
|
|||
|
05488bfb8f
|
|||
|
dd94818f20
|
|||
|
0fd357e7f6
|
|||
|
57231d4acf
|
|||
|
c5aefe5e9d
|
|||
|
0f8ffee44d
|
|||
|
1685a4d000
|
|||
|
6c338b433a
|
|||
|
8accd3b219
|
|||
|
c5f59c5488
|
|||
|
fcd9becf9a
|
|||
|
622f945c22
|
|||
|
e94acc424c
|
|||
|
b1a4d801be
|
|||
|
56beae17fe
|
|||
|
ea978101b1
|
|||
|
fbd1638e7f
|
|||
|
d42067df7c
|
|||
|
b9459a80c7
|
|||
|
f8189d1488
|
|||
|
5063b774c1
|
|||
|
766dd89ffa
|
|||
|
699c19e972
|
|||
|
b5b30aea2e
|
|||
|
c0e860000a
|
|||
|
d87020f0ca
|
|||
|
e47aebb7a0
|
|||
|
543bf69102
|
|||
|
4cfb1fda8f
|
|||
|
c12183959a
|
|||
|
f5845e312e
|
|||
|
a103c4a7c7
|
|||
|
67ec82ae1b
|
|||
|
f6f0cb56ae
|
|||
|
d4284c109d
|
|||
|
030ad2a73b
|
|||
|
78d7955abd
|
|||
|
b066495a7d
|
|||
|
82299d34c6
|
|||
|
792013cefb
|
|||
|
3f39132935
|
|||
|
c922c3f80e
|
|||
|
6cf58ca1b3
|
|||
|
425421d9b1
|
|||
|
5e0f15d76b
|
|||
|
ae65491223
|
|||
|
52e3324ef4
|
|||
|
f95e0a7568
|
|||
|
4c647add0d
|
|||
|
a341466942
|
|||
|
e4ee8df83c
|
|||
|
048c1957f1
|
|||
|
790d77075e
|
|||
|
e5ff40e7d3
|
|||
|
123d7fbfd5
|
|||
|
7638a44fa6
|
|||
|
a14b6535a6
|
|||
|
763ab27e09
|
|||
|
bff2a1e748
|
|||
|
8a91234cb4
|
|||
|
db7051a368
|
|||
|
36f312b3ba
|
|||
|
037144b06e
|
|||
|
f5a597c406
|
|||
|
8874aaf81b
|
|||
|
04a27c8e47
|
|||
|
9e3df0905b
|
|||
|
9290748761
|
|||
|
23084888a0
|
|||
|
50f6fcb326
|
|||
|
070e346587
|
|||
|
24de7c50a0
|
|||
|
f6dd9dab6a
|
|||
|
776650af01
|
|||
|
109aaee659
|
|||
|
22ee5ae151
|
|||
|
4246256d78
|
|||
|
a941ac025f
|
|||
|
87b5c30ef6
|
|||
|
df9b77b077
|
|||
|
a40d182706
|
|||
|
e5baaf416f
|
|||
|
ee6c471fe6
|
|||
|
16bf3178d3
|
|||
|
034c59a26a
|
|||
|
5bf28901a4
|
|||
|
9b507715d4
|
|||
|
12ab7ea3b4
|
|||
|
1f0226f7e0
|
|||
|
584ce3da68
|
|||
|
5d18af0007
|
|||
|
0e6c1a5026
|
|||
|
d23b4dc9e6
|
|||
|
3ce63e95d7
|
|||
|
2489766efe
|
|||
|
9e48d7f562
|
|||
|
f280994957
|
|||
|
ae7b343cde
|
|||
|
a63a372fe0
|
|||
|
16f9001f5f
|
|||
|
80ad2e4e23
|
|||
|
92b83bd599
|
|||
|
8ace214832
|
|||
|
eb5ee4fece
|
|||
|
9462af08f3
|
|||
|
a5f0aa3f30
|
|||
|
dd0bb0a391
|
|||
|
d16da6da8c
|
|||
|
e58181a930
|
|||
|
71e70b7b5f
|
|||
|
afa1a8043e
|
|||
|
1ba1cb8865
|
|||
|
44ba7a5f02
|
|||
|
dc467493d8
|
|||
|
46cd3a28c8
|
|||
|
ad1bc6794f
|
|||
|
e55822c62f
|
|||
|
802e6afa34
|
|||
|
e906cae9ee
|
|||
|
ae2df2c450
|
|||
|
6e3f34f2ec
|
|||
|
65a0bb9729
|
|||
|
afa7a0800d
|
|||
|
773253fdf5
|
|||
|
409ed172c8
|
|||
|
1c4f593566
|
|||
|
b99c63337d
|
|||
|
f09133a224
|
|||
|
16409b37a2
|
|||
|
a2a291791c
|
|||
|
8690419c2d
|
|||
|
1cdc6b4246
|
|||
|
56aad8dc11
|
|||
|
83c4f8b767
|
|||
|
d0ddd71934
|
|||
|
70e02090f7
|
|||
|
ca247b8037
|
|||
|
3f25c3f0af
|
|||
|
e271fa77aa
|
|||
|
f876043844
|
|||
|
6265aea73a
|
|||
|
c8a0effe90
|
|||
|
8df01b71d4
|
|||
|
985c4dd2fc
|
|||
|
da2b9c01ce
|
|||
|
323d132c40
|
|||
|
6cc2b406a4
|
|||
|
fcd0f2ede7
|
|||
|
e68db7fbfc
|
|||
|
ac81cfbedc
|
|||
|
05db06c87b
|
|||
|
e603b688ca
|
|||
|
a9def08533
|
|||
|
ecaf43358d
|
|||
|
197fa65b8f
|
|||
|
e81a45e849
|
|||
|
3920acf8c2
|
|||
|
19630a9593
|
|||
|
4051577d6b
|
|||
|
ddfb865e2d
|
|||
|
024d2ff782
|
|||
|
6f719bc3c1
|
|||
|
1b5d20a39b
|
|||
|
49600a6f46
|
|||
|
b489a3bba1
|
|||
|
780e3e5465
|
|||
|
712cfc06d7
|
|||
|
f5abce9df5
|
|||
|
ddb003e39b
|
|||
|
b12c290f12
|
|||
|
0122593312
|
|||
|
6aa431d57a
|
|||
|
08eeafe817
|
|||
|
d7c7c69a13
|
|||
|
50972096cd
|
|||
|
905b9f9785
|
|||
|
1c7e634f09
|
|||
|
8d472ebf2b
|
|||
|
4da6463135
|
|||
|
eb3385d490
|
|||
|
b8669338da
|
|||
|
f24dd4ab8c
|
|||
|
a462341a0a
|
|||
|
84ad9791e2
|
|||
|
b14690aa77
|
|||
|
d0b6852cd7
|
|||
|
da0459aca1
|
|||
|
1be8de6f5c
|
|||
|
0f41d96671
|
|||
|
92f510a647
|
|||
|
acb6931f3e
|
@@ -0,0 +1,2 @@
|
||||
ColumnLimit: 0
|
||||
IndentWidth: 4
|
||||
@@ -2,7 +2,6 @@ name: Test
|
||||
|
||||
on:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
hakurei:
|
||||
@@ -73,20 +72,20 @@ jobs:
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
hpkg:
|
||||
name: Hpkg
|
||||
sharefs:
|
||||
name: ShareFS
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run NixOS test
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.hpkg
|
||||
run: nix build --out-link "result" --print-out-paths --print-build-logs .#checks.x86_64-linux.sharefs
|
||||
|
||||
- name: Upload test output
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "hpkg-vm-output"
|
||||
name: "sharefs-vm-output"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
|
||||
@@ -97,7 +96,7 @@ jobs:
|
||||
- race
|
||||
- sandbox
|
||||
- sandbox-race
|
||||
- hpkg
|
||||
- sharefs
|
||||
runs-on: nix
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
DO NOT ADD NEW ACTIONS HERE
|
||||
|
||||
This port is solely for releasing to the github mirror and serves no purpose during development.
|
||||
All development happens at https://git.gensokyo.uk/security/hakurei. If you wish to contribute,
|
||||
request for an account on git.gensokyo.uk.
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v32
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||
restore-prefixes-first-match: build-${{ runner.os }}-
|
||||
gc-max-store-size-linux: 1G
|
||||
purge: true
|
||||
purge-prefixes: build-${{ runner.os }}-
|
||||
purge-created: 60
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Build for release
|
||||
run: nix build --print-out-paths --print-build-logs .#dist
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |-
|
||||
result/hakurei-**
|
||||
@@ -1,48 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
- push
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
name: Create distribution
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Nix
|
||||
uses: nixbuild/nix-quick-install-action@v32
|
||||
with:
|
||||
nix_conf: |
|
||||
keep-env-derivations = true
|
||||
keep-outputs = true
|
||||
|
||||
- name: Restore and cache Nix store
|
||||
uses: nix-community/cache-nix-action@v6
|
||||
with:
|
||||
primary-key: build-${{ runner.os }}-${{ hashFiles('**/*.nix') }}
|
||||
restore-prefixes-first-match: build-${{ runner.os }}-
|
||||
gc-max-store-size-linux: 1G
|
||||
purge: true
|
||||
purge-prefixes: build-${{ runner.os }}-
|
||||
purge-created: 60
|
||||
purge-primary-key: never
|
||||
|
||||
- name: Build for test
|
||||
id: build-test
|
||||
run: >-
|
||||
export HAKUREI_REV="$(git rev-parse --short HEAD)" &&
|
||||
sed -i.old 's/version = /version = "0.0.0-'$HAKUREI_REV'"; # version = /' package.nix &&
|
||||
nix build --print-out-paths --print-build-logs .#dist &&
|
||||
mv package.nix.old package.nix &&
|
||||
echo "rev=$HAKUREI_REV" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload test build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "hakurei-${{ steps.build-test.outputs.rev }}"
|
||||
path: result/*
|
||||
retention-days: 1
|
||||
+9
-26
@@ -1,35 +1,18 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
# produced by tools and text editors
|
||||
*.qcow2
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# go generate
|
||||
/cmd/hakurei/LICENSE
|
||||
/cmd/mbf/internal/pkgserver/ui/static
|
||||
/internal/pkg/internal/testtool/testtool
|
||||
/internal/rosa/hakurei_current.tar.gz
|
||||
|
||||
# release
|
||||
/dist/hakurei-*
|
||||
# cmd/dist default destination
|
||||
/dist
|
||||
|
||||
# interactive nixos vm
|
||||
nixos.qcow2
|
||||
# local packages
|
||||
/internal/rosa/package/local
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<a href="https://git.gensokyo.uk/security/hakurei">
|
||||
<a href="https://git.gensokyo.uk/rosa/hakurei">
|
||||
<picture>
|
||||
<img src="https://basement.gensokyo.uk/images/yukari1.png" width="200px" alt="Yukari">
|
||||
</picture>
|
||||
@@ -8,171 +8,58 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/hakurei.app"><img src="https://pkg.go.dev/badge/hakurei.app.svg" alt="Go Reference" /></a>
|
||||
<a href="https://git.gensokyo.uk/security/hakurei/actions"><img src="https://git.gensokyo.uk/security/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
||||
<a href="https://git.gensokyo.uk/rosa/hakurei/actions"><img src="https://git.gensokyo.uk/rosa/hakurei/actions/workflows/test.yml/badge.svg?branch=staging&style=flat-square" alt="Gitea Workflow Status" /></a>
|
||||
<br/>
|
||||
<a href="https://git.gensokyo.uk/security/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/security/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
||||
<a href="https://git.gensokyo.uk/rosa/hakurei/releases"><img src="https://img.shields.io/gitea/v/release/rosa/hakurei?gitea_url=https%3A%2F%2Fgit.gensokyo.uk&color=purple" alt="Release" /></a>
|
||||
<a href="https://goreportcard.com/report/hakurei.app"><img src="https://goreportcard.com/badge/hakurei.app" alt="Go Report Card" /></a>
|
||||
<a href="https://hakurei.app"><img src="https://img.shields.io/website?url=https%3A%2F%2Fhakurei.app" alt="Website" /></a>
|
||||
</p>
|
||||
|
||||
Hakurei is a tool for running sandboxed graphical applications as dedicated subordinate users on the Linux kernel.
|
||||
It implements the application container of [planterette (WIP)](https://git.gensokyo.uk/security/planterette),
|
||||
a self-contained Android-like package manager with modern security features.
|
||||
Hakurei is a tool for running sandboxed desktop applications as dedicated
|
||||
subordinate users on the Linux kernel. It implements the application container
|
||||
of [planterette (WIP)](https://git.gensokyo.uk/rosa/planterette), a
|
||||
self-contained Android-like package manager with modern security features.
|
||||
|
||||
## NixOS Module usage
|
||||
Interaction with hakurei happens entirely through structures described by
|
||||
package [hst](https://pkg.go.dev/hakurei.app/hst). No native API is available
|
||||
due to internal details of uid isolation.
|
||||
|
||||
The NixOS module currently requires home-manager to configure subordinate users. Full module documentation can be found [here](options.md).
|
||||
## Notable Packages
|
||||
|
||||
To use the module, import it into your configuration with
|
||||
Package [container](https://pkg.go.dev/hakurei.app/container) is general purpose
|
||||
container tooling. It is used by the hakurei shim process running as the target
|
||||
subordinate user to set up the application container. It has a single dependency,
|
||||
[libseccomp](https://github.com/seccomp/libseccomp), to create BPF programs
|
||||
for the [system call filter](https://www.kernel.org/doc/html/latest/userspace-api/seccomp_filter.html).
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
Package [internal/pkg](https://pkg.go.dev/hakurei.app/internal/pkg) provides
|
||||
infrastructure for hermetic builds. This replaces the legacy nix-based testing
|
||||
framework and serves as the build system of Rosa OS, currently developed under
|
||||
package [internal/rosa](https://pkg.go.dev/hakurei.app/internal/rosa).
|
||||
|
||||
hakurei = {
|
||||
url = "git+https://git.gensokyo.uk/security/hakurei";
|
||||
## Dependencies
|
||||
|
||||
# Optional but recommended to limit the size of your system closure.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
`container` depends on:
|
||||
|
||||
outputs = { self, nixpkgs, hakurei, ... }:
|
||||
{
|
||||
nixosConfigurations.hakurei = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
hakurei.nixosModules.hakurei
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
- [libseccomp](https://github.com/seccomp/libseccomp) to generate BPF programs.
|
||||
|
||||
This adds the `environment.hakurei` option:
|
||||
`cmd/hakurei` depends on:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
- [acl](https://savannah.nongnu.org/projects/acl/) to export sockets to
|
||||
subordinate users.
|
||||
- [wayland](https://gitlab.freedesktop.org/wayland/wayland) to set up
|
||||
[security-context-v1](https://wayland.app/protocols/security-context-v1).
|
||||
- [xcb](https://xcb.freedesktop.org/) to grant and revoke subordinate users
|
||||
access to the X server.
|
||||
|
||||
{
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users = {
|
||||
alice = 0;
|
||||
nixos = 10;
|
||||
};
|
||||
`cmd/sharefs` depends on:
|
||||
|
||||
commonPaths = [
|
||||
{
|
||||
src = "/sdcard";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
- [fuse](https://github.com/libfuse/libfuse) to implement the filesystem.
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
New dependencies will generally not be added. Patches adding new dependencies
|
||||
are very likely to be rejected.
|
||||
|
||||
apps = {
|
||||
"org.chromium.Chromium" = {
|
||||
name = "chromium";
|
||||
identity = 1;
|
||||
packages = [ pkgs.chromium ];
|
||||
userns = true;
|
||||
mapRealUid = true;
|
||||
dbus = {
|
||||
system = {
|
||||
filter = true;
|
||||
talk = [
|
||||
"org.bluez"
|
||||
"org.freedesktop.Avahi"
|
||||
"org.freedesktop.UPower"
|
||||
];
|
||||
};
|
||||
session =
|
||||
f:
|
||||
f {
|
||||
talk = [
|
||||
"org.freedesktop.FileManager1"
|
||||
"org.freedesktop.Notifications"
|
||||
"org.freedesktop.ScreenSaver"
|
||||
"org.freedesktop.secrets"
|
||||
"org.kde.kwalletd5"
|
||||
"org.kde.kwalletd6"
|
||||
];
|
||||
own = [
|
||||
"org.chromium.Chromium.*"
|
||||
"org.mpris.MediaPlayer2.org.chromium.Chromium.*"
|
||||
"org.mpris.MediaPlayer2.chromium.*"
|
||||
];
|
||||
call = { };
|
||||
broadcast = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
## NixOS Module (deprecated)
|
||||
|
||||
"org.claws_mail.Claws-Mail" = {
|
||||
name = "claws-mail";
|
||||
identity = 2;
|
||||
packages = [ pkgs.claws-mail ];
|
||||
gpu = false;
|
||||
capability.pulse = false;
|
||||
};
|
||||
|
||||
"org.weechat" = {
|
||||
name = "weechat";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.weechat ];
|
||||
capability = {
|
||||
wayland = false;
|
||||
x11 = false;
|
||||
dbus = true;
|
||||
pulse = false;
|
||||
};
|
||||
};
|
||||
|
||||
"dev.vencord.Vesktop" = {
|
||||
name = "discord";
|
||||
identity = 3;
|
||||
shareUid = true;
|
||||
packages = [ pkgs.vesktop ];
|
||||
share = pkgs.vesktop;
|
||||
command = "vesktop --ozone-platform-hint=wayland";
|
||||
userns = true;
|
||||
mapRealUid = true;
|
||||
capability.x11 = true;
|
||||
dbus = {
|
||||
session =
|
||||
f:
|
||||
f {
|
||||
talk = [ "org.kde.StatusNotifierWatcher" ];
|
||||
own = [ ];
|
||||
call = { };
|
||||
broadcast = { };
|
||||
};
|
||||
system.filter = true;
|
||||
};
|
||||
};
|
||||
|
||||
"io.looking-glass" = {
|
||||
name = "looking-glass-client";
|
||||
identity = 4;
|
||||
useCommonPaths = false;
|
||||
groups = [ "plugdev" ];
|
||||
extraPaths = [
|
||||
{
|
||||
src = "/dev/shm/looking-glass";
|
||||
write = true;
|
||||
}
|
||||
];
|
||||
extraConfig = {
|
||||
programs.looking-glass-client.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
The NixOS module is in maintenance mode and will be removed once planterette is
|
||||
feature-complete. Full module documentation can be found [here](options.md).
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
HAKUREI_DIST_MAKE='' exec "$(dirname -- "$0")/cmd/dist/dist.sh"
|
||||
@@ -0,0 +1,137 @@
|
||||
// Package check provides types yielding values checked to meet a condition.
|
||||
package check
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unique"
|
||||
)
|
||||
|
||||
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
|
||||
type AbsoluteError string
|
||||
|
||||
func (e AbsoluteError) Error() string {
|
||||
return fmt.Sprintf("path %q is not absolute", string(e))
|
||||
}
|
||||
|
||||
func (e AbsoluteError) Is(target error) bool {
|
||||
ce, ok := errors.AsType[AbsoluteError](target)
|
||||
if !ok {
|
||||
return errors.Is(target, syscall.EINVAL)
|
||||
}
|
||||
return e == ce
|
||||
}
|
||||
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
type Absolute struct{ pathname unique.Handle[string] }
|
||||
|
||||
var (
|
||||
_ fmt.GoStringer = new(Absolute)
|
||||
|
||||
_ encoding.TextAppender = new(Absolute)
|
||||
_ encoding.TextMarshaler = new(Absolute)
|
||||
_ encoding.TextUnmarshaler = new(Absolute)
|
||||
|
||||
_ encoding.BinaryAppender = new(Absolute)
|
||||
_ encoding.BinaryMarshaler = new(Absolute)
|
||||
_ encoding.BinaryUnmarshaler = new(Absolute)
|
||||
)
|
||||
|
||||
func (a *Absolute) GoString() string {
|
||||
return fmt.Sprintf("check.MustAbs(%q)", a.String())
|
||||
}
|
||||
|
||||
// ok returns whether [Absolute] is not the zero value.
|
||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
||||
|
||||
// unsafeAbs returns [check.Absolute] on any string value.
|
||||
func unsafeAbs(pathname string) *Absolute {
|
||||
return &Absolute{unique.Make(pathname)}
|
||||
}
|
||||
|
||||
// String returns the checked pathname.
|
||||
func (a *Absolute) String() string {
|
||||
if !a.ok() {
|
||||
panic("attempted use of zero Absolute")
|
||||
}
|
||||
return a.pathname.Value()
|
||||
}
|
||||
|
||||
// Handle returns the underlying [unique.Handle].
|
||||
func (a *Absolute) Handle() unique.Handle[string] {
|
||||
return a.pathname
|
||||
}
|
||||
|
||||
// Is efficiently compares the underlying pathname.
|
||||
func (a *Absolute) Is(v *Absolute) bool {
|
||||
if a == nil && v == nil {
|
||||
return true
|
||||
}
|
||||
return a.ok() && v.ok() && a.pathname == v.pathname
|
||||
}
|
||||
|
||||
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
||||
func NewAbs(pathname string) (*Absolute, error) {
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return nil, AbsoluteError(pathname)
|
||||
}
|
||||
return unsafeAbs(pathname), nil
|
||||
}
|
||||
|
||||
// MustAbs calls [NewAbs] and panics on error.
|
||||
func MustAbs(pathname string) *Absolute {
|
||||
if a, err := NewAbs(pathname); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
// Append calls [filepath.Join] with [Absolute] as the first element.
|
||||
func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
|
||||
}
|
||||
|
||||
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
||||
|
||||
// AppendText appends the checked pathname.
|
||||
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
|
||||
return append(data, a.String()...), nil
|
||||
}
|
||||
|
||||
// MarshalText returns the checked pathname.
|
||||
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
|
||||
|
||||
// UnmarshalText stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) UnmarshalText(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
}
|
||||
a.pathname = unique.Make(pathname)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
|
||||
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
|
||||
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
slices.SortFunc(x, func(a, b *Absolute) int {
|
||||
return strings.Compare(a.String(), b.String())
|
||||
})
|
||||
}
|
||||
|
||||
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
|
||||
func CompactAbs(s []*Absolute) []*Absolute {
|
||||
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool {
|
||||
return a.Is(b)
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container
|
||||
package check_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,9 +9,19 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
. "hakurei.app/check"
|
||||
)
|
||||
|
||||
// unsafeAbs returns check.Absolute on any string value.
|
||||
//
|
||||
//go:linkname unsafeAbs hakurei.app/check.unsafeAbs
|
||||
func unsafeAbs(pathname string) *Absolute
|
||||
|
||||
func TestAbsoluteError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -21,8 +31,8 @@ func TestAbsoluteError(t *testing.T) {
|
||||
}{
|
||||
{"EINVAL", new(AbsoluteError), syscall.EINVAL, true},
|
||||
{"not EINVAL", new(AbsoluteError), syscall.EBADE, false},
|
||||
{"ne val", new(AbsoluteError), &AbsoluteError{"etc"}, false},
|
||||
{"equals", &AbsoluteError{"etc"}, &AbsoluteError{"etc"}, true},
|
||||
{"ne val", new(AbsoluteError), AbsoluteError("etc"), false},
|
||||
{"equals", AbsoluteError("etc"), AbsoluteError("etc"), true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -32,14 +42,18 @@ func TestAbsoluteError(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("string", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := `path "etc" is not absolute`
|
||||
if got := (&AbsoluteError{"etc"}).Error(); got != want {
|
||||
if got := (AbsoluteError("etc")).Error(); got != want {
|
||||
t.Errorf("Error: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewAbs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -48,12 +62,14 @@ func TestNewAbs(t *testing.T) {
|
||||
wantErr error
|
||||
}{
|
||||
{"good", "/etc", MustAbs("/etc"), nil},
|
||||
{"not absolute", "etc", nil, &AbsoluteError{"etc"}},
|
||||
{"zero", "", nil, &AbsoluteError{""}},
|
||||
{"not absolute", "etc", nil, AbsoluteError("etc")},
|
||||
{"zero", "", nil, AbsoluteError("")},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := NewAbs(tc.pathname)
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("NewAbs: %#v, want %#v", got, tc.want)
|
||||
@@ -65,10 +81,12 @@ func TestNewAbs(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("must", func(t *testing.T) {
|
||||
defer func() {
|
||||
wantPanic := `path "etc" is not absolute`
|
||||
t.Parallel()
|
||||
|
||||
if r := recover(); r != wantPanic {
|
||||
defer func() {
|
||||
wantPanic := AbsoluteError("etc")
|
||||
|
||||
if r := recover(); !reflect.DeepEqual(r, wantPanic) {
|
||||
t.Errorf("MustAbs: panic = %v; want %v", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
@@ -79,13 +97,17 @@ func TestNewAbs(t *testing.T) {
|
||||
|
||||
func TestAbsoluteString(t *testing.T) {
|
||||
t.Run("passthrough", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pathname := "/etc"
|
||||
if got := (&Absolute{pathname}).String(); got != pathname {
|
||||
if got := unsafeAbs(pathname).String(); got != pathname {
|
||||
t.Errorf("String: %q, want %q", got, pathname)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defer func() {
|
||||
wantPanic := "attempted use of zero Absolute"
|
||||
|
||||
@@ -99,6 +121,8 @@ func TestAbsoluteString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAbsoluteIs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
a, v *Absolute
|
||||
@@ -114,6 +138,8 @@ func TestAbsoluteIs(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := tc.a.Is(tc.v); got != tc.want {
|
||||
t.Errorf("Is: %v, want %v", got, tc.want)
|
||||
}
|
||||
@@ -123,10 +149,12 @@ func TestAbsoluteIs(t *testing.T) {
|
||||
|
||||
type sCheck struct {
|
||||
Pathname *Absolute `json:"val"`
|
||||
Magic int `json:"magic"`
|
||||
Magic uint64 `json:"magic"`
|
||||
}
|
||||
|
||||
func TestCodecAbsolute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
a *Absolute
|
||||
@@ -142,32 +170,37 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
|
||||
{"good", MustAbs("/etc"),
|
||||
nil,
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x10\xff\x84\x01\x04/etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||
|
||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||
{"not absolute", nil,
|
||||
&AbsoluteError{"etc"},
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
AbsoluteError("etc"),
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
|
||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||
{"zero", nil,
|
||||
new(AbsoluteError),
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x04\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
`""`, `{"val":"","magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("gob", func(t *testing.T) {
|
||||
if tc.gob == "\x00" && tc.sGob == "\x00" {
|
||||
// these values mark the current test to skip gob
|
||||
return
|
||||
}
|
||||
t.Parallel()
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// encode is unchecked
|
||||
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
||||
return
|
||||
@@ -204,6 +237,8 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
{
|
||||
var gotA *Absolute
|
||||
err := gob.NewDecoder(strings.NewReader(tc.gob)).Decode(&gotA)
|
||||
@@ -238,7 +273,11 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("marshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// marshal is unchecked
|
||||
if errors.Is(tc.wantErr, syscall.EINVAL) {
|
||||
return
|
||||
@@ -273,6 +312,8 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("unmarshal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
{
|
||||
var gotA *Absolute
|
||||
err := json.Unmarshal([]byte(tc.json), &gotA)
|
||||
@@ -306,17 +347,14 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json passthrough", func(t *testing.T) {
|
||||
wantErr := "invalid character ':' looking for beginning of value"
|
||||
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbsoluteWrap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("join", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := "/etc/nix/nix.conf"
|
||||
if got := MustAbs("/etc").Append("nix", "nix.conf"); got.String() != want {
|
||||
t.Errorf("Append: %q, want %q", got, want)
|
||||
@@ -324,6 +362,8 @@ func TestAbsoluteWrap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("dir", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := "/"
|
||||
if got := MustAbs("/etc").Dir(); got.String() != want {
|
||||
t.Errorf("Dir: %q, want %q", got, want)
|
||||
@@ -331,6 +371,8 @@ func TestAbsoluteWrap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("sort", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
||||
got := []*Absolute{MustAbs("/proc"), MustAbs("/sys"), MustAbs("/etc")}
|
||||
SortAbs(got)
|
||||
@@ -340,6 +382,8 @@ func TestAbsoluteWrap(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("compact", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := []*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/sys")}
|
||||
if got := CompactAbs([]*Absolute{MustAbs("/etc"), MustAbs("/proc"), MustAbs("/proc"), MustAbs("/sys")}); !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("CompactAbs: %#v, want %#v", got, want)
|
||||
@@ -0,0 +1,38 @@
|
||||
package check
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
// SpecialOverlayEscape is the escape string for overlay mount options.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
SpecialOverlayEscape = `\`
|
||||
// SpecialOverlayOption is the separator string between overlay mount options.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
SpecialOverlayOption = ","
|
||||
// SpecialOverlayPath is the separator string between overlay paths.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
SpecialOverlayPath = ":"
|
||||
)
|
||||
|
||||
// EscapeOverlayDataSegment escapes a string for formatting into the data
|
||||
// argument of an overlay mount system call.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
func EscapeOverlayDataSegment(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if f := strings.SplitN(s, "\x00", 2); len(f) > 0 {
|
||||
s = f[0]
|
||||
}
|
||||
|
||||
return strings.NewReplacer(
|
||||
SpecialOverlayEscape, SpecialOverlayEscape+SpecialOverlayEscape,
|
||||
SpecialOverlayOption, SpecialOverlayEscape+SpecialOverlayOption,
|
||||
SpecialOverlayPath, SpecialOverlayEscape+SpecialOverlayPath,
|
||||
).Replace(s)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package check_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
)
|
||||
|
||||
func TestEscapeOverlayDataSegment(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
s string
|
||||
want string
|
||||
}{
|
||||
{"zero", "", ""},
|
||||
{"multi", `\\\:,:,\\\`, `\\\\\\\:\,\:\,\\\\\\`},
|
||||
{"bwrap", `/path :,\`, `/path \:\,\\`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := check.EscapeOverlayDataSegment(tc.s); got != tc.want {
|
||||
t.Errorf("escapeOverlayDataSegment: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
// parsePair parses a NUL-delimited quoted paths pair.
|
||||
func parsePair(s string) (source, target *check.Absolute, err error) {
|
||||
var p string
|
||||
if p, err = strconv.Unquote(s); err != nil {
|
||||
return
|
||||
}
|
||||
_source, _target, ok := strings.Cut(p, "\x00")
|
||||
if source, err = check.NewAbs(_source); err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
target, err = check.NewAbs(_target)
|
||||
return
|
||||
}
|
||||
|
||||
// parse decodes a high-level configuration stream and returns its
|
||||
// corresponding [hst.Config].
|
||||
func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) {
|
||||
shell := fhs.AbsRoot.Append("bin", "zsh")
|
||||
home := hst.AbsPrivateTmp.Append("home")
|
||||
|
||||
c := hst.Config{
|
||||
ID: id,
|
||||
Enablements: new(hst.Enablements),
|
||||
|
||||
SessionBus: &hst.BusConfig{
|
||||
Own: []string{
|
||||
id + ".*",
|
||||
"org.mpris.MediaPlayer2." + id + ".*",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
SystemBus: &hst.BusConfig{Filter: true},
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: make(map[string]string),
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSOverlay{
|
||||
Target: fhs.AbsRoot,
|
||||
Lower: []*check.Absolute{
|
||||
base.Append("template", "initial"),
|
||||
},
|
||||
Upper: base.Append("template", "upper"),
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: home,
|
||||
Source: base.Append("state", id),
|
||||
Write: true,
|
||||
Ensure: true,
|
||||
}},
|
||||
|
||||
{FilesystemConfig: &hst.FSEphemeral{
|
||||
Target: fhs.AbsVar.Append("tmp"),
|
||||
Write: true,
|
||||
Perm: 01777,
|
||||
}},
|
||||
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
|
||||
},
|
||||
|
||||
Username: "chronos",
|
||||
Shell: shell,
|
||||
Home: home,
|
||||
Path: shell,
|
||||
Args: []string{"zsh", "-c"},
|
||||
|
||||
Flags: hst.FCoverRun,
|
||||
},
|
||||
}
|
||||
|
||||
s := bufio.NewScanner(r)
|
||||
scanOnce := func() error {
|
||||
if s.Scan() {
|
||||
return nil
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
if err := scanOnce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v, err := strconv.Atoi(s.Text()); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
c.Identity = v
|
||||
}
|
||||
|
||||
if err := scanOnce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Container.Args = append(c.Container.Args, s.Text())
|
||||
|
||||
var flagGPU, flagSystemBus bool
|
||||
flags := map[string]*bool{
|
||||
"gpu": &flagGPU,
|
||||
"system_bus": &flagSystemBus,
|
||||
}
|
||||
|
||||
for s.Scan() {
|
||||
key, value, ok := strings.Cut(s.Text(), " ")
|
||||
if key != "" && key[0] == ';' {
|
||||
continue
|
||||
}
|
||||
|
||||
if !ok {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var p *bool
|
||||
if p, ok = flags[key]; ok {
|
||||
*p = true
|
||||
continue
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "wayland":
|
||||
*c.Enablements |= hst.EWayland
|
||||
case "x11":
|
||||
*c.Enablements |= hst.EX11
|
||||
case "dbus":
|
||||
*c.Enablements |= hst.EDBus
|
||||
case "pipewire":
|
||||
*c.Enablements |= hst.EPipeWire
|
||||
|
||||
case "multiarch":
|
||||
c.Container.Flags |= hst.FMultiarch
|
||||
case "devel":
|
||||
c.Container.Flags |= hst.FDevel
|
||||
case "userns":
|
||||
c.Container.Flags |= hst.FUserns
|
||||
case "net":
|
||||
c.Container.Flags |= hst.FHostNet
|
||||
case "abstract":
|
||||
c.Container.Flags |= hst.FHostAbstract
|
||||
case "tty":
|
||||
c.Container.Flags |= hst.FTty
|
||||
case "mapuid":
|
||||
c.Container.Flags |= hst.FMapRealUID
|
||||
case "device":
|
||||
c.Container.Flags |= hst.FDevice
|
||||
|
||||
case "share_runtime":
|
||||
c.Container.Flags |= hst.FShareRuntime
|
||||
case "share_tmpdir":
|
||||
c.Container.Flags |= hst.FShareTmpdir
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid flag %q", key)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "group":
|
||||
c.Groups = append(c.Groups, value)
|
||||
continue
|
||||
|
||||
case "env":
|
||||
if key, value, ok = strings.Cut(value, "="); !ok {
|
||||
return nil, fmt.Errorf("invalid environment %q", key)
|
||||
}
|
||||
c.Container.Env[key] = value
|
||||
continue
|
||||
|
||||
case "ro":
|
||||
source, target, err := parsePair(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||
Target: target,
|
||||
Source: source,
|
||||
}},
|
||||
)
|
||||
continue
|
||||
|
||||
case "rw":
|
||||
source, target, err := parsePair(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||
Target: target,
|
||||
Source: source,
|
||||
Write: true,
|
||||
}},
|
||||
)
|
||||
continue
|
||||
|
||||
case "own":
|
||||
c.SessionBus.Own = append(c.SessionBus.Own, value)
|
||||
continue
|
||||
case "own_system":
|
||||
c.SystemBus.Own = append(c.SystemBus.Own, value)
|
||||
continue
|
||||
|
||||
case "talk":
|
||||
c.SessionBus.Talk = append(c.SessionBus.Talk, value)
|
||||
continue
|
||||
case "talk_system":
|
||||
c.SystemBus.Talk = append(c.SystemBus.Talk, value)
|
||||
continue
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid key %q", key)
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if flagGPU {
|
||||
c.Container.Filesystem = append(c.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("dri"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}},
|
||||
}...)
|
||||
}
|
||||
|
||||
if !flagSystemBus {
|
||||
c.SystemBus = nil
|
||||
}
|
||||
|
||||
if c.Container.Flags&hst.FShareTmpdir == 0 {
|
||||
c.Container.Filesystem = append(c.Container.Filesystem,
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{
|
||||
Target: fhs.AbsTmp,
|
||||
Write: true,
|
||||
Perm: 01777,
|
||||
}},
|
||||
)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := fhs.AbsProc.Append("nonexistent")
|
||||
testCases := []struct {
|
||||
name string
|
||||
data string
|
||||
want *hst.Config
|
||||
err error
|
||||
}{
|
||||
{"com.discordapp.Discord", `8
|
||||
exec Discord --ozone-platform-hint=wayland
|
||||
|
||||
gpu
|
||||
wayland
|
||||
dbus
|
||||
system_bus
|
||||
pipewire
|
||||
userns
|
||||
net
|
||||
mapuid
|
||||
|
||||
share_runtime
|
||||
share_tmpdir
|
||||
|
||||
group media_rw
|
||||
env ELECTRON_TRASH=gio
|
||||
rw "/sdcard"
|
||||
; remove before reusing
|
||||
ro "/bin\x00/.hakurei/bin"
|
||||
|
||||
talk org.kde.StatusNotifierWatcher
|
||||
talk com.canonical.AppMenu.Registrar
|
||||
talk com.canonical.indicator.application
|
||||
talk com.canonical.Unity
|
||||
`, &hst.Config{
|
||||
Identity: 8,
|
||||
ID: "com.discordapp.Discord",
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire),
|
||||
Groups: []string{"media_rw"},
|
||||
|
||||
SessionBus: &hst.BusConfig{
|
||||
Talk: []string{
|
||||
"org.kde.StatusNotifierWatcher",
|
||||
"com.canonical.AppMenu.Registrar",
|
||||
"com.canonical.indicator.application",
|
||||
"com.canonical.Unity",
|
||||
},
|
||||
Own: []string{
|
||||
"com.discordapp.Discord.*",
|
||||
"org.mpris.MediaPlayer2.com.discordapp.Discord.*",
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
SystemBus: &hst.BusConfig{Filter: true},
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: map[string]string{
|
||||
"ELECTRON_TRASH": "gio",
|
||||
},
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSOverlay{
|
||||
Target: fhs.AbsRoot,
|
||||
Lower: []*check.Absolute{
|
||||
base.Append("template", "initial"),
|
||||
},
|
||||
Upper: base.Append("template", "upper"),
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: hst.AbsPrivateTmp.Append("home"),
|
||||
Source: base.Append("state", "com.discordapp.Discord"),
|
||||
Write: true,
|
||||
Ensure: true,
|
||||
}},
|
||||
|
||||
{FilesystemConfig: &hst.FSEphemeral{
|
||||
Target: fhs.AbsVar.Append("tmp"),
|
||||
Write: true,
|
||||
Perm: 01777,
|
||||
}},
|
||||
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
|
||||
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Source: check.MustAbs("/sdcard"),
|
||||
Write: true,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: check.MustAbs("/.hakurei/bin"),
|
||||
Source: check.MustAbs("/bin"),
|
||||
}},
|
||||
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("dri"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}},
|
||||
},
|
||||
|
||||
Username: "chronos",
|
||||
Shell: fhs.AbsRoot.Append("bin", "zsh"),
|
||||
Home: hst.AbsPrivateTmp.Append("home"),
|
||||
Path: fhs.AbsRoot.Append("bin", "zsh"),
|
||||
Args: []string{
|
||||
"zsh", "-c",
|
||||
"exec Discord --ozone-platform-hint=wayland",
|
||||
},
|
||||
|
||||
Flags: hst.FCoverRun | hst.FUserns | hst.FHostNet | hst.FMapRealUID |
|
||||
hst.FShareRuntime | hst.FShareTmpdir,
|
||||
},
|
||||
}, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := parse(
|
||||
tc.name,
|
||||
base,
|
||||
strings.NewReader(tc.data),
|
||||
)
|
||||
|
||||
if !reflect.DeepEqual(err, tc.err) {
|
||||
t.Errorf("parse: error = %v, want %v", err, tc.err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("parse: %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// The app program is a proof-of-concept frontend for cmd/hakurei.
|
||||
//
|
||||
// This program is not covered by the compatibility promise. The command line
|
||||
// interface and configuration syntax may change at any time.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("app: ")
|
||||
msg := message.New(log.Default())
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagBase string
|
||||
|
||||
base, template, initial, upper, work *check.Absolute
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) {
|
||||
msg.SwapVerbose(flagVerbose)
|
||||
flagBase = os.ExpandEnv(flagBase)
|
||||
if flagBase == "" {
|
||||
flagBase = "state"
|
||||
}
|
||||
if flagBase, err = filepath.Abs(flagBase); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(flagBase); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
template = base.Append("template")
|
||||
initial = template.Append("initial")
|
||||
upper = template.Append("upper")
|
||||
work = template.Append("work")
|
||||
return
|
||||
}).Flag(
|
||||
&flagVerbose,
|
||||
"v", command.BoolFlag(false),
|
||||
"Increase log verbosity",
|
||||
).Flag(
|
||||
&flagBase,
|
||||
"d", command.StringFlag("$HAKUREI_APP_PATH"),
|
||||
"Configuration and state directory",
|
||||
)
|
||||
|
||||
{
|
||||
var (
|
||||
flagShell string
|
||||
flagHome string
|
||||
)
|
||||
c.NewCommand(
|
||||
"enter", "Enter mutable state template",
|
||||
func([]string) error {
|
||||
config := hst.Config{
|
||||
ID: "app.hakurei.mutable",
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: "mutable",
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSOverlay{
|
||||
Target: fhs.AbsRoot,
|
||||
Lower: []*check.Absolute{initial},
|
||||
Upper: upper,
|
||||
Work: work,
|
||||
}},
|
||||
{FilesystemConfig: &hst.FSEphemeral{
|
||||
Target: fhs.AbsTmp,
|
||||
Write: true,
|
||||
Perm: 0755,
|
||||
}},
|
||||
},
|
||||
Username: "chronos",
|
||||
Flags: hst.FMultiarch |
|
||||
hst.FDevel |
|
||||
hst.FUserns |
|
||||
hst.FHostNet |
|
||||
hst.FTty,
|
||||
},
|
||||
}
|
||||
|
||||
if a, err := check.NewAbs(flagShell); err != nil {
|
||||
return err
|
||||
} else {
|
||||
config.Container.Shell = a
|
||||
config.Container.Path = a
|
||||
config.Container.Args = []string{
|
||||
"-" + filepath.Base(flagShell),
|
||||
}
|
||||
}
|
||||
|
||||
if a, err := check.NewAbs(flagHome); err != nil {
|
||||
return err
|
||||
} else {
|
||||
config.Container.Home = a
|
||||
}
|
||||
|
||||
return run(ctx, msg, &config)
|
||||
},
|
||||
).Flag(
|
||||
&flagShell,
|
||||
"shell", command.StringFlag("/bin/zsh"),
|
||||
"Shell program within container",
|
||||
).Flag(
|
||||
&flagHome,
|
||||
"home", command.StringFlag("/home/chronos"),
|
||||
"Home directory within container",
|
||||
)
|
||||
}
|
||||
|
||||
c.NewCommand(
|
||||
"run", "Start the named application",
|
||||
func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("run requires 1 argument")
|
||||
}
|
||||
|
||||
var config *hst.Config
|
||||
f, err := os.Open(base.Append("app", args[0]).String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, err = parse(args[0], base, f)
|
||||
if closeErr := f.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return run(ctx, msg, config)
|
||||
},
|
||||
)
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if e, ok := errors.AsType[*exec.ExitError](err); ok && e != nil {
|
||||
os.Exit(e.ExitCode())
|
||||
}
|
||||
|
||||
if w, ok := err.(interface{ Unwrap() []error }); !ok {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
errs := w.Unwrap()
|
||||
for i, e := range errs {
|
||||
if i == len(errs)-1 {
|
||||
log.Fatal(e)
|
||||
}
|
||||
log.Println(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// run starts a container via cmd/hakurei and returns after it terminates.
|
||||
func run(ctx context.Context, msg message.Msg, config *hst.Config) error {
|
||||
c, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(c, "hakurei")
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Cancel = func() error {
|
||||
return cmd.Process.Signal(syscall.SIGINT)
|
||||
}
|
||||
if msg.IsVerbose() {
|
||||
cmd.Args = append(cmd.Args, "-v")
|
||||
}
|
||||
cmd.Args = append(cmd.Args, "run", "3")
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
|
||||
|
||||
if err = cmd.Start(); err != nil {
|
||||
_, _ = r.Close(), w.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.Close(); err != nil {
|
||||
_ = w.Close()
|
||||
return err
|
||||
} else if err = json.NewEncoder(w).Encode(&config); err != nil {
|
||||
_ = w.Close()
|
||||
return err
|
||||
} else if err = w.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
v0.4.4
|
||||
+9
-6
@@ -1,21 +1,24 @@
|
||||
#compdef hakurei
|
||||
|
||||
_hakurei_app() {
|
||||
_hakurei_run() {
|
||||
__hakurei_files
|
||||
return $?
|
||||
}
|
||||
|
||||
_hakurei_run() {
|
||||
_hakurei_exec() {
|
||||
_arguments \
|
||||
'--id[Reverse-DNS style Application identifier, leave empty to inherit instance identifier]:id' \
|
||||
'-a[Application identity]: :_numbers' \
|
||||
'-g[Groups inherited by all container processes]: :_groups' \
|
||||
'-d[Container home directory]: :_files -/' \
|
||||
'-u[Passwd user name within sandbox]: :_users' \
|
||||
'--private-runtime[Do not share XDG_RUNTIME_DIR between containers under the same identity]' \
|
||||
'--private-tmpdir[Do not share TMPDIR between containers under the same identity]' \
|
||||
'--wayland[Enable connection to Wayland via security-context-v1]' \
|
||||
'-X[Enable direct connection to X11]' \
|
||||
'--dbus[Enable proxied connection to D-Bus]' \
|
||||
'--pulse[Enable direct connection to PulseAudio]' \
|
||||
'--pipewire[Enable connection to PipeWire via SecurityContext]' \
|
||||
'--pulse[Enable PulseAudio compatibility daemon]' \
|
||||
'--dbus-config[Path to session bus proxy config file]: :_files -g "*.json"' \
|
||||
'--dbus-system[Path to system bus proxy config file]: :_files -g "*.json"' \
|
||||
'--mpris[Allow owning MPRIS D-Bus path]' \
|
||||
@@ -54,9 +57,9 @@ __hakurei_instances() {
|
||||
{
|
||||
local -a _hakurei_cmds
|
||||
_hakurei_cmds=(
|
||||
"app:Load app from configuration file"
|
||||
"run:Configure and start a permissive default sandbox"
|
||||
"show:Show live or local app configuration"
|
||||
"run:Load and start container from configuration file"
|
||||
"exec:Configure and start a permissive container"
|
||||
"show:Show live or local instance configuration"
|
||||
"ps:List active instances"
|
||||
"version:Display version information"
|
||||
"license:Show full license text"
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
TOOLCHAIN_VERSION="$(go version)"
|
||||
cd "$(dirname -- "$0")/../.."
|
||||
echo "Building cmd/dist using ${TOOLCHAIN_VERSION}."
|
||||
FLAGS=''
|
||||
if test -n "$VERBOSE"; then
|
||||
FLAGS="$FLAGS -v"
|
||||
fi
|
||||
go run $FLAGS --tags=dist ./cmd/dist
|
||||
Vendored
+254
@@ -0,0 +1,254 @@
|
||||
//go:build dist
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate sh -c "git describe --tags > VERSION"
|
||||
//go:embed VERSION
|
||||
var version string
|
||||
|
||||
// getenv looks up an environment variable, and returns fallback if it is unset.
|
||||
func getenv(key, fallback string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// mustRun runs a command with the current process's environment and panics
|
||||
// on error or non-zero exit code.
|
||||
func mustRun(ctx context.Context, name string, arg ...string) {
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed comp/_hakurei
|
||||
var comp []byte
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("")
|
||||
|
||||
verbose := os.Getenv("VERBOSE") != ""
|
||||
runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
|
||||
version = getenv("HAKUREI_VERSION", strings.TrimSpace(version))
|
||||
prefix := getenv("PREFIX", "/usr")
|
||||
destdir := getenv("DESTDIR", "dist")
|
||||
|
||||
if verbose {
|
||||
log.Println()
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destdir, 0755); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s, err := os.MkdirTemp(destdir, ".dist.*")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
var code int
|
||||
|
||||
if err = os.RemoveAll(s); err != nil {
|
||||
code = 1
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if r := recover(); r != nil {
|
||||
code = 1
|
||||
log.Println(r)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}()
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
verboseFlag := "-v"
|
||||
if !verbose {
|
||||
verboseFlag = "-buildvcs=false"
|
||||
}
|
||||
|
||||
log.Printf("Building hakurei for %s/%s.", runtime.GOOS, runtime.GOARCH)
|
||||
mustRun(ctx, "go", "generate", "./...")
|
||||
mustRun(
|
||||
ctx, "go", "build",
|
||||
"-trimpath",
|
||||
verboseFlag, "-o", s,
|
||||
"-ldflags=-s -w "+
|
||||
"-buildid= -linkmode external -extldflags=-static "+
|
||||
"-X hakurei.app/internal/info.buildVersion="+version+" "+
|
||||
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
|
||||
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
|
||||
"-X main.hakureiPath="+prefix+"/bin/hakurei",
|
||||
"./...",
|
||||
)
|
||||
log.Println()
|
||||
|
||||
if runTests {
|
||||
log.Println("##### Testing Hakurei.")
|
||||
mustRun(
|
||||
ctx, "go", "test",
|
||||
"-ldflags=-buildid= -linkmode external -extldflags=-static",
|
||||
"./...",
|
||||
)
|
||||
log.Println()
|
||||
}
|
||||
|
||||
log.Println("##### Creating distribution.")
|
||||
const suffix = ".tar.gz"
|
||||
distName := "hakurei-" + version + "-" + runtime.GOARCH
|
||||
var f *os.File
|
||||
if f, err = os.OpenFile(
|
||||
filepath.Join(s, distName+suffix),
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
||||
0644,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
h := sha512.New()
|
||||
gw, _ := gzip.NewWriterLevel(io.MultiWriter(f, h), gzip.BestCompression)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
|
||||
header := tar.Header{
|
||||
Name: filepath.Join(distName, name),
|
||||
Size: size,
|
||||
Mode: int64(mode),
|
||||
Uname: "root",
|
||||
Gname: "root",
|
||||
}
|
||||
|
||||
if mode&os.ModeDir != 0 {
|
||||
header.Typeflag = tar.TypeDir
|
||||
fmt.Printf("%s %s\n", mode, name)
|
||||
} else {
|
||||
header.Typeflag = tar.TypeReg
|
||||
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
|
||||
}
|
||||
|
||||
if err = tw.WriteHeader(&header); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
|
||||
mustWriteHeader(name, int64(len(data)), mode)
|
||||
if mode&os.ModeDir != 0 {
|
||||
return
|
||||
}
|
||||
if _, err = tw.Write(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
|
||||
var r *os.File
|
||||
if r, err = os.Open(src); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var fi os.FileInfo
|
||||
if fi, err = r.Stat(); err != nil {
|
||||
_ = r.Close()
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if mode == 0 {
|
||||
mode = fi.Mode()
|
||||
}
|
||||
|
||||
mustWriteHeader(dst, fi.Size(), mode)
|
||||
if _, err = io.Copy(tw, r); err != nil {
|
||||
_ = r.Close()
|
||||
panic(err)
|
||||
} else if err = r.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFile(".", nil, fs.ModeDir|0755)
|
||||
mustWriteFile("comp/", nil, os.ModeDir|0755)
|
||||
mustWriteFile("comp/_hakurei", comp, 0644)
|
||||
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
|
||||
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
|
||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||
fi
|
||||
|
||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
|
||||
`), 0755)
|
||||
|
||||
mustWriteFromPath("README.md", "README.md", 0)
|
||||
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
|
||||
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
|
||||
for _, name := range []string{
|
||||
"hakurei",
|
||||
"sharefs",
|
||||
} {
|
||||
mustWriteFromPath(
|
||||
filepath.Join("bin", name),
|
||||
filepath.Join(s, name),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
if err = tw.Close(); err != nil {
|
||||
panic(err)
|
||||
} else if err = gw.Close(); err != nil {
|
||||
panic(err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f = nil
|
||||
|
||||
if err = os.WriteFile(
|
||||
filepath.Join(destdir, distName+suffix+".sha512"),
|
||||
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
|
||||
0644,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = os.Rename(
|
||||
filepath.Join(s, distName+suffix),
|
||||
filepath.Join(destdir, distName+suffix),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// The earlyinit is part of the Rosa OS initramfs and serves as the system init.
|
||||
//
|
||||
// This program is an internal detail of Rosa OS and is not usable on its own.
|
||||
// It is not covered by the compatibility promise.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"slices"
|
||||
"strings"
|
||||
. "syscall"
|
||||
|
||||
"hakurei.app/internal/kobject"
|
||||
"hakurei.app/internal/report"
|
||||
"hakurei.app/internal/uevent"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
var r report.Reporter
|
||||
|
||||
func init() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("earlyinit: ")
|
||||
r.SetOutput(log.Default())
|
||||
|
||||
// this handles SIGQUIT to provide useful debugging information without
|
||||
// terminating, and prevents the runtime from throwing on the must family
|
||||
// of early error reporting functions, DO NOT REMOVE
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, SIGQUIT)
|
||||
go func() {
|
||||
for {
|
||||
<-c
|
||||
if p := pprof.Lookup("goroutine"); p == nil {
|
||||
log.Println("initial built-in goroutine profile does not exist")
|
||||
} else if err := p.WriteTo(os.Stderr, 2); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// fatal calls [log.Println] with v and blocks forever. Must be called from
|
||||
// main. Must not be used after error reporting is set up.
|
||||
func fatal(v ...any) {
|
||||
log.Println(v...)
|
||||
log.Println("unable to continue, please reboot and resolve the problem manually")
|
||||
select {}
|
||||
}
|
||||
|
||||
// must calls fatal with err if it is non-nil.
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
select {}
|
||||
}
|
||||
}
|
||||
|
||||
// mustSyscall is like must, but with an additional action name.
|
||||
func mustSyscall(action string, err error) {
|
||||
if err != nil {
|
||||
fatal("cannot "+action+":", err)
|
||||
select {}
|
||||
}
|
||||
}
|
||||
|
||||
// must1 is like must, but with an additional passed through value.
|
||||
func must1[T any](v T, err error) T {
|
||||
must(err)
|
||||
return v
|
||||
}
|
||||
|
||||
const (
|
||||
// optionSystem specifies devpath of the system device.
|
||||
optionSystem = "system"
|
||||
|
||||
// flagVerbose increases output verbosity.
|
||||
flagVerbose = "verbose"
|
||||
// flagStrict sets [report.DStrict] on r.
|
||||
flagStrict = "strict"
|
||||
// flagNoRecover sets [report.DNoRecover] on r.
|
||||
flagNoRecover = "no_recover"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.LockOSThread()
|
||||
|
||||
var (
|
||||
option map[string]string
|
||||
flags []string
|
||||
)
|
||||
if len(os.Args) > 1 {
|
||||
option = make(map[string]string)
|
||||
for _, s := range os.Args[1:] {
|
||||
key, value, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
flags = append(flags, s)
|
||||
continue
|
||||
}
|
||||
option[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var flag uint64
|
||||
if slices.Contains(flags, flagStrict) {
|
||||
flag |= report.DStrict
|
||||
}
|
||||
if slices.Contains(flags, flagNoRecover) {
|
||||
flag |= report.DNoRecover
|
||||
}
|
||||
log.Printf("reporting flags %x", flag)
|
||||
r.SetFlags(flag)
|
||||
}
|
||||
|
||||
msg := message.New(log.Default())
|
||||
msg.SwapVerbose(slices.Contains(flags, flagVerbose))
|
||||
|
||||
mustSyscall("mount devtmpfs", Mount(
|
||||
"devtmpfs",
|
||||
"/dev/",
|
||||
"devtmpfs",
|
||||
MS_NOSUID|MS_NOEXEC,
|
||||
"",
|
||||
))
|
||||
must(os.Mkdir("/dev/pts/", 0))
|
||||
mustSyscall("mount devpts", Mount(
|
||||
"devpts",
|
||||
"/dev/pts/",
|
||||
"devpts",
|
||||
MS_NOSUID|MS_NOEXEC,
|
||||
"mode=620,ptmxmode=666",
|
||||
))
|
||||
must(os.Mkdir("/dev/shm/", 0))
|
||||
mustSyscall("mount shm", Mount(
|
||||
"shm",
|
||||
"/dev/shm/",
|
||||
"tmpfs",
|
||||
MS_NOSUID|MS_NODEV,
|
||||
"",
|
||||
))
|
||||
|
||||
// The kernel might be unable to set up the console. When that happens,
|
||||
// printk is called with "Warning: unable to open an initial console."
|
||||
// and the init runs with no files. The checkfds runtime function
|
||||
// populates 0-2 by opening /dev/null for them.
|
||||
//
|
||||
// This check replaces 1 and 2 with /dev/kmsg to improve the chance
|
||||
// of output being visible to the user.
|
||||
if fi, err := os.Stdout.Stat(); err == nil {
|
||||
if stat, ok := fi.Sys().(*Stat_t); ok {
|
||||
if stat.Rdev == 0x103 {
|
||||
var fd int
|
||||
if fd, err = Open(
|
||||
"/dev/kmsg",
|
||||
O_WRONLY|O_CLOEXEC,
|
||||
0,
|
||||
); err != nil {
|
||||
log.Fatalf("cannot open kmsg: %v", err)
|
||||
}
|
||||
|
||||
if err = Dup3(fd, Stdout, 0); err != nil {
|
||||
log.Fatalf("cannot open stdout: %v", err)
|
||||
}
|
||||
if err = Dup3(fd, Stderr, 0); err != nil {
|
||||
log.Fatalf("cannot open stderr: %v", err)
|
||||
}
|
||||
|
||||
if err = Close(fd); err != nil {
|
||||
log.Printf("cannot close kmsg: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// staying in rootfs, these are no longer used
|
||||
must(os.Remove("/root"))
|
||||
must(os.Remove("/init"))
|
||||
|
||||
must(os.Mkdir("/proc", 0))
|
||||
mustSyscall("mount proc", Mount(
|
||||
"proc",
|
||||
"/proc",
|
||||
"proc",
|
||||
MS_NOSUID|MS_NOEXEC|MS_NODEV,
|
||||
"hidepid=1",
|
||||
))
|
||||
|
||||
must(os.Mkdir("/sys", 0))
|
||||
mustSyscall("mount sysfs", Mount(
|
||||
"sysfs",
|
||||
"/sys",
|
||||
"sysfs",
|
||||
0,
|
||||
"",
|
||||
))
|
||||
|
||||
conn := must1(uevent.Dial(-128 * 1024 * 1024))
|
||||
events := make(chan *uevent.Message, 1<<10)
|
||||
var uuid uevent.UUID
|
||||
must1(rand.Read(uuid[:]))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go consume(ctx, msg, &r, conn, uuid, events)
|
||||
s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) {
|
||||
p := make([]string, 0, len(env))
|
||||
for k, v := range env {
|
||||
p = append(p, k+"="+v)
|
||||
}
|
||||
slices.Sort(p)
|
||||
log.Printf("change %s: %s", o.DevPath, strings.Join(p, ", "))
|
||||
}, func(err error) {
|
||||
severity := report.Inconsistent
|
||||
if e, ok := err.(kobject.EventError); ok && e.Kind == kobject.EBadTarget {
|
||||
severity = report.Trivial
|
||||
}
|
||||
r.Dispatch(
|
||||
severity,
|
||||
"processed inconsistent uevent",
|
||||
err,
|
||||
)
|
||||
})
|
||||
go func() {
|
||||
s.Consume(ctx, events)
|
||||
|
||||
log.Println("closing NETLINK_KOBJECT_UEVENT socket")
|
||||
cancel()
|
||||
if err := conn.Close(); err != nil {
|
||||
log.Fatal(err) // not reached
|
||||
}
|
||||
}()
|
||||
|
||||
must(os.Mkdir("/system", 0))
|
||||
if devpath := option[optionSystem]; devpath == "" {
|
||||
fatal("system must be nonempty")
|
||||
} else {
|
||||
log.Printf("waiting for devpath pattern %q", devpath)
|
||||
mustMountSystem(ctx, s, devpath)
|
||||
}
|
||||
|
||||
// after top level has been set up
|
||||
mustSyscall("remount root", Mount(
|
||||
"",
|
||||
"/",
|
||||
"",
|
||||
MS_REMOUNT|MS_BIND|
|
||||
MS_RDONLY|MS_NODEV|MS_NOSUID|MS_NOEXEC,
|
||||
"",
|
||||
))
|
||||
|
||||
must(os.WriteFile(
|
||||
"/sys/module/firmware_class/parameters/path",
|
||||
[]byte("/system/lib/firmware"),
|
||||
0,
|
||||
))
|
||||
go dispatchModprobe(ctx, s)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/kobject"
|
||||
"hakurei.app/internal/report"
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
// ModprobeError describes an unsuccessful modprobe invocation.
|
||||
type ModprobeError struct {
|
||||
ModAlias string `json:"modalias"`
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
}
|
||||
|
||||
var _ report.RepresentableError = ModprobeError{}
|
||||
|
||||
func (ModprobeError) Representable() {}
|
||||
func (e ModprobeError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"modprobe exit status %d: %s",
|
||||
e.ExitCode, strings.TrimSpace(e.Stderr),
|
||||
)
|
||||
}
|
||||
|
||||
// dispatchModprobe invokes modprobe for [uevent.KOBJ_ADD] events raising new
|
||||
// MODALIAS strings.
|
||||
func dispatchModprobe(
|
||||
ctx context.Context,
|
||||
s *kobject.State,
|
||||
) {
|
||||
aliases := make(chan string, 1<<8)
|
||||
go func() {
|
||||
defer close(aliases)
|
||||
s.Range(ctx, func(o *kobject.Object, act uevent.KobjectAction) bool {
|
||||
if act == uevent.KOBJ_ADD && o.Driver == "" && o.ModAlias != "" {
|
||||
aliases <- o.ModAlias
|
||||
}
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
for alias := range aliases {
|
||||
stdout, err := exec.Command("/system/sbin/modprobe", alias).Output()
|
||||
if err == nil {
|
||||
if len(stdout) > 0 {
|
||||
log.Println(string(stdout))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
exitError, ok := errors.AsType[*exec.ExitError](err)
|
||||
if !ok || exitError == nil {
|
||||
r.Dispatch(report.Degraded, "invoke modprobe", err)
|
||||
continue
|
||||
}
|
||||
|
||||
r.Dispatch(report.Trivial, "load device driver", ModprobeError{
|
||||
ModAlias: alias,
|
||||
Stdout: string(stdout),
|
||||
Stderr: string(exitError.Stderr),
|
||||
ExitCode: exitError.ExitCode(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/kobject"
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
// mustMountSystem waits for and mounts a system device matching pattern.
|
||||
func mustMountSystem(
|
||||
ctx context.Context,
|
||||
s *kobject.State,
|
||||
pattern string,
|
||||
) {
|
||||
c, stop := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer stop()
|
||||
|
||||
for {
|
||||
var matchErr error
|
||||
var systemPath *check.Absolute
|
||||
s.Range(c, func(o *kobject.Object, act uevent.KobjectAction) bool {
|
||||
if (act != uevent.KOBJ_ADD && act != uevent.KOBJ_CHANGE) ||
|
||||
o.Subsystem != "block" ||
|
||||
o.Env["DEVTYPE"] != "disk" {
|
||||
return true
|
||||
}
|
||||
|
||||
if ok, err := filepath.Match(pattern, o.DevPath); err != nil {
|
||||
matchErr = err
|
||||
return false
|
||||
} else if !ok {
|
||||
return true
|
||||
}
|
||||
|
||||
name, ok := o.Env["DEVNAME"]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
systemPath = fhs.AbsDev.Append(name)
|
||||
return false
|
||||
})
|
||||
if c.Err() != nil {
|
||||
fatal("devpath", strconv.Quote(pattern), "never appeared")
|
||||
}
|
||||
if matchErr != nil {
|
||||
fatal("cannot match system devpath:", matchErr)
|
||||
}
|
||||
err := syscall.Mount(
|
||||
systemPath.String(),
|
||||
"/system/",
|
||||
"squashfs",
|
||||
0,
|
||||
"threads=multi",
|
||||
)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
fatal("cannot mount system:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/report"
|
||||
"hakurei.app/internal/uevent"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// newRejectColdboot returns a function to be called on every subsequent pending
|
||||
// coldboot, and returns whether coldboot should proceed. Rejection is sticky.
|
||||
func newRejectColdboot() func() bool {
|
||||
// one coldboot per five minutes, two consecutive coldboot
|
||||
const (
|
||||
coldbootInterval = 5 * time.Minute
|
||||
coldbootBurst = 2
|
||||
)
|
||||
|
||||
done := make(chan struct{})
|
||||
s := make(chan struct{}, coldbootBurst)
|
||||
s <- struct{}{} // for early fault before reporting is ready
|
||||
go func() {
|
||||
t := time.NewTicker(coldbootInterval)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
|
||||
case <-t.C:
|
||||
select {
|
||||
case s <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return func() bool {
|
||||
select {
|
||||
case <-s:
|
||||
return true
|
||||
|
||||
case <-done:
|
||||
return false
|
||||
|
||||
default:
|
||||
close(done)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// consume continuously consumes events from conn with retries.
|
||||
func consume(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
r *report.Reporter,
|
||||
conn *uevent.Conn,
|
||||
uuid uevent.UUID,
|
||||
events chan<- *uevent.Message,
|
||||
) {
|
||||
defer close(events)
|
||||
|
||||
nextColdboot := newRejectColdboot()
|
||||
coldboot := true
|
||||
retry:
|
||||
if dispatchErr := conn.Consume(ctx, fhs.Sys, &uuid, events, coldboot, func(path string) {
|
||||
msg.Verbose("coldboot visited", path)
|
||||
}, func(err error) bool {
|
||||
if _, ok := err.(uevent.NeedsColdboot); ok && !nextColdboot() {
|
||||
r.Dispatch(
|
||||
report.Degraded,
|
||||
"rejecting coldboot loop",
|
||||
err,
|
||||
)
|
||||
return false
|
||||
}
|
||||
r.Dispatch(
|
||||
report.Inconsistent,
|
||||
"consumed invalid message",
|
||||
err,
|
||||
)
|
||||
return true
|
||||
}, nil); dispatchErr != nil {
|
||||
if _, ok := dispatchErr.(uevent.Recoverable); !ok {
|
||||
r.Dispatch(
|
||||
report.Fatal,
|
||||
"discontinuing uevent processing due to nonrecoverable error",
|
||||
dispatchErr,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := dispatchErr.(uevent.NeedsColdboot); ok {
|
||||
// coldboot loop rejected by handler
|
||||
coldboot = false
|
||||
}
|
||||
|
||||
goto retry
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRejectColdboot(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
nextColdboot := newRejectColdboot()
|
||||
want := func(want bool) {
|
||||
if got := nextColdboot(); got != want {
|
||||
t.Fatalf("nextColdboot: %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
synctest.Wait()
|
||||
want(true)
|
||||
time.Sleep(time.Hour)
|
||||
synctest.Wait()
|
||||
want(true)
|
||||
want(true)
|
||||
time.Sleep(5 * time.Minute)
|
||||
synctest.Wait()
|
||||
want(true)
|
||||
want(false)
|
||||
time.Sleep(time.Hour)
|
||||
synctest.Wait()
|
||||
want(false)
|
||||
want(false)
|
||||
})
|
||||
}
|
||||
+296
-171
@@ -2,77 +2,117 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/app"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func buildCommand(out io.Writer) command.Command {
|
||||
// optionalErrorUnwrap calls [errors.Unwrap] and returns the resulting value
|
||||
// if it is not nil, or the original value if it is.
|
||||
func optionalErrorUnwrap(err error) error {
|
||||
if underlyingErr := errors.Unwrap(err); underlyingErr != nil {
|
||||
return underlyingErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var errSuccess = errors.New("success")
|
||||
|
||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
flagVerbose bool
|
||||
flagInsecure bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
||||
msg.SwapVerbose(flagVerbose)
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { app.ShimMain(); return errSuccess })
|
||||
|
||||
c.Command("app", "Load app from configuration file", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("app requires at least 1 argument")
|
||||
if early.yamaLSM != nil {
|
||||
msg.Verbosef("cannot enable ptrace protection via Yama LSM: %v", early.yamaLSM)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
// config extraArgs...
|
||||
config := tryPath(args[0])
|
||||
config.Args = append(config.Args, args[1:]...)
|
||||
if early.dumpable != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", early.dumpable)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
runApp(config)
|
||||
panic("unreachable")
|
||||
})
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
||||
|
||||
{
|
||||
var (
|
||||
dbusConfigSession string
|
||||
dbusConfigSystem string
|
||||
mpris bool
|
||||
dbusVerbose bool
|
||||
|
||||
fid string
|
||||
aid int
|
||||
groups command.RepeatableFlag
|
||||
homeDir string
|
||||
userName string
|
||||
|
||||
wayland, x11, dBus, pulse bool
|
||||
flagIdentifierFile int
|
||||
)
|
||||
|
||||
c.NewCommand("run", "Configure and start a permissive default sandbox", func(args []string) error {
|
||||
// initialise config from flags
|
||||
config := &hst.Config{
|
||||
ID: fid,
|
||||
Args: args,
|
||||
c.NewCommand("run", "Load and start container from configuration file", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("run requires at least 1 argument")
|
||||
}
|
||||
|
||||
if aid < 0 || aid > 9999 {
|
||||
log.Fatalf("aid %d out of range", aid)
|
||||
config := tryPath(msg, args[0])
|
||||
if config != nil && config.Container != nil {
|
||||
config.Container.Args = append(config.Container.Args, args[1:]...)
|
||||
}
|
||||
|
||||
var flags int
|
||||
if flagInsecure {
|
||||
flags |= hst.VAllowInsecure
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
||||
"Write identifier of current instance to fd after successful startup")
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDBusConfigSession string
|
||||
flagDBusConfigSystem string
|
||||
flagDBusMpris bool
|
||||
flagDBusVerbose bool
|
||||
|
||||
flagID string
|
||||
flagIdentity int
|
||||
flagGroups command.RepeatableFlag
|
||||
flagHomeDir string
|
||||
flagUserName string
|
||||
|
||||
flagSchedPolicy string
|
||||
flagSchedPriority int
|
||||
|
||||
flagPrivateRuntime, flagPrivateTmpdir bool
|
||||
|
||||
flagWayland, flagX11, flagDBus, flagPipeWire, flagPulse bool
|
||||
)
|
||||
|
||||
c.NewCommand("exec", "Configure and start a permissive container", func(args []string) error {
|
||||
if flagIdentity < hst.IdentityStart || flagIdentity > hst.IdentityEnd {
|
||||
log.Fatalf("identity %d out of range", flagIdentity)
|
||||
}
|
||||
|
||||
// resolve home/username from os when flag is unset
|
||||
@@ -80,22 +120,15 @@ func buildCommand(out io.Writer) command.Command {
|
||||
passwd *user.User
|
||||
passwdOnce sync.Once
|
||||
passwdFunc = func() {
|
||||
var us string
|
||||
if uid, err := std.Uid(aid); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
us = strconv.Itoa(uid)
|
||||
}
|
||||
|
||||
us := strconv.Itoa(hst.ToUser(new(outcome.Hsu).MustID(msg), flagIdentity))
|
||||
if u, err := user.LookupId(us); err != nil {
|
||||
hlog.Verbosef("cannot look up uid %s", us)
|
||||
msg.Verbosef("cannot look up uid %s", us)
|
||||
passwd = &user.User{
|
||||
Uid: us,
|
||||
Gid: us,
|
||||
Username: "chronos",
|
||||
Name: "Hakurei Permissive Default",
|
||||
HomeDir: container.FHSVarEmpty,
|
||||
HomeDir: fhs.VarEmpty,
|
||||
}
|
||||
} else {
|
||||
passwd = u
|
||||
@@ -103,164 +136,256 @@ func buildCommand(out io.Writer) command.Command {
|
||||
}
|
||||
)
|
||||
|
||||
if homeDir == "os" {
|
||||
// paths are identical, resolve inner shell and program path
|
||||
shell := fhs.AbsRoot.Append("bin", "sh")
|
||||
if a, err := check.NewAbs(os.Getenv("SHELL")); err == nil {
|
||||
shell = a
|
||||
}
|
||||
progPath := shell
|
||||
if len(args) > 0 {
|
||||
if p, err := exec.LookPath(args[0]); err != nil {
|
||||
log.Fatal(optionalErrorUnwrap(err))
|
||||
return err
|
||||
} else if progPath, err = check.NewAbs(p); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var et hst.Enablements
|
||||
if flagWayland {
|
||||
et |= hst.EWayland
|
||||
}
|
||||
if flagX11 {
|
||||
et |= hst.EX11
|
||||
}
|
||||
if flagDBus {
|
||||
et |= hst.EDBus
|
||||
}
|
||||
if flagPipeWire || flagPulse {
|
||||
et |= hst.EPipeWire
|
||||
}
|
||||
|
||||
config := hst.Config{
|
||||
ID: flagID,
|
||||
Identity: flagIdentity,
|
||||
Groups: flagGroups,
|
||||
Enablements: &et,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
// autoroot, includes the home directory
|
||||
{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsRoot,
|
||||
Source: fhs.AbsRoot,
|
||||
Write: true,
|
||||
Special: true,
|
||||
}},
|
||||
},
|
||||
|
||||
Username: flagUserName,
|
||||
Shell: shell,
|
||||
|
||||
Path: progPath,
|
||||
Args: args,
|
||||
|
||||
Flags: hst.FUserns | hst.FHostNet | hst.FHostAbstract | hst.FTty,
|
||||
},
|
||||
}
|
||||
|
||||
if err := config.SchedPolicy.UnmarshalText(
|
||||
[]byte(flagSchedPolicy),
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
config.SchedPriority = ext.Int(flagSchedPriority)
|
||||
|
||||
// bind GPU stuff
|
||||
if et&(hst.EX11|hst.EWayland) != 0 {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("dri"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}})
|
||||
}
|
||||
|
||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||
// opportunistically bind kvm
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||
Source: fhs.AbsDev.Append("kvm"),
|
||||
Device: true,
|
||||
Optional: true,
|
||||
}},
|
||||
|
||||
// do autoetc last
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
|
||||
Target: fhs.AbsEtc,
|
||||
Source: fhs.AbsEtc,
|
||||
Special: true,
|
||||
}},
|
||||
)
|
||||
|
||||
if config.Container.Username == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
config.Container.Username = passwd.Username
|
||||
}
|
||||
|
||||
if userName == "chronos" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
userName = passwd.Username
|
||||
{
|
||||
homeDir := flagHomeDir
|
||||
if homeDir == "os" {
|
||||
passwdOnce.Do(passwdFunc)
|
||||
homeDir = passwd.HomeDir
|
||||
}
|
||||
if a, err := check.NewAbs(homeDir); err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
} else {
|
||||
config.Container.Home = a
|
||||
}
|
||||
}
|
||||
|
||||
config.Identity = aid
|
||||
config.Groups = groups
|
||||
config.Username = userName
|
||||
|
||||
if a, err := container.NewAbs(homeDir); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
} else {
|
||||
config.Home = a
|
||||
if !flagPrivateRuntime {
|
||||
config.Container.Flags |= hst.FShareRuntime
|
||||
}
|
||||
|
||||
var e system.Enablement
|
||||
if wayland {
|
||||
e |= system.EWayland
|
||||
if !flagPrivateTmpdir {
|
||||
config.Container.Flags |= hst.FShareTmpdir
|
||||
}
|
||||
if x11 {
|
||||
e |= system.EX11
|
||||
}
|
||||
if dBus {
|
||||
e |= system.EDBus
|
||||
}
|
||||
if pulse {
|
||||
e |= system.EPulse
|
||||
}
|
||||
config.Enablements = hst.NewEnablements(e)
|
||||
|
||||
// parse D-Bus config file from flags if applicable
|
||||
if dBus {
|
||||
if dbusConfigSession == "builtin" {
|
||||
config.SessionBus = dbus.NewConfig(fid, true, mpris)
|
||||
if flagDBus {
|
||||
if flagDBusConfigSession == "builtin" {
|
||||
config.SessionBus = dbus.NewConfig(flagID, true, flagDBusMpris)
|
||||
} else {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSession); err != nil {
|
||||
log.Fatalf("cannot load session bus proxy config from %q: %s", dbusConfigSession, err)
|
||||
if f, err := os.Open(flagDBusConfigSession); err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
config.SessionBus = conf
|
||||
decodeJSON(log.Fatal, "load session bus proxy config", f, &config.SessionBus)
|
||||
if err = f.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// system bus proxy is optional
|
||||
if dbusConfigSystem != "nil" {
|
||||
if conf, err := dbus.NewConfigFromFile(dbusConfigSystem); err != nil {
|
||||
log.Fatalf("cannot load system bus proxy config from %q: %s", dbusConfigSystem, err)
|
||||
if flagDBusConfigSystem != "nil" {
|
||||
if f, err := os.Open(flagDBusConfigSystem); err != nil {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
config.SystemBus = conf
|
||||
decodeJSON(log.Fatal, "load system bus proxy config", f, &config.SystemBus)
|
||||
if err = f.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// override log from configuration
|
||||
if dbusVerbose {
|
||||
config.SessionBus.Log = true
|
||||
config.SystemBus.Log = true
|
||||
if flagDBusVerbose {
|
||||
if config.SessionBus != nil {
|
||||
config.SessionBus.Log = true
|
||||
}
|
||||
if config.SystemBus != nil {
|
||||
config.SystemBus.Log = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// invoke app
|
||||
runApp(config)
|
||||
outcome.Main(ctx, msg, &config, 0, -1)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&dbusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
"Path to session bus proxy config file, or \"builtin\" for defaults").
|
||||
Flag(&dbusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
Flag(&flagDBusConfigSystem, "dbus-system", command.StringFlag("nil"),
|
||||
"Path to system bus proxy config file, or \"nil\" to disable").
|
||||
Flag(&mpris, "mpris", command.BoolFlag(false),
|
||||
Flag(&flagDBusMpris, "mpris", command.BoolFlag(false),
|
||||
"Allow owning MPRIS D-Bus path, has no effect if custom config is available").
|
||||
Flag(&dbusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
Flag(&flagDBusVerbose, "dbus-log", command.BoolFlag(false),
|
||||
"Force buffered logging in the D-Bus proxy").
|
||||
Flag(&fid, "id", command.StringFlag(""),
|
||||
Flag(&flagID, "id", command.StringFlag(""),
|
||||
"Reverse-DNS style Application identifier, leave empty to inherit instance identifier").
|
||||
Flag(&aid, "a", command.IntFlag(0),
|
||||
Flag(&flagIdentity, "a", command.IntFlag(0),
|
||||
"Application identity").
|
||||
Flag(nil, "g", &groups,
|
||||
Flag(nil, "g", &flagGroups,
|
||||
"Groups inherited by all container processes").
|
||||
Flag(&homeDir, "d", command.StringFlag("os"),
|
||||
Flag(&flagHomeDir, "d", command.StringFlag("os"),
|
||||
"Container home directory").
|
||||
Flag(&userName, "u", command.StringFlag("chronos"),
|
||||
Flag(&flagUserName, "u", command.StringFlag("chronos"),
|
||||
"Passwd user name within sandbox").
|
||||
Flag(&wayland, "wayland", command.BoolFlag(false),
|
||||
Flag(&flagSchedPolicy, "policy", command.StringFlag(""),
|
||||
"Scheduling policy to set for the container").
|
||||
Flag(&flagSchedPriority, "priority", command.IntFlag(0),
|
||||
"Scheduling priority to set for the container").
|
||||
Flag(&flagPrivateRuntime, "private-runtime", command.BoolFlag(false),
|
||||
"Do not share XDG_RUNTIME_DIR between containers under the same identity").
|
||||
Flag(&flagPrivateTmpdir, "private-tmpdir", command.BoolFlag(false),
|
||||
"Do not share TMPDIR between containers under the same identity").
|
||||
Flag(&flagWayland, "wayland", command.BoolFlag(false),
|
||||
"Enable connection to Wayland via security-context-v1").
|
||||
Flag(&x11, "X", command.BoolFlag(false),
|
||||
Flag(&flagX11, "X", command.BoolFlag(false),
|
||||
"Enable direct connection to X11").
|
||||
Flag(&dBus, "dbus", command.BoolFlag(false),
|
||||
Flag(&flagDBus, "dbus", command.BoolFlag(false),
|
||||
"Enable proxied connection to D-Bus").
|
||||
Flag(&pulse, "pulse", command.BoolFlag(false),
|
||||
"Enable direct connection to PulseAudio")
|
||||
Flag(&flagPipeWire, "pipewire", command.BoolFlag(false),
|
||||
"Enable connection to PipeWire via SecurityContext").
|
||||
Flag(&flagPulse, "pulse", command.BoolFlag(false),
|
||||
"Enable PulseAudio compatibility daemon")
|
||||
}
|
||||
|
||||
var showFlagShort bool
|
||||
c.NewCommand("show", "Show live or local app configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, showFlagShort, flagJSON)
|
||||
{
|
||||
var (
|
||||
flagShort bool
|
||||
flagNoStore bool
|
||||
)
|
||||
c.NewCommand("show", "Show live or local instance configuration", func(args []string) error {
|
||||
switch len(args) {
|
||||
case 0: // system
|
||||
printShowSystem(os.Stdout, flagShort, flagJSON)
|
||||
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
config, entry := tryShort(name)
|
||||
if config == nil {
|
||||
config = tryPath(name)
|
||||
case 1: // instance
|
||||
name := args[0]
|
||||
|
||||
var (
|
||||
config *hst.Config
|
||||
entry *hst.State
|
||||
)
|
||||
if !flagNoStore {
|
||||
var sc hst.Paths
|
||||
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
||||
entry = tryIdentifier(msg, name, outcome.NewStore(&sc))
|
||||
}
|
||||
|
||||
if entry == nil {
|
||||
config = tryPath(msg, name)
|
||||
} else {
|
||||
config = entry.Config
|
||||
}
|
||||
|
||||
if !printShowInstance(os.Stdout, time.Now().UTC(), entry, config, flagShort, flagJSON) {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
printShowInstance(os.Stdout, time.Now().UTC(), entry, config, showFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagShort, "short", command.BoolFlag(false), "Omit filesystem information").
|
||||
Flag(&flagNoStore, "no-store", command.BoolFlag(false), "Do not attempt to match from active instances")
|
||||
}
|
||||
|
||||
default:
|
||||
log.Fatal("show requires 1 argument")
|
||||
}
|
||||
return errSuccess
|
||||
}).Flag(&showFlagShort, "short", command.BoolFlag(false), "Omit filesystem information")
|
||||
{
|
||||
var flagShort bool
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
var sc hst.Paths
|
||||
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
|
||||
printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
}
|
||||
|
||||
var psFlagShort bool
|
||||
c.NewCommand("ps", "List active instances", func(args []string) error {
|
||||
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(std.Paths().RunDirPath.String()), psFlagShort, flagJSON)
|
||||
return errSuccess
|
||||
}).Flag(&psFlagShort, "short", command.BoolFlag(false), "Print instance id")
|
||||
|
||||
c.Command("version", "Display version information", func(args []string) error {
|
||||
fmt.Println(internal.Version())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("license", "Show full license text", func(args []string) error {
|
||||
fmt.Println(license)
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("template", "Produce a config template", func(args []string) error {
|
||||
printJSON(os.Stdout, false, hst.Template())
|
||||
return errSuccess
|
||||
})
|
||||
|
||||
c.Command("help", "Show this help message", func([]string) error {
|
||||
c.PrintHelp()
|
||||
return errSuccess
|
||||
})
|
||||
c.Command("version", "Display version information", func(args []string) error { fmt.Println(info.Version()); return errSuccess })
|
||||
c.Command("license", "Show full license text", func(args []string) error { fmt.Println(license); return errSuccess })
|
||||
c.Command("template", "Produce a config template", func(args []string) error { encodeJSON(log.Fatal, os.Stdout, false, hst.Template()); return errSuccess })
|
||||
c.Command("help", "Show this help message", func([]string) error { c.PrintHelp(); return errSuccess })
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func runApp(config *hst.Config) {
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
a := app.MustNew(ctx, std)
|
||||
|
||||
rs := new(app.RunState)
|
||||
if sa, err := a.Seal(config); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot seal app:")
|
||||
internal.Exit(1)
|
||||
} else {
|
||||
internal.Exit(app.PrintRunStateErr(rs, sa.Run(rs)))
|
||||
}
|
||||
|
||||
*(*int)(nil) = 0 // not reached
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ import (
|
||||
"testing"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestHelp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
@@ -17,12 +20,12 @@ func TestHelp(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
app Load app from configuration file
|
||||
run Configure and start a permissive default sandbox
|
||||
show Show live or local app configuration
|
||||
run Load and start container from configuration file
|
||||
exec Configure and start a permissive container
|
||||
show Show live or local instance configuration
|
||||
ps List active instances
|
||||
version Display version information
|
||||
license Show full license text
|
||||
@@ -32,8 +35,8 @@ Commands:
|
||||
`,
|
||||
},
|
||||
{
|
||||
"run", []string{"run", "-h"}, `
|
||||
Usage: hakurei run [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--wayland] [-X] [--dbus] [--pulse] COMMAND [OPTIONS]
|
||||
"exec", []string{"exec", "-h"}, `
|
||||
Usage: hakurei exec [-h | --help] [--dbus-config <value>] [--dbus-system <value>] [--mpris] [--dbus-log] [--id <value>] [-a <int>] [-g <value>] [-d <value>] [-u <value>] [--policy <value>] [--priority <int>] [--private-runtime] [--private-tmpdir] [--wayland] [-X] [--dbus] [--pipewire] [--pulse] COMMAND [OPTIONS]
|
||||
|
||||
Flags:
|
||||
-X Enable direct connection to X11
|
||||
@@ -55,8 +58,18 @@ Flags:
|
||||
Reverse-DNS style Application identifier, leave empty to inherit instance identifier
|
||||
-mpris
|
||||
Allow owning MPRIS D-Bus path, has no effect if custom config is available
|
||||
-pipewire
|
||||
Enable connection to PipeWire via SecurityContext
|
||||
-policy string
|
||||
Scheduling policy to set for the container
|
||||
-priority int
|
||||
Scheduling priority to set for the container
|
||||
-private-runtime
|
||||
Do not share XDG_RUNTIME_DIR between containers under the same identity
|
||||
-private-tmpdir
|
||||
Do not share TMPDIR between containers under the same identity
|
||||
-pulse
|
||||
Enable direct connection to PulseAudio
|
||||
Enable PulseAudio compatibility daemon
|
||||
-u string
|
||||
Passwd user name within sandbox (default "chronos")
|
||||
-wayland
|
||||
@@ -67,8 +80,10 @@ Flags:
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
out := new(bytes.Buffer)
|
||||
c := buildCommand(out)
|
||||
c := buildCommand(t.Context(), message.New(nil), new(earlyHardeningErrs), out)
|
||||
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
|
||||
t.Errorf("Parse: error = %v; want %v",
|
||||
err, command.ErrHelp)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// decodeJSON decodes json from r and stores it in v. A non-nil error results in
|
||||
// a call to fatal.
|
||||
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
|
||||
err := json.NewDecoder(r).Decode(v)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
syntaxError *json.SyntaxError
|
||||
unmarshalTypeError *json.UnmarshalTypeError
|
||||
|
||||
msg string
|
||||
)
|
||||
|
||||
switch {
|
||||
case errors.As(err, &syntaxError) && syntaxError != nil:
|
||||
msg = syntaxError.Error() +
|
||||
" at byte " + strconv.FormatInt(syntaxError.Offset, 10)
|
||||
|
||||
case errors.As(err, &unmarshalTypeError) && unmarshalTypeError != nil:
|
||||
msg = "inappropriate " + unmarshalTypeError.Value +
|
||||
" at byte " + strconv.FormatInt(unmarshalTypeError.Offset, 10)
|
||||
|
||||
default:
|
||||
// InvalidUnmarshalError: incorrect usage, does not need to be handled
|
||||
// io.ErrUnexpectedEOF: no additional error information available
|
||||
msg = err.Error()
|
||||
}
|
||||
|
||||
fatal("cannot " + op + ": " + msg)
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to output. A non-nil error results in a call to fatal.
|
||||
func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
|
||||
encoder := json.NewEncoder(output)
|
||||
if !short {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
if e, ok := errors.AsType[*json.MarshalerError](err); ok && e != nil {
|
||||
// this likely indicates an implementation error in hst
|
||||
fatal("cannot encode json for " + e.Type.String() + ": " + e.Err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does
|
||||
// not need to be handled
|
||||
fatal("cannot write json: " + err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
func TestDecodeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
t reflect.Type
|
||||
data string
|
||||
want any
|
||||
msg string
|
||||
}{
|
||||
{"success", reflect.TypeFor[uintptr](), "3735928559\n", uintptr(0xdeadbeef), ""},
|
||||
|
||||
{"syntax", reflect.TypeFor[*int](), "\x00", nil,
|
||||
`cannot load sample: invalid character '\x00' looking for beginning of value at byte 1`},
|
||||
{"type", reflect.TypeFor[uintptr](), "-1", nil,
|
||||
`cannot load sample: inappropriate number -1 at byte 2`},
|
||||
{"default", reflect.TypeFor[*int](), "{", nil,
|
||||
"cannot load sample: unexpected EOF"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
gotP = reflect.New(tc.t)
|
||||
gotMsg *string
|
||||
)
|
||||
decodeJSON(func(v ...any) {
|
||||
if gotMsg != nil {
|
||||
t.Fatal("fatal called twice")
|
||||
}
|
||||
msg := v[0].(string)
|
||||
gotMsg = &msg
|
||||
}, "load sample", strings.NewReader(tc.data), gotP.Interface())
|
||||
if tc.msg != "" {
|
||||
if gotMsg == nil {
|
||||
t.Errorf("decodeJSON: success, want fatal %q", tc.msg)
|
||||
} else if *gotMsg != tc.msg {
|
||||
t.Errorf("decodeJSON: fatal = %q, want %q", *gotMsg, tc.msg)
|
||||
}
|
||||
} else if gotMsg != nil {
|
||||
t.Errorf("decodeJSON: fatal = %q", *gotMsg)
|
||||
} else if !reflect.DeepEqual(gotP.Elem().Interface(), tc.want) {
|
||||
t.Errorf("decodeJSON: %#v, want %#v", gotP.Elem().Interface(), tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
v any
|
||||
want string
|
||||
}{
|
||||
{"marshaler", errorJSONMarshaler{},
|
||||
`cannot encode json for main.errorJSONMarshaler: unique error 3735928559 injected by the test suite`},
|
||||
{"default", func() {},
|
||||
`cannot write json: json: unsupported type: func()`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var called bool
|
||||
encodeJSON(func(v ...any) {
|
||||
if called {
|
||||
t.Fatal("fatal called twice")
|
||||
}
|
||||
called = true
|
||||
|
||||
if v[0].(string) != tc.want {
|
||||
t.Errorf("encodeJSON: fatal = %q, want %q", v[0].(string), tc.want)
|
||||
}
|
||||
}, nil, false, tc.v)
|
||||
|
||||
if !called {
|
||||
t.Errorf("encodeJSON: success, want fatal %q", tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// errorJSONMarshaler implements json.Marshaler.
|
||||
type errorJSONMarshaler struct{}
|
||||
|
||||
func (errorJSONMarshaler) MarshalJSON() ([]byte, error) { return nil, stub.UniqueError(0xdeadbeef) }
|
||||
+61
-26
@@ -1,53 +1,88 @@
|
||||
// Hakurei runs user-specified containers as subordinate users.
|
||||
//
|
||||
// This program is generally invoked by another, higher level program, which
|
||||
// creates container configuration via package [hst] or an implementation of it.
|
||||
//
|
||||
// The parent may leave files open and specify their file descriptor for various
|
||||
// uses. In these cases, standard streams and netpoll files are treated as
|
||||
// invalid file descriptors and rejected. All string representations must be in
|
||||
// decimal.
|
||||
//
|
||||
// When specifying a [hst.Config] JSON stream or file to the run subcommand, the
|
||||
// argument "-" is equivalent to stdin. Otherwise, file descriptor rules
|
||||
// described above applies. Invalid file descriptors are treated as file names
|
||||
// in their string representation, with the exception that if a netpoll file
|
||||
// descriptor is attempted, the program fails.
|
||||
//
|
||||
// The flag --identifier-fd can be optionally specified to the run subcommand to
|
||||
// receive the identifier of the newly started instance. File descriptor rules
|
||||
// described above applies, and the file must be writable. This is sent after
|
||||
// its state is made available, so the client must not attempt to poll for it.
|
||||
// This uses the internal binary format of [hst.ID].
|
||||
//
|
||||
// For the show and ps subcommands, the flag --json can be applied to the main
|
||||
// hakurei command to serialise output in JSON when applicable. Additionally,
|
||||
// the flag --short targeting each subcommand is used to omit some information
|
||||
// in both JSON and user-facing output. Only JSON-encoded output is covered
|
||||
// under the compatibility promise.
|
||||
//
|
||||
// A template for [hst.Config] demonstrating all available configuration fields
|
||||
// is returned by [hst.Template]. The JSON-encoded equivalent of this can be
|
||||
// obtained via the template subcommand. Fields left unpopulated in the template
|
||||
// (the direct_* family of fields, which are insecure under any configuration if
|
||||
// enabled) are unsupported.
|
||||
//
|
||||
// For simple (but insecure) testing scenarios, the exec subcommand can be used
|
||||
// to generate a simple, permissive configuration in-memory. See its help
|
||||
// message for all available options.
|
||||
package main
|
||||
|
||||
// this works around go:embed '..' limitation
|
||||
//go:generate cp ../../LICENSE .
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/sys"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
//go:generate cp ../../LICENSE .
|
||||
//go:embed LICENSE
|
||||
var license string
|
||||
|
||||
//go:embed LICENSE
|
||||
license string
|
||||
)
|
||||
|
||||
func init() { hlog.Prepare("hakurei") }
|
||||
|
||||
var std sys.State = new(sys.Std)
|
||||
// earlyHardeningErrs are errors collected while setting up early hardening feature.
|
||||
type earlyHardeningErrs struct{ yamaLSM, dumpable error }
|
||||
|
||||
func main() {
|
||||
// early init path, skips root check and duplicate PR_SET_DUMPABLE
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
container.TryArgv0(nil)
|
||||
|
||||
if err := container.SetPtracer(0); err != nil {
|
||||
hlog.Verbosef("cannot enable ptrace protection via Yama LSM: %v", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
}
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("hakurei: ")
|
||||
msg := message.New(log.Default())
|
||||
|
||||
if err := container.SetDumpable(container.SUID_DUMP_DISABLE); err != nil {
|
||||
log.Printf("cannot set SUID_DUMP_DISABLE: %s", err)
|
||||
// not fatal: this program runs as the privileged user
|
||||
early := earlyHardeningErrs{
|
||||
yamaLSM: ext.SetPtracer(0),
|
||||
dumpable: ext.SetDumpable(ext.SUID_DUMP_DISABLE),
|
||||
}
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
buildCommand(ctx, msg, &early, os.Stderr).MustParse(os.Args[1:], func(err error) {
|
||||
msg.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
// this catches faulty command handlers that fail to return before this point
|
||||
|
||||
+121
-48
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
@@ -11,66 +11,104 @@ import (
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/internal/store"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func tryPath(name string) (config *hst.Config) {
|
||||
var r io.Reader
|
||||
// tryPath attempts to read [hst.Config] from multiple sources.
|
||||
//
|
||||
// tryPath reads from [os.Stdin] if name has value "-". Otherwise, name is
|
||||
// passed to tryFd, and if that returns nil, name is passed to [os.Open].
|
||||
func tryPath(msg message.Msg, name string) (config *hst.Config) {
|
||||
var r io.ReadCloser
|
||||
config = new(hst.Config)
|
||||
|
||||
if name != "-" {
|
||||
r = tryFd(name)
|
||||
r = tryFd(msg, name)
|
||||
if r == nil {
|
||||
hlog.Verbose("load configuration from file")
|
||||
msg.Verbose("load configuration from file")
|
||||
|
||||
if f, err := os.Open(name); err != nil {
|
||||
log.Fatalf("cannot access configuration file %q: %s", name, err)
|
||||
log.Fatal(err.Error())
|
||||
return
|
||||
} else {
|
||||
// finalizer closes f
|
||||
r = f
|
||||
}
|
||||
} else {
|
||||
defer func() {
|
||||
if err := r.(io.ReadCloser).Close(); err != nil {
|
||||
log.Printf("cannot close config fd: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
r = os.Stdin
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
||||
log.Fatalf("cannot load configuration: %v", err)
|
||||
decodeJSON(log.Fatal, "load configuration", r, &config)
|
||||
if err := r.Close(); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func tryFd(name string) io.ReadCloser {
|
||||
// tryFd returns a [io.ReadCloser] if name represents an integer corresponding
|
||||
// to a valid file descriptor.
|
||||
func tryFd(msg message.Msg, name string) io.ReadCloser {
|
||||
if v, err := strconv.Atoi(name); err != nil {
|
||||
if !errors.Is(err, strconv.ErrSyntax) {
|
||||
hlog.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
msg.Verbosef("name cannot be interpreted as int64: %v", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
hlog.Verbosef("trying config stream from %d", v)
|
||||
if v < 3 { // reject standard streams
|
||||
return nil
|
||||
}
|
||||
|
||||
msg.Verbosef("trying config stream from %d", v)
|
||||
fd := uintptr(v)
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, fd, syscall.F_GETFD, 0); errno != 0 {
|
||||
if errors.Is(errno, syscall.EBADF) {
|
||||
if _, _, errno := syscall.Syscall(
|
||||
syscall.SYS_FCNTL,
|
||||
fd,
|
||||
syscall.F_GETFD,
|
||||
0,
|
||||
); errno != 0 {
|
||||
if errors.Is(errno, syscall.EBADF) { // reject bad fd
|
||||
return nil
|
||||
}
|
||||
log.Fatalf("cannot get fd %d: %v", fd, errno)
|
||||
}
|
||||
|
||||
if outcome.IsPollDescriptor(fd) { // reject runtime internals
|
||||
log.Fatalf("invalid config stream %d", fd)
|
||||
}
|
||||
|
||||
return os.NewFile(fd, strconv.Itoa(v))
|
||||
}
|
||||
}
|
||||
|
||||
func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
likePrefix := false
|
||||
if len(name) <= 32 {
|
||||
likePrefix = true
|
||||
// shortLengthMin is the minimum length a short form identifier can have and
|
||||
// still be interpreted as an identifier.
|
||||
const shortLengthMin = 1 << 3
|
||||
|
||||
// shortIdentifier returns an eight character short representation of [hst.ID]
|
||||
// from its random bytes.
|
||||
func shortIdentifier(id *hst.ID) string {
|
||||
return shortIdentifierString(id.String())
|
||||
}
|
||||
|
||||
// shortIdentifierString implements shortIdentifier on an arbitrary string.
|
||||
func shortIdentifierString(s string) string {
|
||||
return s[len(hst.ID{}) : len(hst.ID{})+shortLengthMin]
|
||||
}
|
||||
|
||||
// tryIdentifier attempts to match [hst.State] from a [hex] representation of
|
||||
// [hst.ID] or a prefix of its lower half.
|
||||
func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State {
|
||||
const (
|
||||
likeShort = 1 << iota
|
||||
likeFull
|
||||
)
|
||||
|
||||
var likely uintptr
|
||||
// half the hex representation
|
||||
if len(name) >= shortLengthMin && len(name) <= len(hst.ID{}) {
|
||||
// cannot safely decode here due to unknown alignment
|
||||
for _, c := range name {
|
||||
if c >= '0' && c <= '9' {
|
||||
continue
|
||||
@@ -78,33 +116,68 @@ func tryShort(name string) (config *hst.Config, entry *state.State) {
|
||||
if c >= 'a' && c <= 'f' {
|
||||
continue
|
||||
}
|
||||
likePrefix = false
|
||||
break
|
||||
return nil
|
||||
}
|
||||
likely |= likeShort
|
||||
} else if len(name) == hex.EncodedLen(len(hst.ID{})) {
|
||||
likely |= likeFull
|
||||
}
|
||||
|
||||
// try to match from state store
|
||||
if likePrefix && len(name) >= 8 {
|
||||
hlog.Verbose("argument looks like prefix")
|
||||
if likely == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := state.NewMulti(std.Paths().RunDirPath.String())
|
||||
if entries, err := state.Join(s); err != nil {
|
||||
log.Printf("cannot join store: %v", err)
|
||||
// drop to fetch from file
|
||||
} else {
|
||||
for id := range entries {
|
||||
v := id.String()
|
||||
if strings.HasPrefix(v, name) {
|
||||
// match, use config from this state entry
|
||||
entry = entries[id]
|
||||
config = entry.Config
|
||||
break
|
||||
entries, copyError := s.All()
|
||||
defer func() {
|
||||
if err := copyError(); err != nil {
|
||||
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
|
||||
}
|
||||
}()
|
||||
|
||||
switch {
|
||||
case likely&likeShort != 0:
|
||||
msg.Verbose("argument looks like short identifier")
|
||||
for eh := range entries {
|
||||
if eh.DecodeErr != nil {
|
||||
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(eh.ID.String()[len(hst.ID{}):], name) {
|
||||
var entry hst.State
|
||||
if _, err := eh.Load(&entry); err != nil {
|
||||
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
|
||||
continue
|
||||
}
|
||||
|
||||
hlog.Verbosef("instance %s skipped", v)
|
||||
return &entry
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return
|
||||
case likely&likeFull != 0:
|
||||
var likelyID hst.ID
|
||||
if likelyID.UnmarshalText([]byte(name)) != nil {
|
||||
return nil
|
||||
}
|
||||
msg.Verbose("argument looks like identifier")
|
||||
for eh := range entries {
|
||||
if eh.DecodeErr != nil {
|
||||
msg.Verbose(getMessage("skipping instance:", eh.DecodeErr))
|
||||
continue
|
||||
}
|
||||
|
||||
if eh.ID == likelyID {
|
||||
var entry hst.State
|
||||
if _, err := eh.Load(&entry); err != nil {
|
||||
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
|
||||
continue
|
||||
}
|
||||
return &entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/store"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestShortIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
id := hst.ID{
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
|
||||
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
|
||||
}
|
||||
|
||||
const want = "fedcba98"
|
||||
if got := shortIdentifier(&id); got != want {
|
||||
t.Errorf("shortIdentifier: %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryIdentifier(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
msg := message.New(nil)
|
||||
id := hst.ID{
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef,
|
||||
0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10,
|
||||
}
|
||||
withBase := func(extra ...hst.State) []hst.State {
|
||||
return append([]hst.State{
|
||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xaa}, len(hst.ID{}))), PID: 0xbeef, ShimPID: 0xcafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef0)},
|
||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xab}, len(hst.ID{}))), PID: 0x1beef, ShimPID: 0x1cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef1)},
|
||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xf0}, len(hst.ID{}))), PID: 0x2beef, ShimPID: 0x2cafe, Config: hst.Template(), Time: time.Unix(0, 0xdeadbeef2)},
|
||||
|
||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xfe}, len(hst.ID{}))), PID: 0xbed, ShimPID: 0xfff, Config: func() *hst.Config {
|
||||
template := hst.Template()
|
||||
template.Identity = hst.IdentityEnd
|
||||
return template
|
||||
}(), Time: time.Unix(0, 0xcafebabe0)},
|
||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xfc}, len(hst.ID{}))), PID: 0x1bed, ShimPID: 0x1fff, Config: func() *hst.Config {
|
||||
template := hst.Template()
|
||||
template.Identity = 0xfc
|
||||
return template
|
||||
}(), Time: time.Unix(0, 0xcafebabe1)},
|
||||
{ID: (hst.ID)(bytes.Repeat([]byte{0xce}, len(hst.ID{}))), PID: 0x2bed, ShimPID: 0x2fff, Config: func() *hst.Config {
|
||||
template := hst.Template()
|
||||
template.Identity = 0xce
|
||||
return template
|
||||
}(), Time: time.Unix(0, 0xcafebabe2)},
|
||||
}, extra...)
|
||||
}
|
||||
sampleEntry := hst.State{
|
||||
ID: id,
|
||||
PID: 0xcafe,
|
||||
ShimPID: 0xdead,
|
||||
Config: hst.Template(),
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
s string
|
||||
data []hst.State
|
||||
want *hst.State
|
||||
}{
|
||||
{"likely entries fault", "ffffffff", nil, nil},
|
||||
|
||||
{"likely short too short", "ff", nil, nil},
|
||||
{"likely short too long", "fffffffffffffffff", nil, nil},
|
||||
{"likely short invalid lower", "fffffff\x00", nil, nil},
|
||||
{"likely short invalid higher", "0000000\xff", nil, nil},
|
||||
{"short no match", "fedcba98", withBase(), nil},
|
||||
{"short match", "fedcba98", withBase(sampleEntry), &sampleEntry},
|
||||
{"short match single", "fedcba98", []hst.State{sampleEntry}, &sampleEntry},
|
||||
{"short match longer", "fedcba98765", withBase(sampleEntry), &sampleEntry},
|
||||
|
||||
{"likely long invalid", "0123456789abcdeffedcba987654321\x00", nil, nil},
|
||||
{"long no match", "0123456789abcdeffedcba9876543210", withBase(), nil},
|
||||
{"long match", "0123456789abcdeffedcba9876543210", withBase(sampleEntry), &sampleEntry},
|
||||
{"long match single", "0123456789abcdeffedcba9876543210", []hst.State{sampleEntry}, &sampleEntry},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
base := check.MustAbs(t.TempDir()).Append("store")
|
||||
s := store.New(base)
|
||||
for i := range tc.data {
|
||||
if h, err := s.Handle(tc.data[i].Identity); err != nil {
|
||||
t.Fatalf("Handle: error = %v", err)
|
||||
} else {
|
||||
var unlock func()
|
||||
if unlock, err = h.Lock(); err != nil {
|
||||
t.Fatalf("Lock: error = %v", err)
|
||||
}
|
||||
_, err = h.Save(&tc.data[i])
|
||||
unlock()
|
||||
if err != nil {
|
||||
t.Fatalf("Save: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// store must not be written to beyond this point
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := tryIdentifier(msg, tc.s, store.New(base))
|
||||
if !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+121
-132
@@ -1,11 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -13,46 +12,43 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/app/state"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/system/dbus"
|
||||
"hakurei.app/internal/outcome"
|
||||
"hakurei.app/internal/store"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// printShowSystem populates and writes a representation of [hst.Info] to output.
|
||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
info := &hst.Info{Paths: std.Paths()}
|
||||
|
||||
// get hid by querying uid of identity 0
|
||||
if uid, err := std.Uid(0); err != nil {
|
||||
hlog.PrintBaseError(err, "cannot obtain uid from setuid wrapper:")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
info.User = (uid / 10000) - 100
|
||||
}
|
||||
hi := outcome.Info()
|
||||
|
||||
if flagJSON {
|
||||
printJSON(output, short, info)
|
||||
encodeJSON(log.Fatal, output, short, hi)
|
||||
return
|
||||
}
|
||||
|
||||
t.Printf("User:\t%d\n", info.User)
|
||||
t.Printf("TempDir:\t%s\n", info.TempDir)
|
||||
t.Printf("SharePath:\t%s\n", info.SharePath)
|
||||
t.Printf("RuntimePath:\t%s\n", info.RuntimePath)
|
||||
t.Printf("RunDirPath:\t%s\n", info.RunDirPath)
|
||||
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
||||
t.Printf("User:\t%d\n", hi.User)
|
||||
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
||||
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
||||
t.Printf("RuntimePath:\t%s\n", hi.RuntimePath)
|
||||
t.Printf("RunDirPath:\t%s\n", hi.RunDirPath)
|
||||
}
|
||||
|
||||
// printShowInstance writes a representation of [hst.State] or [hst.Config] to output.
|
||||
func printShowInstance(
|
||||
output io.Writer, now time.Time,
|
||||
instance *state.State, config *hst.Config,
|
||||
short, flagJSON bool) {
|
||||
instance *hst.State, config *hst.Config,
|
||||
short, flagJSON bool,
|
||||
) (valid bool) {
|
||||
valid = true
|
||||
|
||||
if flagJSON {
|
||||
if instance != nil {
|
||||
printJSON(output, short, instance)
|
||||
encodeJSON(log.Fatal, output, short, instance)
|
||||
} else {
|
||||
printJSON(output, short, config)
|
||||
encodeJSON(log.Fatal, output, short, config)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -60,13 +56,21 @@ func printShowInstance(
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
if config.Container == nil {
|
||||
mustPrint(output, "Warning: this configuration uses permissive defaults!\n\n")
|
||||
if err := config.Validate(hst.VAllowInsecure); err != nil {
|
||||
valid = false
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
mustPrint(output, "Error: "+m+"!\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
// nothing to print
|
||||
return
|
||||
}
|
||||
|
||||
if instance != nil {
|
||||
t.Printf("State\n")
|
||||
t.Printf(" Instance:\t%s (%d)\n", instance.ID.String(), instance.PID)
|
||||
t.Printf(" Instance:\t%s (%d -> %d)\n", instance.ID.String(), instance.PID, instance.ShimPID)
|
||||
t.Printf(" Uptime:\t%s\n", now.Sub(instance.Time).Round(time.Second).String())
|
||||
t.Printf("\n")
|
||||
}
|
||||
@@ -81,39 +85,33 @@ func printShowInstance(
|
||||
if len(config.Groups) > 0 {
|
||||
t.Printf(" Groups:\t%s\n", strings.Join(config.Groups, ", "))
|
||||
}
|
||||
if config.Home != nil {
|
||||
t.Printf(" Home:\t%s\n", config.Home)
|
||||
}
|
||||
if config.Container != nil {
|
||||
params := config.Container
|
||||
if params.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%s\n", params.Hostname)
|
||||
}
|
||||
flags := make([]string, 0, 7)
|
||||
writeFlag := func(name string, value bool) {
|
||||
if value {
|
||||
flags = append(flags, name)
|
||||
flags := config.Container.Flags.String()
|
||||
|
||||
// this is included in the upper hst.Config struct but is relevant here
|
||||
const flagDirectWayland = "directwl"
|
||||
if config.DirectWayland {
|
||||
// hardcoded value when every flag is unset
|
||||
if flags == "none" {
|
||||
flags = flagDirectWayland
|
||||
} else {
|
||||
flags += ", " + flagDirectWayland
|
||||
}
|
||||
}
|
||||
writeFlag("userns", params.Userns)
|
||||
writeFlag("devel", params.Devel)
|
||||
writeFlag("net", params.HostNet)
|
||||
writeFlag("abstract", params.HostAbstract)
|
||||
writeFlag("device", params.Device)
|
||||
writeFlag("tty", params.Tty)
|
||||
writeFlag("mapuid", params.MapRealUID)
|
||||
writeFlag("directwl", config.DirectWayland)
|
||||
if len(flags) == 0 {
|
||||
flags = append(flags, "none")
|
||||
}
|
||||
t.Printf(" Flags:\t%s\n", strings.Join(flags, " "))
|
||||
t.Printf(" Flags:\t%s\n", flags)
|
||||
|
||||
if config.Path != nil {
|
||||
t.Printf(" Path:\t%s\n", config.Path)
|
||||
if config.Container.Home != nil {
|
||||
t.Printf(" Home:\t%s\n", config.Container.Home)
|
||||
}
|
||||
if config.Container.Hostname != "" {
|
||||
t.Printf(" Hostname:\t%s\n", config.Container.Hostname)
|
||||
}
|
||||
if config.Container.Path != nil {
|
||||
t.Printf(" Path:\t%s\n", config.Container.Path)
|
||||
}
|
||||
if len(config.Container.Args) > 0 {
|
||||
t.Printf(" Arguments:\t%s\n", strings.Join(config.Container.Args, " "))
|
||||
}
|
||||
}
|
||||
if len(config.Args) > 0 {
|
||||
t.Printf(" Arguments:\t%s\n", strings.Join(config.Args, " "))
|
||||
}
|
||||
t.Printf("\n")
|
||||
|
||||
@@ -122,6 +120,7 @@ func printShowInstance(
|
||||
t.Printf("Filesystem\n")
|
||||
for _, f := range config.Container.Filesystem {
|
||||
if !f.Valid() {
|
||||
valid = false
|
||||
t.Println(" <invalid>")
|
||||
continue
|
||||
}
|
||||
@@ -131,17 +130,14 @@ func printShowInstance(
|
||||
}
|
||||
if len(config.ExtraPerms) > 0 {
|
||||
t.Printf("Extra ACL\n")
|
||||
for _, p := range config.ExtraPerms {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
t.Printf(" %s\n", p.String())
|
||||
for i := range config.ExtraPerms {
|
||||
t.Printf(" %s\n", config.ExtraPerms[i].String())
|
||||
}
|
||||
t.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
printDBus := func(c *dbus.Config) {
|
||||
printDBus := func(c *hst.BusConfig) {
|
||||
t.Printf(" Filter:\t%v\n", c.Filter)
|
||||
if len(c.See) > 0 {
|
||||
t.Printf(" See:\t%q\n", c.See)
|
||||
@@ -169,59 +165,57 @@ func printShowInstance(
|
||||
printDBus(config.SystemBus)
|
||||
t.Printf("\n")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
|
||||
var entries state.Entries
|
||||
if e, err := state.Join(s); err != nil {
|
||||
log.Fatalf("cannot join store: %v", err)
|
||||
} else {
|
||||
entries = e
|
||||
}
|
||||
if err := s.Close(); err != nil {
|
||||
log.Printf("cannot close store: %v", err)
|
||||
// printPs writes a representation of active instances to output.
|
||||
func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) {
|
||||
f := func(a func(eh *store.EntryHandle)) {
|
||||
entries, copyError := s.All()
|
||||
for eh := range entries {
|
||||
a(eh)
|
||||
}
|
||||
if err := copyError(); err != nil {
|
||||
msg.GetLogger().Println(getMessage("cannot iterate over store:", err))
|
||||
}
|
||||
}
|
||||
|
||||
if !short && flagJSON {
|
||||
es := make(map[string]*state.State, len(entries))
|
||||
for id, instance := range entries {
|
||||
es[id.String()] = instance
|
||||
if short { // short output requires identifier only
|
||||
var identifiers []*hst.ID
|
||||
f(func(eh *store.EntryHandle) {
|
||||
if _, err := eh.Load(nil); err != nil { // passes through decode error
|
||||
msg.GetLogger().Println(getMessage("cannot validate state entry header:", err))
|
||||
return
|
||||
}
|
||||
identifiers = append(identifiers, &eh.ID)
|
||||
})
|
||||
slices.SortFunc(identifiers, func(a, b *hst.ID) int { return bytes.Compare(a[:], b[:]) })
|
||||
|
||||
if flagJSON {
|
||||
encodeJSON(log.Fatal, output, short, identifiers)
|
||||
} else {
|
||||
for _, id := range identifiers {
|
||||
mustPrintln(output, shortIdentifier(id))
|
||||
}
|
||||
}
|
||||
printJSON(output, short, es)
|
||||
return
|
||||
}
|
||||
|
||||
// sort state entries by id string to ensure consistency between runs
|
||||
exp := make([]*expandedStateEntry, 0, len(entries))
|
||||
for id, instance := range entries {
|
||||
// gracefully skip nil states
|
||||
if instance == nil {
|
||||
log.Printf("got invalid state entry %s", id.String())
|
||||
continue
|
||||
// long output requires full instance state
|
||||
var instances []*hst.State
|
||||
f(func(eh *store.EntryHandle) {
|
||||
var state hst.State
|
||||
if _, err := eh.Load(&state); err != nil { // passes through decode error
|
||||
msg.GetLogger().Println(getMessage("cannot load state entry:", err))
|
||||
return
|
||||
}
|
||||
instances = append(instances, &state)
|
||||
})
|
||||
slices.SortFunc(instances, func(a, b *hst.State) int { return bytes.Compare(a.ID[:], b.ID[:]) })
|
||||
|
||||
// gracefully skip inconsistent states
|
||||
if id != instance.ID {
|
||||
log.Printf("possible store corruption: entry %s has id %s",
|
||||
id.String(), instance.ID.String())
|
||||
continue
|
||||
}
|
||||
exp = append(exp, &expandedStateEntry{s: id.String(), State: instance})
|
||||
}
|
||||
slices.SortFunc(exp, func(a, b *expandedStateEntry) int { return a.Time.Compare(b.Time) })
|
||||
|
||||
if short {
|
||||
if flagJSON {
|
||||
v := make([]string, len(exp))
|
||||
for i, e := range exp {
|
||||
v[i] = e.s
|
||||
}
|
||||
printJSON(output, short, v)
|
||||
} else {
|
||||
for _, e := range exp {
|
||||
mustPrintln(output, e.s[:8])
|
||||
}
|
||||
}
|
||||
if flagJSON {
|
||||
encodeJSON(log.Fatal, output, short, instances)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -229,61 +223,48 @@ func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON boo
|
||||
defer t.MustFlush()
|
||||
|
||||
t.Println("\tInstance\tPID\tApplication\tUptime")
|
||||
for _, e := range exp {
|
||||
if len(e.s) != 1<<5 {
|
||||
// unreachable
|
||||
log.Printf("possible store corruption: invalid instance string %s", e.s)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, instance := range instances {
|
||||
as := "(No configuration information)"
|
||||
if e.Config != nil {
|
||||
as = strconv.Itoa(e.Config.Identity)
|
||||
id := e.Config.ID
|
||||
if instance.Config != nil {
|
||||
as = strconv.Itoa(instance.Config.Identity)
|
||||
id := instance.Config.ID
|
||||
if id == "" {
|
||||
id = "app.hakurei." + e.s[:8]
|
||||
id = "app.hakurei." + shortIdentifier(&instance.ID)
|
||||
}
|
||||
as += " (" + id + ")"
|
||||
}
|
||||
t.Printf("\t%s\t%d\t%s\t%s\n",
|
||||
e.s[:8], e.PID, as, now.Sub(e.Time).Round(time.Second).String())
|
||||
}
|
||||
}
|
||||
|
||||
type expandedStateEntry struct {
|
||||
s string
|
||||
*state.State
|
||||
}
|
||||
|
||||
func printJSON(output io.Writer, short bool, v any) {
|
||||
encoder := json.NewEncoder(output)
|
||||
if !short {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
if err := encoder.Encode(v); err != nil {
|
||||
log.Fatalf("cannot serialise: %v", err)
|
||||
shortIdentifier(&instance.ID), instance.PID, as, now.Sub(instance.Time).Round(time.Second).String())
|
||||
}
|
||||
}
|
||||
|
||||
// newPrinter returns a configured, wrapped [tabwriter.Writer].
|
||||
func newPrinter(output io.Writer) *tp { return &tp{tabwriter.NewWriter(output, 0, 1, 4, ' ', 0)} }
|
||||
|
||||
// tp wraps [tabwriter.Writer] to provide additional formatting methods.
|
||||
type tp struct{ *tabwriter.Writer }
|
||||
|
||||
// Printf calls [fmt.Fprintf] on the underlying [tabwriter.Writer].
|
||||
func (p *tp) Printf(format string, a ...any) {
|
||||
if _, err := fmt.Fprintf(p, format, a...); err != nil {
|
||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Println calls [fmt.Fprintln] on the underlying [tabwriter.Writer].
|
||||
func (p *tp) Println(a ...any) {
|
||||
if _, err := fmt.Fprintln(p, a...); err != nil {
|
||||
log.Fatalf("cannot write to tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MustFlush calls the Flush method of [tabwriter.Writer] and calls [log.Fatalf] on a non-nil error.
|
||||
func (p *tp) MustFlush() {
|
||||
if err := p.Writer.Flush(); err != nil {
|
||||
log.Fatalf("cannot flush tabwriter: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustPrint(output io.Writer, a ...any) {
|
||||
if _, err := fmt.Fprint(output, a...); err != nil {
|
||||
log.Fatalf("cannot print: %v", err)
|
||||
@@ -294,3 +275,11 @@ func mustPrintln(output io.Writer, a ...any) {
|
||||
log.Fatalf("cannot print: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// getMessage returns a [message.Error] message if available, or err prefixed with fallback otherwise.
|
||||
func getMessage(fallback string, err error) string {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
return m
|
||||
}
|
||||
return fmt.Sprintln(fallback, err)
|
||||
}
|
||||
|
||||
+489
-441
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
This program is a proof of concept and is now deprecated. It is only kept
|
||||
around for API demonstration purposes and to make the most out of the test
|
||||
suite.
|
||||
|
||||
This program is replaced by planterette, which can be found at
|
||||
https://git.gensokyo.uk/security/planterette. Development effort should be
|
||||
focused there instead.
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/system/dbus"
|
||||
)
|
||||
|
||||
type appInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
|
||||
// passed through to [hst.Config]
|
||||
ID string `json:"id"`
|
||||
// passed through to [hst.Config]
|
||||
Identity int `json:"identity"`
|
||||
// passed through to [hst.Config]
|
||||
Groups []string `json:"groups,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Devel bool `json:"devel,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Userns bool `json:"userns,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
HostNet bool `json:"net,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
HostAbstract bool `json:"abstract,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Device bool `json:"dev,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Tty bool `json:"tty,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
MapRealUID bool `json:"map_real_uid,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
DirectWayland bool `json:"direct_wayland,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
SystemBus *dbus.Config `json:"system_bus,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
SessionBus *dbus.Config `json:"session_bus,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Enablements *hst.Enablements `json:"enablements,omitempty"`
|
||||
|
||||
// passed through to [hst.Config]
|
||||
Multiarch bool `json:"multiarch,omitempty"`
|
||||
// passed through to [hst.Config]
|
||||
Bluetooth bool `json:"bluetooth,omitempty"`
|
||||
|
||||
// allow gpu access within sandbox
|
||||
GPU bool `json:"gpu"`
|
||||
// store path to nixGL mesa wrappers
|
||||
Mesa string `json:"mesa,omitempty"`
|
||||
// store path to nixGL source
|
||||
NixGL string `json:"nix_gl,omitempty"`
|
||||
// store path to activate-and-exec script
|
||||
Launcher *container.Absolute `json:"launcher"`
|
||||
// store path to /run/current-system
|
||||
CurrentSystem *container.Absolute `json:"current_system"`
|
||||
// store path to home-manager activation package
|
||||
ActivationPackage string `json:"activation_package"`
|
||||
}
|
||||
|
||||
func (app *appInfo) toHst(pathSet *appPathSet, pathname *container.Absolute, argv []string, flagDropShell bool) *hst.Config {
|
||||
config := &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathname,
|
||||
Args: argv,
|
||||
|
||||
Enablements: app.Enablements,
|
||||
|
||||
SystemBus: app.SystemBus,
|
||||
SessionBus: app.SessionBus,
|
||||
DirectWayland: app.DirectWayland,
|
||||
|
||||
Username: "hakurei",
|
||||
Shell: pathShell,
|
||||
Home: pathDataData.Append(app.ID),
|
||||
|
||||
Identity: app.Identity,
|
||||
Groups: app.Groups,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name),
|
||||
Devel: app.Devel,
|
||||
Userns: app.Userns,
|
||||
HostNet: app.HostNet,
|
||||
HostAbstract: app.HostAbstract,
|
||||
Device: app.Device,
|
||||
Tty: app.Tty || flagDropShell,
|
||||
MapRealUID: app.MapRealUID,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append("store"), Target: pathNixStore}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: pathSet.metaPath, Target: hst.AbsTmp.Append("app")}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
|
||||
},
|
||||
},
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
}
|
||||
if app.Multiarch {
|
||||
config.Container.SeccompFlags |= seccomp.AllowMultiarch
|
||||
}
|
||||
if app.Bluetooth {
|
||||
config.Container.SeccompFlags |= seccomp.AllowBluetooth
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func loadAppInfo(name string, beforeFail func()) *appInfo {
|
||||
bundle := new(appInfo)
|
||||
if f, err := os.Open(name); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot open bundle: %v", err)
|
||||
} else if err = json.NewDecoder(f).Decode(&bundle); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot parse bundle metadata: %v", err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close bundle metadata: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if bundle.ID == "" {
|
||||
beforeFail()
|
||||
log.Fatal("application identifier must not be empty")
|
||||
}
|
||||
if bundle.Launcher == nil {
|
||||
beforeFail()
|
||||
log.Fatal("launcher must not be empty")
|
||||
}
|
||||
if bundle.CurrentSystem == nil {
|
||||
beforeFail()
|
||||
log.Fatal("current-system must not be empty")
|
||||
}
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
func formatHostname(name string) string {
|
||||
if h, err := os.Hostname(); err != nil {
|
||||
log.Printf("cannot get hostname: %v", err)
|
||||
return "hakurei-" + name
|
||||
} else {
|
||||
return h + "-" + name
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
{
|
||||
nixpkgsFor,
|
||||
system,
|
||||
nixpkgs,
|
||||
home-manager,
|
||||
}:
|
||||
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
closureInfo,
|
||||
writeScript,
|
||||
runtimeShell,
|
||||
writeText,
|
||||
symlinkJoin,
|
||||
vmTools,
|
||||
runCommand,
|
||||
fetchFromGitHub,
|
||||
|
||||
zstd,
|
||||
nix,
|
||||
sqlite,
|
||||
|
||||
name ? throw "name is required",
|
||||
version ? throw "version is required",
|
||||
pname ? "${name}-${version}",
|
||||
modules ? [ ],
|
||||
nixosModules ? [ ],
|
||||
script ? ''
|
||||
exec "$SHELL" "$@"
|
||||
'',
|
||||
|
||||
id ? name,
|
||||
identity ? throw "identity is required",
|
||||
groups ? [ ],
|
||||
userns ? false,
|
||||
net ? true,
|
||||
dev ? false,
|
||||
no_new_session ? false,
|
||||
map_real_uid ? false,
|
||||
direct_wayland ? false,
|
||||
system_bus ? null,
|
||||
session_bus ? null,
|
||||
|
||||
allow_wayland ? true,
|
||||
allow_x11 ? false,
|
||||
allow_dbus ? true,
|
||||
allow_pulse ? true,
|
||||
gpu ? allow_wayland || allow_x11,
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib) optionals;
|
||||
|
||||
homeManagerConfiguration = home-manager.lib.homeManagerConfiguration {
|
||||
pkgs = nixpkgsFor.${system};
|
||||
modules = modules ++ [
|
||||
{
|
||||
home = {
|
||||
username = "hakurei";
|
||||
homeDirectory = "/data/data/${id}";
|
||||
stateVersion = "22.11";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
launcher = writeScript "hakurei-${pname}" ''
|
||||
#!${runtimeShell} -el
|
||||
${script}
|
||||
'';
|
||||
|
||||
extraNixOSConfig =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment = {
|
||||
etc.nixpkgs.source = nixpkgs.outPath;
|
||||
systemPackages = [ pkgs.nix ];
|
||||
};
|
||||
|
||||
imports = nixosModules;
|
||||
};
|
||||
nixos = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [
|
||||
extraNixOSConfig
|
||||
{ nix.settings.experimental-features = [ "flakes" ]; }
|
||||
{ nix.settings.experimental-features = [ "nix-command" ]; }
|
||||
{ boot.isContainer = true; }
|
||||
{ system.stateVersion = "22.11"; }
|
||||
];
|
||||
};
|
||||
|
||||
etc = vmTools.runInLinuxVM (
|
||||
runCommand "etc" { } ''
|
||||
mkdir -p /etc
|
||||
${nixos.config.system.build.etcActivationCommands}
|
||||
|
||||
# remove unused files
|
||||
rm -rf /etc/sudoers
|
||||
|
||||
mkdir -p $out
|
||||
tar -C /etc -cf "$out/etc.tar" .
|
||||
''
|
||||
);
|
||||
|
||||
extendSessionDefault = id: ext: {
|
||||
filter = true;
|
||||
|
||||
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
|
||||
own =
|
||||
(optionals (id != null) [
|
||||
"${id}.*"
|
||||
"org.mpris.MediaPlayer2.${id}.*"
|
||||
])
|
||||
++ ext.own;
|
||||
|
||||
inherit (ext) call broadcast;
|
||||
};
|
||||
|
||||
nixGL = fetchFromGitHub {
|
||||
owner = "nix-community";
|
||||
repo = "nixGL";
|
||||
rev = "310f8e49a149e4c9ea52f1adf70cdc768ec53f8a";
|
||||
hash = "sha256-lnzZQYG0+EXl/6NkGpyIz+FEOc/DSEG57AP1VsdeNrM=";
|
||||
};
|
||||
|
||||
mesaWrappers =
|
||||
let
|
||||
isIntelX86Platform = system == "x86_64-linux";
|
||||
nixGLPackages = import (nixGL + "/default.nix") {
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
enable32bits = isIntelX86Platform;
|
||||
enableIntelX86Extensions = isIntelX86Platform;
|
||||
};
|
||||
in
|
||||
symlinkJoin {
|
||||
name = "nixGL-mesa";
|
||||
paths = with nixGLPackages; [
|
||||
nixGLIntel
|
||||
nixVulkanIntel
|
||||
];
|
||||
};
|
||||
|
||||
info = builtins.toJSON {
|
||||
inherit
|
||||
name
|
||||
version
|
||||
id
|
||||
identity
|
||||
launcher
|
||||
groups
|
||||
userns
|
||||
net
|
||||
dev
|
||||
no_new_session
|
||||
map_real_uid
|
||||
direct_wayland
|
||||
system_bus
|
||||
gpu
|
||||
;
|
||||
|
||||
session_bus =
|
||||
if session_bus != null then
|
||||
(session_bus (extendSessionDefault id))
|
||||
else
|
||||
(extendSessionDefault id {
|
||||
talk = [ ];
|
||||
own = [ ];
|
||||
call = { };
|
||||
broadcast = { };
|
||||
});
|
||||
|
||||
enablements = {
|
||||
wayland = allow_wayland;
|
||||
x11 = allow_x11;
|
||||
dbus = allow_dbus;
|
||||
pulse = allow_pulse;
|
||||
};
|
||||
|
||||
mesa = if gpu then mesaWrappers else null;
|
||||
nix_gl = if gpu then nixGL else null;
|
||||
current_system = nixos.config.system.build.toplevel;
|
||||
activation_package = homeManagerConfiguration.activationPackage;
|
||||
};
|
||||
in
|
||||
|
||||
stdenv.mkDerivation {
|
||||
name = "${pname}.pkg";
|
||||
inherit version;
|
||||
__structuredAttrs = true;
|
||||
|
||||
nativeBuildInputs = [
|
||||
zstd
|
||||
nix
|
||||
sqlite
|
||||
];
|
||||
|
||||
buildCommand = ''
|
||||
NIX_ROOT="$(mktemp -d)"
|
||||
export USER="nobody"
|
||||
|
||||
# create bootstrap store
|
||||
bootstrapClosureInfo="${
|
||||
closureInfo {
|
||||
rootPaths = [
|
||||
nix
|
||||
nixos.config.system.build.toplevel
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying bootstrap store paths..."
|
||||
mkdir -p "$NIX_ROOT/nix/store"
|
||||
xargs -n 1 -a "$bootstrapClosureInfo/store-paths" cp -at "$NIX_ROOT/nix/store/"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --load-db < "$bootstrapClosureInfo/registration"
|
||||
NIX_REMOTE="local?root=$NIX_ROOT" nix-store --optimise
|
||||
sqlite3 "$NIX_ROOT/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
chmod -R +r "$NIX_ROOT/nix/var"
|
||||
|
||||
# create binary cache
|
||||
closureInfo="${
|
||||
closureInfo {
|
||||
rootPaths = [
|
||||
homeManagerConfiguration.activationPackage
|
||||
launcher
|
||||
]
|
||||
++ optionals gpu [
|
||||
mesaWrappers
|
||||
nixGL
|
||||
];
|
||||
}
|
||||
}"
|
||||
echo "copying application paths..."
|
||||
TMP_STORE="$(mktemp -d)"
|
||||
mkdir -p "$TMP_STORE/nix/store"
|
||||
xargs -n 1 -a "$closureInfo/store-paths" cp -at "$TMP_STORE/nix/store/"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix-store --load-db < "$closureInfo/registration"
|
||||
sqlite3 "$TMP_STORE/nix/var/nix/db/db.sqlite" "UPDATE ValidPaths SET registrationTime = ''${SOURCE_DATE_EPOCH}"
|
||||
NIX_REMOTE="local?root=$TMP_STORE" nix --offline --extra-experimental-features nix-command \
|
||||
--verbose --log-format raw-with-logs \
|
||||
copy --all --no-check-sigs --to \
|
||||
"file://$NIX_ROOT/res?compression=zstd&compression-level=19¶llel-compression=true"
|
||||
|
||||
# package /etc
|
||||
mkdir -p "$NIX_ROOT/etc"
|
||||
tar -C "$NIX_ROOT/etc" -xf "${etc}/etc.tar"
|
||||
|
||||
# write metadata
|
||||
cp "${writeText "bundle.json" info}" "$NIX_ROOT/bundle.json"
|
||||
|
||||
# create an intermediate file to improve zstd performance
|
||||
INTER="$(mktemp)"
|
||||
tar -C "$NIX_ROOT" -cf "$INTER" .
|
||||
zstd -T0 -19 -fo "$out" "$INTER"
|
||||
'';
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var (
|
||||
errSuccess = errors.New("success")
|
||||
)
|
||||
|
||||
func init() {
|
||||
hlog.Prepare("hpkg")
|
||||
if err := os.Setenv("SHELL", pathShell.String()); err != nil {
|
||||
log.Fatalf("cannot set $SHELL: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop() // unreachable
|
||||
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagDropShell bool
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "hpkg", func([]string) error { internal.InstallOutput(flagVerbose); return nil }).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Print debug messages to the console").
|
||||
Flag(&flagDropShell, "s", command.BoolFlag(false), "Drop to a shell in place of next hakurei action")
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellActivate bool
|
||||
)
|
||||
c.NewCommand("install", "Install an application from its package", func(args []string) error {
|
||||
if len(args) != 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
pkgPath := args[0]
|
||||
if !path.IsAbs(pkgPath) {
|
||||
if dir, err := os.Getwd(); err != nil {
|
||||
log.Printf("cannot get current directory: %v", err)
|
||||
return err
|
||||
} else {
|
||||
pkgPath = path.Join(dir, pkgPath)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Look up paths to programs started by hpkg.
|
||||
This is done here to ease error handling as cleanup is not yet required.
|
||||
*/
|
||||
|
||||
var (
|
||||
_ = lookPath("zstd")
|
||||
tar = lookPath("tar")
|
||||
chmod = lookPath("chmod")
|
||||
rm = lookPath("rm")
|
||||
)
|
||||
|
||||
/*
|
||||
Extract package and set up for cleanup.
|
||||
*/
|
||||
|
||||
var workDir *container.Absolute
|
||||
if p, err := os.MkdirTemp("", "hpkg.*"); err != nil {
|
||||
log.Printf("cannot create temporary directory: %v", err)
|
||||
return err
|
||||
} else if workDir, err = container.NewAbs(p); err != nil {
|
||||
log.Printf("invalid temporary directory: %v", err)
|
||||
return err
|
||||
}
|
||||
cleanup := func() {
|
||||
// should be faster than a native implementation
|
||||
mustRun(chmod, "-R", "+w", workDir.String())
|
||||
mustRun(rm, "-rf", workDir.String())
|
||||
}
|
||||
beforeRunFail.Store(&cleanup)
|
||||
|
||||
mustRun(tar, "-C", workDir.String(), "-xf", pkgPath)
|
||||
|
||||
/*
|
||||
Parse bundle and app metadata, do pre-install checks.
|
||||
*/
|
||||
|
||||
bundle := loadAppInfo(path.Join(workDir.String(), "bundle.json"), cleanup)
|
||||
pathSet := pathSetByApp(bundle.ID)
|
||||
|
||||
a := bundle
|
||||
if s, err := os.Stat(pathSet.metaPath.String()); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
cleanup()
|
||||
log.Printf("cannot access %q: %v", pathSet.metaPath, err)
|
||||
return err
|
||||
}
|
||||
// did not modify app, clean installation condition met later
|
||||
} else if s.IsDir() {
|
||||
cleanup()
|
||||
log.Printf("metadata path %q is not a file", pathSet.metaPath)
|
||||
return syscall.EBADMSG
|
||||
} else {
|
||||
a = loadAppInfo(pathSet.metaPath.String(), cleanup)
|
||||
if a.ID != bundle.ID {
|
||||
cleanup()
|
||||
log.Printf("app %q claims to have identifier %q",
|
||||
bundle.ID, a.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
// sec: should verify credentials
|
||||
}
|
||||
|
||||
if a != bundle {
|
||||
// do not try to re-install
|
||||
if a.NixGL == bundle.NixGL &&
|
||||
a.CurrentSystem == bundle.CurrentSystem &&
|
||||
a.Launcher == bundle.Launcher &&
|
||||
a.ActivationPackage == bundle.ActivationPackage {
|
||||
cleanup()
|
||||
log.Printf("package %q is identical to local application %q",
|
||||
pkgPath, a.ID)
|
||||
return errSuccess
|
||||
}
|
||||
|
||||
// identity determines uid
|
||||
if a.Identity != bundle.Identity {
|
||||
cleanup()
|
||||
log.Printf("package %q identity %d differs from installed %d",
|
||||
pkgPath, bundle.Identity, a.Identity)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
// sec: should compare version string
|
||||
hlog.Verbosef("installing application %q version %q over local %q",
|
||||
bundle.ID, bundle.Version, a.Version)
|
||||
} else {
|
||||
hlog.Verbosef("application %q clean installation", bundle.ID)
|
||||
// sec: should install credentials
|
||||
}
|
||||
|
||||
/*
|
||||
Setup steps for files owned by the target user.
|
||||
*/
|
||||
|
||||
withCacheDir(ctx, "install", []string{
|
||||
// export inner bundle path in the environment
|
||||
"export BUNDLE=" + hst.Tmp + "/bundle",
|
||||
// replace inner /etc
|
||||
"mkdir -p etc",
|
||||
"chmod -R +w etc",
|
||||
"rm -rf etc",
|
||||
"cp -dRf $BUNDLE/etc etc",
|
||||
// replace inner /nix
|
||||
"mkdir -p nix",
|
||||
"chmod -R +w nix",
|
||||
"rm -rf nix",
|
||||
"cp -dRf /nix nix",
|
||||
// copy from binary cache
|
||||
"nix copy --offline --no-check-sigs --all --from file://$BUNDLE/res --to $PWD",
|
||||
// deduplicate nix store
|
||||
"nix store --offline --store $PWD optimise",
|
||||
// make cache directory world-readable for autoetc
|
||||
"chmod 0755 .",
|
||||
}, workDir, bundle, pathSet, flagDropShell, cleanup)
|
||||
|
||||
if bundle.GPU {
|
||||
withCacheDir(ctx, "mesa-wrappers", []string{
|
||||
// link nixGL mesa wrappers
|
||||
"mkdir -p nix/.nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixGLIntel nix/.nixGL/nixGL",
|
||||
"ln -s " + bundle.Mesa + "/bin/nixVulkanIntel nix/.nixGL/nixVulkan",
|
||||
}, workDir, bundle, pathSet, false, cleanup)
|
||||
}
|
||||
|
||||
/*
|
||||
Activate home-manager generation.
|
||||
*/
|
||||
|
||||
withNixDaemon(ctx, "activate", []string{
|
||||
// clean up broken links
|
||||
"mkdir -p .local/state/{nix,home-manager}",
|
||||
"chmod -R +w .local/state/{nix,home-manager}",
|
||||
"rm -rf .local/state/{nix,home-manager}",
|
||||
// run activation script
|
||||
bundle.ActivationPackage + "/activate",
|
||||
}, false, func(config *hst.Config) *hst.Config { return config },
|
||||
bundle, pathSet, flagDropShellActivate, cleanup)
|
||||
|
||||
/*
|
||||
Installation complete. Write metadata to block re-installs or downgrades.
|
||||
*/
|
||||
|
||||
// serialise metadata to ensure consistency
|
||||
if f, err := os.OpenFile(pathSet.metaPath.String()+"~", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot create metadata file: %v", err)
|
||||
return err
|
||||
} else if err = json.NewEncoder(f).Encode(bundle); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot write metadata: %v", err)
|
||||
return err
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Printf("cannot close metadata file: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
|
||||
if err := os.Rename(pathSet.metaPath.String()+"~", pathSet.metaPath.String()); err != nil {
|
||||
cleanup()
|
||||
log.Printf("cannot rename metadata file: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cleanup()
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellActivate, "s", command.BoolFlag(false), "Drop to a shell on activation")
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDropShellNixGL bool
|
||||
flagAutoDrivers bool
|
||||
)
|
||||
c.NewCommand("start", "Start an application", func(args []string) error {
|
||||
if len(args) < 1 {
|
||||
log.Println("invalid argument")
|
||||
return syscall.EINVAL
|
||||
}
|
||||
|
||||
/*
|
||||
Parse app metadata.
|
||||
*/
|
||||
|
||||
id := args[0]
|
||||
pathSet := pathSetByApp(id)
|
||||
a := loadAppInfo(pathSet.metaPath.String(), func() {})
|
||||
if a.ID != id {
|
||||
log.Printf("app %q claims to have identifier %q", id, a.ID)
|
||||
return syscall.EBADE
|
||||
}
|
||||
|
||||
/*
|
||||
Prepare nixGL.
|
||||
*/
|
||||
|
||||
if a.GPU && flagAutoDrivers {
|
||||
withNixDaemon(ctx, "nix-gl", []string{
|
||||
"mkdir -p /nix/.nixGL/auto",
|
||||
"rm -rf /nix/.nixGL/auto",
|
||||
"export NIXPKGS_ALLOW_UNFREE=1",
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/opengl " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL,
|
||||
"nix build --impure " +
|
||||
"--out-link /nix/.nixGL/auto/vulkan " +
|
||||
"--override-input nixpkgs path:/etc/nixpkgs " +
|
||||
"path:" + a.NixGL + "#nixVulkanNvidia",
|
||||
}, true, func(config *hst.Config) *hst.Config {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSEtc.Append("resolv.conf"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("block"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("bus"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("class"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("dev"), Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSSys.Append("devices"), Optional: true}},
|
||||
}...)
|
||||
appendGPUFilesystem(config)
|
||||
return config
|
||||
}, a, pathSet, flagDropShellNixGL, func() {})
|
||||
}
|
||||
|
||||
/*
|
||||
Create app configuration.
|
||||
*/
|
||||
|
||||
pathname := a.Launcher
|
||||
argv := make([]string, 1, len(args))
|
||||
if flagDropShell {
|
||||
pathname = pathShell
|
||||
argv[0] = bash
|
||||
} else {
|
||||
argv[0] = a.Launcher.String()
|
||||
}
|
||||
argv = append(argv, args[1:]...)
|
||||
config := a.toHst(pathSet, pathname, argv, flagDropShell)
|
||||
|
||||
/*
|
||||
Expose GPU devices.
|
||||
*/
|
||||
|
||||
if a.GPU {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem,
|
||||
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath.Append(".nixGL"), Target: hst.AbsTmp.Append("nixGL")}})
|
||||
appendGPUFilesystem(config)
|
||||
}
|
||||
|
||||
/*
|
||||
Spawn app.
|
||||
*/
|
||||
|
||||
mustRunApp(ctx, config, func() {})
|
||||
return errSuccess
|
||||
}).
|
||||
Flag(&flagDropShellNixGL, "s", command.BoolFlag(false), "Drop to a shell on nixGL build").
|
||||
Flag(&flagAutoDrivers, "auto-drivers", command.BoolFlag(false), "Attempt automatic opengl driver detection")
|
||||
}
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
hlog.Verbosef("command returned %v", err)
|
||||
if errors.Is(err, errSuccess) {
|
||||
hlog.BeforeExit()
|
||||
os.Exit(0)
|
||||
}
|
||||
})
|
||||
log.Fatal("unreachable")
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
const bash = "bash"
|
||||
|
||||
var (
|
||||
dataHome *container.Absolute
|
||||
)
|
||||
|
||||
func init() {
|
||||
// dataHome
|
||||
if a, err := container.NewAbs(os.Getenv("HAKUREI_DATA_HOME")); err == nil {
|
||||
dataHome = a
|
||||
} else {
|
||||
dataHome = container.AbsFHSVarLib.Append("hakurei/" + strconv.Itoa(os.Getuid()))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
pathBin = container.AbsFHSRoot.Append("bin")
|
||||
|
||||
pathNix = container.MustAbs("/nix/")
|
||||
pathNixStore = pathNix.Append("store/")
|
||||
pathCurrentSystem = container.AbsFHSRun.Append("current-system")
|
||||
pathSwBin = pathCurrentSystem.Append("sw/bin/")
|
||||
pathShell = pathSwBin.Append(bash)
|
||||
|
||||
pathData = container.MustAbs("/data")
|
||||
pathDataData = pathData.Append("data")
|
||||
)
|
||||
|
||||
func lookPath(file string) string {
|
||||
if p, err := exec.LookPath(file); err != nil {
|
||||
log.Fatalf("%s: command not found", file)
|
||||
return ""
|
||||
} else {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
var beforeRunFail = new(atomic.Pointer[func()])
|
||||
|
||||
func mustRun(name string, arg ...string) {
|
||||
hlog.Verbosef("spawning process: %q %q", name, arg)
|
||||
cmd := exec.Command(name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
if f := beforeRunFail.Swap(nil); f != nil {
|
||||
(*f)()
|
||||
}
|
||||
log.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
type appPathSet struct {
|
||||
// ${dataHome}/${id}
|
||||
baseDir *container.Absolute
|
||||
// ${baseDir}/app
|
||||
metaPath *container.Absolute
|
||||
// ${baseDir}/files
|
||||
homeDir *container.Absolute
|
||||
// ${baseDir}/cache
|
||||
cacheDir *container.Absolute
|
||||
// ${baseDir}/cache/nix
|
||||
nixPath *container.Absolute
|
||||
}
|
||||
|
||||
func pathSetByApp(id string) *appPathSet {
|
||||
pathSet := new(appPathSet)
|
||||
pathSet.baseDir = dataHome.Append(id)
|
||||
pathSet.metaPath = pathSet.baseDir.Append("app")
|
||||
pathSet.homeDir = pathSet.baseDir.Append("files")
|
||||
pathSet.cacheDir = pathSet.baseDir.Append("cache")
|
||||
pathSet.nixPath = pathSet.cacheDir.Append("nix")
|
||||
return pathSet
|
||||
}
|
||||
|
||||
func appendGPUFilesystem(config *hst.Config) {
|
||||
config.Container.Filesystem = append(config.Container.Filesystem, []hst.FilesystemConfigJSON{
|
||||
// flatpak commit 763a686d874dd668f0236f911de00b80766ffe79
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("dri"), Device: true, Optional: true}},
|
||||
// mali
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("mali0"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("umplock"), Device: true, Optional: true}},
|
||||
// nvidia
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidiactl"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-modeset"), Device: true, Optional: true}},
|
||||
// nvidia OpenCL/CUDA
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia-uvm-tools"), Device: true, Optional: true}},
|
||||
|
||||
// flatpak commit d2dff2875bb3b7e2cd92d8204088d743fd07f3ff
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia0"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia1"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia2"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia3"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia4"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia5"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia6"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia7"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia8"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia9"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia10"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia11"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia12"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia13"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia14"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia15"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia16"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia17"), Device: true, Optional: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia18"), Device: true, Optional: true}}, {FilesystemConfig: &hst.FSBind{Source: container.AbsFHSDev.Append("nvidia19"), Device: true, Optional: true}},
|
||||
}...)
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
)
|
||||
|
||||
var hakureiPath = internal.MustHakureiPath()
|
||||
|
||||
func mustRunApp(ctx context.Context, config *hst.Config, beforeFail func()) {
|
||||
var (
|
||||
cmd *exec.Cmd
|
||||
st io.WriteCloser
|
||||
)
|
||||
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot pipe: %v", err)
|
||||
} else {
|
||||
if hlog.Load() {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "-v", "app", "3")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, hakureiPath, "app", "3")
|
||||
}
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.ExtraFiles = []*os.File{r}
|
||||
st = w
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := json.NewEncoder(st).Encode(config); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot send configuration: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot start hakurei: %v", err)
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) {
|
||||
beforeFail()
|
||||
internal.Exit(exitError.ExitCode())
|
||||
} else {
|
||||
beforeFail()
|
||||
log.Fatalf("cannot wait: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
|
||||
# Automatically login on tty1 as a normal user:
|
||||
services.getty.autologinUser = "alice";
|
||||
|
||||
environment = {
|
||||
variables = {
|
||||
SWAYSOCK = "/tmp/sway-ipc.sock";
|
||||
WLR_RENDERER = "pixman";
|
||||
};
|
||||
};
|
||||
|
||||
# Automatically configure and start Sway when logging in on tty1:
|
||||
programs.bash.loginShellInit = ''
|
||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
||||
set -e
|
||||
|
||||
mkdir -p ~/.config/sway
|
||||
(sed s/Mod4/Mod1/ /etc/sway/config &&
|
||||
echo 'output * bg ${pkgs.nixos-artwork.wallpapers.simple-light-gray.gnomeFilePath} fill' &&
|
||||
echo 'output Virtual-1 res 1680x1050') > ~/.config/sway/config
|
||||
|
||||
sway --validate
|
||||
systemd-cat --identifier=session sway && touch /tmp/sway-exit-ok
|
||||
fi
|
||||
'';
|
||||
|
||||
programs.sway.enable = true;
|
||||
|
||||
virtualisation = {
|
||||
diskSize = 6 * 1024;
|
||||
|
||||
qemu.options = [
|
||||
# Need to switch to a different GPU driver than the default one (-vga std) so that Sway can launch:
|
||||
"-vga none -device virtio-gpu-pci"
|
||||
|
||||
# Increase zstd performance:
|
||||
"-smp 8"
|
||||
];
|
||||
};
|
||||
|
||||
environment.hakurei = {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/hakurei";
|
||||
users.alice = 0;
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
nixosTest,
|
||||
callPackage,
|
||||
|
||||
system,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
buildPackage = self.buildPackage.${system};
|
||||
in
|
||||
nixosTest {
|
||||
name = "hpkg";
|
||||
nodes.machine = {
|
||||
environment.etc = {
|
||||
"foot.pkg".source = callPackage ./foot.nix { inherit buildPackage; };
|
||||
};
|
||||
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
# adapted from nixos sway integration tests
|
||||
|
||||
# testScriptWithTypes:49: error: Cannot call function of unknown type
|
||||
# (machine.succeed if succeed else machine.execute)(
|
||||
# ^
|
||||
# Found 1 error in 1 file (checked 1 source file)
|
||||
skipTypeCheck = true;
|
||||
testScript = builtins.readFile ./test.py;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
buildPackage,
|
||||
foot,
|
||||
wayland-utils,
|
||||
inconsolata,
|
||||
}:
|
||||
|
||||
buildPackage {
|
||||
name = "foot";
|
||||
inherit (foot) version;
|
||||
|
||||
identity = 2;
|
||||
id = "org.codeberg.dnkl.foot";
|
||||
|
||||
modules = [
|
||||
{
|
||||
home.packages = [
|
||||
foot
|
||||
|
||||
# For wayland-info:
|
||||
wayland-utils
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
nixosModules = [
|
||||
{
|
||||
# To help with OCR:
|
||||
environment.etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
|
||||
main = {
|
||||
font = "inconsolata:size=14";
|
||||
};
|
||||
colors = rec {
|
||||
foreground = "000000";
|
||||
background = "ffffff";
|
||||
regular2 = foreground;
|
||||
};
|
||||
};
|
||||
|
||||
fonts.packages = [ inconsolata ];
|
||||
}
|
||||
];
|
||||
|
||||
script = ''
|
||||
exec foot "$@"
|
||||
'';
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import json
|
||||
import shlex
|
||||
|
||||
q = shlex.quote
|
||||
NODE_GROUPS = ["nodes", "floating_nodes"]
|
||||
|
||||
|
||||
def swaymsg(command: str = "", succeed=True, type="command"):
|
||||
assert command != "" or type != "command", "Must specify command or type"
|
||||
shell = q(f"swaymsg -t {q(type)} -- {q(command)}")
|
||||
with machine.nested(
|
||||
f"sending swaymsg {shell!r}" + " (allowed to fail)" * (not succeed)
|
||||
):
|
||||
ret = (machine.succeed if succeed else machine.execute)(
|
||||
f"su - alice -c {shell}"
|
||||
)
|
||||
|
||||
# execute also returns a status code, but disregard.
|
||||
if not succeed:
|
||||
_, ret = ret
|
||||
|
||||
if not succeed and not ret:
|
||||
return None
|
||||
|
||||
parsed = json.loads(ret)
|
||||
return parsed
|
||||
|
||||
|
||||
def walk(tree):
|
||||
yield tree
|
||||
for group in NODE_GROUPS:
|
||||
for node in tree.get(group, []):
|
||||
yield from walk(node)
|
||||
|
||||
|
||||
def wait_for_window(pattern):
|
||||
def func(last_chance):
|
||||
nodes = (node["name"] for node in walk(swaymsg(type="get_tree")))
|
||||
|
||||
if last_chance:
|
||||
nodes = list(nodes)
|
||||
machine.log(f"Last call! Current list of windows: {nodes}")
|
||||
|
||||
return any(pattern in name for name in nodes)
|
||||
|
||||
retry(func)
|
||||
|
||||
|
||||
def collect_state_ui(name):
|
||||
swaymsg(f"exec hakurei ps > '/tmp/{name}.ps'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.ps", "")
|
||||
swaymsg(f"exec hakurei --json ps > '/tmp/{name}.json'")
|
||||
machine.copy_from_vm(f"/tmp/{name}.json", "")
|
||||
machine.screenshot(name)
|
||||
|
||||
|
||||
def check_state(name, enablements):
|
||||
instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 hakurei --json ps"))
|
||||
if len(instances) != 1:
|
||||
raise Exception(f"unexpected state length {len(instances)}")
|
||||
instance = next(iter(instances.values()))
|
||||
|
||||
config = instance['config']
|
||||
|
||||
if len(config['args']) != 1 or not (config['args'][0].startswith("/nix/store/")) or f"hakurei-{name}-" not in (config['args'][0]):
|
||||
raise Exception(f"unexpected args {instance['config']['args']}")
|
||||
|
||||
if config['enablements'] != enablements:
|
||||
raise Exception(f"unexpected enablements {instance['config']['enablements']}")
|
||||
|
||||
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check hakurei's version:
|
||||
print(machine.succeed("sudo -u alice -i hakurei version"))
|
||||
|
||||
# Wait for Sway to complete startup:
|
||||
machine.wait_for_file("/run/user/1000/wayland-1")
|
||||
machine.wait_for_file("/tmp/sway-ipc.sock")
|
||||
|
||||
# Prepare hpkg directory:
|
||||
machine.succeed("install -dm 0700 -o alice -g users /var/lib/hakurei/1000")
|
||||
|
||||
# Install hpkg app:
|
||||
swaymsg("exec hpkg -v install /etc/foot.pkg && touch /tmp/hpkg-install-ok")
|
||||
machine.wait_for_file("/tmp/hpkg-install-ok")
|
||||
|
||||
# Start app (foot) with Wayland enablement:
|
||||
swaymsg("exec hpkg -v start org.codeberg.dnkl.foot")
|
||||
wait_for_window("hakurei@machine-foot")
|
||||
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
|
||||
machine.wait_for_file("/tmp/hakurei.0/tmpdir/2/success-client")
|
||||
collect_state_ui("app_wayland")
|
||||
check_state("foot", {"wayland": True, "dbus": True, "pulse": True})
|
||||
# Verify acl on XDG_RUNTIME_DIR:
|
||||
print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002"))
|
||||
machine.send_chars("exit\n")
|
||||
machine.wait_until_fails("pgrep foot")
|
||||
# Verify acl cleanup on XDG_RUNTIME_DIR:
|
||||
machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000002")
|
||||
|
||||
# Exit Sway and verify process exit status 0:
|
||||
swaymsg("exit", succeed=False)
|
||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||
|
||||
# Print hakurei runDir contents:
|
||||
print(machine.succeed("find /run/user/1000/hakurei"))
|
||||
@@ -1,108 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
)
|
||||
|
||||
func withNixDaemon(
|
||||
ctx context.Context,
|
||||
action string, command []string, net bool, updateConfig func(config *hst.Config) *hst.Config,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func(),
|
||||
) {
|
||||
mustRunAppDropShell(ctx, updateConfig(&hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathShell,
|
||||
Args: []string{bash, "-lc", "rm -f /nix/var/nix/daemon-socket/socket && " +
|
||||
// start nix-daemon
|
||||
"nix-daemon --store / & " +
|
||||
// wait for socket to appear
|
||||
"(while [ ! -S /nix/var/nix/daemon-socket/socket ]; do sleep 0.01; done) && " +
|
||||
// create directory so nix stops complaining
|
||||
"mkdir -p /nix/var/nix/profiles/per-user/root/channels && " +
|
||||
strings.Join(command, " && ") +
|
||||
// terminate nix-daemon
|
||||
" && pkill nix-daemon",
|
||||
},
|
||||
|
||||
Username: "hakurei",
|
||||
Shell: pathShell,
|
||||
Home: pathDataData.Append(app.ID),
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
},
|
||||
|
||||
Identity: app.Identity,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
Userns: true, // nix sandbox requires userns
|
||||
HostNet: net,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: pathSet.cacheDir.Append("etc"), Special: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: pathSet.nixPath, Target: pathNix, Write: true}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID), Source: pathSet.homeDir, Write: true, Ensure: true}},
|
||||
},
|
||||
},
|
||||
}), dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func withCacheDir(
|
||||
ctx context.Context,
|
||||
action string, command []string, workDir *container.Absolute,
|
||||
app *appInfo, pathSet *appPathSet, dropShell bool, beforeFail func()) {
|
||||
mustRunAppDropShell(ctx, &hst.Config{
|
||||
ID: app.ID,
|
||||
|
||||
Path: pathShell,
|
||||
Args: []string{bash, "-lc", strings.Join(command, " && ")},
|
||||
|
||||
Username: "nixos",
|
||||
Shell: pathShell,
|
||||
Home: pathDataData.Append(app.ID, "cache"),
|
||||
ExtraPerms: []*hst.ExtraPermConfig{
|
||||
{Path: dataHome, Execute: true},
|
||||
{Ensure: true, Path: pathSet.baseDir, Read: true, Write: true, Execute: true},
|
||||
{Path: workDir, Execute: true},
|
||||
},
|
||||
|
||||
Identity: app.Identity,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Hostname: formatHostname(app.Name) + "-" + action,
|
||||
SeccompFlags: seccomp.AllowMultiarch,
|
||||
Tty: dropShell,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
{FilesystemConfig: &hst.FSBind{Target: container.AbsFHSEtc, Source: workDir.Append(container.FHSEtc), Special: true}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: workDir.Append("nix"), Target: pathNix}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathCurrentSystem, Linkname: app.CurrentSystem.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: pathBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSLink{Target: container.AbsFHSUsrBin, Linkname: pathSwBin.String()}},
|
||||
{FilesystemConfig: &hst.FSBind{Source: workDir, Target: hst.AbsTmp.Append("bundle")}},
|
||||
{FilesystemConfig: &hst.FSBind{Target: pathDataData.Append(app.ID, "cache"), Source: pathSet.cacheDir, Write: true, Ensure: true}},
|
||||
},
|
||||
},
|
||||
}, dropShell, beforeFail)
|
||||
}
|
||||
|
||||
func mustRunAppDropShell(ctx context.Context, config *hst.Config, dropShell bool, beforeFail func()) {
|
||||
if dropShell {
|
||||
config.Args = []string{bash, "-l"}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
beforeFail()
|
||||
internal.Exit(0)
|
||||
}
|
||||
mustRunApp(ctx, config, beforeFail)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !rosa
|
||||
|
||||
package main
|
||||
|
||||
// hsuConfPath is an absolute pathname to the hsu configuration file. Its
|
||||
// contents are interpreted by parseConfig.
|
||||
const hsuConfPath = "/etc/hsurc"
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build rosa
|
||||
|
||||
package main
|
||||
|
||||
// hsuConfPath is the pathname to the hsu configuration file, specific to
|
||||
// Rosa OS. Its contents are interpreted by parseConfig.
|
||||
const hsuConfPath = "/system/etc/hsurc"
|
||||
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
/* keep in sync with hst */
|
||||
|
||||
const (
|
||||
userOffset = 100000
|
||||
rangeSize = userOffset / 10
|
||||
|
||||
identityStart = 0
|
||||
identityEnd = appEnd - appStart
|
||||
|
||||
appStart = rangeSize * 1
|
||||
appEnd = appStart + rangeSize - 1
|
||||
)
|
||||
|
||||
func toUser(userid, appid uint32) uint32 { return userid*userOffset + appStart + appid }
|
||||
+102
-52
@@ -1,3 +1,47 @@
|
||||
// hsu starts the hakurei shim as the target subordinate user.
|
||||
//
|
||||
// The hsu program must be installed with the setuid and setgid bit set, and
|
||||
// owned by root. A configuration file must be installed at /etc/hsurc with
|
||||
// permission bits 0400, and owned by root. Each line of the file specifies a
|
||||
// hakurei userid to kernel uid mapping. A line consists of the decimal string
|
||||
// representation of the uid of the user wishing to start hakurei containers,
|
||||
// followed by a space, followed by the decimal string representation of its
|
||||
// userid. Duplicate uid entries are ignored, with the first occurrence taking
|
||||
// effect.
|
||||
//
|
||||
// For example, to map the kernel uid 1000 to the hakurei user id 0:
|
||||
//
|
||||
// 1000 0
|
||||
//
|
||||
// # Internals
|
||||
//
|
||||
// Hakurei and hsu holds pathnames pointing to each other set at link time. For
|
||||
// this reason, a distribution of hakurei has fixed installation prefix. Since
|
||||
// this program is never invoked by the user, behaviour described in the
|
||||
// following paragraphs are considered an internal detail and not covered by the
|
||||
// compatibility promise.
|
||||
//
|
||||
// Since target container environment variables are set up in shim via the
|
||||
// [container] infrastructure, the environment is used for parameters from the
|
||||
// parent process.
|
||||
//
|
||||
// HAKUREI_SHIM specifies a single byte between '3' and '9' representing the
|
||||
// setup pipe file descriptor. It is passed as is to the shim process and is the
|
||||
// only value in the environment of the shim process. Since hsurc is not
|
||||
// accessible to the parent process, leaving this unset causes hsu to print the
|
||||
// corresponding hakurei user id of the parent and terminate.
|
||||
//
|
||||
// HAKUREI_IDENTITY specifies the identity of the instance being started and is
|
||||
// used to produce the kernel uid alongside hakurei user id looked up from hsurc.
|
||||
//
|
||||
// HAKUREI_GROUPS specifies supplementary groups to inherit from the credentials
|
||||
// of the parent process in a ' ' separated list of decimal string
|
||||
// representations of gid. This has the unfortunate consequence of allowing
|
||||
// users mapped via hsurc to effectively drop group membership, so special care
|
||||
// must be taken to ensure this does not lead to an increase in access. This is
|
||||
// not applicable to Rosa OS since unsigned code execution is not permitted
|
||||
// outside hakurei containers, and is generally nonapplicable to the security
|
||||
// model of hakurei, where all untrusted code runs within containers.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -5,55 +49,56 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
hsuConfFile = "/etc/hsurc"
|
||||
envShim = "HAKUREI_SHIM"
|
||||
envAID = "HAKUREI_APP_ID"
|
||||
envGroups = "HAKUREI_GROUPS"
|
||||
|
||||
PR_SET_NO_NEW_PRIVS = 0x26
|
||||
// envShim is the name of the environment variable holding a single byte
|
||||
// representing the shim setup pipe file descriptor.
|
||||
envShim = "HAKUREI_SHIM"
|
||||
// envIdentity is the name of the environment variable holding a decimal
|
||||
// string representation of the current application identity.
|
||||
envIdentity = "HAKUREI_IDENTITY"
|
||||
// envGroups holds a ' ' separated list of decimal string representations of
|
||||
// supplementary group gid. Membership requirements are enforced.
|
||||
envGroups = "HAKUREI_GROUPS"
|
||||
)
|
||||
|
||||
// hakureiPath is the absolute path to Hakurei.
|
||||
//
|
||||
// This is set by the linker.
|
||||
var hakureiPath string
|
||||
|
||||
func main() {
|
||||
const PR_SET_NO_NEW_PRIVS = 0x26
|
||||
runtime.LockOSThread()
|
||||
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("hsu: ")
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
log.Fatal("this program must be owned by uid 0 and have the setuid bit set")
|
||||
}
|
||||
if os.Getegid() != os.Getgid() {
|
||||
log.Fatal("this program must not have the setgid bit set")
|
||||
}
|
||||
|
||||
puid := os.Getuid()
|
||||
if puid == 0 {
|
||||
log.Fatal("this program must not be started by root")
|
||||
}
|
||||
|
||||
var toolPath string
|
||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||
if p, err := os.Readlink(pexe); err != nil {
|
||||
log.Fatalf("cannot read parent executable path: %v", err)
|
||||
} else if strings.HasSuffix(p, " (deleted)") {
|
||||
log.Fatal("hakurei executable has been deleted")
|
||||
} else if p != mustCheckPath(hmain) {
|
||||
log.Fatal("this program must be started by hakurei")
|
||||
} else {
|
||||
toolPath = p
|
||||
if !filepath.IsAbs(hakureiPath) {
|
||||
log.Fatal("this program is compiled incorrectly")
|
||||
return
|
||||
}
|
||||
|
||||
// uid = 1000000 +
|
||||
// fid * 10000 +
|
||||
// aid
|
||||
uid := 1000000
|
||||
|
||||
// refuse to run if hsurc is not protected correctly
|
||||
if s, err := os.Stat(hsuConfFile); err != nil {
|
||||
if s, err := os.Stat(hsuConfPath); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if s.Mode().Perm() != 0400 {
|
||||
log.Fatal("bad hsurc perm")
|
||||
@@ -62,29 +107,13 @@ func main() {
|
||||
}
|
||||
|
||||
// authenticate before accepting user input
|
||||
if f, err := os.Open(hsuConfFile); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if fid, ok := mustParseConfig(f, puid); !ok {
|
||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||
} else {
|
||||
uid += fid * 10000
|
||||
}
|
||||
|
||||
// allowed aid range 0 to 9999
|
||||
if as, ok := os.LookupEnv(envAID); !ok {
|
||||
log.Fatal("HAKUREI_APP_ID not set")
|
||||
} else if aid, err := parseUint32Fast(as); err != nil || aid < 0 || aid > 9999 {
|
||||
log.Fatal("invalid aid")
|
||||
} else {
|
||||
uid += aid
|
||||
}
|
||||
userid := mustParseConfig(puid)
|
||||
|
||||
// pass through setup fd to shim
|
||||
var shimSetupFd string
|
||||
if s, ok := os.LookupEnv(envShim); !ok {
|
||||
// hakurei requests target uid
|
||||
// print resolved uid and exit
|
||||
fmt.Print(uid)
|
||||
// hakurei requests hsurc user id
|
||||
fmt.Print(userid)
|
||||
os.Exit(0)
|
||||
} else if len(s) != 1 || s[0] > '9' || s[0] < '3' {
|
||||
log.Fatal("HAKUREI_SHIM holds an invalid value")
|
||||
@@ -92,6 +121,22 @@ func main() {
|
||||
shimSetupFd = s
|
||||
}
|
||||
|
||||
// start is going ahead at this point
|
||||
identity := mustReadIdentity()
|
||||
|
||||
const (
|
||||
// first possible uid outcome
|
||||
uidStart = 10000
|
||||
// last possible uid outcome
|
||||
uidEnd = 999919999
|
||||
)
|
||||
uid := int(toUser(userid, identity))
|
||||
|
||||
// final bounds check to catch any bugs
|
||||
if uid < uidStart || uid >= uidEnd {
|
||||
panic("uid out of bounds")
|
||||
}
|
||||
|
||||
// supplementary groups
|
||||
var suppGroups, suppCurrent []int
|
||||
|
||||
@@ -119,13 +164,7 @@ func main() {
|
||||
suppGroups = []int{uid}
|
||||
}
|
||||
|
||||
// final bounds check to catch any bugs
|
||||
if uid < 1000000 || uid >= 2000000 {
|
||||
panic("uid out of bounds")
|
||||
}
|
||||
|
||||
// careful! users in the allowlist is effectively allowed to drop groups via hsu
|
||||
|
||||
if err := syscall.Setresgid(uid, uid, uid); err != nil {
|
||||
log.Fatalf("cannot set gid: %v", err)
|
||||
}
|
||||
@@ -135,10 +174,21 @@ func main() {
|
||||
if err := syscall.Setresuid(uid, uid, uid); err != nil {
|
||||
log.Fatalf("cannot set uid: %v", err)
|
||||
}
|
||||
if _, _, errno := syscall.AllThreadsSyscall(syscall.SYS_PRCTL, PR_SET_NO_NEW_PRIVS, 1, 0); errno != 0 {
|
||||
|
||||
if _, _, errno := syscall.AllThreadsSyscall(
|
||||
syscall.SYS_PRCTL,
|
||||
PR_SET_NO_NEW_PRIVS, 1,
|
||||
0,
|
||||
); errno != 0 {
|
||||
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
|
||||
}
|
||||
if err := syscall.Exec(toolPath, []string{"hakurei", "shim"}, []string{envShim + "=" + shimSetupFd}); err != nil {
|
||||
|
||||
if err := syscall.Exec(hakureiPath, []string{
|
||||
"hakurei",
|
||||
"shim",
|
||||
}, []string{
|
||||
envShim + "=" + shimSetupFd,
|
||||
}); err != nil {
|
||||
log.Fatalf("cannot start shim: %v", err)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -19,5 +19,5 @@ buildGoModule {
|
||||
ldflags = lib.attrsets.foldlAttrs (
|
||||
ldflags: name: value:
|
||||
ldflags ++ [ "-X main.${name}=${value}" ]
|
||||
) [ "-s -w" ] { hmain = "${hakurei}/libexec/hakurei"; };
|
||||
) [ "-s -w" ] { hakureiPath = "${hakurei}/libexec/hakurei"; };
|
||||
}
|
||||
|
||||
+80
-19
@@ -6,62 +6,123 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseUint32Fast(s string) (int, error) {
|
||||
const (
|
||||
// useridStart is the first userid.
|
||||
useridStart = 0
|
||||
// useridEnd is the last userid.
|
||||
useridEnd = useridStart + rangeSize - 1
|
||||
)
|
||||
|
||||
// parseUint32Fast parses a string representation of an unsigned 32-bit integer
|
||||
// value using the fast path only. This limits the range of values it is defined
|
||||
// in but is perfectly adequate for this use case.
|
||||
func parseUint32Fast(s string) (uint32, error) {
|
||||
sLen := len(s)
|
||||
if sLen < 1 {
|
||||
return -1, errors.New("zero length string")
|
||||
return 0, errors.New("zero length string")
|
||||
}
|
||||
if sLen > 10 {
|
||||
return -1, errors.New("string too long")
|
||||
return 0, errors.New("string too long")
|
||||
}
|
||||
|
||||
n := 0
|
||||
var n uint32
|
||||
for i, ch := range []byte(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
|
||||
return 0, fmt.Errorf("invalid character '%s' at index %d", string(ch+'0'), i)
|
||||
}
|
||||
n = n*10 + int(ch)
|
||||
n = n*10 + uint32(ch)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func parseConfig(r io.Reader, puid int) (fid int, ok bool, err error) {
|
||||
// parseConfig reads a list of allowed users from r until it encounters puid or
|
||||
// [io.EOF].
|
||||
//
|
||||
// Each line of the file specifies a hakurei userid to kernel uid mapping. A
|
||||
// line consists of the string representation of the uid of the user wishing to
|
||||
// start hakurei containers, followed by a space, followed by the string
|
||||
// representation of its userid. Duplicate uid entries are ignored, with the
|
||||
// first occurrence taking effect.
|
||||
//
|
||||
// All string representations are parsed by calling parseUint32Fast.
|
||||
func parseConfig(r io.Reader, puid uint32) (userid uint32, ok bool, err error) {
|
||||
s := bufio.NewScanner(r)
|
||||
var line, puid0 int
|
||||
var (
|
||||
line uintptr
|
||||
puid0 uint32
|
||||
)
|
||||
for s.Scan() {
|
||||
line++
|
||||
|
||||
// <puid> <fid>
|
||||
// <puid> <userid>
|
||||
lf := strings.SplitN(s.Text(), " ", 2)
|
||||
if len(lf) != 2 {
|
||||
return -1, false, fmt.Errorf("invalid entry on line %d", line)
|
||||
return useridEnd + 1, false, fmt.Errorf("invalid entry on line %d", line)
|
||||
}
|
||||
|
||||
puid0, err = parseUint32Fast(lf[0])
|
||||
if err != nil || puid0 < 1 {
|
||||
return -1, false, fmt.Errorf("invalid parent uid on line %d", line)
|
||||
return useridEnd + 1, false, fmt.Errorf("invalid parent uid on line %d", line)
|
||||
}
|
||||
|
||||
ok = puid0 == puid
|
||||
if ok {
|
||||
// allowed fid range 0 to 99
|
||||
if fid, err = parseUint32Fast(lf[1]); err != nil || fid < 0 || fid > 99 {
|
||||
return -1, false, fmt.Errorf("invalid identity on line %d", line)
|
||||
// userid bound to a range, uint32 size allows this to be increased if needed
|
||||
if userid, err = parseUint32Fast(lf[1]); err != nil ||
|
||||
userid < useridStart || userid > useridEnd {
|
||||
return useridEnd + 1, false, fmt.Errorf("invalid userid on line %d", line)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return -1, false, s.Err()
|
||||
return useridEnd + 1, false, s.Err()
|
||||
}
|
||||
|
||||
func mustParseConfig(r io.Reader, puid int) (int, bool) {
|
||||
fid, ok, err := parseConfig(r, puid)
|
||||
if err != nil {
|
||||
// mustParseConfig calls parseConfig to interpret the contents of hsuConfPath,
|
||||
// terminating the program if an error is encountered, the syntax is incorrect,
|
||||
// or the current user is not authorised to use hsu because its uid is missing.
|
||||
//
|
||||
// Therefore, code after this function call can assume an authenticated state.
|
||||
//
|
||||
// mustParseConfig returns the userid value of the current user.
|
||||
func mustParseConfig(puid int) (userid uint32) {
|
||||
if puid > math.MaxUint32 {
|
||||
log.Fatalf("got impossible uid %d", puid)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if f, err := os.Open(hsuConfPath); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if userid, ok, err = parseConfig(f, uint32(puid)); err != nil {
|
||||
log.Fatal(err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return fid, ok
|
||||
if !ok {
|
||||
log.Fatalf("uid %d is not in the hsurc file", puid)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// mustReadIdentity calls parseUint32Fast to interpret the value stored in envIdentity,
|
||||
// terminating the program if the value is not set, malformed, or out of bounds.
|
||||
func mustReadIdentity() uint32 {
|
||||
// ranges defined in hst and copied to this package to avoid importing hst
|
||||
if as, ok := os.LookupEnv(envIdentity); !ok {
|
||||
log.Fatal("HAKUREI_IDENTITY not set")
|
||||
panic("unreachable")
|
||||
} else if identity, err := parseUint32Fast(as); err != nil ||
|
||||
identity < identityStart || identity > identityEnd {
|
||||
log.Fatal("invalid identity")
|
||||
panic("unreachable")
|
||||
} else {
|
||||
return identity
|
||||
}
|
||||
}
|
||||
|
||||
+41
-30
@@ -2,94 +2,105 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parseUint32Fast(t *testing.T) {
|
||||
func TestParseUint32Fast(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("zero-length", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := parseUint32Fast(""); err == nil || err.Error() != "zero length string" {
|
||||
t.Errorf(`parseUint32Fast(""): error = %v`, err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("overflow", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := parseUint32Fast("10000000000"); err == nil || err.Error() != "string too long" {
|
||||
t.Errorf("parseUint32Fast: error = %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid byte", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := parseUint32Fast("meow"); err == nil || err.Error() != "invalid character 'm' at index 0" {
|
||||
t.Errorf(`parseUint32Fast("meow"): error = %v`, err)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("full range", func(t *testing.T) {
|
||||
testRange := func(i, end int) {
|
||||
|
||||
t.Run("range", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testRange := func(i, end uint32) {
|
||||
for ; i < end; i++ {
|
||||
s := strconv.Itoa(i)
|
||||
s := strconv.Itoa(int(i))
|
||||
w := i
|
||||
t.Run("parse "+s, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v, err := parseUint32Fast(s)
|
||||
if err != nil {
|
||||
t.Errorf("parseUint32Fast(%q): error = %v",
|
||||
s, err)
|
||||
t.Errorf("parseUint32Fast(%q): error = %v", s, err)
|
||||
return
|
||||
}
|
||||
if v != w {
|
||||
t.Errorf("parseUint32Fast(%q): got %v",
|
||||
s, v)
|
||||
t.Errorf("parseUint32Fast(%q): got %v", s, v)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
testRange(0, 5000)
|
||||
testRange(105000, 110000)
|
||||
testRange(23005000, 23010000)
|
||||
testRange(456005000, 456010000)
|
||||
testRange(7890005000, 7890010000)
|
||||
testRange(0, 2500)
|
||||
testRange(23002500, 23005000)
|
||||
testRange(math.MaxUint32-2500, math.MaxUint32)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_parseConfig(t *testing.T) {
|
||||
func TestParseConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
puid, want int
|
||||
puid, want uint32
|
||||
wantErr string
|
||||
rc string
|
||||
}{
|
||||
{"empty", 0, -1, "", ``},
|
||||
{"invalid field", 0, -1, "invalid entry on line 1", `9`},
|
||||
{"invalid puid", 0, -1, "invalid parent uid on line 1", `f 9`},
|
||||
{"invalid fid", 1000, -1, "invalid identity on line 1", `1000 f`},
|
||||
{"empty", 0, useridEnd + 1, "", ``},
|
||||
{"invalid field", 0, useridEnd + 1, "invalid entry on line 1", `9`},
|
||||
{"invalid puid", 0, useridEnd + 1, "invalid parent uid on line 1", `f 9`},
|
||||
{"invalid userid", 1000, useridEnd + 1, "invalid userid on line 1", `1000 f`},
|
||||
{"match", 1000, 0, "", `1000 0`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
|
||||
t.Parallel()
|
||||
|
||||
userid, ok, err := parseConfig(bytes.NewBufferString(tc.rc), tc.puid)
|
||||
if err == nil && tc.wantErr != "" {
|
||||
t.Errorf("parseConfig: error = %v; wantErr %q",
|
||||
err, tc.wantErr)
|
||||
t.Errorf("parseConfig: error = %v; want %q", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil && err.Error() != tc.wantErr {
|
||||
t.Errorf("parseConfig: error = %q; wantErr %q",
|
||||
err, tc.wantErr)
|
||||
t.Errorf("parseConfig: error = %q; want %q", err, tc.wantErr)
|
||||
return
|
||||
}
|
||||
if ok == (tc.want == -1) {
|
||||
t.Errorf("parseConfig: ok = %v; want %v",
|
||||
ok, tc.want)
|
||||
if ok == (tc.want == useridEnd+1) {
|
||||
t.Errorf("parseConfig: ok = %v; want %v", ok, tc.want)
|
||||
return
|
||||
}
|
||||
if fid != tc.want {
|
||||
t.Errorf("parseConfig: fid = %v; want %v",
|
||||
fid, tc.want)
|
||||
if userid != tc.want {
|
||||
t.Errorf("parseConfig: %v; want %v", userid, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path"
|
||||
)
|
||||
|
||||
const compPoison = "INVALIDINVALIDINVALIDINVALIDINVALID"
|
||||
|
||||
var (
|
||||
hmain = compPoison
|
||||
)
|
||||
|
||||
func mustCheckPath(p string) string {
|
||||
if p != compPoison && p != "" && path.IsAbs(p) {
|
||||
return p
|
||||
}
|
||||
log.Fatal("this program is compiled incorrectly")
|
||||
return compPoison
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// cache refers to an instance of [pkg.Cache] that might be open.
|
||||
type cache struct {
|
||||
ctx context.Context
|
||||
msg message.Msg
|
||||
|
||||
// Should generally not be used directly.
|
||||
c *pkg.Cache
|
||||
|
||||
cures, jobs int
|
||||
// Primarily to work around missing landlock LSM.
|
||||
hostAbstract bool
|
||||
// Set SCHED_IDLE.
|
||||
idle bool
|
||||
// Unset [pkg.CSuppressInit].
|
||||
verboseInit bool
|
||||
// Loaded artifact of [rosa.QEMU].
|
||||
qemu pkg.Artifact
|
||||
|
||||
base, mirror string
|
||||
}
|
||||
|
||||
// open opens the underlying [pkg.Cache].
|
||||
func (cache *cache) open() (err error) {
|
||||
if cache.c != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
var base *check.Absolute
|
||||
if cache.base, err = filepath.Abs(cache.base); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(cache.base); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var flags int
|
||||
if cache.idle {
|
||||
flags |= pkg.CSchedIdle
|
||||
}
|
||||
if cache.hostAbstract {
|
||||
flags |= pkg.CHostAbstract
|
||||
}
|
||||
if !cache.verboseInit {
|
||||
flags |= pkg.CSuppressInit
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-cache.ctx.Done():
|
||||
if testing.Testing() {
|
||||
return
|
||||
}
|
||||
os.Exit(2)
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
cache.msg.Verbosef("opening cache at %s", base)
|
||||
cache.c, err = pkg.Open(
|
||||
cache.ctx,
|
||||
cache.msg,
|
||||
flags,
|
||||
cache.cures,
|
||||
cache.jobs,
|
||||
base,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
done <- struct{}{}
|
||||
|
||||
if cache.mirror != "" {
|
||||
var pub []byte
|
||||
pub, err = os.ReadFile(base.Append("ed25519.pub").String())
|
||||
if err != nil {
|
||||
cache.c.Close()
|
||||
return
|
||||
}
|
||||
var r rosa.Remote
|
||||
if r, err = rosa.NewRemote(cache.mirror, pub, http.DefaultClient); err != nil {
|
||||
cache.c.Close()
|
||||
return err
|
||||
}
|
||||
cache.c.SetExternal(r)
|
||||
}
|
||||
|
||||
if cache.qemu != nil {
|
||||
var pathname *check.Absolute
|
||||
pathname, _, err = cache.c.Cure(cache.qemu)
|
||||
if err != nil {
|
||||
cache.c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
for arch, entry := range rosa.Arches(pathname) {
|
||||
pkg.RegisterArch(arch, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the underlying [pkg.Cache] if it is open.
|
||||
func (cache *cache) Close() {
|
||||
if cache.c != nil {
|
||||
cache.c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Do calls f on the underlying cache and returns its error value.
|
||||
func (cache *cache) Do(f func(cache *pkg.Cache) error) error {
|
||||
if cache.c == nil {
|
||||
if err := cache.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return f(cache.c)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cm := cache{
|
||||
ctx: t.Context(),
|
||||
msg: message.New(log.New(os.Stderr, "check: ", 0)),
|
||||
base: t.TempDir(),
|
||||
|
||||
hostAbstract: true, idle: true,
|
||||
}
|
||||
defer cm.Close()
|
||||
cm.Close()
|
||||
|
||||
if err := cm.open(); err != nil {
|
||||
t.Fatalf("open: error = %v", err)
|
||||
}
|
||||
if err := cm.open(); err != os.ErrInvalid {
|
||||
t.Errorf("(duplicate) open: error = %v", err)
|
||||
}
|
||||
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.Scrub(0)
|
||||
}); err != nil {
|
||||
t.Errorf("Scrub: error = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unique"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
)
|
||||
|
||||
// daemonTimeout is the maximum amount of time cureFromIR will wait on I/O.
|
||||
const daemonTimeout = 30 * time.Second
|
||||
|
||||
// daemonDeadline returns the deadline corresponding to daemonTimeout, or the
|
||||
// zero value when running in a test.
|
||||
func daemonDeadline() time.Time {
|
||||
if testing.Testing() {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Now().Add(daemonTimeout)
|
||||
}
|
||||
|
||||
const (
|
||||
// remoteNoReply notifies that the client will not receive a cure reply.
|
||||
remoteNoReply = 1 << iota
|
||||
)
|
||||
|
||||
// cureFromIR services an IR curing request.
|
||||
func cureFromIR(
|
||||
cache *pkg.Cache,
|
||||
conn net.Conn,
|
||||
flags uint64,
|
||||
) (pkg.Artifact, error) {
|
||||
a, decodeErr := cache.NewDecoder(conn).Decode()
|
||||
if decodeErr != nil {
|
||||
_, err := conn.Write([]byte("\x00" + decodeErr.Error()))
|
||||
return nil, errors.Join(decodeErr, err, conn.Close())
|
||||
}
|
||||
|
||||
pathname, _, cureErr := cache.Cure(a)
|
||||
if flags&remoteNoReply != 0 {
|
||||
return a, errors.Join(cureErr, conn.Close())
|
||||
}
|
||||
if err := conn.SetWriteDeadline(daemonDeadline()); err != nil {
|
||||
return a, errors.Join(cureErr, err, conn.Close())
|
||||
}
|
||||
if cureErr != nil {
|
||||
_, err := conn.Write([]byte("\x00" + cureErr.Error()))
|
||||
return a, errors.Join(cureErr, err, conn.Close())
|
||||
}
|
||||
_, err := conn.Write([]byte(pathname.String()))
|
||||
if testing.Testing() && errors.Is(err, io.ErrClosedPipe) {
|
||||
return a, nil
|
||||
}
|
||||
return a, errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
const (
|
||||
// specialCancel is a message consisting of a single identifier referring
|
||||
// to a curing artifact to be cancelled.
|
||||
specialCancel = iota
|
||||
// specialAbort requests for all pending cures to be aborted. It has no
|
||||
// message body.
|
||||
specialAbort
|
||||
|
||||
// remoteSpecial denotes a special message with custom layout.
|
||||
remoteSpecial = math.MaxUint64
|
||||
)
|
||||
|
||||
// writeSpecialHeader writes the header of a remoteSpecial message.
|
||||
func writeSpecialHeader(conn net.Conn, kind uint64) error {
|
||||
var sh [16]byte
|
||||
binary.LittleEndian.PutUint64(sh[:], remoteSpecial)
|
||||
binary.LittleEndian.PutUint64(sh[8:], kind)
|
||||
if n, err := conn.Write(sh[:]); err != nil {
|
||||
return err
|
||||
} else if n != len(sh) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cancelIdent reads an identifier from conn and cancels the corresponding cure.
|
||||
func cancelIdent(
|
||||
cache *pkg.Cache,
|
||||
conn net.Conn,
|
||||
) (*pkg.ID, bool, error) {
|
||||
var ident pkg.ID
|
||||
if _, err := io.ReadFull(conn, ident[:]); err != nil {
|
||||
return nil, false, errors.Join(err, conn.Close())
|
||||
}
|
||||
ok := cache.Cancel(unique.Make(ident))
|
||||
return &ident, ok, conn.Close()
|
||||
}
|
||||
|
||||
// serve services connections from a [net.UnixListener].
|
||||
func serve(
|
||||
ctx context.Context,
|
||||
log *log.Logger,
|
||||
cm *cache,
|
||||
ul *net.UnixListener,
|
||||
) error {
|
||||
ul.SetUnlinkOnClose(true)
|
||||
if cm.c == nil {
|
||||
if err := cm.open(); err != nil {
|
||||
return errors.Join(err, ul.Close())
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
wg.Go(func() {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
conn, err := ul.AcceptUnix()
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
log.Println(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
wg.Go(func() {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if _err := conn.SetReadDeadline(daemonDeadline()); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var word [8]byte
|
||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
flags := binary.LittleEndian.Uint64(word[:])
|
||||
|
||||
if flags == remoteSpecial {
|
||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
switch special := binary.LittleEndian.Uint64(word[:]); special {
|
||||
default:
|
||||
log.Printf("invalid special %d", special)
|
||||
|
||||
case specialCancel:
|
||||
if id, ok, _err := cancelIdent(cm.c, conn); _err != nil {
|
||||
log.Println(_err)
|
||||
} else if !ok {
|
||||
log.Println(
|
||||
"attempting to cancel invalid artifact",
|
||||
pkg.Encode(*id),
|
||||
)
|
||||
} else {
|
||||
log.Println(
|
||||
"cancelled artifact",
|
||||
pkg.Encode(*id),
|
||||
)
|
||||
}
|
||||
|
||||
case specialAbort:
|
||||
log.Println("aborting all pending cures")
|
||||
cm.c.Abort()
|
||||
if _err := conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if a, _err := cureFromIR(cm.c, conn, flags); _err != nil {
|
||||
log.Println(_err)
|
||||
} else {
|
||||
log.Printf(
|
||||
"fulfilled artifact %s",
|
||||
pkg.Encode(cm.c.Ident(a).Value()),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
<-ctx.Done()
|
||||
if err := ul.SetDeadline(time.Now()); err != nil {
|
||||
return errors.Join(err, ul.Close())
|
||||
}
|
||||
wg.Wait()
|
||||
return ul.Close()
|
||||
}
|
||||
|
||||
// dial wraps [net.DialUnix] with a context.
|
||||
func dial(ctx context.Context, addr *net.UnixAddr) (
|
||||
done chan<- struct{},
|
||||
conn *net.UnixConn,
|
||||
err error,
|
||||
) {
|
||||
conn, err = net.DialUnix("unix", nil, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d := make(chan struct{})
|
||||
done = d
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
|
||||
case <-d:
|
||||
return
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// cureRemote cures a [pkg.Artifact] on a daemon.
|
||||
func cureRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
a pkg.Artifact,
|
||||
flags uint64,
|
||||
) (*check.Absolute, error) {
|
||||
if flags == remoteSpecial {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
if n, flagErr := conn.Write(binary.LittleEndian.AppendUint64(nil, flags)); flagErr != nil {
|
||||
return nil, errors.Join(flagErr, conn.Close())
|
||||
} else if n != 8 {
|
||||
return nil, errors.Join(io.ErrShortWrite, conn.Close())
|
||||
}
|
||||
|
||||
if err = pkg.NewIR().EncodeAll(conn, a); err != nil {
|
||||
return nil, errors.Join(err, conn.Close())
|
||||
} else if err = conn.CloseWrite(); err != nil {
|
||||
return nil, errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
if flags&remoteNoReply != 0 {
|
||||
return nil, conn.Close()
|
||||
}
|
||||
|
||||
payload, recvErr := io.ReadAll(conn)
|
||||
if err = errors.Join(recvErr, conn.Close()); err != nil {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
err = cancelErr
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(payload) > 0 && payload[0] == 0 {
|
||||
return nil, errors.New(string(payload[1:]))
|
||||
}
|
||||
|
||||
var p *check.Absolute
|
||||
p, err = check.NewAbs(string(payload))
|
||||
return p, err
|
||||
}
|
||||
|
||||
// cancelRemote cancels a [pkg.Artifact] curing on a daemon.
|
||||
func cancelRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
a pkg.Artifact,
|
||||
wait bool,
|
||||
) error {
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
if err = writeSpecialHeader(conn, specialCancel); err != nil {
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
var n int
|
||||
id := pkg.NewIR().Ident(a).Value()
|
||||
if n, err = conn.Write(id[:]); err != nil {
|
||||
return errors.Join(err, conn.Close())
|
||||
} else if n != len(id) {
|
||||
return errors.Join(io.ErrShortWrite, conn.Close())
|
||||
}
|
||||
if wait {
|
||||
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
// abortRemote aborts all [pkg.Artifact] curing on a daemon.
|
||||
func abortRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
wait bool,
|
||||
) error {
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
err = writeSpecialHeader(conn, specialAbort)
|
||||
if wait && err == nil {
|
||||
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestNoReply(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !daemonDeadline().IsZero() {
|
||||
t.Fatal("daemonDeadline did not return the zero value")
|
||||
}
|
||||
|
||||
c, err := pkg.Open(
|
||||
t.Context(),
|
||||
message.New(log.New(os.Stderr, "cir: ", 0)),
|
||||
0, 0, 0,
|
||||
check.MustAbs(t.TempDir()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
client, server := net.Pipe()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
go func() {
|
||||
<-t.Context().Done()
|
||||
if _err := client.SetDeadline(time.Now()); _err != nil && !errors.Is(_err, io.ErrClosedPipe) {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _err := c.EncodeAll(
|
||||
client,
|
||||
pkg.NewFile("check", []byte{0}),
|
||||
); _err != nil {
|
||||
panic(_err)
|
||||
} else if _err = client.Close(); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
a, cureErr := cureFromIR(c, server, remoteNoReply)
|
||||
if cureErr != nil {
|
||||
t.Fatalf("cureFromIR: error = %v", cureErr)
|
||||
}
|
||||
|
||||
<-done
|
||||
wantIdent := pkg.MustDecode("fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG")
|
||||
if gotIdent := c.Ident(a).Value(); gotIdent != wantIdent {
|
||||
t.Errorf(
|
||||
"cureFromIR: %s, want %s",
|
||||
pkg.Encode(gotIdent), pkg.Encode(wantIdent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "daemon: ", 0)
|
||||
|
||||
addr := net.UnixAddr{
|
||||
Name: filepath.Join(t.TempDir(), "daemon"),
|
||||
Net: "unix",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
cm := cache{
|
||||
ctx: ctx,
|
||||
msg: message.New(logger),
|
||||
base: t.TempDir(),
|
||||
}
|
||||
defer cm.Close()
|
||||
|
||||
ul, err := net.ListenUnix("unix", &addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ListenUnix: error = %v", err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
if _err := serve(ctx, logger, &cm, ul); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil), true); err != nil {
|
||||
t.Fatalf("cancelRemote: error = %v", err)
|
||||
}
|
||||
|
||||
if err = abortRemote(ctx, &addr, true); err != nil {
|
||||
t.Fatalf("abortRemote: error = %v", err)
|
||||
}
|
||||
|
||||
// keep this last for synchronisation
|
||||
var p *check.Absolute
|
||||
p, err = cureRemote(ctx, &addr, pkg.NewFile("check", []byte{0}), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("cureRemote: error = %v", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
const want = "fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG"
|
||||
if got := filepath.Base(p.String()); got != want {
|
||||
t.Errorf("cureRemote: %s, want %s", got, want)
|
||||
}
|
||||
|
||||
wantLog := []string{
|
||||
"",
|
||||
"daemon: aborting all pending cures",
|
||||
"daemon: attempting to cancel invalid artifact kQm9fmnCmXST1-MMmxzcau2oKZCXXrlZydo4PkeV5hO_2PKfeC8t98hrbV_ZZx_j",
|
||||
"daemon: fulfilled artifact fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG",
|
||||
}
|
||||
gotLog := strings.Split(buf.String(), "\n")
|
||||
slices.Sort(gotLog)
|
||||
if !slices.Equal(gotLog, wantLog) {
|
||||
t.Errorf(
|
||||
"serve: logged\n%s\nwant\n%s",
|
||||
strings.Join(gotLog, "\n"), strings.Join(wantLog, "\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"unique"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// commandInfo implements the info subcommand.
|
||||
func commandInfo(
|
||||
cm *cache,
|
||||
args []string,
|
||||
w io.Writer,
|
||||
writeStatus bool,
|
||||
r *rosa.Report,
|
||||
) (err error) {
|
||||
if len(args) == 0 {
|
||||
return errors.New("info requires at least 1 argument")
|
||||
}
|
||||
|
||||
// recovered by HandleAccess
|
||||
mustPrintln := func(a ...any) {
|
||||
if _, _err := fmt.Fprintln(w, a...); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}
|
||||
mustPrint := func(a ...any) {
|
||||
if _, _err := fmt.Fprint(w, a...); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}
|
||||
|
||||
t := rosa.Native().Std()
|
||||
for i, name := range args {
|
||||
handle := rosa.ArtifactH(unique.Make(name))
|
||||
if meta, a := t.Load(handle); meta == nil {
|
||||
return fmt.Errorf("unknown artifact %q", name)
|
||||
} else {
|
||||
var suffix string
|
||||
|
||||
if meta.Version != rosa.Unversioned {
|
||||
suffix += "-" + meta.Version
|
||||
}
|
||||
mustPrintln("name : " + name + suffix)
|
||||
|
||||
mustPrintln("description : " + meta.Description)
|
||||
if meta.Website != "" {
|
||||
mustPrintln("website : " +
|
||||
strings.TrimSuffix(meta.Website, "/"))
|
||||
}
|
||||
if len(meta.Dependencies) > 0 {
|
||||
mustPrint("depends on :")
|
||||
for _, d := range meta.Dependencies {
|
||||
_meta, _ := rosa.Native().Std().MustLoad(d)
|
||||
s := _meta.Name
|
||||
if _meta.Version != rosa.Unversioned {
|
||||
s += "-" + _meta.Version
|
||||
}
|
||||
mustPrint(" " + s)
|
||||
}
|
||||
mustPrintln()
|
||||
}
|
||||
|
||||
const statusPrefix = "status : "
|
||||
if writeStatus {
|
||||
if r == nil {
|
||||
var f io.ReadSeekCloser
|
||||
err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
f, err = cache.OpenStatus(a)
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
mustPrintln(
|
||||
statusPrefix + "not yet cured",
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mustPrint(statusPrefix)
|
||||
_, err = io.Copy(w, f)
|
||||
if err = errors.Join(err, f.Close()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
status, n := r.ArtifactOf(cache.Ident(a))
|
||||
if status == nil {
|
||||
mustPrintln(
|
||||
statusPrefix + "not in report",
|
||||
)
|
||||
} else {
|
||||
mustPrintln("size :", n)
|
||||
mustPrint(statusPrefix)
|
||||
if _, err = w.Write(status); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if i != len(args)-1 {
|
||||
mustPrintln()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unique"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_t := rosa.Native().Std()
|
||||
qemuMeta, _ := _t.Load(rosa.H("qemu"))
|
||||
glibMeta, _ := _t.Load(rosa.H("glib"))
|
||||
zlibMeta, zlib := _t.Load(rosa.H("zlib"))
|
||||
zstdMeta, _ := _t.Load(rosa.H("zstd"))
|
||||
hakureiMeta, _ := _t.Load(rosa.H("hakurei"))
|
||||
hakureiDistMeta, _ := _t.Load(rosa.H("hakurei-dist"))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
status map[string]string
|
||||
report string
|
||||
want string
|
||||
wantErr any
|
||||
}{
|
||||
{"qemu", []string{"qemu"}, nil, "", `
|
||||
name : qemu-` + qemuMeta.Version + `
|
||||
description : a generic and open source machine emulator and virtualizer
|
||||
website : https://www.qemu.org
|
||||
depends on : glib-` + glibMeta.Version + ` zstd-` + zstdMeta.Version + `
|
||||
`, nil},
|
||||
|
||||
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
||||
name : hakurei-` + hakureiMeta.Version + `
|
||||
description : low-level userspace tooling for Rosa OS
|
||||
website : https://hakurei.app
|
||||
|
||||
name : hakurei-dist-` + hakureiDistMeta.Version + `
|
||||
description : low-level userspace tooling for Rosa OS (distribution tarball)
|
||||
website : https://hakurei.app
|
||||
`, nil},
|
||||
|
||||
{"nonexistent", []string{"zlib", "\x00"}, nil, "", `
|
||||
name : zlib-` + zlibMeta.Version + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
|
||||
`, fmt.Errorf("unknown artifact %q", "\x00")},
|
||||
|
||||
{"status cache", []string{"zlib", "zstd"}, map[string]string{
|
||||
"zstd": "internal/pkg (amd64) on satori\n",
|
||||
"hakurei": "internal/pkg (amd64) on satori\n\n",
|
||||
}, "", `
|
||||
name : zlib-` + zlibMeta.Version + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
status : not yet cured
|
||||
|
||||
name : zstd-` + zstdMeta.Version + `
|
||||
description : a fast compression algorithm
|
||||
website : https://facebook.github.io/zstd
|
||||
status : internal/pkg (amd64) on satori
|
||||
`, nil},
|
||||
|
||||
{"status cache perm", []string{"zlib"}, map[string]string{
|
||||
"zlib": "\x00",
|
||||
}, "", `
|
||||
name : zlib-` + zlibMeta.Version + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
`, func(cm *cache) error {
|
||||
return &os.PathError{
|
||||
Op: "open",
|
||||
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(zlib).Value())),
|
||||
Err: syscall.EACCES,
|
||||
}
|
||||
}},
|
||||
|
||||
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
|
||||
name : zlib-` + zlibMeta.Version + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
status : not in report
|
||||
`, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
cm *cache
|
||||
buf strings.Builder
|
||||
r *rosa.Report
|
||||
)
|
||||
|
||||
if tc.status != nil || tc.report != "" {
|
||||
cm = &cache{
|
||||
ctx: context.Background(),
|
||||
msg: message.New(log.New(os.Stderr, "info: ", 0)),
|
||||
base: t.TempDir(),
|
||||
}
|
||||
defer cm.Close()
|
||||
}
|
||||
|
||||
if tc.report != "" {
|
||||
pathname := filepath.Join(t.TempDir(), "report")
|
||||
err := os.WriteFile(
|
||||
pathname,
|
||||
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
|
||||
0400,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err = rosa.OpenReport(pathname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err = r.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if tc.status != nil {
|
||||
for name, status := range tc.status {
|
||||
_, a := _t.Load(rosa.ArtifactH(unique.Make(name)))
|
||||
if a == nil {
|
||||
t.Fatalf("invalid name %q", name)
|
||||
}
|
||||
perm := os.FileMode(0400)
|
||||
if status == "\x00" {
|
||||
perm = 0
|
||||
}
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
return os.WriteFile(filepath.Join(
|
||||
cm.base,
|
||||
"status",
|
||||
pkg.Encode(cache.Ident(a).Value()),
|
||||
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
|
||||
}); err != nil {
|
||||
t.Fatalf("Do: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wantErr error
|
||||
switch c := tc.wantErr.(type) {
|
||||
case error:
|
||||
wantErr = c
|
||||
case func(cm *cache) error:
|
||||
wantErr = c(cm)
|
||||
default:
|
||||
if tc.wantErr != nil {
|
||||
t.Fatalf("invalid wantErr %#v", tc.wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := commandInfo(
|
||||
cm,
|
||||
tc.args,
|
||||
&buf,
|
||||
cm != nil,
|
||||
r,
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
|
||||
if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
|
||||
t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// Package pkgserver implements the package metadata service backend.
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// for lazy initialisation of serveInfo
|
||||
var (
|
||||
infoPayload struct {
|
||||
// Current package count.
|
||||
Count int `json:"count"`
|
||||
// Hakurei version, set at link time.
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}
|
||||
infoPayloadOnce sync.Once
|
||||
)
|
||||
|
||||
// handleInfo writes constant system information.
|
||||
func handleInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
infoPayloadOnce.Do(func() {
|
||||
infoPayload.Count = len(rosa.Native().Collect())
|
||||
infoPayload.HakureiVersion = info.Version()
|
||||
})
|
||||
// TODO(mae): cache entire response if no additional fields are planned
|
||||
writeAPIPayload(w, infoPayload)
|
||||
}
|
||||
|
||||
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
|
||||
// viewing or download, if available.
|
||||
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := index.names[path.Base(r.URL.Path)]
|
||||
if !ok || !m.HasReport {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := "text/plain; charset=utf-8"
|
||||
if disposition {
|
||||
contentType = "application/octet-stream"
|
||||
|
||||
// quoting like this is unsound, but okay, because metadata is hardcoded
|
||||
contentDisposition := `attachment; filename="`
|
||||
contentDisposition += m.Name + "-"
|
||||
if m.Version != "" {
|
||||
contentDisposition += m.Version + "-"
|
||||
}
|
||||
contentDisposition += m.ids + `.log"`
|
||||
w.Header().Set("Content-Disposition", contentDisposition)
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := func() (err error) {
|
||||
defer index.handleAccess(&err)()
|
||||
_, err = w.Write(m.status)
|
||||
return
|
||||
}(); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(
|
||||
w, "cannot deliver status, contact maintainers",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleGet writes a slice of metadata with specified order.
|
||||
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil || limit > 100 || limit < 1 {
|
||||
http.Error(
|
||||
w, "limit must be an integer between 1 and 100",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
i, err := strconv.Atoi(q.Get("index"))
|
||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||
http.Error(
|
||||
w, "index must be an integer between 0 and "+
|
||||
strconv.Itoa(len(index.sorts[0])-1),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
sort, err := strconv.Atoi(q.Get("sort"))
|
||||
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
||||
http.Error(
|
||||
w, "sort must be an integer between 0 and "+
|
||||
strconv.Itoa(sortOrderEnd),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
||||
writeAPIPayload(w, &struct {
|
||||
Values []*metadata `json:"values"`
|
||||
}{values})
|
||||
}
|
||||
|
||||
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil || limit > 100 || limit < 1 {
|
||||
http.Error(
|
||||
w, "limit must be an integer between 1 and 100",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
i, err := strconv.Atoi(q.Get("index"))
|
||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||
http.Error(
|
||||
w, "index must be an integer between 0 and "+
|
||||
strconv.Itoa(len(index.sorts[0])-1),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
search, err := url.QueryUnescape(q.Get("search"))
|
||||
if len(search) > 100 || err != nil {
|
||||
http.Error(
|
||||
w, "search must be a string between 0 and 100 characters long",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
desc := q.Get("desc") == "true"
|
||||
n, res, err := index.performSearchQuery(limit, i, search, desc)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
writeAPIPayload(w, &struct {
|
||||
Count int `json:"count"`
|
||||
Values []searchResult `json:"values"`
|
||||
}{n, res})
|
||||
}
|
||||
|
||||
// apiVersion is the name of the current API revision, as part of the pattern.
|
||||
const apiVersion = "v1"
|
||||
|
||||
// registerAPI registers API handler functions.
|
||||
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
|
||||
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
|
||||
}
|
||||
|
||||
// Register arranges for mux to service API requests.
|
||||
func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
|
||||
var index packageIndex
|
||||
index.search = make(searchCache)
|
||||
if err := index.populate(report); err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
index.search.clean()
|
||||
}
|
||||
}
|
||||
}()
|
||||
index.registerAPI(mux)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeAPIPayload sets headers common to API responses and encodes payload as
|
||||
// JSON for the response body.
|
||||
func writeAPIPayload(w http.ResponseWriter, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(
|
||||
w, "cannot encode payload, contact maintainers",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// prefix is prepended to every API path.
|
||||
const prefix = "/api/" + apiVersion + "/"
|
||||
|
||||
func TestAPIInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handleInfo(w, httptest.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodGet,
|
||||
prefix+"info",
|
||||
nil,
|
||||
))
|
||||
|
||||
resp := w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
checkAPIHeader(t, w.Header())
|
||||
|
||||
checkPayload(t, resp, struct {
|
||||
Count int `json:"count"`
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}{len(rosa.Native().Collect()), info.Version()})
|
||||
}
|
||||
|
||||
func TestAPIGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
const target = prefix + "get"
|
||||
|
||||
index := newIndex(t)
|
||||
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
||||
w := httptest.NewRecorder()
|
||||
index.handleGet(w, httptest.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodGet,
|
||||
target+suffix,
|
||||
nil,
|
||||
))
|
||||
return w
|
||||
}
|
||||
|
||||
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=invalid")
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("min", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
|
||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
||||
resp = w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("max", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
|
||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
||||
resp = w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "index=0&sort=0&limit", 1, 100,
|
||||
"limit must be an integer between 1 and 100",
|
||||
)
|
||||
})
|
||||
|
||||
count := len(rosa.Native().Collect())
|
||||
t.Run("index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "limit=1&sort=0&index", 0, count-1,
|
||||
"index must be an integer between 0 and "+strconv.Itoa(count-1),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("sort", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
||||
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
const (
|
||||
declarationAscending = iota
|
||||
declarationDescending
|
||||
nameAscending
|
||||
nameDescending
|
||||
sizeAscending
|
||||
sizeDescending
|
||||
|
||||
sortOrderEnd = iota - 1
|
||||
)
|
||||
|
||||
// packageIndex refers to metadata by name and various sort orders.
|
||||
type packageIndex struct {
|
||||
sorts [sortOrderEnd + 1][]*metadata
|
||||
names map[string]*metadata
|
||||
search searchCache
|
||||
// Taken from [rosa.Report] if available.
|
||||
handleAccess func(*error) func()
|
||||
}
|
||||
|
||||
// metadata holds [rosa.Metadata] extended with additional information.
|
||||
type metadata struct {
|
||||
handle rosa.ArtifactH
|
||||
*rosa.Metadata
|
||||
|
||||
// Copied from [rosa.Metadata], [rosa.Unversioned] is equivalent to the zero
|
||||
// value. Otherwise, the zero value is invalid.
|
||||
Version string `json:"version,omitempty"`
|
||||
// Output data size, available if present in report.
|
||||
Size int64 `json:"size,omitempty"`
|
||||
// Whether the underlying [pkg.Artifact] is present in the report.
|
||||
HasReport bool `json:"report"`
|
||||
|
||||
// Ident string encoded ahead of time.
|
||||
ids string
|
||||
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
||||
status []byte
|
||||
}
|
||||
|
||||
// populate deterministically populates packageIndex, optionally with a report.
|
||||
func (index *packageIndex) populate(report *rosa.Report) (err error) {
|
||||
if report != nil {
|
||||
defer report.HandleAccess(&err)()
|
||||
index.handleAccess = report.HandleAccess
|
||||
}
|
||||
|
||||
handles := rosa.Native().Collect()
|
||||
work := make([]*metadata, len(handles))
|
||||
index.names = make(map[string]*metadata)
|
||||
ir := pkg.NewIR()
|
||||
for i, handle := range handles {
|
||||
meta, a := rosa.Native().Std().MustLoad(handle)
|
||||
m := metadata{
|
||||
handle: handle,
|
||||
|
||||
Metadata: meta,
|
||||
Version: meta.Version,
|
||||
}
|
||||
if m.Version == "" {
|
||||
return errors.New("invalid version from " + m.Name)
|
||||
}
|
||||
if m.Version == rosa.Unversioned {
|
||||
m.Version = ""
|
||||
}
|
||||
|
||||
if report != nil {
|
||||
id := ir.Ident(a)
|
||||
m.ids = pkg.Encode(id.Value())
|
||||
m.status, m.Size = report.ArtifactOf(id)
|
||||
m.HasReport = m.Size >= 0
|
||||
}
|
||||
|
||||
work[i] = &m
|
||||
index.names[m.Name] = &m
|
||||
}
|
||||
|
||||
index.sorts[declarationAscending] = work
|
||||
index.sorts[declarationDescending] = slices.Clone(work)
|
||||
slices.Reverse(index.sorts[declarationDescending][:])
|
||||
|
||||
index.sorts[nameAscending] = slices.Clone(work)
|
||||
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
index.sorts[nameDescending] = slices.Clone(index.sorts[nameAscending])
|
||||
slices.Reverse(index.sorts[nameDescending][:])
|
||||
|
||||
index.sorts[sizeAscending] = slices.Clone(work)
|
||||
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
||||
return cmp.Compare(a.Size, b.Size)
|
||||
})
|
||||
index.sorts[sizeDescending] = slices.Clone(index.sorts[sizeAscending])
|
||||
slices.Reverse(index.sorts[sizeDescending][:])
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newIndex returns the address of a newly populated packageIndex.
|
||||
func newIndex(t *testing.T) *packageIndex {
|
||||
t.Helper()
|
||||
|
||||
var index packageIndex
|
||||
if err := index.populate(nil); err != nil {
|
||||
t.Fatalf("populate: error = %v", err)
|
||||
}
|
||||
return &index
|
||||
}
|
||||
|
||||
// checkStatus checks response status code.
|
||||
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
||||
t.Helper()
|
||||
|
||||
if resp.StatusCode != want {
|
||||
t.Errorf(
|
||||
"StatusCode: %s, want %s",
|
||||
http.StatusText(resp.StatusCode),
|
||||
http.StatusText(want),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// checkHeader checks the value of a header entry.
|
||||
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
||||
t.Helper()
|
||||
|
||||
if got := h.Get(key); got != want {
|
||||
t.Errorf("%s: %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAPIHeader checks common entries set for API endpoints.
|
||||
func checkAPIHeader(t *testing.T, h http.Header) {
|
||||
t.Helper()
|
||||
|
||||
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
||||
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
checkHeader(t, h, "Pragma", "no-cache")
|
||||
checkHeader(t, h, "Expires", "0")
|
||||
}
|
||||
|
||||
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
||||
func checkPayloadFunc[T any](
|
||||
t *testing.T,
|
||||
resp *http.Response,
|
||||
f func(got *T) bool,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
var got T
|
||||
r := io.Reader(resp.Body)
|
||||
if testing.Verbose() {
|
||||
var buf bytes.Buffer
|
||||
r = io.TeeReader(r, &buf)
|
||||
defer func() { t.Helper(); t.Log(buf.String()) }()
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
||||
t.Fatalf("Decode: error = %v", err)
|
||||
}
|
||||
|
||||
if !f(&got) {
|
||||
t.Errorf("Body: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPayload checks the JSON response of an API endpoint.
|
||||
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
||||
t.Helper()
|
||||
|
||||
checkPayloadFunc(t, resp, func(got *T) bool {
|
||||
return reflect.DeepEqual(got, &want)
|
||||
})
|
||||
}
|
||||
|
||||
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
||||
t.Helper()
|
||||
|
||||
checkStatus(t, resp, code)
|
||||
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
||||
t.Errorf("Body: %q, want %q", string(got), error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type searchCache map[string]searchCacheEntry
|
||||
type searchResult struct {
|
||||
NameIndices [][]int `json:"name_matches"`
|
||||
DescIndices [][]int `json:"desc_matches,omitempty"`
|
||||
Score float64 `json:"score"`
|
||||
*metadata
|
||||
}
|
||||
type searchCacheEntry struct {
|
||||
query string
|
||||
results []searchResult
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
|
||||
query := search
|
||||
if desc {
|
||||
query += ";withDesc"
|
||||
}
|
||||
entry, ok := index.search[query]
|
||||
if ok && len(entry.results) > 0 {
|
||||
return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
return 0, make([]searchResult, 0), err
|
||||
}
|
||||
res := make([]searchResult, 0)
|
||||
for p := range maps.Values(index.names) {
|
||||
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
|
||||
var descIndices [][]int = nil
|
||||
if desc {
|
||||
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
|
||||
}
|
||||
if nameIndices == nil && descIndices == nil {
|
||||
continue
|
||||
}
|
||||
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
|
||||
if desc {
|
||||
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
|
||||
}
|
||||
res = append(res, searchResult{
|
||||
NameIndices: nameIndices,
|
||||
DescIndices: descIndices,
|
||||
Score: score,
|
||||
metadata: p,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
|
||||
expiry := time.Now().Add(1 * time.Minute)
|
||||
entry = searchCacheEntry{
|
||||
query: search,
|
||||
results: res,
|
||||
expiry: expiry,
|
||||
}
|
||||
index.search[query] = entry
|
||||
|
||||
return len(res), res[i:min(i+limit, len(entry.results))], nil
|
||||
}
|
||||
func (s *searchCache) clean() {
|
||||
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
|
||||
return v.expiry.Before(time.Now())
|
||||
})
|
||||
}
|
||||
func indexsum(in [][]int) int {
|
||||
sum := 0
|
||||
for i := range in {
|
||||
sum += in[i][1] - in[i][0]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="icon" href="https://hakurei.app/favicon.ico"/>
|
||||
<title>Rosa OS Packages</title>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rosa OS Packages</h1>
|
||||
<div class="top-controls" id="top-controls-regular">
|
||||
<p>Showing entries <span id="entry-counter"></span>.</p>
|
||||
<span id="search-bar">
|
||||
<label for="search">Search: </label>
|
||||
<input type="text" name="search" id="search"/>
|
||||
<button onclick="doSearch()">Find</button>
|
||||
<label for="include-desc">Include descriptions: </label>
|
||||
<input type="checkbox" name="include-desc" id="include-desc" checked/>
|
||||
</span>
|
||||
<div><label for="count">Entries per page: </label><select name="count" id="count">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="50">50</option>
|
||||
</select></div>
|
||||
<div><label for="sort">Sort by: </label><select name="sort" id="sort">
|
||||
<option value="0">Definition (ascending)</option>
|
||||
<option value="1">Definition (descending)</option>
|
||||
<option value="2">Name (ascending)</option>
|
||||
<option value="3">Name (descending)</option>
|
||||
<option value="4">Size (ascending)</option>
|
||||
<option value="5">Size (descending)</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="top-controls" id="search-top-controls" hidden>
|
||||
<p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
|
||||
<button onclick="exitSearch()">Back</button>
|
||||
<div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="50">50</option>
|
||||
</select></div>
|
||||
<p>Sorted by best match</p>
|
||||
</div>
|
||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||
<table id="pkg-list">
|
||||
<tr><td>Loading...</td></tr>
|
||||
</table>
|
||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||
<footer>
|
||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
||||
</footer>
|
||||
<script>main();</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,331 @@
|
||||
interface PackageIndexEntry {
|
||||
name: string
|
||||
size?: number
|
||||
description?: string
|
||||
website?: string
|
||||
version?: string
|
||||
report?: boolean
|
||||
}
|
||||
|
||||
function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
|
||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
||||
let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
|
||||
let n: string
|
||||
let d: string
|
||||
if ('name_matches' in entry) {
|
||||
n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
|
||||
} else {
|
||||
n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
|
||||
}
|
||||
if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
|
||||
d = descMatches(entry as SearchResult)
|
||||
} else {
|
||||
d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
|
||||
}
|
||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
||||
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
|
||||
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
||||
row.innerHTML = `<td>
|
||||
${n}
|
||||
${d}
|
||||
${s}
|
||||
${w}
|
||||
${r}
|
||||
</td>`
|
||||
return row
|
||||
}
|
||||
|
||||
function nameMatches(sr: SearchResult): string {
|
||||
return markMatches(sr.name, sr.name_matches)
|
||||
}
|
||||
|
||||
function descMatches(sr: SearchResult): string {
|
||||
return markMatches(sr.description!, sr.desc_matches)
|
||||
}
|
||||
|
||||
function markMatches(str: string, indices: [number, number][]): string {
|
||||
if (indices == null) {
|
||||
return str
|
||||
}
|
||||
let out: string = ""
|
||||
let j = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (j < indices.length) {
|
||||
if (i === indices[j][0]) {
|
||||
out += `<mark>${escapeHtmlChar(str[i])}`
|
||||
continue
|
||||
}
|
||||
if (i === indices[j][1]) {
|
||||
out += `</mark>${escapeHtmlChar(str[i])}`
|
||||
j++
|
||||
continue
|
||||
}
|
||||
}
|
||||
out += escapeHtmlChar(str[i])
|
||||
}
|
||||
if (indices[j] !== undefined) {
|
||||
out += "</mark>"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function toByteSizeString(bytes: number): string {
|
||||
if (bytes == null) return `unspecified`
|
||||
if (bytes < 1024) return `${bytes}B`
|
||||
if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
|
||||
if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
|
||||
if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
|
||||
if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
|
||||
return "not only is it big, it's large"
|
||||
}
|
||||
|
||||
const API_VERSION = 1
|
||||
const ENDPOINT = `/api/v${API_VERSION}`
|
||||
|
||||
interface InfoPayload {
|
||||
count?: number
|
||||
hakurei_version?: string
|
||||
}
|
||||
|
||||
async function infoRequest(): Promise<InfoPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/info`)
|
||||
const payload = await res.json()
|
||||
return payload as InfoPayload
|
||||
}
|
||||
|
||||
interface GetPayload {
|
||||
values?: PackageIndexEntry[]
|
||||
}
|
||||
|
||||
enum SortOrders {
|
||||
DeclarationAscending,
|
||||
DeclarationDescending,
|
||||
NameAscending,
|
||||
NameDescending
|
||||
}
|
||||
|
||||
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
||||
const payload = await res.json()
|
||||
return payload as GetPayload
|
||||
}
|
||||
|
||||
interface SearchResult extends PackageIndexEntry {
|
||||
name_matches: [number, number][]
|
||||
desc_matches: [number, number][]
|
||||
score: number
|
||||
}
|
||||
|
||||
interface SearchPayload {
|
||||
count?: number
|
||||
values?: SearchResult[]
|
||||
}
|
||||
|
||||
async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
|
||||
if (!res.ok) {
|
||||
exitSearch()
|
||||
alert("invalid search query!")
|
||||
return Promise.reject(res.statusText)
|
||||
}
|
||||
const payload = await res.json()
|
||||
return payload as SearchPayload
|
||||
}
|
||||
|
||||
class State {
|
||||
entriesPerPage: number = 10
|
||||
entryIndex: number = 0
|
||||
maxTotal: number = 0
|
||||
maxEntries: number = 0
|
||||
sort: SortOrders = SortOrders.DeclarationAscending
|
||||
search: boolean = false
|
||||
|
||||
getEntriesPerPage(): number {
|
||||
return this.entriesPerPage
|
||||
}
|
||||
|
||||
setEntriesPerPage(entriesPerPage: number) {
|
||||
this.entriesPerPage = entriesPerPage
|
||||
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
|
||||
}
|
||||
|
||||
getEntryIndex(): number {
|
||||
return this.entryIndex
|
||||
}
|
||||
|
||||
setEntryIndex(entryIndex: number) {
|
||||
this.entryIndex = entryIndex
|
||||
this.updatePage()
|
||||
this.updateRange()
|
||||
this.updateListings()
|
||||
}
|
||||
|
||||
getMaxTotal(): number {
|
||||
return this.maxTotal
|
||||
}
|
||||
|
||||
setMaxTotal(max: number) {
|
||||
this.maxTotal = max
|
||||
}
|
||||
|
||||
getSortOrder(): SortOrders {
|
||||
return this.sort
|
||||
}
|
||||
|
||||
setSortOrder(sortOrder: SortOrders) {
|
||||
this.sort = sortOrder
|
||||
this.setEntryIndex(0)
|
||||
}
|
||||
|
||||
updatePage() {
|
||||
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
|
||||
for (let e of document.getElementsByClassName("page-number")) {
|
||||
(e as HTMLInputElement).value = String(page)
|
||||
}
|
||||
}
|
||||
|
||||
updateRange() {
|
||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
|
||||
document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
|
||||
if (this.search) {
|
||||
document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
|
||||
document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
|
||||
}
|
||||
}
|
||||
|
||||
getSearchQuery(): string {
|
||||
let queryString = document.getElementById("search")!;
|
||||
return (queryString as HTMLInputElement).value
|
||||
}
|
||||
|
||||
getIncludeDescriptions(): boolean {
|
||||
let includeDesc = document.getElementById("include-desc")!;
|
||||
return (includeDesc as HTMLInputElement).checked
|
||||
}
|
||||
|
||||
updateListings() {
|
||||
if (this.search) {
|
||||
searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list")!
|
||||
table.innerHTML = ''
|
||||
for (let row of res.values!) {
|
||||
table.appendChild(entryToHTML(row))
|
||||
}
|
||||
STATE.maxTotal = res.count!
|
||||
STATE.updateRange()
|
||||
if(res.count! < 1) {
|
||||
exitSearch()
|
||||
alert("no results found!")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list")!
|
||||
table.innerHTML = ''
|
||||
for (let row of res.values!) {
|
||||
table.appendChild(entryToHTML(row))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let STATE: State
|
||||
|
||||
|
||||
function lastPageIndex(): number {
|
||||
return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
|
||||
}
|
||||
|
||||
function setPage(page: number) {
|
||||
STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
|
||||
}
|
||||
|
||||
|
||||
function escapeHtml(str?: string): string {
|
||||
let out: string = ''
|
||||
if (str == undefined) return ""
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
out += escapeHtmlChar(str[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function escapeHtmlChar(char: string): string {
|
||||
if (char.length != 1) return char
|
||||
switch (char[0]) {
|
||||
case '&':
|
||||
return "&"
|
||||
case '<':
|
||||
return "<"
|
||||
case '>':
|
||||
return ">"
|
||||
case '"':
|
||||
return """
|
||||
case "'":
|
||||
return "'"
|
||||
default:
|
||||
return char
|
||||
}
|
||||
}
|
||||
|
||||
function firstPage() {
|
||||
STATE.setEntryIndex(0)
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
STATE.setEntryIndex(lastPageIndex())
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
||||
STATE.search = true;
|
||||
STATE.setEntryIndex(0);
|
||||
}
|
||||
|
||||
function exitSearch() {
|
||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
||||
STATE.search = false;
|
||||
STATE.setMaxTotal(STATE.maxEntries)
|
||||
STATE.setEntryIndex(0)
|
||||
}
|
||||
|
||||
function main() {
|
||||
STATE = new State()
|
||||
infoRequest()
|
||||
.then(res => {
|
||||
STATE.maxEntries = res.count!
|
||||
STATE.setMaxTotal(STATE.maxEntries)
|
||||
document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
|
||||
STATE.updateRange()
|
||||
STATE.updateListings()
|
||||
})
|
||||
for (let e of document.getElementsByClassName("page-number")) {
|
||||
e.addEventListener("change", (_) => {
|
||||
setPage(parseInt((e as HTMLInputElement).value))
|
||||
})
|
||||
}
|
||||
document.getElementById("count")?.addEventListener("change", (event) => {
|
||||
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
document.getElementById("sort")?.addEventListener("change", (event) => {
|
||||
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
document.getElementById("search")?.addEventListener("keyup", (event) => {
|
||||
if (event.key === 'Enter') doSearch()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.page-number {
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.page-number {
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #2c2c2c;
|
||||
color: ghostwhite;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background-color: #d3d3d3;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"outDir": "static"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Package ui holds the static web UI.
|
||||
package ui
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Register arranges for mux to serve the embedded frontend.
|
||||
func Register(mux *http.ServeMux) {
|
||||
mux.Handle("GET /", http.FileServer(http.FS(static)))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build frontend
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:generate tsc
|
||||
//go:generate cp index.html style.css static
|
||||
//go:embed static
|
||||
var _static embed.FS
|
||||
|
||||
var static = func() fs.FS {
|
||||
if f, err := fs.Sub(_static, "static"); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return f
|
||||
}
|
||||
}()
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !frontend
|
||||
|
||||
package ui
|
||||
|
||||
import "testing/fstest"
|
||||
|
||||
var static fstest.MapFS
|
||||
+1038
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rosa.Native().DropCaches("", rosa.OptLLVMNoLTO)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestCureAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
const env = "ROSA_TEST_DAEMON"
|
||||
|
||||
if !testing.Verbose() {
|
||||
t.Skip("verbose flag not set")
|
||||
}
|
||||
|
||||
pathname, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
t.Skip(env + " not set")
|
||||
}
|
||||
|
||||
addr := net.UnixAddr{Net: "unix", Name: pathname}
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
if err := abortRemote(t.Context(), &addr, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for _, handle := range rosa.Native().Collect() {
|
||||
_, a := rosa.Native().Std().MustLoad(handle)
|
||||
t.Run(handle.String(), func(t *testing.T) {
|
||||
_, err := cureRemote(t.Context(), &addr, a, 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE /* O_DIRECT */
|
||||
#endif
|
||||
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* TODO(ophestra): remove after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||
#include <sys/syscall.h>
|
||||
|
||||
#include "fuse-operations.h"
|
||||
|
||||
/* MUST_TRANSLATE_PATHNAME translates a userspace pathname to a relative pathname;
|
||||
* the resulting address points to a constant string or part of pathname, it is never heap allocated. */
|
||||
#define MUST_TRANSLATE_PATHNAME(pathname) \
|
||||
do { \
|
||||
if (pathname == NULL) \
|
||||
return -EINVAL; \
|
||||
while (*pathname == '/') \
|
||||
pathname++; \
|
||||
if (*pathname == '\0') \
|
||||
pathname = "."; \
|
||||
} while (0)
|
||||
|
||||
/* GET_CONTEXT_PRIV obtains fuse context and private data for the calling thread. */
|
||||
#define GET_CONTEXT_PRIV(ctx, priv) \
|
||||
do { \
|
||||
ctx = fuse_get_context(); \
|
||||
priv = ctx->private_data; \
|
||||
} while (0)
|
||||
|
||||
/* impl_getattr modifies a struct stat from the kernel to present to userspace;
|
||||
* impl_getattr returns a negative errno style error code. */
|
||||
static int impl_getattr(struct fuse_context *ctx, struct stat *statbuf) {
|
||||
/* allowlist of permitted types */
|
||||
if (!S_ISDIR(statbuf->st_mode) && !S_ISREG(statbuf->st_mode) && !S_ISLNK(statbuf->st_mode)) {
|
||||
return -ENOTRECOVERABLE; /* returning an errno causes all operations on the file to return EIO */
|
||||
}
|
||||
|
||||
#define OVERRIDE_PERM(v) (statbuf->st_mode & ~0777) | (v & 0777)
|
||||
if (S_ISDIR(statbuf->st_mode))
|
||||
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_DIR);
|
||||
else if (S_ISREG(statbuf->st_mode))
|
||||
statbuf->st_mode = OVERRIDE_PERM(SHAREFS_PERM_REG);
|
||||
else
|
||||
statbuf->st_mode = 0; /* should always be symlink in this case */
|
||||
|
||||
statbuf->st_uid = ctx->uid;
|
||||
statbuf->st_gid = SHAREFS_MEDIA_RW_ID;
|
||||
statbuf->st_ctim = statbuf->st_mtim;
|
||||
statbuf->st_nlink = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* fuse_operations implementation */
|
||||
|
||||
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)fi;
|
||||
|
||||
if (fstatat(priv->dirfd, pathname, statbuf, AT_SYMLINK_NOFOLLOW) == -1)
|
||||
return -errno;
|
||||
return impl_getattr(ctx, statbuf);
|
||||
}
|
||||
|
||||
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags) {
|
||||
int fd;
|
||||
DIR *dp;
|
||||
struct stat st;
|
||||
int ret = 0;
|
||||
struct dirent *de;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)offset;
|
||||
(void)fi;
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_DIRECTORY | O_CLOEXEC)) == -1)
|
||||
return -errno;
|
||||
if ((dp = fdopendir(fd)) == NULL) {
|
||||
close(fd);
|
||||
return -errno;
|
||||
}
|
||||
|
||||
errno = 0; /* for the next readdir call */
|
||||
while ((de = readdir(dp)) != NULL) {
|
||||
if (flags & FUSE_READDIR_PLUS) {
|
||||
if (fstatat(dirfd(dp), de->d_name, &st, AT_SYMLINK_NOFOLLOW) == -1) {
|
||||
ret = -errno;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((ret = impl_getattr(ctx, &st)) < 0)
|
||||
break;
|
||||
|
||||
errno = 0;
|
||||
ret = filler(buf, de->d_name, &st, 0, FUSE_FILL_DIR_PLUS);
|
||||
} else
|
||||
ret = filler(buf, de->d_name, NULL, 0, 0);
|
||||
|
||||
if (ret != 0) {
|
||||
ret = errno != 0 ? -errno : -EIO; /* filler */
|
||||
break;
|
||||
}
|
||||
|
||||
errno = 0; /* for the next readdir call */
|
||||
}
|
||||
if (ret == 0 && errno != 0)
|
||||
ret = -errno; /* readdir */
|
||||
|
||||
closedir(dp);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_mkdir(const char *pathname, mode_t mode) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)mode;
|
||||
|
||||
if (mkdirat(priv->dirfd, pathname, SHAREFS_PERM_DIR) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_unlink(const char *pathname) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if (unlinkat(priv->dirfd, pathname, 0) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_rmdir(const char *pathname) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if (unlinkat(priv->dirfd, pathname, AT_REMOVEDIR) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(oldpath);
|
||||
MUST_TRANSLATE_PATHNAME(newpath);
|
||||
|
||||
/* TODO(ophestra): replace with wrapper after 05ce67fea99ca09cd4b6625cff7aec9cc222dd5a reaches a release */
|
||||
if (syscall(__NR_renameat2, priv->dirfd, oldpath, priv->dirfd, newpath, flags) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi) {
|
||||
int fd;
|
||||
int ret;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)fi;
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, O_WRONLY | O_CLOEXEC)) == -1)
|
||||
return -errno;
|
||||
if ((ret = ftruncate(fd, length)) == -1)
|
||||
ret = -errno;
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi) {
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)fi;
|
||||
|
||||
if (utimensat(priv->dirfd, pathname, times, AT_SYMLINK_NOFOLLOW) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi) {
|
||||
int fd;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
(void)mode;
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS, SHAREFS_PERM_REG)) == -1)
|
||||
return -errno;
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_open(const char *pathname, struct fuse_file_info *fi) {
|
||||
int fd;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, fi->flags & ~SHAREFS_FORBIDDEN_FLAGS)) == -1)
|
||||
return -errno;
|
||||
fi->fh = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||
int ret;
|
||||
|
||||
(void)pathname;
|
||||
|
||||
if ((ret = pread(fi->fh, buf, count, offset)) == -1)
|
||||
return -errno;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi) {
|
||||
int ret;
|
||||
|
||||
(void)pathname;
|
||||
|
||||
if ((ret = pwrite(fi->fh, buf, count, offset)) == -1)
|
||||
return -errno;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_statfs(const char *pathname, struct statvfs *statbuf) {
|
||||
int fd;
|
||||
int ret;
|
||||
|
||||
struct fuse_context *ctx;
|
||||
struct sharefs_private *priv;
|
||||
GET_CONTEXT_PRIV(ctx, priv);
|
||||
MUST_TRANSLATE_PATHNAME(pathname);
|
||||
|
||||
if ((fd = openat(priv->dirfd, pathname, O_RDONLY | O_CLOEXEC)) == -1)
|
||||
return -errno;
|
||||
if ((ret = fstatvfs(fd, statbuf)) == -1)
|
||||
ret = -errno;
|
||||
close(fd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int sharefs_release(const char *pathname, struct fuse_file_info *fi) {
|
||||
(void)pathname;
|
||||
|
||||
return close(fi->fh);
|
||||
}
|
||||
|
||||
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi) {
|
||||
(void)pathname;
|
||||
|
||||
if (datasync ? fdatasync(fi->fh) : fsync(fi->fh) == -1)
|
||||
return -errno;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#define FUSE_USE_VERSION FUSE_MAKE_VERSION(3, 12)
|
||||
#include <fuse.h>
|
||||
#include <fuse_lowlevel.h> /* for fuse_cmdline_help */
|
||||
|
||||
#if (FUSE_VERSION < FUSE_MAKE_VERSION(3, 12))
|
||||
#error This package requires libfuse >= v3.12
|
||||
#endif
|
||||
|
||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||
|
||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||
struct sharefs_private {
|
||||
int dirfd; /* source dirfd opened during sharefs_init */
|
||||
uintptr_t setup; /* cgo handle of opaque setup state */
|
||||
};
|
||||
|
||||
int sharefs_getattr(const char *pathname, struct stat *statbuf, struct fuse_file_info *fi);
|
||||
int sharefs_readdir(const char *pathname, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags);
|
||||
int sharefs_mkdir(const char *pathname, mode_t mode);
|
||||
int sharefs_unlink(const char *pathname);
|
||||
int sharefs_rmdir(const char *pathname);
|
||||
int sharefs_rename(const char *oldpath, const char *newpath, unsigned int flags);
|
||||
int sharefs_truncate(const char *pathname, off_t length, struct fuse_file_info *fi);
|
||||
int sharefs_utimens(const char *pathname, const struct timespec times[2], struct fuse_file_info *fi);
|
||||
int sharefs_create(const char *pathname, mode_t mode, struct fuse_file_info *fi);
|
||||
int sharefs_open(const char *pathname, struct fuse_file_info *fi);
|
||||
int sharefs_read(const char *pathname, char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||
int sharefs_write(const char *pathname, const char *buf, size_t count, off_t offset, struct fuse_file_info *fi);
|
||||
int sharefs_statfs(const char *pathname, struct statvfs *statbuf);
|
||||
int sharefs_release(const char *pathname, struct fuse_file_info *fi);
|
||||
int sharefs_fsync(const char *pathname, int datasync, struct fuse_file_info *fi);
|
||||
@@ -0,0 +1,570 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo pkg-config: --static fuse3
|
||||
|
||||
#include "fuse-operations.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void *sharefs_init(struct fuse_conn_info *conn, struct fuse_config *cfg);
|
||||
extern void sharefs_destroy(void *private_data);
|
||||
|
||||
typedef void (*closure)();
|
||||
static inline struct fuse_opt _FUSE_OPT_END() { return (struct fuse_opt)FUSE_OPT_END; };
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/helper/proc"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
type (
|
||||
// closure represents a C function pointer.
|
||||
closure = C.closure
|
||||
|
||||
// fuseArgs represents the fuse_args structure.
|
||||
fuseArgs = C.struct_fuse_args
|
||||
|
||||
// setupState holds state used for setup. Its cgo handle is included in
|
||||
// sharefs_private and considered opaque to non-setup callbacks.
|
||||
setupState struct {
|
||||
// Whether sharefs_init failed.
|
||||
initFailed bool
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir bool
|
||||
|
||||
// Open file descriptor to fuse.
|
||||
Fuse int
|
||||
|
||||
// Pathname to open for dirfd.
|
||||
Source *check.Absolute
|
||||
// New uid and gid to set by sharefs_init when starting as root.
|
||||
Setuid, Setgid int
|
||||
}
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(setupState)) }
|
||||
|
||||
// destroySetup invalidates the setup [cgo.Handle] in a sharefs_private structure.
|
||||
func destroySetup(private_data unsafe.Pointer) (ok bool) {
|
||||
if private_data == nil {
|
||||
return false
|
||||
}
|
||||
priv := (*C.struct_sharefs_private)(private_data)
|
||||
|
||||
if h := cgo.Handle(priv.setup); h != 0 {
|
||||
priv.setup = 0
|
||||
h.Delete()
|
||||
ok = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//export sharefs_init
|
||||
func sharefs_init(
|
||||
_ *C.struct_fuse_conn_info,
|
||||
cfg *C.struct_fuse_config,
|
||||
) unsafe.Pointer {
|
||||
ctx := C.fuse_get_context()
|
||||
priv := (*C.struct_sharefs_private)(ctx.private_data)
|
||||
setup := cgo.Handle(priv.setup).Value().(*setupState)
|
||||
|
||||
if os.Geteuid() == 0 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
goto fail
|
||||
}
|
||||
|
||||
cfg.use_ino = C.true
|
||||
cfg.direct_io = C.false
|
||||
// getattr is context-dependent
|
||||
cfg.attr_timeout = 0
|
||||
cfg.entry_timeout = 0
|
||||
cfg.negative_timeout = 0
|
||||
|
||||
// all future filesystem operations happen through this dirfd
|
||||
if fd, err := syscall.Open(
|
||||
setup.Source.String(),
|
||||
syscall.O_DIRECTORY|syscall.O_RDONLY|syscall.O_CLOEXEC,
|
||||
0,
|
||||
); err != nil {
|
||||
log.Printf("cannot open %q: %v", setup.Source, err)
|
||||
goto fail
|
||||
} else if err = syscall.Fchdir(fd); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
log.Printf("cannot enter %q: %s", setup.Source, err)
|
||||
goto fail
|
||||
} else {
|
||||
priv.dirfd = C.int(fd)
|
||||
}
|
||||
|
||||
return ctx.private_data
|
||||
|
||||
fail:
|
||||
setup.initFailed = true
|
||||
C.fuse_exit(ctx.fuse)
|
||||
return nil
|
||||
}
|
||||
|
||||
//export sharefs_destroy
|
||||
func sharefs_destroy(private_data unsafe.Pointer) {
|
||||
if private_data != nil {
|
||||
destroySetup(private_data)
|
||||
priv := (*C.struct_sharefs_private)(private_data)
|
||||
|
||||
if err := syscall.Close(int(priv.dirfd)); err != nil {
|
||||
log.Printf("cannot close source directory: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// showHelp prints the help message.
|
||||
func showHelp(args *fuseArgs) {
|
||||
executableName := sharefsName
|
||||
if args.argc > 0 {
|
||||
executableName = filepath.Base(C.GoString(*args.argv))
|
||||
} else if name, err := os.Executable(); err == nil {
|
||||
executableName = filepath.Base(name)
|
||||
}
|
||||
|
||||
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
||||
|
||||
fmt.Println("Filesystem options:")
|
||||
fmt.Println(" -o source=/data/media source directory to be mounted")
|
||||
fmt.Println(" -o setuid=1023 uid to run as when starting as root")
|
||||
fmt.Println(" -o setgid=1023 gid to run as when starting as root")
|
||||
|
||||
fmt.Println("\nFUSE options:")
|
||||
C.fuse_cmdline_help()
|
||||
C.fuse_lib_help(args)
|
||||
}
|
||||
|
||||
// parseOpts parses fuse options via fuse_opt_parse.
|
||||
func parseOpts(args *fuseArgs, setup *setupState, log *log.Logger) (ok bool) {
|
||||
var unsafeOpts struct {
|
||||
// Pathname to writable source directory.
|
||||
source *C.char
|
||||
|
||||
// Whether to create source directory as root.
|
||||
mkdir C.int
|
||||
|
||||
// Decimal string representation of uid to set when running as root.
|
||||
setuid *C.char
|
||||
// Decimal string representation of gid to set when running as root.
|
||||
setgid *C.char
|
||||
|
||||
// Decimal string representation of open file descriptor to read
|
||||
// setupState from.
|
||||
//
|
||||
// This is an internal detail for containerisation and must not be
|
||||
// specified directly.
|
||||
setup *C.char
|
||||
}
|
||||
|
||||
if C.fuse_opt_parse(args, unsafe.Pointer(&unsafeOpts), &[]C.struct_fuse_opt{
|
||||
{templ: C.CString("source=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.source)), value: 0},
|
||||
{templ: C.CString("mkdir"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.mkdir)), value: 1},
|
||||
{templ: C.CString("setuid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setuid)), value: 0},
|
||||
{templ: C.CString("setgid=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setgid)), value: 0},
|
||||
|
||||
{templ: C.CString("setup=%s"), offset: C.ulong(unsafe.Offsetof(unsafeOpts.setup)), value: 0},
|
||||
|
||||
C._FUSE_OPT_END(),
|
||||
}[0], nil) == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
if unsafeOpts.source != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.source))
|
||||
}
|
||||
if unsafeOpts.setuid != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setuid))
|
||||
}
|
||||
if unsafeOpts.setgid != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setgid))
|
||||
}
|
||||
|
||||
if unsafeOpts.setup != nil {
|
||||
defer C.free(unsafe.Pointer(unsafeOpts.setup))
|
||||
|
||||
if v, err := strconv.Atoi(C.GoString(unsafeOpts.setup)); err != nil || v < 3 {
|
||||
log.Println("invalid value for option setup")
|
||||
return false
|
||||
} else {
|
||||
r := os.NewFile(uintptr(v), "setup")
|
||||
defer func() {
|
||||
if err = r.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
if err = gob.NewDecoder(r).Decode(setup); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if setup.Fuse < 3 {
|
||||
log.Println("invalid file descriptor", setup.Fuse)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if unsafeOpts.source == nil {
|
||||
showHelp(args)
|
||||
return false
|
||||
} else if a, err := check.NewAbs(C.GoString(unsafeOpts.source)); err != nil {
|
||||
log.Println(err)
|
||||
return false
|
||||
} else {
|
||||
setup.Source = a
|
||||
}
|
||||
setup.mkdir = unsafeOpts.mkdir != 0
|
||||
|
||||
if unsafeOpts.setuid == nil {
|
||||
setup.Setuid = -1
|
||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setuid)); err != nil || v <= 0 {
|
||||
log.Println("invalid value for option setuid")
|
||||
return false
|
||||
} else {
|
||||
setup.Setuid = v
|
||||
}
|
||||
if unsafeOpts.setgid == nil {
|
||||
setup.Setgid = -1
|
||||
} else if v, err := strconv.Atoi(C.GoString(unsafeOpts.setgid)); err != nil || v <= 0 {
|
||||
log.Println("invalid value for option setgid")
|
||||
return false
|
||||
} else {
|
||||
setup.Setgid = v
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// copyArgs returns a heap allocated copy of an argument slice in fuse_args
|
||||
// representation.
|
||||
func copyArgs(s ...string) fuseArgs {
|
||||
if len(s) == 0 {
|
||||
return fuseArgs{argc: 0, argv: nil, allocated: 0}
|
||||
}
|
||||
args := unsafe.Slice((**C.char)(C.malloc(C.size_t(uintptr(len(s))*unsafe.Sizeof(s[0])))), len(s))
|
||||
for i, arg := range s {
|
||||
args[i] = C.CString(arg)
|
||||
}
|
||||
return fuseArgs{argc: C.int(len(s)), argv: &args[0], allocated: 1}
|
||||
}
|
||||
|
||||
// freeArgs frees the contents of argument list.
|
||||
func freeArgs(args *fuseArgs) { C.fuse_opt_free_args(args) }
|
||||
|
||||
// unsafeAddArgument adds an argument to fuseArgs via fuse_opt_add_arg.
|
||||
//
|
||||
// The last byte of arg must be 0.
|
||||
func unsafeAddArgument(args *fuseArgs, arg string) {
|
||||
C.fuse_opt_add_arg(args, (*C.char)(unsafe.Pointer(unsafe.StringData(arg))))
|
||||
}
|
||||
|
||||
func _main(s ...string) (exitCode int) {
|
||||
msg := message.New(log.Default())
|
||||
container.TryArgv0(msg)
|
||||
runtime.LockOSThread()
|
||||
|
||||
// don't mask creation mode, kernel already did that
|
||||
syscall.Umask(0)
|
||||
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
|
||||
args := copyArgs(s...)
|
||||
defer freeArgs(&args)
|
||||
|
||||
// this causes the kernel to enforce access control based on struct stat
|
||||
// populated by sharefs_getattr
|
||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||
|
||||
var priv C.struct_sharefs_private
|
||||
pinner.Pin(&priv)
|
||||
var setup setupState
|
||||
priv.setup = C.uintptr_t(cgo.NewHandle(&setup))
|
||||
defer destroySetup(unsafe.Pointer(&priv))
|
||||
|
||||
var opts C.struct_fuse_cmdline_opts
|
||||
if C.fuse_parse_cmdline(&args, &opts) != 0 {
|
||||
return 1
|
||||
}
|
||||
if opts.mountpoint != nil {
|
||||
defer C.free(unsafe.Pointer(opts.mountpoint))
|
||||
}
|
||||
|
||||
if opts.show_version != 0 {
|
||||
fmt.Println("hakurei version", info.Version())
|
||||
fmt.Println("FUSE library version", C.GoString(C.fuse_pkgversion()))
|
||||
C.fuse_lowlevel_version()
|
||||
return 0
|
||||
}
|
||||
|
||||
if opts.show_help != 0 {
|
||||
showHelp(&args)
|
||||
return 0
|
||||
} else if opts.mountpoint == nil {
|
||||
log.Println("no mountpoint specified")
|
||||
return 2
|
||||
} else {
|
||||
// hack to keep fuse_parse_cmdline happy in the container
|
||||
mountpoint := C.GoString(opts.mountpoint)
|
||||
pathnameArg := -1
|
||||
for i, arg := range s {
|
||||
if arg == mountpoint {
|
||||
pathnameArg = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if pathnameArg < 0 {
|
||||
log.Println("mountpoint must be absolute")
|
||||
return 2
|
||||
}
|
||||
s[pathnameArg] = container.Nonexistent
|
||||
}
|
||||
|
||||
if !parseOpts(&args, &setup, msg.GetLogger()) {
|
||||
return 1
|
||||
}
|
||||
asRoot := os.Geteuid() == 0
|
||||
|
||||
if asRoot {
|
||||
if setup.Setuid <= 0 || setup.Setgid <= 0 {
|
||||
log.Println("setuid and setgid must not be 0")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.Fuse >= 3 {
|
||||
log.Println("filesystem daemon must not run as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
if setup.mkdir {
|
||||
if err := os.MkdirAll(setup.Source.String(), 0700); err != nil {
|
||||
if !errors.Is(err, os.ErrExist) {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
// skip setup for existing source directory
|
||||
} else if err = os.Chown(setup.Source.String(), setup.Setuid, setup.Setgid); err != nil {
|
||||
log.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
} else if setup.Fuse < 3 && (setup.Setuid > 0 || setup.Setgid > 0) {
|
||||
log.Println("setuid and setgid has no effect when not starting as root")
|
||||
return 1
|
||||
} else if setup.mkdir {
|
||||
log.Println("mkdir has no effect when not starting as root")
|
||||
return 1
|
||||
}
|
||||
|
||||
op := C.struct_fuse_operations{
|
||||
init: closure(C.sharefs_init),
|
||||
destroy: closure(C.sharefs_destroy),
|
||||
|
||||
// implemented in fuse-helper.c
|
||||
getattr: closure(C.sharefs_getattr),
|
||||
readdir: closure(C.sharefs_readdir),
|
||||
mkdir: closure(C.sharefs_mkdir),
|
||||
unlink: closure(C.sharefs_unlink),
|
||||
rmdir: closure(C.sharefs_rmdir),
|
||||
rename: closure(C.sharefs_rename),
|
||||
truncate: closure(C.sharefs_truncate),
|
||||
utimens: closure(C.sharefs_utimens),
|
||||
create: closure(C.sharefs_create),
|
||||
open: closure(C.sharefs_open),
|
||||
read: closure(C.sharefs_read),
|
||||
write: closure(C.sharefs_write),
|
||||
statfs: closure(C.sharefs_statfs),
|
||||
release: closure(C.sharefs_release),
|
||||
fsync: closure(C.sharefs_fsync),
|
||||
}
|
||||
|
||||
fuse := C.fuse_new_fn(&args, &op, C.size_t(unsafe.Sizeof(op)), unsafe.Pointer(&priv))
|
||||
if fuse == nil {
|
||||
return 3
|
||||
}
|
||||
defer C.fuse_destroy(fuse)
|
||||
se := C.fuse_get_session(fuse)
|
||||
|
||||
if setup.Fuse < 3 {
|
||||
// unconfined, set up mount point and container
|
||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||
return 4
|
||||
}
|
||||
// unmounted by initial process
|
||||
defer func() {
|
||||
if exitCode == 5 {
|
||||
C.fuse_unmount(fuse)
|
||||
}
|
||||
}()
|
||||
|
||||
if asRoot {
|
||||
if err := syscall.Setresgid(setup.Setgid, setup.Setgid, setup.Setgid); err != nil {
|
||||
log.Printf("cannot set gid: %v", err)
|
||||
return 5
|
||||
}
|
||||
if err := syscall.Setgroups(nil); err != nil {
|
||||
log.Printf("cannot set supplementary groups: %v", err)
|
||||
return 5
|
||||
}
|
||||
if err := syscall.Setresuid(setup.Setuid, setup.Setuid, setup.Setuid); err != nil {
|
||||
log.Printf("cannot set uid: %v", err)
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
msg.SwapVerbose(opts.debug != 0)
|
||||
ctx := context.Background()
|
||||
if opts.foreground != 0 {
|
||||
c, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
ctx = c
|
||||
}
|
||||
z := container.New(ctx, msg)
|
||||
z.AllowOrphan = opts.foreground == 0
|
||||
z.Env = os.Environ()
|
||||
|
||||
// keep fuse_parse_cmdline happy in the container
|
||||
z.Tmpfs(check.MustAbs(container.Nonexistent), 1<<10, 0755)
|
||||
|
||||
z.Path = fhs.AbsProcSelfExe
|
||||
z.Args = s
|
||||
z.ForwardCancel = true
|
||||
z.SeccompPresets |= std.PresetStrict
|
||||
z.ParentPerm = 0700
|
||||
z.Bind(setup.Source, setup.Source, std.BindWritable)
|
||||
if !z.AllowOrphan {
|
||||
z.WaitDelay = hst.WaitDelayMax
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
}
|
||||
z.Bind(z.Path, z.Path, 0)
|
||||
setup.Fuse = int(proc.ExtraFileSlice(
|
||||
&z.ExtraFiles,
|
||||
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
||||
))
|
||||
|
||||
var setupPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
|
||||
z.ExtraFiles = append(z.ExtraFiles, r)
|
||||
setupPipe[0], setupPipe[1] = r, w
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
return 5
|
||||
}
|
||||
if err := setupPipe[0].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
return 5
|
||||
}
|
||||
|
||||
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else if err = setupPipe[1].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if !z.AllowOrphan {
|
||||
if err := z.Wait(); err != nil {
|
||||
exitError, ok := errors.AsType[*exec.ExitError](err)
|
||||
if !ok || exitError == nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
}
|
||||
switch code := exitError.ExitCode(); syscall.Signal(code & 0x7f) {
|
||||
case syscall.SIGINT:
|
||||
case syscall.SIGTERM:
|
||||
|
||||
default:
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else { // confined
|
||||
C.free(unsafe.Pointer(opts.mountpoint))
|
||||
// must be heap allocated
|
||||
opts.mountpoint = C.CString("/dev/fd/" + strconv.Itoa(setup.Fuse))
|
||||
|
||||
if err := os.Chdir("/"); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
if C.fuse_mount(fuse, opts.mountpoint) != 0 {
|
||||
return 4
|
||||
}
|
||||
defer C.fuse_unmount(fuse)
|
||||
|
||||
if C.fuse_set_signal_handlers(se) != 0 {
|
||||
return 6
|
||||
}
|
||||
defer C.fuse_remove_signal_handlers(se)
|
||||
|
||||
if opts.singlethread != 0 {
|
||||
if C.fuse_loop(fuse) != 0 {
|
||||
return 8
|
||||
}
|
||||
} else {
|
||||
loopConfig := C.fuse_loop_cfg_create()
|
||||
if loopConfig == nil {
|
||||
return 7
|
||||
}
|
||||
defer C.fuse_loop_cfg_destroy(loopConfig)
|
||||
|
||||
C.fuse_loop_cfg_set_clone_fd(loopConfig, C.uint(opts.clone_fd))
|
||||
|
||||
C.fuse_loop_cfg_set_idle_threads(loopConfig, opts.max_idle_threads)
|
||||
C.fuse_loop_cfg_set_max_threads(loopConfig, opts.max_threads)
|
||||
if C.fuse_loop_mt(fuse, loopConfig) != 0 {
|
||||
return 8
|
||||
}
|
||||
}
|
||||
|
||||
if setup.initFailed {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
)
|
||||
|
||||
func TestParseOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want setupState
|
||||
wantLog string
|
||||
wantOk bool
|
||||
}{
|
||||
{"zero length", []string{}, setupState{}, "", false},
|
||||
|
||||
{"not absolute", []string{"sharefs",
|
||||
"-o", "source=nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{}, "sharefs: path \"nonexistent\" is not absolute\n", false},
|
||||
|
||||
{"not specified", []string{"sharefs",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{}, "", false},
|
||||
|
||||
{"invalid setuid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=ff",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
}, "sharefs: invalid value for option setuid\n", false},
|
||||
|
||||
{"invalid setgid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=ff",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
}, "sharefs: invalid value for option setgid\n", false},
|
||||
|
||||
{"simple", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: -1,
|
||||
Setgid: -1,
|
||||
}, "", true},
|
||||
|
||||
{"root", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
Setgid: 1023,
|
||||
}, "", true},
|
||||
|
||||
{"setuid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setuid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: 1023,
|
||||
Setgid: -1,
|
||||
}, "", true},
|
||||
|
||||
{"setgid", []string{"sharefs",
|
||||
"-o", "source=/proc/nonexistent",
|
||||
"-o", "setgid=1023",
|
||||
}, setupState{
|
||||
Source: check.MustAbs("/proc/nonexistent"),
|
||||
Setuid: -1,
|
||||
Setgid: 1023,
|
||||
}, "", true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
got setupState
|
||||
buf bytes.Buffer
|
||||
)
|
||||
args := copyArgs(tc.args...)
|
||||
defer freeArgs(&args)
|
||||
unsafeAddArgument(&args, "-odefault_permissions\x00")
|
||||
|
||||
if ok := parseOpts(&args, &got, log.New(&buf, "sharefs: ", 0)); ok != tc.wantOk {
|
||||
t.Errorf("parseOpts: ok = %v, want %v", ok, tc.wantOk)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&got, &tc.want) {
|
||||
t.Errorf("parseOpts: setup = %#v, want %#v", got, tc.want)
|
||||
}
|
||||
|
||||
if buf.String() != tc.wantLog {
|
||||
t.Errorf("parseOpts: log =\n%s\nwant\n%s", buf.String(), tc.wantLog)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// The sharefs FUSE filesystem is a permissionless shared filesystem.
|
||||
//
|
||||
// This filesystem is the primary means of file sharing between hakurei
|
||||
// application containers. It serves the same purpose in Rosa OS as /sdcard
|
||||
// does in AOSP.
|
||||
//
|
||||
// See help message for all available options.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// sharefsName is the prefix used by log.std in the sharefs process.
|
||||
const sharefsName = "sharefs"
|
||||
|
||||
// handleMountArgs returns an alternative, libfuse-compatible args slice for
|
||||
// args passed by mount -t fuse.sharefs [options] sharefs <mountpoint>.
|
||||
//
|
||||
// In this case, args always has a length of 5 with index 0 being what comes
|
||||
// after "fuse." in the filesystem type, 1 is the uninterpreted string passed
|
||||
// to mount (sharefsName is used as the magic string to enable this hack),
|
||||
// 2 is passed through to libfuse as mountpoint, and 3 is always "-o".
|
||||
func handleMountArgs(args []string) []string {
|
||||
if len(args) == 5 && args[1] == sharefsName && args[3] == "-o" {
|
||||
return []string{sharefsName, args[2], "-o", args[4]}
|
||||
}
|
||||
return slices.Clone(args)
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix(sharefsName + ": ")
|
||||
|
||||
os.Exit(_main(handleMountArgs(os.Args)...))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHandleMountArgs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{"nil", nil, nil},
|
||||
{"passthrough", []string{"sharefs", "-V"}, []string{"sharefs", "-V"}},
|
||||
{"replace", []string{"/sbin/sharefs", "sharefs", "/sdcard", "-o", "rw"}, []string{"sharefs", "/sdcard", "-o", "rw"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := handleMountArgs(tc.args); !slices.Equal(got, tc.want) {
|
||||
t.Errorf("handleMountArgs: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
users.users = {
|
||||
alice = {
|
||||
isNormalUser = true;
|
||||
description = "Alice Foobar";
|
||||
password = "foobar";
|
||||
uid = 1000;
|
||||
};
|
||||
};
|
||||
|
||||
home-manager.users.alice.home.stateVersion = "24.11";
|
||||
|
||||
# Automatically login on tty1 as a normal user:
|
||||
services.getty.autologinUser = "alice";
|
||||
|
||||
environment = {
|
||||
# For benchmarking sharefs:
|
||||
systemPackages = [ pkgs.fsmark ];
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
# Hopefully reduces spurious test failures:
|
||||
memorySize = if pkgs.stdenv.hostPlatform.is32bit then 2046 else 8192;
|
||||
|
||||
diskSize = 6 * 1024;
|
||||
|
||||
qemu.options = [
|
||||
# Increase test performance:
|
||||
"-smp 16"
|
||||
];
|
||||
};
|
||||
|
||||
environment.hakurei = rec {
|
||||
enable = true;
|
||||
stateDir = "/var/lib/hakurei";
|
||||
sharefs.source = "${stateDir}/sdcard";
|
||||
users.alice = 0;
|
||||
|
||||
extraHomeConfig = {
|
||||
home.stateVersion = "23.05";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
testers,
|
||||
|
||||
system,
|
||||
self,
|
||||
}:
|
||||
testers.nixosTest {
|
||||
name = "sharefs";
|
||||
nodes.machine =
|
||||
{ options, pkgs, ... }:
|
||||
let
|
||||
fhs =
|
||||
let
|
||||
hakurei = options.environment.hakurei.package.default;
|
||||
in
|
||||
pkgs.buildFHSEnv {
|
||||
pname = "hakurei-fhs";
|
||||
inherit (hakurei) version;
|
||||
targetPkgs = _: hakurei.targetPkgs;
|
||||
extraOutputsToInstall = [ "dev" ];
|
||||
profile = ''
|
||||
export PKG_CONFIG_PATH="/usr/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [
|
||||
# For go tests:
|
||||
(pkgs.writeShellScriptBin "sharefs-workload-hakurei-tests" ''
|
||||
cp -r "${self.packages.${system}.hakurei.src}" "/sdcard/hakurei" && cd "/sdcard/hakurei"
|
||||
${fhs}/bin/hakurei-fhs -c 'ROSA_SKIP_BINFMT=1 CC="clang -O3 -Werror" go test ./...'
|
||||
'')
|
||||
];
|
||||
|
||||
imports = [
|
||||
./configuration.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
self.inputs.home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
testScript = builtins.readFile ./test.py;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//go:build raceattr
|
||||
|
||||
// The raceattr program reproduces vfs inode file attribute race.
|
||||
//
|
||||
// Even though libfuse high-level API presents the address of a struct stat
|
||||
// alongside struct fuse_context, file attributes are actually inherent to the
|
||||
// inode, instead of the specific call from userspace. The kernel implementation
|
||||
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
|
||||
// call) impossible or very unlikely to reach userspace via the stat family of
|
||||
// syscalls. However, when using default_permissions to have the VFS check
|
||||
// permissions, this race still happens, despite the resulting struct stat being
|
||||
// correct when overriding the check via capabilities otherwise.
|
||||
//
|
||||
// This program reproduces the failure, but because of its continuous nature, it
|
||||
// is provided independent of the vm integration test suite.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func newStatAs(
|
||||
ctx context.Context, cancel context.CancelFunc,
|
||||
n *atomic.Uint64, ok *atomic.Bool,
|
||||
uid uint32, pathname string,
|
||||
continuous bool,
|
||||
) func() {
|
||||
return func() {
|
||||
runtime.LockOSThread()
|
||||
defer cancel()
|
||||
|
||||
if _, _, errno := syscall.Syscall(
|
||||
syscall.SYS_SETUID, uintptr(uid),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
cancel()
|
||||
log.Printf("cannot set uid to %d: %s", uid, errno)
|
||||
}
|
||||
|
||||
var stat syscall.Stat_t
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.Lstat(pathname, &stat); err != nil {
|
||||
// SHAREFS_PERM_DIR not world executable, or
|
||||
// SHAREFS_PERM_REG not world readable
|
||||
if !continuous {
|
||||
cancel()
|
||||
}
|
||||
ok.Store(true)
|
||||
log.Printf("uid %d: %v", uid, err)
|
||||
} else if stat.Uid != uid {
|
||||
// appears to be unreachable
|
||||
if !continuous {
|
||||
cancel()
|
||||
}
|
||||
ok.Store(true)
|
||||
log.Printf("got uid %d instead of %d", stat.Uid, uid)
|
||||
}
|
||||
n.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("raceattr: ")
|
||||
|
||||
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
|
||||
u0 := flag.Int("uid0", 1<<10-1, "first uid")
|
||||
u1 := flag.Int("uid1", 1<<10-2, "second uid")
|
||||
count := flag.Int("count", 1, "threads per uid")
|
||||
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
|
||||
flag.Parse()
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
log.Fatal("this program must run as root")
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(
|
||||
context.Background(),
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGHUP,
|
||||
)
|
||||
|
||||
if err := os.WriteFile(*p, nil, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
|
||||
n atomic.Uint64
|
||||
ok atomic.Bool
|
||||
)
|
||||
|
||||
if *count < 1 {
|
||||
*count = 1
|
||||
}
|
||||
for range *count {
|
||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
|
||||
if *u1 >= 0 {
|
||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if !*continuous && ok.Load() {
|
||||
log.Printf("reproduced after %d calls", n.Load())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
start_all()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# To check sharefs version:
|
||||
print(machine.succeed("sharefs -V"))
|
||||
|
||||
# Make sure sharefs started:
|
||||
machine.wait_for_unit("sdcard.mount")
|
||||
|
||||
machine.succeed("mkdir /mnt")
|
||||
def check_bad_opts_output(opts, want, source="/etc", privileged=False):
|
||||
output = machine.fail(("" if privileged else "sudo -u alice -i ") + f"sharefs -f -o source={source},{opts} /mnt 2>&1")
|
||||
if output != want:
|
||||
raise Exception(f"unexpected output: {output}")
|
||||
|
||||
# Malformed setuid/setgid representation:
|
||||
check_bad_opts_output("setuid=ff", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=ff", "sharefs: invalid value for option setgid\n")
|
||||
|
||||
# Bounds check for setuid/setgid:
|
||||
check_bad_opts_output("setuid=0", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=0", "sharefs: invalid value for option setgid\n")
|
||||
check_bad_opts_output("setuid=-1", "sharefs: invalid value for option setuid\n")
|
||||
check_bad_opts_output("setgid=-1", "sharefs: invalid value for option setgid\n")
|
||||
|
||||
# Non-root setuid/setgid:
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("setuid=1023,setgid=1023", "sharefs: setuid and setgid has no effect when not starting as root\n")
|
||||
check_bad_opts_output("mkdir", "sharefs: mkdir has no effect when not starting as root\n")
|
||||
|
||||
# Starting as root without setuid/setgid:
|
||||
check_bad_opts_output("allow_other", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
check_bad_opts_output("setuid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
check_bad_opts_output("setgid=1023", "sharefs: setuid and setgid must not be 0\n", privileged=True)
|
||||
|
||||
# Make sure nothing actually got mounted:
|
||||
machine.fail("umount /mnt")
|
||||
machine.succeed("rmdir /mnt")
|
||||
|
||||
# Unprivileged mount/unmount:
|
||||
machine.succeed("sudo -u alice -i mkdir /home/alice/{sdcard,persistent}")
|
||||
machine.succeed("sudo -u alice -i sharefs -o source=/home/alice/persistent /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i touch /home/alice/sdcard/check")
|
||||
machine.succeed("sudo -u alice -i umount /home/alice/sdcard")
|
||||
machine.succeed("sudo -u alice -i rm /home/alice/persistent/check")
|
||||
machine.succeed("sudo -u alice -i rmdir /home/alice/{sdcard,persistent}")
|
||||
|
||||
# Benchmark sharefs:
|
||||
machine.succeed("fs_mark -v -d /sdcard/fs_mark -l /tmp/fs_log.txt")
|
||||
machine.copy_from_vm("/tmp/fs_log.txt", "")
|
||||
|
||||
# Check permissions:
|
||||
machine.succeed("sudo -u sharefs touch /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||
machine.succeed("sudo -u sharefs rm /var/lib/hakurei/sdcard/fs_mark/.check")
|
||||
machine.succeed("sudo -u alice rm -rf /sdcard/fs_mark")
|
||||
machine.fail("ls /var/lib/hakurei/sdcard/fs_mark")
|
||||
|
||||
# Run hakurei tests on sharefs:
|
||||
machine.succeed("sudo -u alice -i sharefs-workload-hakurei-tests")
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := command.New(nil, nil, "test", nil)
|
||||
stubHandler := func([]string) error { panic("unreachable") }
|
||||
|
||||
|
||||
+2
-2
@@ -91,8 +91,8 @@ func (n *node) MustParse(arguments []string, handleError func(error)) {
|
||||
case ErrEmptyTree:
|
||||
os.Exit(1)
|
||||
default:
|
||||
var flagError FlagError
|
||||
if !errors.As(err, &flagError) { // returned by HandlerFunc
|
||||
flagError, ok := errors.AsType[FlagError](err)
|
||||
if !ok { // returned by HandlerFunc
|
||||
handleError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
buildTree func(wout, wlog io.Writer) command.Command
|
||||
@@ -251,6 +253,7 @@ Commands:
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
wout, wlog := new(bytes.Buffer), new(bytes.Buffer)
|
||||
c := tc.buildTree(wout, wlog)
|
||||
|
||||
|
||||
@@ -6,15 +6,19 @@ import (
|
||||
)
|
||||
|
||||
func TestParseUnreachable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// top level bypasses name matching and recursive calls to Parse
|
||||
// returns when encountering zero-length args
|
||||
t.Run("zero-length args", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer checkRecover(t, "Parse", "attempted to parse with zero length args")
|
||||
_ = newNode(panicWriter{}, nil, " ", " ").Parse(nil)
|
||||
})
|
||||
|
||||
// top level must not have siblings
|
||||
t.Run("toplevel siblings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer checkRecover(t, "Parse", "invalid toplevel state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
n.append(newNode(panicWriter{}, nil, " ", " "))
|
||||
@@ -23,6 +27,7 @@ func TestParseUnreachable(t *testing.T) {
|
||||
|
||||
// a node with descendents must not have a direct handler
|
||||
t.Run("sub handle conflict", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer checkRecover(t, "Parse", "invalid subcommand tree state")
|
||||
n := newNode(panicWriter{}, nil, " ", " ")
|
||||
n.adopt(newNode(panicWriter{}, nil, " ", " "))
|
||||
@@ -32,6 +37,7 @@ func TestParseUnreachable(t *testing.T) {
|
||||
|
||||
// this would only happen if a node was matched twice
|
||||
t.Run("parsed flag set", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
defer checkRecover(t, "Parse", "invalid set state")
|
||||
n := newNode(panicWriter{}, nil, " ", "")
|
||||
set := flag.NewFlagSet("parsed", flag.ContinueOnError)
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// AbsoluteError is returned by [NewAbs] and holds the invalid pathname.
|
||||
type AbsoluteError struct {
|
||||
Pathname string
|
||||
}
|
||||
|
||||
func (e *AbsoluteError) Error() string { return fmt.Sprintf("path %q is not absolute", e.Pathname) }
|
||||
func (e *AbsoluteError) Is(target error) bool {
|
||||
var ce *AbsoluteError
|
||||
if !errors.As(target, &ce) {
|
||||
return errors.Is(target, syscall.EINVAL)
|
||||
}
|
||||
return *e == *ce
|
||||
}
|
||||
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
type Absolute struct {
|
||||
pathname string
|
||||
}
|
||||
|
||||
// isAbs wraps [path.IsAbs] in case additional checks are added in the future.
|
||||
func isAbs(pathname string) bool { return path.IsAbs(pathname) }
|
||||
|
||||
func (a *Absolute) String() string {
|
||||
if a.pathname == zeroString {
|
||||
panic("attempted use of zero Absolute")
|
||||
}
|
||||
return a.pathname
|
||||
}
|
||||
|
||||
func (a *Absolute) Is(v *Absolute) bool {
|
||||
if a == nil && v == nil {
|
||||
return true
|
||||
}
|
||||
return a != nil && v != nil &&
|
||||
a.pathname != zeroString && v.pathname != zeroString &&
|
||||
a.pathname == v.pathname
|
||||
}
|
||||
|
||||
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
||||
func NewAbs(pathname string) (*Absolute, error) {
|
||||
if !isAbs(pathname) {
|
||||
return nil, &AbsoluteError{pathname}
|
||||
}
|
||||
return &Absolute{pathname}, nil
|
||||
}
|
||||
|
||||
// MustAbs calls [NewAbs] and panics on error.
|
||||
func MustAbs(pathname string) *Absolute {
|
||||
if a, err := NewAbs(pathname); err != nil {
|
||||
panic(err.Error())
|
||||
} else {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
// Append calls [path.Join] with [Absolute] as the first element.
|
||||
func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
return &Absolute{path.Join(append([]string{a.String()}, elem...)...)}
|
||||
}
|
||||
|
||||
// Dir calls [path.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return &Absolute{path.Dir(a.String())} }
|
||||
|
||||
func (a *Absolute) GobEncode() ([]byte, error) { return []byte(a.String()), nil }
|
||||
func (a *Absolute) GobDecode(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !isAbs(pathname) {
|
||||
return &AbsoluteError{pathname}
|
||||
}
|
||||
a.pathname = pathname
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) { return json.Marshal(a.String()) }
|
||||
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
||||
var pathname string
|
||||
if err := json.Unmarshal(data, &pathname); err != nil {
|
||||
return err
|
||||
}
|
||||
if !isAbs(pathname) {
|
||||
return &AbsoluteError{pathname}
|
||||
}
|
||||
a.pathname = pathname
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
slices.SortFunc(x, func(a, b *Absolute) int { return strings.Compare(a.String(), b.String()) })
|
||||
}
|
||||
|
||||
// CompactAbs calls [slices.CompactFunc] for a slice of [Absolute].
|
||||
func CompactAbs(s []*Absolute) []*Absolute {
|
||||
return slices.CompactFunc(s, func(a *Absolute, b *Absolute) bool { return a.String() == b.String() })
|
||||
}
|
||||
+21
-16
@@ -3,39 +3,43 @@ package container
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(AutoEtcOp)) }
|
||||
|
||||
// Etc appends an [Op] that expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
func (f *Ops) Etc(host *Absolute, prefix string) *Ops {
|
||||
// Etc is a helper for appending [AutoEtcOp] to [Ops].
|
||||
func (f *Ops) Etc(host *check.Absolute, prefix string) *Ops {
|
||||
e := &AutoEtcOp{prefix}
|
||||
f.Mkdir(AbsFHSEtc, 0755)
|
||||
f.Mkdir(fhs.AbsEtc, 0755)
|
||||
f.Bind(host, e.hostPath(), 0)
|
||||
*f = append(*f, e)
|
||||
return f
|
||||
}
|
||||
|
||||
// AutoEtcOp expands host /etc into a toplevel symlink mirror with /etc semantics.
|
||||
//
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
type AutoEtcOp struct{ Prefix string }
|
||||
|
||||
func (e *AutoEtcOp) Valid() bool { return e != nil }
|
||||
func (e *AutoEtcOp) early(*setupState, syscallDispatcher) error { return nil }
|
||||
func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
if state.nonrepeatable&nrAutoEtc != 0 {
|
||||
return msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
|
||||
return OpRepeatError("autoetc")
|
||||
}
|
||||
state.nonrepeatable |= nrAutoEtc
|
||||
|
||||
const target = sysrootPath + FHSEtc
|
||||
const target = sysrootPath + fhs.Etc
|
||||
rel := e.hostRel() + "/"
|
||||
|
||||
if err := k.mkdirAll(target, 0755); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
return err
|
||||
}
|
||||
if d, err := k.readdir(toSysroot(e.hostPath().String())); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
return err
|
||||
} else {
|
||||
for _, ent := range d {
|
||||
n := ent.Name()
|
||||
@@ -43,13 +47,13 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
case ".host", "passwd", "group":
|
||||
|
||||
case "mtab":
|
||||
if err = k.symlink(FHSProc+"mounts", target+n); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
if err = k.symlink(fhs.Proc+"mounts", target+n); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
default:
|
||||
if err = k.symlink(rel+n, target+n); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,13 +61,14 @@ func (e *AutoEtcOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (e *AutoEtcOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (e *AutoEtcOp) hostPath() *Absolute { return AbsFHSEtc.Append(e.hostRel()) }
|
||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||
func (e *AutoEtcOp) hostPath() *check.Absolute { return fhs.AbsEtc.Append(e.hostRel()) }
|
||||
func (e *AutoEtcOp) hostRel() string { return ".host/" + e.Prefix }
|
||||
|
||||
func (e *AutoEtcOp) Is(op Op) bool {
|
||||
ve, ok := op.(*AutoEtcOp)
|
||||
return ok && e.Valid() && ve.Valid() && *e == *ve
|
||||
}
|
||||
func (*AutoEtcOp) prefix() string { return "setting up" }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
func (*AutoEtcOp) prefix() (string, bool) { return "setting up", true }
|
||||
func (e *AutoEtcOp) String() string { return fmt.Sprintf("auto etc %s", e.Prefix) }
|
||||
|
||||
+195
-189
@@ -2,14 +2,19 @@ package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
func TestAutoEtcOp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("nonrepeatable", func(t *testing.T) {
|
||||
wantErr := msg.WrapErr(fs.ErrInvalid, "autoetc is not repeatable")
|
||||
t.Parallel()
|
||||
wantErr := OpRepeatError("autoetc")
|
||||
if err := (&AutoEtcOp{Prefix: "81ceabb30d37bbdb3868004629cb84e9"}).apply(&setupState{nonrepeatable: nrAutoEtc}, nil); !errors.Is(err, wantErr) {
|
||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
@@ -18,22 +23,22 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"mkdirAll", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
}, nil, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, stub.UniqueError(3)),
|
||||
}, stub.UniqueError(3)},
|
||||
|
||||
{"readdir", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
}, nil, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(), stub.UniqueError(2)),
|
||||
}, stub.UniqueError(2)},
|
||||
|
||||
{"symlink", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
}, nil, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
@@ -41,15 +46,15 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, stub.UniqueError(1)),
|
||||
}, stub.UniqueError(1)},
|
||||
|
||||
{"symlink mtab", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
}, nil, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
@@ -57,41 +62,41 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, stub.UniqueError(0)),
|
||||
}, stub.UniqueError(0)},
|
||||
|
||||
{"success nested", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
}, nil, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(".host",
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
@@ -99,79 +104,79 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", new(Params), &AutoEtcOp{
|
||||
Prefix: "81ceabb30d37bbdb3868004629cb84e9",
|
||||
}, nil, nil, []kexpect{
|
||||
{"mkdirAll", expectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil},
|
||||
{"readdir", expectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
|
||||
}, nil, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/etc/", os.FileMode(0755)}, nil, nil),
|
||||
call("readdir", stub.ExpectArgs{"/sysroot/etc/.host/81ceabb30d37bbdb3868004629cb84e9"}, stubDir(
|
||||
"alsa", "bash_logout", "bashrc", "binfmt.d", "dbus-1", "default", "dhcpcd.exit-hook", "fonts",
|
||||
"fstab", "fuse.conf", "group", "host.conf", "hostname", "hosts", "hsurc", "inputrc", "issue", "kbd",
|
||||
"locale.conf", "login.defs", "lsb-release", "lvm", "machine-id", "man_db.conf", "mdadm.conf",
|
||||
@@ -179,72 +184,72 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
"nsswitch.conf", "os-release", "pam", "pam.d", "passwd", "pipewire", "pki", "polkit-1", "profile",
|
||||
"protocols", "resolv.conf", "resolvconf.conf", "rpc", "services", "set-environment", "shadow", "shells",
|
||||
"ssh", "ssl", "static", "subgid", "subuid", "sudoers", "sway", "sysctl.d", "systemd", "terminfo",
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil},
|
||||
{"symlink", expectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil},
|
||||
{"symlink", expectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil},
|
||||
"tmpfiles.d", "udev", "vconsole.conf", "X11", "xdg", "zoneinfo"), nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/alsa", "/sysroot/etc/alsa"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bash_logout", "/sysroot/etc/bash_logout"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/bashrc", "/sysroot/etc/bashrc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/binfmt.d", "/sysroot/etc/binfmt.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dbus-1", "/sysroot/etc/dbus-1"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/default", "/sysroot/etc/default"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/dhcpcd.exit-hook", "/sysroot/etc/dhcpcd.exit-hook"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fonts", "/sysroot/etc/fonts"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fstab", "/sysroot/etc/fstab"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/fuse.conf", "/sysroot/etc/fuse.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/host.conf", "/sysroot/etc/host.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hostname", "/sysroot/etc/hostname"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hosts", "/sysroot/etc/hosts"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/hsurc", "/sysroot/etc/hsurc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/inputrc", "/sysroot/etc/inputrc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/issue", "/sysroot/etc/issue"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/kbd", "/sysroot/etc/kbd"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/locale.conf", "/sysroot/etc/locale.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/login.defs", "/sysroot/etc/login.defs"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lsb-release", "/sysroot/etc/lsb-release"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/lvm", "/sysroot/etc/lvm"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/machine-id", "/sysroot/etc/machine-id"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/man_db.conf", "/sysroot/etc/man_db.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/mdadm.conf", "/sysroot/etc/mdadm.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modprobe.d", "/sysroot/etc/modprobe.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/modules-load.d", "/sysroot/etc/modules-load.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{"/proc/mounts", "/sysroot/etc/mtab"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nanorc", "/sysroot/etc/nanorc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/netgroup", "/sysroot/etc/netgroup"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nix", "/sysroot/etc/nix"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nixos", "/sysroot/etc/nixos"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/NIXOS", "/sysroot/etc/NIXOS"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nscd.conf", "/sysroot/etc/nscd.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/nsswitch.conf", "/sysroot/etc/nsswitch.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/os-release", "/sysroot/etc/os-release"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam", "/sysroot/etc/pam"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pam.d", "/sysroot/etc/pam.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pipewire", "/sysroot/etc/pipewire"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/pki", "/sysroot/etc/pki"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/polkit-1", "/sysroot/etc/polkit-1"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/profile", "/sysroot/etc/profile"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/protocols", "/sysroot/etc/protocols"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolv.conf", "/sysroot/etc/resolv.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/resolvconf.conf", "/sysroot/etc/resolvconf.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/rpc", "/sysroot/etc/rpc"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/services", "/sysroot/etc/services"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/set-environment", "/sysroot/etc/set-environment"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shadow", "/sysroot/etc/shadow"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/shells", "/sysroot/etc/shells"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssh", "/sysroot/etc/ssh"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/ssl", "/sysroot/etc/ssl"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/static", "/sysroot/etc/static"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subgid", "/sysroot/etc/subgid"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/subuid", "/sysroot/etc/subuid"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sudoers", "/sysroot/etc/sudoers"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sway", "/sysroot/etc/sway"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/sysctl.d", "/sysroot/etc/sysctl.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/systemd", "/sysroot/etc/systemd"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/terminfo", "/sysroot/etc/terminfo"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/tmpfiles.d", "/sysroot/etc/tmpfiles.d"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/udev", "/sysroot/etc/udev"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/vconsole.conf", "/sysroot/etc/vconsole.conf"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/X11", "/sysroot/etc/X11"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/xdg", "/sysroot/etc/xdg"}, nil, nil),
|
||||
call("symlink", stub.ExpectArgs{".host/81ceabb30d37bbdb3868004629cb84e9/zoneinfo", "/sysroot/etc/zoneinfo"}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
@@ -255,11 +260,11 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pd", new(Ops).Etc(MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
|
||||
&MkdirOp{Path: MustAbs("/etc/"), Perm: 0755},
|
||||
{"pd", new(Ops).Etc(check.MustAbs("/etc/"), "048090b6ed8f9ebb10e275ff5d8c0659"), Ops{
|
||||
&MkdirOp{Path: check.MustAbs("/etc/"), Perm: 0755},
|
||||
&BindMountOp{
|
||||
Source: MustAbs("/etc/"),
|
||||
Target: MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
Source: check.MustAbs("/etc/"),
|
||||
Target: check.MustAbs("/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"),
|
||||
},
|
||||
&AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"},
|
||||
}},
|
||||
@@ -278,6 +283,7 @@ func TestAutoEtcOp(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("host path rel", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
op := &AutoEtcOp{Prefix: "048090b6ed8f9ebb10e275ff5d8c0659"}
|
||||
wantHostPath := "/etc/.host/048090b6ed8f9ebb10e275ff5d8c0659"
|
||||
wantHostRel := ".host/048090b6ed8f9ebb10e275ff5d8c0659"
|
||||
|
||||
+21
-14
@@ -3,42 +3,48 @@ package container
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(AutoRootOp)) }
|
||||
|
||||
// Root appends an [Op] that expands a directory into a toplevel bind mount mirror on container root.
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
func (f *Ops) Root(host *Absolute, flags int) *Ops {
|
||||
// Root is a helper for appending [AutoRootOp] to [Ops].
|
||||
func (f *Ops) Root(host *check.Absolute, flags int) *Ops {
|
||||
*f = append(*f, &AutoRootOp{host, flags, nil})
|
||||
return f
|
||||
}
|
||||
|
||||
// AutoRootOp expands a directory into a toplevel bind mount mirror on container root.
|
||||
//
|
||||
// This is not a generic setup op. It is implemented here to reduce ipc overhead.
|
||||
type AutoRootOp struct {
|
||||
Host *Absolute
|
||||
Host *check.Absolute
|
||||
// passed through to bindMount
|
||||
Flags int
|
||||
|
||||
// obtained during early;
|
||||
// these wrap the underlying Op because BindMountOp is relatively complex,
|
||||
// so duplicating that code would be unwise
|
||||
resolved []Op
|
||||
resolved []*BindMountOp
|
||||
}
|
||||
|
||||
func (r *AutoRootOp) Valid() bool { return r != nil && r.Host != nil }
|
||||
|
||||
func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
||||
if d, err := k.readdir(r.Host.String()); err != nil {
|
||||
return wrapErrSelf(err)
|
||||
return err
|
||||
} else {
|
||||
r.resolved = make([]Op, 0, len(d))
|
||||
r.resolved = make([]*BindMountOp, 0, len(d))
|
||||
for _, ent := range d {
|
||||
name := ent.Name()
|
||||
if IsAutoRootBindable(name) {
|
||||
if IsAutoRootBindable(state, name) {
|
||||
// careful: the Valid method is skipped, make sure this is always valid
|
||||
op := &BindMountOp{
|
||||
Source: r.Host.Append(name),
|
||||
Target: AbsFHSRoot.Append(name),
|
||||
Target: fhs.AbsRoot.Append(name),
|
||||
Flags: r.Flags,
|
||||
}
|
||||
if err = op.early(state, k); err != nil {
|
||||
@@ -53,18 +59,19 @@ func (r *AutoRootOp) early(state *setupState, k syscallDispatcher) error {
|
||||
|
||||
func (r *AutoRootOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
if state.nonrepeatable&nrAutoRoot != 0 {
|
||||
return msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
|
||||
return OpRepeatError("autoroot")
|
||||
}
|
||||
state.nonrepeatable |= nrAutoRoot
|
||||
|
||||
for _, op := range r.resolved {
|
||||
k.verbosef("%s %s", op.prefix(), op)
|
||||
// these are exclusively BindMountOp, do not attempt to print identifying message
|
||||
if err := op.apply(state, k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (r *AutoRootOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
func (r *AutoRootOp) Is(op Op) bool {
|
||||
vr, ok := op.(*AutoRootOp)
|
||||
@@ -72,13 +79,13 @@ func (r *AutoRootOp) Is(op Op) bool {
|
||||
r.Host.Is(vr.Host) &&
|
||||
r.Flags == vr.Flags
|
||||
}
|
||||
func (*AutoRootOp) prefix() string { return "setting up" }
|
||||
func (*AutoRootOp) prefix() (string, bool) { return "setting up", true }
|
||||
func (r *AutoRootOp) String() string {
|
||||
return fmt.Sprintf("auto root %q flags %#x", r.Host, r.Flags)
|
||||
}
|
||||
|
||||
// IsAutoRootBindable returns whether a dir entry name is selected for AutoRoot.
|
||||
func IsAutoRootBindable(name string) bool {
|
||||
func IsAutoRootBindable(msg message.Msg, name string) bool {
|
||||
switch name {
|
||||
case "proc", "dev", "tmp", "mnt", "etc":
|
||||
|
||||
|
||||
+134
-120
@@ -2,14 +2,19 @@ package container
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/internal/stub"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestAutoRootOp(t *testing.T) {
|
||||
t.Run("nonrepeatable", func(t *testing.T) {
|
||||
wantErr := msg.WrapErr(fs.ErrInvalid, "autoroot is not repeatable")
|
||||
t.Parallel()
|
||||
wantErr := OpRepeatError("autoroot")
|
||||
if err := new(AutoRootOp).apply(&setupState{nonrepeatable: nrAutoRoot}, nil); !errors.Is(err, wantErr) {
|
||||
t.Errorf("apply: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
@@ -17,116 +22,115 @@ func TestAutoRootOp(t *testing.T) {
|
||||
|
||||
checkOpBehaviour(t, []opBehaviourTestCase{
|
||||
{"readdir", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir(), errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, []stub.Call{
|
||||
call("readdir", stub.ExpectArgs{"/"}, stubDir(), stub.UniqueError(2)),
|
||||
}, stub.UniqueError(2), nil, nil},
|
||||
|
||||
{"early", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/bin"}, "", errUnique},
|
||||
}, wrapErrSelf(errUnique), nil, nil},
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, []stub.Call{
|
||||
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "", stub.UniqueError(1)),
|
||||
}, stub.UniqueError(1), nil, nil},
|
||||
|
||||
{"apply", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
|
||||
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
|
||||
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
|
||||
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
|
||||
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
|
||||
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
|
||||
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
|
||||
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
|
||||
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
|
||||
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
|
||||
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
|
||||
}, nil, []kexpect{
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil},
|
||||
{"stat", expectArgs{"/host/usr/bin"}, isDirFi(false), errUnique},
|
||||
}, wrapErrSelf(errUnique)},
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, []stub.Call{
|
||||
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(false), stub.UniqueError(0)),
|
||||
}, stub.UniqueError(0)},
|
||||
|
||||
{"success pd", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/bin"}, "/usr/bin", nil},
|
||||
{"evalSymlinks", expectArgs{"/home"}, "/home", nil},
|
||||
{"evalSymlinks", expectArgs{"/lib64"}, "/lib64", nil},
|
||||
{"evalSymlinks", expectArgs{"/lost+found"}, "/lost+found", nil},
|
||||
{"evalSymlinks", expectArgs{"/nix"}, "/nix", nil},
|
||||
{"evalSymlinks", expectArgs{"/root"}, "/root", nil},
|
||||
{"evalSymlinks", expectArgs{"/run"}, "/run", nil},
|
||||
{"evalSymlinks", expectArgs{"/srv"}, "/srv", nil},
|
||||
{"evalSymlinks", expectArgs{"/sys"}, "/sys", nil},
|
||||
{"evalSymlinks", expectArgs{"/usr"}, "/usr", nil},
|
||||
{"evalSymlinks", expectArgs{"/var"}, "/var", nil},
|
||||
}, nil, []kexpect{
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr/bin"), MustAbs("/bin"), MustAbs("/bin"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/home"), MustAbs("/home"), MustAbs("/home"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lib64"), MustAbs("/lib64"), MustAbs("/lib64"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/lost+found"), MustAbs("/lost+found"), MustAbs("/lost+found"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/nix"), MustAbs("/nix"), MustAbs("/nix"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/root"), MustAbs("/root"), MustAbs("/root"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/run"), MustAbs("/run"), MustAbs("/run"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/srv"), MustAbs("/srv"), MustAbs("/srv"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/sys"), MustAbs("/sys"), MustAbs("/sys"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/usr"), MustAbs("/usr"), MustAbs("/usr"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var"), MustAbs("/var"), MustAbs("/var"), BindWritable}}}, nil, nil}, {"stat", expectArgs{"/host/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil},
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, []stub.Call{
|
||||
call("readdir", stub.ExpectArgs{"/"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/bin"}, "/usr/bin", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/home"}, "/home", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/lib64"}, "/lib64", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/lost+found"}, "/lost+found", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/nix"}, "/nix", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/root"}, "/root", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/run"}, "/run", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/srv"}, "/srv", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/sys"}, "/sys", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/usr"}, "/usr", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var"}, "/var", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{"/host/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr/bin", "/sysroot/bin", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/home", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/home", "/sysroot/home", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lib64", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lib64", "/sysroot/lib64", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/lost+found", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/lost+found", "/sysroot/lost+found", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/nix", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/nix", "/sysroot/nix", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/root", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/root", "/sysroot/root", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/run", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/run", "/sysroot/run", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/srv", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/srv", "/sysroot/srv", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/sys", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/sys", "/sysroot/sys", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/usr", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/usr", "/sysroot/usr", uintptr(0x4004), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q flags %#x", []any{"/sysroot/var", uintptr(0x4004)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var", "/sysroot/var", uintptr(0x4004), false}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0750}, &AutoRootOp{
|
||||
Host: MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
}, []kexpect{
|
||||
{"readdir", expectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil},
|
||||
{"evalSymlinks", expectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil},
|
||||
}, nil, []kexpect{
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr/bin"), MustAbs("/var/lib/planterette/base/debian:f92c9052/bin"), MustAbs("/bin"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/var/lib/planterette/base/debian:f92c9052/home"), MustAbs("/home"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lib64"), MustAbs("/lib64"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/var/lib/planterette/base/debian:f92c9052/lost+found"), MustAbs("/lost+found"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/var/lib/planterette/base/debian:f92c9052/nix"), MustAbs("/nix"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/var/lib/planterette/base/debian:f92c9052/root"), MustAbs("/root"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/var/lib/planterette/base/debian:f92c9052/run"), MustAbs("/run"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/var/lib/planterette/base/debian:f92c9052/srv"), MustAbs("/srv"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/var/lib/planterette/base/debian:f92c9052/sys"), MustAbs("/sys"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/var/lib/planterette/base/debian:f92c9052/usr"), MustAbs("/usr"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil},
|
||||
{"verbosef", expectArgs{"%s %s", []any{"mounting", &BindMountOp{MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var/lib/planterette/base/debian:f92c9052/var"), MustAbs("/var"), 0}}}, nil, nil}, {"stat", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil}, {"mkdirAll", expectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil}, {"bindMount", expectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil},
|
||||
Host: check.MustAbs("/var/lib/planterette/base/debian:f92c9052"),
|
||||
}, []stub.Call{
|
||||
call("readdir", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052"}, stubDir("bin", "dev", "etc", "home", "lib64",
|
||||
"lost+found", "mnt", "nix", "proc", "root", "run", "srv", "sys", "tmp", "usr", "var"), nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/bin"}, "/var/lib/planterette/base/debian:f92c9052/usr/bin", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/home"}, "/var/lib/planterette/base/debian:f92c9052/home", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lib64"}, "/var/lib/planterette/base/debian:f92c9052/lib64", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/lost+found"}, "/var/lib/planterette/base/debian:f92c9052/lost+found", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/nix"}, "/var/lib/planterette/base/debian:f92c9052/nix", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/root"}, "/var/lib/planterette/base/debian:f92c9052/root", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/run"}, "/var/lib/planterette/base/debian:f92c9052/run", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/srv"}, "/var/lib/planterette/base/debian:f92c9052/srv", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/sys"}, "/var/lib/planterette/base/debian:f92c9052/sys", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/usr"}, "/var/lib/planterette/base/debian:f92c9052/usr", nil),
|
||||
call("evalSymlinks", stub.ExpectArgs{"/var/lib/planterette/base/debian:f92c9052/var"}, "/var/lib/planterette/base/debian:f92c9052/var", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/bin", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr/bin", "/sysroot/bin", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/home", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/home", "/sysroot/home", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lib64", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lib64", "/sysroot/lib64", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/lost+found", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/lost+found", "/sysroot/lost+found", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/nix", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/nix", "/sysroot/nix", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/root", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/root", "/sysroot/root", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/run", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/run", "/sysroot/run", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/srv", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/srv", "/sysroot/srv", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/sys", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/sys", "/sysroot/sys", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/usr", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/usr", "/sysroot/usr", uintptr(0x4005), false}, nil, nil),
|
||||
call("stat", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var"}, isDirFi(true), nil), call("mkdirAll", stub.ExpectArgs{"/sysroot/var", os.FileMode(0700)}, nil, nil), call("verbosef", stub.ExpectArgs{"mounting %q on %q flags %#x", []any{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005)}}, nil, nil), call("bindMount", stub.ExpectArgs{"/host/var/lib/planterette/base/debian:f92c9052/var", "/sysroot/var", uintptr(0x4005), false}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
checkOpsValid(t, []opValidTestCase{
|
||||
{"nil", (*AutoRootOp)(nil), false},
|
||||
{"zero", new(AutoRootOp), false},
|
||||
{"valid", &AutoRootOp{Host: MustAbs("/")}, true},
|
||||
{"valid", &AutoRootOp{Host: check.MustAbs("/")}, true},
|
||||
})
|
||||
|
||||
checkOpsBuilder(t, []opsBuilderTestCase{
|
||||
{"pd", new(Ops).Root(MustAbs("/"), BindWritable), Ops{
|
||||
{"pd", new(Ops).Root(check.MustAbs("/"), std.BindWritable), Ops{
|
||||
&AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
},
|
||||
}},
|
||||
})
|
||||
@@ -135,64 +139,74 @@ func TestAutoRootOp(t *testing.T) {
|
||||
{"zero", new(AutoRootOp), new(AutoRootOp), false},
|
||||
|
||||
{"internal ne", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
resolved: []Op{new(BindMountOp)},
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
resolved: []*BindMountOp{new(BindMountOp)},
|
||||
}, true},
|
||||
|
||||
{"flags differs", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable | BindDevice,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable | std.BindDevice,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, false},
|
||||
|
||||
{"host differs", &AutoRootOp{
|
||||
Host: MustAbs("/tmp/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/tmp/"),
|
||||
Flags: std.BindWritable,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, false},
|
||||
|
||||
{"equals", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, true},
|
||||
})
|
||||
|
||||
checkOpMeta(t, []opMetaTestCase{
|
||||
{"root", &AutoRootOp{
|
||||
Host: MustAbs("/"),
|
||||
Flags: BindWritable,
|
||||
Host: check.MustAbs("/"),
|
||||
Flags: std.BindWritable,
|
||||
}, "setting up", `auto root "/" flags 0x2`},
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsAutoRootBindable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
want bool
|
||||
log bool
|
||||
}{
|
||||
{"proc", false},
|
||||
{"dev", false},
|
||||
{"tmp", false},
|
||||
{"mnt", false},
|
||||
{"etc", false},
|
||||
{"", false},
|
||||
{"proc", false, false},
|
||||
{"dev", false, false},
|
||||
{"tmp", false, false},
|
||||
{"mnt", false, false},
|
||||
{"etc", false, false},
|
||||
{"", false, true},
|
||||
|
||||
{"var", true},
|
||||
{"var", true, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := IsAutoRootBindable(tc.name); got != tc.want {
|
||||
t.Parallel()
|
||||
var msg message.Msg
|
||||
if tc.log {
|
||||
msg = &kstub{nil, nil, stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { panic("unreachable") }, stub.Expect{Calls: []stub.Call{
|
||||
call("verbose", stub.ExpectArgs{[]any{"got unexpected root entry"}}, nil, nil),
|
||||
}})}
|
||||
}
|
||||
if got := IsAutoRootBindable(msg, tc.name); got != tc.want {
|
||||
t.Errorf("IsAutoRootBindable: %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
)
|
||||
|
||||
// escapeBinfmt escapes magic/mask sequences in a [BinfmtEntry].
|
||||
func escapeBinfmt(buf *strings.Builder, s string) string {
|
||||
const lowerhex = "0123456789abcdef"
|
||||
|
||||
buf.Reset()
|
||||
for _, c := range unsafe.Slice(unsafe.StringData(s), len(s)) {
|
||||
switch c {
|
||||
case 0, '\\', ':':
|
||||
buf.WriteString(`\x`)
|
||||
buf.WriteByte(lowerhex[c>>4])
|
||||
buf.WriteByte(lowerhex[c&0xf])
|
||||
|
||||
default:
|
||||
buf.WriteByte(c)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// BinfmtEntry is an entry to be registered by the init process.
|
||||
type BinfmtEntry struct {
|
||||
// The offset of the magic/mask in the file, counted in bytes.
|
||||
Offset byte
|
||||
// The byte sequence binfmt_misc is matching for.
|
||||
Magic string
|
||||
// An (optional, defaults to all 0xff) mask.
|
||||
Mask string
|
||||
// The program that should be invoked with the binary as first argument.
|
||||
Interpreter *check.Absolute
|
||||
}
|
||||
|
||||
// Valid returns whether e can be registered into the kernel.
|
||||
func (e *BinfmtEntry) Valid() bool {
|
||||
return e != nil &&
|
||||
int(e.Offset)+max(len(e.Magic), len(e.Mask)) < 128 &&
|
||||
e.Interpreter != nil && len(e.Interpreter.String()) < 128
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/fhs"
|
||||
)
|
||||
|
||||
func TestEscapeBinfmt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
magic string
|
||||
want string
|
||||
}{
|
||||
{"packed DOS applications", "\x0eDEX", "\x0eDEX"},
|
||||
|
||||
{"riscv64 magic",
|
||||
"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
|
||||
"\x7fELF\x02\x01\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\x02\\x00\xf3\\x00"},
|
||||
{"riscv64 mask",
|
||||
"\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
|
||||
"\xff\xff\xff\xff\xff\xff\xff\\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := escapeBinfmt(new(strings.Builder), tc.magic)
|
||||
if got != tc.want {
|
||||
t.Errorf("escapeBinfmt: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinfmtEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
e BinfmtEntry
|
||||
valid bool
|
||||
}{
|
||||
{"zero", BinfmtEntry{}, false},
|
||||
{"large offset", BinfmtEntry{Offset: 128}, false},
|
||||
{"long magic", BinfmtEntry{Magic: strings.Repeat("\x00", 128)}, false},
|
||||
{"long mask", BinfmtEntry{Mask: strings.Repeat("\x00", 128)}, false},
|
||||
{"valid", BinfmtEntry{Interpreter: fhs.AbsRoot}, true},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if tc.e.Valid() != tc.valid {
|
||||
t.Errorf("Valid: %v", !tc.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+7
-28
@@ -3,6 +3,8 @@ package container
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/ext"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -14,7 +16,9 @@ const (
|
||||
|
||||
CAP_SYS_ADMIN = 0x15
|
||||
CAP_SETPCAP = 0x8
|
||||
CAP_NET_ADMIN = 0xc
|
||||
CAP_DAC_OVERRIDE = 0x1
|
||||
CAP_SETFCAP = 0x1f
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -50,40 +54,15 @@ func capset(hdrp *capHeader, datap *[2]capData) error {
|
||||
|
||||
// capBoundingSetDrop drops a capability from the calling thread's capability bounding set.
|
||||
func capBoundingSetDrop(cap uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
syscall.PR_CAPBSET_DROP,
|
||||
cap, 0,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
return ext.Prctl(syscall.PR_CAPBSET_DROP, cap, 0)
|
||||
}
|
||||
|
||||
// capAmbientClearAll clears the ambient capability set of the calling thread.
|
||||
func capAmbientClearAll() error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
PR_CAP_AMBIENT,
|
||||
PR_CAP_AMBIENT_CLEAR_ALL, 0,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
return ext.Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0)
|
||||
}
|
||||
|
||||
// capAmbientRaise adds to the ambient capability set of the calling thread.
|
||||
func capAmbientRaise(cap uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
PR_CAP_AMBIENT,
|
||||
PR_CAP_AMBIENT_RAISE,
|
||||
cap,
|
||||
)
|
||||
if r != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
return ext.Prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, cap)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package container
|
||||
import "testing"
|
||||
|
||||
func TestCapToIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
cap uintptr
|
||||
@@ -14,6 +16,7 @@ func TestCapToIndex(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := capToIndex(tc.cap); got != tc.want {
|
||||
t.Errorf("capToIndex: %#x, want %#x", got, tc.want)
|
||||
}
|
||||
@@ -22,6 +25,8 @@ func TestCapToIndex(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCapToMask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
cap uintptr
|
||||
@@ -33,6 +38,7 @@ func TestCapToMask(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := capToMask(tc.cap); got != tc.want {
|
||||
t.Errorf("capToMask: %#x, want %#x", got, tc.want)
|
||||
}
|
||||
|
||||
+370
-92
@@ -1,4 +1,5 @@
|
||||
// Package container implements unprivileged Linux containers with built-in support for syscall filtering.
|
||||
// Package container implements unprivileged Linux containers with built-in
|
||||
// support for syscall filtering.
|
||||
package container
|
||||
|
||||
import (
|
||||
@@ -11,77 +12,108 @@ import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
. "syscall"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
const (
|
||||
// CancelSignal is the signal expected by container init on context cancel.
|
||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||
CancelSignal = SIGTERM
|
||||
CancelSignal = SIGUSR2
|
||||
)
|
||||
|
||||
type (
|
||||
// Container represents a container environment being prepared or run.
|
||||
// None of [Container] methods are safe for concurrent use.
|
||||
Container struct {
|
||||
// Whether the container init should stay alive after its parent terminates.
|
||||
AllowOrphan bool
|
||||
// Whether to set SchedPolicy and SchedPriority via sched_setscheduler(2).
|
||||
SetScheduler bool
|
||||
// Scheduling policy to set via sched_setscheduler(2).
|
||||
SchedPolicy ext.SchedPolicy
|
||||
// Scheduling priority to set via sched_setscheduler(2). The zero value
|
||||
// implies the minimum value supported by the current SchedPolicy.
|
||||
SchedPriority ext.Int
|
||||
// Cgroup fd, nil to disable.
|
||||
Cgroup *int
|
||||
// ExtraFiles passed through to initial process in the container,
|
||||
// with behaviour identical to its [exec.Cmd] counterpart.
|
||||
// ExtraFiles passed through to initial process in the container, with
|
||||
// behaviour identical to its [exec.Cmd] counterpart.
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// param encoder for shim and init
|
||||
setup *gob.Encoder
|
||||
// cancels cmd
|
||||
// Write end of a pipe connected to the init to deliver [Params].
|
||||
setup [2]*os.File
|
||||
// Cancels the context passed to the underlying cmd.
|
||||
cancel context.CancelFunc
|
||||
// closed after Wait returns
|
||||
// Closed after Wait returns. Keeps the spawning thread alive.
|
||||
wait chan struct{}
|
||||
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
Cancel func(cmd *exec.Cmd) error
|
||||
// Custom cancellation behaviour for the underlying [exec.Cmd]. Must
|
||||
// deliver [CancelSignal] before returning.
|
||||
Cancel func(cmd *exec.Cmd) error
|
||||
// Copied to the underlying [exec.Cmd].
|
||||
WaitDelay time.Duration
|
||||
|
||||
// Suppress verbose output of init.
|
||||
Quiet bool
|
||||
|
||||
cmd *exec.Cmd
|
||||
ctx context.Context
|
||||
msg message.Msg
|
||||
Params
|
||||
}
|
||||
|
||||
// Params holds container configuration and is safe to serialise.
|
||||
Params struct {
|
||||
// Working directory in the container.
|
||||
Dir *Absolute
|
||||
Dir *check.Absolute
|
||||
// Initial process environment.
|
||||
Env []string
|
||||
// Pathname of initial process in the container.
|
||||
Path *Absolute
|
||||
Path *check.Absolute
|
||||
// Initial process argv.
|
||||
Args []string
|
||||
// Deliver SIGINT to the initial process on context cancellation.
|
||||
ForwardCancel bool
|
||||
// time to wait for linger processes after death of initial process
|
||||
// Time to wait for processes lingering after the initial process terminates.
|
||||
AdoptWaitDelay time.Duration
|
||||
|
||||
// Map uid/gid 0 in the init process. Requires [FstypeProc] attached to
|
||||
// [fhs.Proc] in the container filesystem.
|
||||
InitAsRoot bool
|
||||
// Mapped Uid in user namespace.
|
||||
Uid int
|
||||
// Mapped Gid in user namespace.
|
||||
Gid int
|
||||
// Hostname value in UTS namespace.
|
||||
Hostname string
|
||||
// Register binfmt_misc entries.
|
||||
Binfmt []BinfmtEntry
|
||||
// Alternative pathname to attach binfmt_misc filesystem. The zero value
|
||||
// requires [FstypeProc] to be made available at [fhs.Proc].
|
||||
BinfmtPath *check.Absolute
|
||||
// Sequential container setup ops.
|
||||
*Ops
|
||||
|
||||
// Seccomp system call filter rules.
|
||||
SeccompRules []seccomp.NativeRule
|
||||
SeccompRules []std.NativeRule
|
||||
// Extra seccomp flags.
|
||||
SeccompFlags seccomp.ExportFlag
|
||||
// Seccomp presets. Has no effect unless SeccompRules is zero-length.
|
||||
SeccompPresets seccomp.FilterPreset
|
||||
SeccompPresets std.FilterPreset
|
||||
// Do not load seccomp program.
|
||||
SeccompDisable bool
|
||||
|
||||
@@ -99,28 +131,114 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// Start starts the container init. The init process blocks until Serve is called.
|
||||
func (p *Container) Start() error {
|
||||
if p.cmd != nil {
|
||||
return errors.New("container: already started")
|
||||
// A StartError contains additional information on a container startup failure.
|
||||
type StartError struct {
|
||||
// Fatal suggests whether this error should be considered fatal for the entire program.
|
||||
Fatal bool
|
||||
// Step refers to the part of the setup this error is returned from.
|
||||
Step string
|
||||
// Err is the underlying error.
|
||||
Err error
|
||||
// Origin is whether this error originated from the [Container.Start] method.
|
||||
Origin bool
|
||||
// Passthrough is whether the Error method is passed through to Err.
|
||||
Passthrough bool
|
||||
}
|
||||
|
||||
func (e *StartError) Unwrap() error { return e.Err }
|
||||
func (e *StartError) Error() string {
|
||||
if e.Passthrough {
|
||||
return e.Err.Error()
|
||||
}
|
||||
if p.Ops == nil || len(*p.Ops) == 0 {
|
||||
return errors.New("container: starting an empty container")
|
||||
if e.Origin {
|
||||
return e.Step
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(p.ctx)
|
||||
p.cancel = cancel
|
||||
if se, ok := errors.AsType[*os.SyscallError](e.Err); ok && se != nil {
|
||||
return e.Step + " " + se.Error()
|
||||
}
|
||||
|
||||
return e.Step + ": " + e.Err.Error()
|
||||
}
|
||||
|
||||
// Message returns a user-facing error message.
|
||||
func (e *StartError) Message() string {
|
||||
if e.Passthrough {
|
||||
var (
|
||||
numError *strconv.NumError
|
||||
)
|
||||
|
||||
switch {
|
||||
case errors.As(e.Err, new(*os.PathError)),
|
||||
errors.As(e.Err, new(*os.SyscallError)):
|
||||
return "cannot " + e.Err.Error()
|
||||
|
||||
case errors.As(e.Err, &numError) && numError != nil:
|
||||
return "cannot parse " + strconv.Quote(numError.Num) + ": " + numError.Err.Error()
|
||||
|
||||
default:
|
||||
return e.Err.Error()
|
||||
}
|
||||
}
|
||||
if e.Origin {
|
||||
return e.Step
|
||||
}
|
||||
return "cannot " + e.Error()
|
||||
}
|
||||
|
||||
// for ensureCloseOnExec
|
||||
var (
|
||||
closeOnExecOnce sync.Once
|
||||
closeOnExecErr error
|
||||
)
|
||||
|
||||
// ensureCloseOnExec ensures all currently open file descriptors have the
|
||||
// syscall.FD_CLOEXEC flag set.
|
||||
//
|
||||
// This is only ran once as it is intended to handle files left open by the
|
||||
// parent, and any file opened on this side should already have
|
||||
// syscall.FD_CLOEXEC set.
|
||||
func ensureCloseOnExec() error {
|
||||
closeOnExecOnce.Do(func() { closeOnExecErr = doCloseOnExec() })
|
||||
|
||||
if closeOnExecErr == nil {
|
||||
return nil
|
||||
}
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set FD_CLOEXEC on all open files",
|
||||
Err: closeOnExecErr,
|
||||
Passthrough: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the container init. The init process blocks until Serve is called.
|
||||
func (p *Container) Start() error {
|
||||
if p == nil || p.cmd == nil ||
|
||||
p.Ops == nil || len(*p.Ops) == 0 {
|
||||
return errors.New("container: starting an invalid container")
|
||||
}
|
||||
if p.cmd.Process != nil {
|
||||
return errors.New("container: already started")
|
||||
}
|
||||
if !p.InitAsRoot && len(p.Binfmt) > 0 {
|
||||
return errors.New("container: init as root required, but not enabled")
|
||||
}
|
||||
|
||||
if err := ensureCloseOnExec(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// map to overflow id to work around ownership checks
|
||||
if p.Uid < 1 {
|
||||
p.Uid = OverflowUid()
|
||||
p.Uid = OverflowUid(p.msg)
|
||||
}
|
||||
if p.Gid < 1 {
|
||||
p.Gid = OverflowGid()
|
||||
p.Gid = OverflowGid(p.msg)
|
||||
}
|
||||
|
||||
if !p.RetainSession {
|
||||
p.SeccompPresets |= seccomp.PresetDenyTTY
|
||||
p.SeccompPresets |= std.PresetDenyTTY
|
||||
}
|
||||
|
||||
if p.AdoptWaitDelay == 0 {
|
||||
@@ -131,19 +249,26 @@ func (p *Container) Start() error {
|
||||
p.AdoptWaitDelay = 0
|
||||
}
|
||||
|
||||
p.cmd = exec.CommandContext(ctx, MustExecutable())
|
||||
if p.cmd.Stdin == nil {
|
||||
p.cmd.Stdin = p.Stdin
|
||||
}
|
||||
if p.cmd.Stdout == nil {
|
||||
p.cmd.Stdout = p.Stdout
|
||||
}
|
||||
if p.cmd.Stderr == nil {
|
||||
p.cmd.Stderr = p.Stderr
|
||||
}
|
||||
|
||||
p.cmd.Args = []string{initName}
|
||||
p.cmd.Stdin, p.cmd.Stdout, p.cmd.Stderr = p.Stdin, p.Stdout, p.Stderr
|
||||
p.cmd.WaitDelay = p.WaitDelay
|
||||
if p.Cancel != nil {
|
||||
p.cmd.Cancel = func() error { return p.Cancel(p.cmd) }
|
||||
} else {
|
||||
p.cmd.Cancel = func() error { return p.cmd.Process.Signal(CancelSignal) }
|
||||
}
|
||||
p.cmd.Dir = FHSRoot
|
||||
p.cmd.Dir = fhs.Root
|
||||
p.cmd.SysProcAttr = &SysProcAttr{
|
||||
Setsid: !p.RetainSession,
|
||||
Pdeathsig: SIGKILL,
|
||||
Setsid: !p.RetainSession,
|
||||
Cloneflags: CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWNS |
|
||||
CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWCGROUP,
|
||||
|
||||
@@ -152,25 +277,47 @@ func (p *Container) Start() error {
|
||||
CAP_SYS_ADMIN,
|
||||
// drop capabilities
|
||||
CAP_SETPCAP,
|
||||
// bring up loopback interface
|
||||
CAP_NET_ADMIN,
|
||||
// overlay access to upperdir and workdir
|
||||
CAP_DAC_OVERRIDE,
|
||||
},
|
||||
|
||||
UseCgroupFD: p.Cgroup != nil,
|
||||
}
|
||||
if !p.AllowOrphan {
|
||||
p.cmd.SysProcAttr.Pdeathsig = SIGKILL
|
||||
}
|
||||
if p.cmd.SysProcAttr.UseCgroupFD {
|
||||
p.cmd.SysProcAttr.CgroupFD = *p.Cgroup
|
||||
}
|
||||
if !p.HostNet {
|
||||
p.cmd.SysProcAttr.Cloneflags |= CLONE_NEWNET
|
||||
}
|
||||
if p.InitAsRoot {
|
||||
p.cmd.SysProcAttr.AmbientCaps = append(p.cmd.SysProcAttr.AmbientCaps,
|
||||
// mappings during init as root
|
||||
CAP_SETFCAP,
|
||||
)
|
||||
|
||||
if !p.SeccompDisable &&
|
||||
len(p.SeccompRules) == 0 &&
|
||||
p.SeccompPresets&std.PresetDenyNS != 0 {
|
||||
return errors.New("container: as root requires late namespace creation")
|
||||
}
|
||||
}
|
||||
|
||||
// place setup pipe before user supplied extra files, this is later restored by init
|
||||
if fd, e, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot create shim setup pipe:")
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
p.setup = e
|
||||
fd := 3 + len(p.cmd.ExtraFiles)
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
|
||||
p.setup[0], p.setup[1] = r, w
|
||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||
}
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||
@@ -180,51 +327,63 @@ func (p *Container) Start() error {
|
||||
runtime.LockOSThread()
|
||||
p.wait = make(chan struct{})
|
||||
|
||||
done <- func() error { // setup depending on per-thread state must happen here
|
||||
// PR_SET_NO_NEW_PRIVS: depends on per-thread state but acts on all processes created from that thread
|
||||
if err := SetNoNewPrivs(); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"prctl(PR_SET_NO_NEW_PRIVS):")
|
||||
// setup depending on per-thread state must happen here
|
||||
done <- func() error {
|
||||
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
||||
// created from the calling thread
|
||||
if err := setNoNewPrivs(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// landlock: depends on per-thread state but acts on a process group
|
||||
{
|
||||
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
||||
rulesetAttr := &landlock.RulesetAttr{
|
||||
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}
|
||||
if !p.HostAbstract {
|
||||
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
}
|
||||
|
||||
if abi, err := LandlockGetABI(); err != nil {
|
||||
if p.HostAbstract {
|
||||
// landlock can be skipped here as it restricts access to resources
|
||||
// already covered by namespaces (pid)
|
||||
if abi, err := landlock.GetABI(); err != nil {
|
||||
if p.HostAbstract || !p.HostNet {
|
||||
// landlock can be skipped here as it restricts access
|
||||
// to resources already covered by namespaces (pid, net)
|
||||
goto landlockOut
|
||||
}
|
||||
return wrapErrSuffix(err,
|
||||
"landlock does not appear to be enabled:")
|
||||
return &StartError{Step: "get landlock ABI", Err: err}
|
||||
} else if abi < 6 {
|
||||
if p.HostAbstract {
|
||||
// see above comment
|
||||
goto landlockOut
|
||||
}
|
||||
return msg.WrapErr(ENOSYS,
|
||||
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET")
|
||||
} else {
|
||||
msg.Verbosef("landlock abi version %d", abi)
|
||||
return &StartError{
|
||||
Step: "kernel too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||
Err: ENOSYS,
|
||||
Origin: true,
|
||||
}
|
||||
}
|
||||
|
||||
if rulesetFd, err := rulesetAttr.Create(0); err != nil {
|
||||
return wrapErrSuffix(err,
|
||||
"cannot create landlock ruleset:")
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "create landlock ruleset",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
|
||||
_ = Close(rulesetFd)
|
||||
return wrapErrSuffix(err,
|
||||
"cannot enforce landlock ruleset:")
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "enforce landlock ruleset",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
if err = Close(rulesetFd); err != nil {
|
||||
msg.Verbosef("cannot close landlock ruleset: %v", err)
|
||||
p.msg.Verbosef("cannot close landlock ruleset: %v", err)
|
||||
// not fatal
|
||||
}
|
||||
}
|
||||
@@ -232,9 +391,51 @@ func (p *Container) Start() error {
|
||||
landlockOut:
|
||||
}
|
||||
|
||||
msg.Verbose("starting container init")
|
||||
// sched_setscheduler: thread-directed but acts on all processes
|
||||
// created from the calling thread
|
||||
if p.SetScheduler {
|
||||
if p.SchedPolicy < 0 || p.SchedPolicy > ext.SCHED_LAST {
|
||||
return &StartError{
|
||||
Fatal: false,
|
||||
Step: "set scheduling policy",
|
||||
Err: EINVAL,
|
||||
}
|
||||
}
|
||||
|
||||
var param schedParam
|
||||
if priority, err := p.SchedPolicy.GetPriorityMin(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "get minimum priority",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
param.priority = max(priority, p.SchedPriority)
|
||||
}
|
||||
|
||||
p.msg.Verbosef(
|
||||
"setting scheduling policy %s priority %d",
|
||||
p.SchedPolicy, param.priority,
|
||||
)
|
||||
if err := schedSetscheduler(
|
||||
0, // calling thread
|
||||
p.SchedPolicy,
|
||||
¶m,
|
||||
); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set scheduling policy",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.cmd.Start(); err != nil {
|
||||
return msg.WrapErr(err, err.Error())
|
||||
return &StartError{
|
||||
Step: "start container init",
|
||||
Err: err,
|
||||
Passthrough: true,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
@@ -246,46 +447,76 @@ func (p *Container) Start() error {
|
||||
}
|
||||
|
||||
// Serve serves [Container.Params] to the container init.
|
||||
//
|
||||
// Serve must only be called once.
|
||||
func (p *Container) Serve() error {
|
||||
if p.setup == nil {
|
||||
func (p *Container) Serve() (err error) {
|
||||
if p.setup[0] == nil || p.setup[1] == nil {
|
||||
panic("invalid serve")
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
done := make(chan struct{})
|
||||
defer func() {
|
||||
if closeErr := p.setup[1].Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
close(done)
|
||||
p.setup[0], p.setup[1] = nil, nil
|
||||
}()
|
||||
if err = p.setup[0].Close(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "close read end of init pipe",
|
||||
Err: err,
|
||||
Passthrough: true,
|
||||
}
|
||||
}
|
||||
|
||||
if p.Path == nil {
|
||||
p.cancel()
|
||||
return msg.WrapErr(EINVAL, "invalid executable pathname")
|
||||
return &StartError{
|
||||
Step: "invalid executable pathname",
|
||||
Err: EINVAL,
|
||||
Origin: true,
|
||||
}
|
||||
}
|
||||
|
||||
// do not transmit nil
|
||||
if p.Dir == nil {
|
||||
p.Dir = AbsFHSRoot
|
||||
p.Dir = fhs.AbsRoot
|
||||
}
|
||||
if p.SeccompRules == nil {
|
||||
p.SeccompRules = make([]seccomp.NativeRule, 0)
|
||||
p.SeccompRules = make([]std.NativeRule, 0)
|
||||
}
|
||||
|
||||
err := setup.Encode(
|
||||
&initParams{
|
||||
p.Params,
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
msg.IsVerbose(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
return err
|
||||
t := time.Now().UTC()
|
||||
go func(f *os.File) {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
|
||||
p.msg.Verbose(err)
|
||||
}
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}(p.setup[1])
|
||||
|
||||
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
|
||||
p.Params,
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
p.msg.IsVerbose() && !p.Quiet,
|
||||
})
|
||||
}
|
||||
|
||||
// Wait waits for the container init process to exit and releases any resources associated with the [Container].
|
||||
// Wait blocks until the container init process to exit and releases any
|
||||
// resources associated with the [Container].
|
||||
func (p *Container) Wait() error {
|
||||
if p.cmd == nil {
|
||||
if p.cmd == nil || p.cmd.Process == nil {
|
||||
return EINVAL
|
||||
}
|
||||
|
||||
@@ -297,12 +528,44 @@ func (p *Container) Wait() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Container) String() string {
|
||||
return fmt.Sprintf("argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets))
|
||||
// StdinPipe calls the [exec.Cmd] method with the same name.
|
||||
func (p *Container) StdinPipe() (w io.WriteCloser, err error) {
|
||||
if p.Stdin != nil {
|
||||
return nil, errors.New("container: Stdin already set")
|
||||
}
|
||||
w, err = p.cmd.StdinPipe()
|
||||
p.Stdin = p.cmd.Stdin
|
||||
return
|
||||
}
|
||||
|
||||
// ProcessState returns the address to os.ProcessState held by the underlying [exec.Cmd].
|
||||
// StdoutPipe calls the [exec.Cmd] method with the same name.
|
||||
func (p *Container) StdoutPipe() (r io.ReadCloser, err error) {
|
||||
if p.Stdout != nil {
|
||||
return nil, errors.New("container: Stdout already set")
|
||||
}
|
||||
r, err = p.cmd.StdoutPipe()
|
||||
p.Stdout = p.cmd.Stdout
|
||||
return
|
||||
}
|
||||
|
||||
// StderrPipe calls the [exec.Cmd] method with the same name.
|
||||
func (p *Container) StderrPipe() (r io.ReadCloser, err error) {
|
||||
if p.Stderr != nil {
|
||||
return nil, errors.New("container: Stderr already set")
|
||||
}
|
||||
r, err = p.cmd.StderrPipe()
|
||||
p.Stderr = p.cmd.Stderr
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Container) String() string {
|
||||
return fmt.Sprintf(
|
||||
"argv: %q, filter: %v, rules: %d, flags: %#x, presets: %#x",
|
||||
p.Args, !p.SeccompDisable, len(p.SeccompRules), int(p.SeccompFlags), int(p.SeccompPresets),
|
||||
)
|
||||
}
|
||||
|
||||
// ProcessState returns the address of os.ProcessState held by the underlying [exec.Cmd].
|
||||
func (p *Container) ProcessState() *os.ProcessState {
|
||||
if p.cmd == nil {
|
||||
return nil
|
||||
@@ -310,14 +573,29 @@ func (p *Container) ProcessState() *os.ProcessState {
|
||||
return p.cmd.ProcessState
|
||||
}
|
||||
|
||||
// New returns the address to a new instance of [Container] that requires further initialisation before use.
|
||||
func New(ctx context.Context) *Container {
|
||||
return &Container{ctx: ctx, Params: Params{Ops: new(Ops)}}
|
||||
// New returns the address to a new instance of [Container]. This value requires
|
||||
// further initialisation before use.
|
||||
func New(ctx context.Context, msg message.Msg) *Container {
|
||||
if msg == nil {
|
||||
msg = message.New(nil)
|
||||
}
|
||||
|
||||
p := &Container{ctx: ctx, msg: msg, Params: Params{Ops: new(Ops)}}
|
||||
c, cancel := context.WithCancel(ctx)
|
||||
p.cancel = cancel
|
||||
p.cmd = exec.CommandContext(c, fhs.ProcSelfExe)
|
||||
return p
|
||||
}
|
||||
|
||||
// NewCommand calls [New] and initialises the [Params.Path] and [Params.Args] fields.
|
||||
func NewCommand(ctx context.Context, pathname *Absolute, name string, args ...string) *Container {
|
||||
z := New(ctx)
|
||||
func NewCommand(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
pathname *check.Absolute,
|
||||
name string,
|
||||
args ...string,
|
||||
) *Container {
|
||||
z := New(ctx, msg)
|
||||
z.Path = pathname
|
||||
z.Args = append([]string{name}, args...)
|
||||
return z
|
||||
|
||||
+499
-151
@@ -7,25 +7,213 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/command"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/vfs"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal"
|
||||
"hakurei.app/internal/hlog"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/ldd"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/vfs"
|
||||
)
|
||||
|
||||
// Note: this package requires cgo, which is unavailable in the Go playground.
|
||||
func Example() {
|
||||
// Must be called early if the current process starts containers.
|
||||
container.TryArgv0(nil)
|
||||
|
||||
// Configure the container.
|
||||
z := container.New(context.Background(), nil)
|
||||
z.Hostname = "hakurei-example"
|
||||
z.Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
|
||||
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
|
||||
// Bind / for demonstration.
|
||||
z.Bind(fhs.AbsRoot, fhs.AbsRoot, 0)
|
||||
if name, err := exec.LookPath("hostname"); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
z.Path = check.MustAbs(name)
|
||||
}
|
||||
|
||||
// This completes the first stage of container setup and starts the container init process.
|
||||
// The new process blocks until the Serve method is called.
|
||||
if err := z.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// This serves the setup payload to the container init process,
|
||||
// starting the second stage of container setup.
|
||||
if err := z.Serve(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Must be called if the Start method succeeds.
|
||||
if err := z.Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Output: hakurei-example
|
||||
}
|
||||
|
||||
func TestStartError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
s string
|
||||
is error
|
||||
isF error
|
||||
msg string
|
||||
}{
|
||||
{"params env", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: params.ErrReceiveEnv,
|
||||
}, "set up params stream: environment variable not set",
|
||||
params.ErrReceiveEnv, syscall.EBADF,
|
||||
"cannot set up params stream: environment variable not set"},
|
||||
|
||||
{"params", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: &os.SyscallError{Syscall: "pipe2", Err: syscall.EBADF},
|
||||
}, "set up params stream pipe2: bad file descriptor",
|
||||
syscall.EBADF, os.ErrInvalid,
|
||||
"cannot set up params stream pipe2: bad file descriptor"},
|
||||
|
||||
{"PR_SET_NO_NEW_PRIVS", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||
Err: syscall.EPERM,
|
||||
}, "prctl(PR_SET_NO_NEW_PRIVS): operation not permitted",
|
||||
syscall.EPERM, syscall.EACCES,
|
||||
"cannot prctl(PR_SET_NO_NEW_PRIVS): operation not permitted"},
|
||||
|
||||
{"landlock abi", &container.StartError{
|
||||
Step: "get landlock ABI",
|
||||
Err: syscall.ENOSYS,
|
||||
}, "get landlock ABI: function not implemented",
|
||||
syscall.ENOSYS, syscall.ENOEXEC,
|
||||
"cannot get landlock ABI: function not implemented"},
|
||||
|
||||
{"landlock old", &container.StartError{
|
||||
Step: "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||
Err: syscall.ENOSYS,
|
||||
Origin: true,
|
||||
}, "kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET",
|
||||
syscall.ENOSYS, syscall.ENOSPC,
|
||||
"kernel version too old for LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET"},
|
||||
|
||||
{"landlock create", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "create landlock ruleset",
|
||||
Err: syscall.EBADFD,
|
||||
}, "create landlock ruleset: file descriptor in bad state",
|
||||
syscall.EBADFD, syscall.EBADF,
|
||||
"cannot create landlock ruleset: file descriptor in bad state"},
|
||||
|
||||
{"landlock enforce", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "enforce landlock ruleset",
|
||||
Err: syscall.ENOTRECOVERABLE,
|
||||
}, "enforce landlock ruleset: state not recoverable",
|
||||
syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT,
|
||||
"cannot enforce landlock ruleset: state not recoverable"},
|
||||
|
||||
{"start", &container.StartError{
|
||||
Step: "start container init",
|
||||
Err: &os.PathError{
|
||||
Op: "fork/exec",
|
||||
Path: "/proc/nonexistent",
|
||||
Err: syscall.ENOENT,
|
||||
}, Passthrough: true,
|
||||
}, "fork/exec /proc/nonexistent: no such file or directory",
|
||||
syscall.ENOENT, syscall.ENOSYS,
|
||||
"cannot fork/exec /proc/nonexistent: no such file or directory"},
|
||||
|
||||
{"start syscall", &container.StartError{
|
||||
Step: "start container init",
|
||||
Err: &os.SyscallError{
|
||||
Syscall: "open",
|
||||
Err: syscall.ENOSYS,
|
||||
}, Passthrough: true,
|
||||
}, "open: function not implemented",
|
||||
syscall.ENOSYS, syscall.ENOENT,
|
||||
"cannot open: function not implemented"},
|
||||
|
||||
{"start FD_CLOEXEC", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "set FD_CLOEXEC on all open files",
|
||||
Err: func() error { _, err := strconv.Atoi("invalid"); return err }(),
|
||||
Passthrough: true,
|
||||
}, `strconv.Atoi: parsing "invalid": invalid syntax`,
|
||||
strconv.ErrSyntax, os.ErrInvalid,
|
||||
`cannot parse "invalid": invalid syntax`},
|
||||
|
||||
{"start other", &container.StartError{
|
||||
Step: "start container init",
|
||||
Err: &net.OpError{
|
||||
Op: "dial",
|
||||
Net: "unix",
|
||||
Err: syscall.ECONNREFUSED,
|
||||
}, Passthrough: true,
|
||||
}, "dial unix: connection refused",
|
||||
syscall.ECONNREFUSED, syscall.ECONNABORTED,
|
||||
"dial unix: connection refused"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("error", func(t *testing.T) {
|
||||
if got := tc.err.Error(); got != tc.s {
|
||||
t.Errorf("Error: %q, want %q", got, tc.s)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("is", func(t *testing.T) {
|
||||
if !errors.Is(tc.err, tc.is) {
|
||||
t.Error("Is: unexpected false")
|
||||
}
|
||||
if errors.Is(tc.err, tc.isF) {
|
||||
t.Errorf("Is: unexpected true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("msg", func(t *testing.T) {
|
||||
if got, ok := message.GetMessage(tc.err); !ok {
|
||||
if tc.msg != "" {
|
||||
t.Errorf("GetMessage: err does not implement MessageError")
|
||||
}
|
||||
return
|
||||
} else if got != tc.msg {
|
||||
t.Errorf("GetMessage: %q, want %q", got, tc.msg)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ignore = "\x00"
|
||||
ignoreV = -1
|
||||
@@ -47,6 +235,9 @@ func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*v
|
||||
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
||||
}
|
||||
|
||||
//go:linkname toHost hakurei.app/container.toHost
|
||||
func toHost(name string) string
|
||||
|
||||
var containerTestCases = []struct {
|
||||
name string
|
||||
filter bool
|
||||
@@ -60,83 +251,83 @@ var containerTestCases = []struct {
|
||||
uid int
|
||||
gid int
|
||||
|
||||
rules []seccomp.NativeRule
|
||||
rules []std.NativeRule
|
||||
flags seccomp.ExportFlag
|
||||
presets seccomp.FilterPreset
|
||||
presets std.FilterPreset
|
||||
}{
|
||||
{"minimal", true, false, false, true,
|
||||
emptyOps, emptyMnt,
|
||||
1000, 100, nil, 0, seccomp.PresetStrict},
|
||||
1000, 100, nil, 0, std.PresetStrict},
|
||||
{"allow", true, true, true, false,
|
||||
emptyOps, emptyMnt,
|
||||
1000, 100, nil, 0, seccomp.PresetExt | seccomp.PresetDenyDevel},
|
||||
1000, 100, nil, 0, std.PresetExt | std.PresetDenyDevel},
|
||||
{"no filter", false, true, true, true,
|
||||
emptyOps, emptyMnt,
|
||||
1000, 100, nil, 0, seccomp.PresetExt},
|
||||
1000, 100, nil, 0, std.PresetExt},
|
||||
{"custom rules", true, true, true, false,
|
||||
emptyOps, emptyMnt,
|
||||
1, 31, []seccomp.NativeRule{{seccomp.ScmpSyscall(syscall.SYS_SETUID), seccomp.ScmpErrno(syscall.EPERM), nil}}, 0, seccomp.PresetExt},
|
||||
1, 31, []std.NativeRule{{Syscall: ext.SyscallNum(syscall.SYS_SETUID), Errno: std.ScmpErrno(syscall.EPERM)}}, 0, std.PresetExt},
|
||||
|
||||
{"tmpfs", true, false, false, true,
|
||||
earlyOps(new(container.Ops).
|
||||
Tmpfs(hst.AbsTmp, 0, 0755),
|
||||
Tmpfs(hst.AbsPrivateTmp, 0, 0755),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", hst.Tmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
|
||||
ent("/", hst.PrivateTmp, "rw,nosuid,nodev,relatime", "tmpfs", "ephemeral", ignore),
|
||||
),
|
||||
9, 9, nil, 0, seccomp.PresetStrict},
|
||||
9, 9, nil, 0, std.PresetStrict},
|
||||
|
||||
{"dev", true, true /* go test output is not a tty */, false, false,
|
||||
earlyOps(new(container.Ops).
|
||||
Dev(container.MustAbs("/dev"), true),
|
||||
Dev(check.MustAbs("/dev"), true),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
|
||||
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
ent("/", "/dev/mqueue", "rw,nosuid,nodev,noexec,relatime", "mqueue", "mqueue", "rw"),
|
||||
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
),
|
||||
1971, 100, nil, 0, seccomp.PresetStrict},
|
||||
1971, 100, nil, 0, std.PresetStrict},
|
||||
|
||||
{"dev no mqueue", true, true /* go test output is not a tty */, false, false,
|
||||
earlyOps(new(container.Ops).
|
||||
Dev(container.MustAbs("/dev"), false),
|
||||
Dev(check.MustAbs("/dev"), false),
|
||||
),
|
||||
earlyMnt(
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", "devtmpfs", ignore),
|
||||
ent("/null", "/dev/null", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/zero", "/dev/zero", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/full", "/dev/full", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/random", "/dev/random", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/urandom", "/dev/urandom", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/tty", "/dev/tty", "rw,nosuid", "devtmpfs", "devtmpfs", ignore),
|
||||
ent("/", "/dev", "ro,nosuid,nodev,relatime", "tmpfs", ignore, ignore),
|
||||
ent("/null", "/dev/null", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/zero", "/dev/zero", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/full", "/dev/full", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/random", "/dev/random", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/urandom", "/dev/urandom", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/tty", "/dev/tty", ignore, "devtmpfs", ignore, ignore),
|
||||
ent("/", "/dev/pts", "rw,nosuid,noexec,relatime", "devpts", "devpts", "rw,mode=620,ptmxmode=666"),
|
||||
ent("/", "/dev/shm", "rw,nosuid,nodev,relatime", "tmpfs", "tmpfs", ignore),
|
||||
),
|
||||
1971, 100, nil, 0, seccomp.PresetStrict},
|
||||
1971, 100, nil, 0, std.PresetStrict},
|
||||
|
||||
{"overlay", true, false, false, true,
|
||||
func(t *testing.T) (*container.Ops, context.Context) {
|
||||
tempDir := container.MustAbs(t.TempDir())
|
||||
tempDir := check.MustAbs(t.TempDir())
|
||||
lower0, lower1, upper, work :=
|
||||
tempDir.Append("lower0"),
|
||||
tempDir.Append("lower1"),
|
||||
tempDir.Append("upper"),
|
||||
tempDir.Append("work")
|
||||
for _, a := range []*container.Absolute{lower0, lower1, upper, work} {
|
||||
for _, a := range []*check.Absolute{lower0, lower1, upper, work} {
|
||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||
t.Fatalf("Mkdir: error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return new(container.Ops).
|
||||
Overlay(hst.AbsTmp, upper, work, lower0, lower1),
|
||||
Overlay(hst.AbsPrivateTmp, upper, work, lower0, lower1),
|
||||
context.WithValue(context.WithValue(context.WithValue(context.WithValue(t.Context(),
|
||||
testVal("lower1"), lower1),
|
||||
testVal("lower0"), lower0),
|
||||
@@ -145,112 +336,91 @@ var containerTestCases = []struct {
|
||||
},
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
ent("/", hst.Tmp, "rw", "overlay", "overlay",
|
||||
"rw,lowerdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
|
||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
||||
"rw"+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||
",upperdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*container.Absolute).String())+
|
||||
toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+
|
||||
",workdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*container.Absolute).String())+
|
||||
toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+
|
||||
",redirect_dir=nofollow,uuid=on,userxattr"),
|
||||
}
|
||||
},
|
||||
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
|
||||
|
||||
{"overlay ephemeral", true, false, false, true,
|
||||
func(t *testing.T) (*container.Ops, context.Context) {
|
||||
tempDir := container.MustAbs(t.TempDir())
|
||||
tempDir := check.MustAbs(t.TempDir())
|
||||
lower0, lower1 :=
|
||||
tempDir.Append("lower0"),
|
||||
tempDir.Append("lower1")
|
||||
for _, a := range []*container.Absolute{lower0, lower1} {
|
||||
for _, a := range []*check.Absolute{lower0, lower1} {
|
||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||
t.Fatalf("Mkdir: error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return new(container.Ops).
|
||||
OverlayEphemeral(hst.AbsTmp, lower0, lower1),
|
||||
OverlayEphemeral(hst.AbsPrivateTmp, lower0, lower1),
|
||||
t.Context()
|
||||
},
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
// contains random suffix
|
||||
ent("/", hst.Tmp, "rw", "overlay", "overlay", ignore),
|
||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay", ignore),
|
||||
}
|
||||
},
|
||||
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
|
||||
|
||||
{"overlay readonly", true, false, false, true,
|
||||
func(t *testing.T) (*container.Ops, context.Context) {
|
||||
tempDir := container.MustAbs(t.TempDir())
|
||||
tempDir := check.MustAbs(t.TempDir())
|
||||
lower0, lower1 :=
|
||||
tempDir.Append("lower0"),
|
||||
tempDir.Append("lower1")
|
||||
for _, a := range []*container.Absolute{lower0, lower1} {
|
||||
for _, a := range []*check.Absolute{lower0, lower1} {
|
||||
if err := os.Mkdir(a.String(), 0755); err != nil {
|
||||
t.Fatalf("Mkdir: error = %v", err)
|
||||
}
|
||||
}
|
||||
return new(container.Ops).
|
||||
OverlayReadonly(hst.AbsTmp, lower0, lower1),
|
||||
OverlayReadonly(hst.AbsPrivateTmp, lower0, lower1),
|
||||
context.WithValue(context.WithValue(t.Context(),
|
||||
testVal("lower1"), lower1),
|
||||
testVal("lower0"), lower0)
|
||||
},
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
ent("/", hst.Tmp, "rw", "overlay", "overlay",
|
||||
"ro,lowerdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*container.Absolute).String())+":"+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*container.Absolute).String())+
|
||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
||||
"ro"+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||
",redirect_dir=nofollow,userxattr"),
|
||||
}
|
||||
},
|
||||
1 << 3, 1 << 14, nil, 0, seccomp.PresetStrict},
|
||||
1 << 3, 1 << 14, nil, 0, std.PresetStrict},
|
||||
}
|
||||
|
||||
func TestContainer(t *testing.T) {
|
||||
replaceOutput(t)
|
||||
|
||||
t.Run("cancel", testContainerCancel(nil, func(t *testing.T, c *container.Container) {
|
||||
wantErr := context.Canceled
|
||||
wantExitCode := 0
|
||||
if err := c.Wait(); !errors.Is(err, wantErr) {
|
||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
||||
t.Errorf("Wait: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if ps := c.ProcessState(); ps == nil {
|
||||
t.Errorf("ProcessState unexpectedly returned nil")
|
||||
} else if code := ps.ExitCode(); code != wantExitCode {
|
||||
t.Errorf("ExitCode: %d, want %d", code, wantExitCode)
|
||||
}
|
||||
}))
|
||||
|
||||
t.Run("forward", testContainerCancel(func(c *container.Container) {
|
||||
c.ForwardCancel = true
|
||||
}, func(t *testing.T, c *container.Container) {
|
||||
var exitError *exec.ExitError
|
||||
if err := c.Wait(); !errors.As(err, &exitError) {
|
||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
||||
t.Errorf("Wait: error = %v", err)
|
||||
}
|
||||
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
||||
}
|
||||
}))
|
||||
t.Parallel()
|
||||
|
||||
var suffix string
|
||||
runTests:
|
||||
for i, tc := range containerTestCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_suffix := suffix
|
||||
t.Run(tc.name+_suffix, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantOps, wantOpsCtx := tc.ops(t)
|
||||
wantMnt := tc.mnt(t, wantOpsCtx)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
var libPaths []*container.Absolute
|
||||
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
|
||||
var libPaths []*check.Absolute
|
||||
c := helperNewContainerLibPaths(t.Context(), &libPaths, "container", strconv.Itoa(i))
|
||||
c.Uid = tc.uid
|
||||
c.Gid = tc.gid
|
||||
c.Hostname = hostnameFromTestCase(tc.name)
|
||||
@@ -260,7 +430,6 @@ func TestContainer(t *testing.T) {
|
||||
} else {
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
}
|
||||
c.WaitDelay = helperDefaultTimeout
|
||||
*c.Ops = append(*c.Ops, *wantOps...)
|
||||
c.SeccompRules = tc.rules
|
||||
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
||||
@@ -268,13 +437,27 @@ func TestContainer(t *testing.T) {
|
||||
c.SeccompDisable = !tc.filter
|
||||
c.RetainSession = tc.session
|
||||
c.HostNet = tc.net
|
||||
c.InitAsRoot = _suffix != ""
|
||||
c.Env = append(c.Env, "HAKUREI_TEST_SUFFIX="+_suffix)
|
||||
if info.CanDegrade {
|
||||
if _, err := landlock.GetABI(); err != nil {
|
||||
if !errors.Is(err, syscall.ENOSYS) {
|
||||
t.Fatalf("LandlockGetABI: error = %v", err)
|
||||
}
|
||||
c.HostAbstract = true
|
||||
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
|
||||
}
|
||||
}
|
||||
if c.InitAsRoot {
|
||||
c.SeccompPresets &= ^std.PresetDenyNS
|
||||
}
|
||||
|
||||
c.
|
||||
Readonly(container.MustAbs(pathReadonly), 0755).
|
||||
Tmpfs(container.MustAbs("/tmp"), 0, 0755).
|
||||
Place(container.MustAbs("/etc/hostname"), []byte(c.Hostname))
|
||||
Readonly(check.MustAbs(pathReadonly), 0755).
|
||||
Tmpfs(check.MustAbs("/tmp"), 0, 0755).
|
||||
Place(check.MustAbs("/etc/hostname"), []byte(c.Hostname))
|
||||
// needs /proc to check mountinfo
|
||||
c.Proc(container.MustAbs("/proc"))
|
||||
c.Proc(check.MustAbs("/proc"))
|
||||
|
||||
// mountinfo cannot be resolved directly by helper due to libPaths nondeterminism
|
||||
mnt := make([]*vfs.MountInfoEntry, 0, 3+len(libPaths))
|
||||
@@ -305,28 +488,42 @@ func TestContainer(t *testing.T) {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
t.Fatalf("cannot serialise expected mount points: %v", err)
|
||||
}
|
||||
c.Place(container.MustAbs(pathWantMnt), want.Bytes())
|
||||
c.Place(check.MustAbs(pathWantMnt), want.Bytes())
|
||||
|
||||
if tc.ro {
|
||||
c.Remount(container.MustAbs("/"), syscall.MS_RDONLY)
|
||||
c.Remount(check.MustAbs("/"), syscall.MS_RDONLY)
|
||||
}
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
container.GetOutput().PrintBaseErr(err, "start:")
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
if m, ok := container.InternalMessageFromError(err); ok {
|
||||
t.Fatal(m)
|
||||
} else {
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
}
|
||||
} else if err = c.Serve(); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
container.GetOutput().PrintBaseErr(err, "serve:")
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
if m, ok := container.InternalMessageFromError(err); ok {
|
||||
t.Error(m)
|
||||
} else {
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
}
|
||||
}
|
||||
if err := c.Wait(); err != nil {
|
||||
_, _ = output.WriteTo(os.Stdout)
|
||||
container.GetOutput().PrintBaseErr(err, "wait:")
|
||||
t.Fatalf("wait: %v", err)
|
||||
if m, ok := container.InternalMessageFromError(err); ok {
|
||||
t.Fatal(m)
|
||||
} else {
|
||||
t.Fatalf("wait: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if suffix == "" {
|
||||
suffix = " as root"
|
||||
goto runTests
|
||||
}
|
||||
}
|
||||
|
||||
func ent(root, target, vfsOptstr, fsType, source, fsOptstr string) *vfs.MountInfoEntry {
|
||||
@@ -349,52 +546,129 @@ func hostnameFromTestCase(name string) string {
|
||||
}
|
||||
|
||||
func testContainerCancel(
|
||||
t *testing.T,
|
||||
containerExtra func(c *container.Container),
|
||||
waitCheck func(t *testing.T, c *container.Container),
|
||||
) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||
waitCheck func(ps *os.ProcessState, waitErr error),
|
||||
) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
c := helperNewContainer(ctx, "block")
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
c.WaitDelay = helperDefaultTimeout
|
||||
if containerExtra != nil {
|
||||
containerExtra(c)
|
||||
c := helperNewContainer(ctx, "block")
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
if containerExtra != nil {
|
||||
containerExtra(c)
|
||||
}
|
||||
|
||||
ready := make(chan struct{})
|
||||
var waitErr error
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("cannot pipe: %v", err)
|
||||
}
|
||||
|
||||
c.ExtraFiles = append(c.ExtraFiles, w)
|
||||
go func() {
|
||||
defer close(ready)
|
||||
if _, _err := r.Read(make([]byte, 1)); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
ready := make(chan struct{})
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
t.Fatalf("cannot pipe: %v", err)
|
||||
if err = c.Start(); err != nil {
|
||||
if m, ok := container.InternalMessageFromError(err); ok {
|
||||
t.Fatal(m)
|
||||
} else {
|
||||
c.ExtraFiles = append(c.ExtraFiles, w)
|
||||
go func() {
|
||||
defer close(ready)
|
||||
if _, err = r.Read(make([]byte, 1)); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := c.Start(); err != nil {
|
||||
container.GetOutput().PrintBaseErr(err, "start:")
|
||||
t.Fatalf("cannot start container: %v", err)
|
||||
} else if err = c.Serve(); err != nil {
|
||||
container.GetOutput().PrintBaseErr(err, "serve:")
|
||||
}
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
waitErr = c.Wait()
|
||||
_ = r.SetReadDeadline(time.Now())
|
||||
}()
|
||||
|
||||
if err = c.Serve(); err != nil {
|
||||
if m, ok := container.InternalMessageFromError(err); ok {
|
||||
t.Error(m)
|
||||
} else {
|
||||
t.Errorf("cannot serve setup params: %v", err)
|
||||
}
|
||||
<-ready
|
||||
cancel()
|
||||
waitCheck(t, c)
|
||||
}
|
||||
<-ready
|
||||
cancel()
|
||||
<-done
|
||||
waitCheck(c.ProcessState(), waitErr)
|
||||
}
|
||||
|
||||
func TestForward(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := func(ps *os.ProcessState, waitErr error) {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(waitErr, &exitError) {
|
||||
if m, ok := container.InternalMessageFromError(waitErr); ok {
|
||||
t.Error(m)
|
||||
}
|
||||
t.Errorf("Wait: error = %v", waitErr)
|
||||
}
|
||||
if code := exitError.ExitCode(); code != blockExitCodeInterrupt {
|
||||
t.Errorf("ExitCode: %d, want %d", code, blockExitCodeInterrupt)
|
||||
}
|
||||
}
|
||||
t.Run("direct", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testContainerCancel(t, func(c *container.Container) {
|
||||
c.ForwardCancel = true
|
||||
}, f)
|
||||
})
|
||||
t.Run("as root", func(t *testing.T) {
|
||||
testContainerCancel(t, func(c *container.Container) {
|
||||
c.ForwardCancel = true
|
||||
c.InitAsRoot = true
|
||||
c.Proc(fhs.AbsProc)
|
||||
}, f)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCancel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := func(ps *os.ProcessState, waitErr error) {
|
||||
wantErr := context.Canceled
|
||||
if !reflect.DeepEqual(waitErr, wantErr) {
|
||||
if m, ok := container.InternalMessageFromError(waitErr); ok {
|
||||
t.Error(m)
|
||||
}
|
||||
t.Errorf("Wait: error = %#v, want %#v", waitErr, wantErr)
|
||||
}
|
||||
if ps == nil {
|
||||
t.Errorf("ProcessState unexpectedly returned nil")
|
||||
} else if code := ps.ExitCode(); code != 0 {
|
||||
t.Errorf("ExitCode: %d, want %d", code, 0)
|
||||
}
|
||||
}
|
||||
t.Run("direct", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testContainerCancel(t, nil, f)
|
||||
})
|
||||
t.Run("as root", func(t *testing.T) {
|
||||
testContainerCancel(t, func(c *container.Container) {
|
||||
c.InitAsRoot = true
|
||||
c.Proc(fhs.AbsProc)
|
||||
}, f)
|
||||
})
|
||||
}
|
||||
|
||||
func TestContainerString(t *testing.T) {
|
||||
c := container.NewCommand(t.Context(), container.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
|
||||
t.Parallel()
|
||||
msg := message.New(nil)
|
||||
c := container.NewCommand(t.Context(), msg, check.MustAbs("/run/current-system/sw/bin/ldd"), "ldd", "/usr/bin/env")
|
||||
c.SeccompFlags |= seccomp.AllowMultiarch
|
||||
c.SeccompRules = seccomp.Preset(
|
||||
seccomp.PresetExt|seccomp.PresetDenyNS|seccomp.PresetDenyTTY,
|
||||
std.PresetExt|std.PresetDenyNS|std.PresetDenyTTY,
|
||||
c.SeccompFlags)
|
||||
c.SeccompPresets = seccomp.PresetStrict
|
||||
c.SeccompPresets = std.PresetStrict
|
||||
want := `argv: ["ldd" "/usr/bin/env"], filter: true, rules: 65, flags: 0x1, presets: 0xf`
|
||||
if got := c.String(); got != want {
|
||||
t.Errorf("String: %s, want %s", got, want)
|
||||
@@ -408,18 +682,19 @@ const (
|
||||
func init() {
|
||||
helperCommands = append(helperCommands, func(c command.Command) {
|
||||
c.Command("block", command.UsageInternal, func(args []string) error {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
||||
|
||||
if _, err := os.NewFile(3, "sync").Write([]byte{0}); err != nil {
|
||||
return fmt.Errorf("write to sync pipe: %v", err)
|
||||
}
|
||||
{
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
go func() { <-sig; os.Exit(blockExitCodeInterrupt) }()
|
||||
}
|
||||
select {}
|
||||
})
|
||||
|
||||
c.Command("container", command.UsageInternal, func(args []string) error {
|
||||
asRoot := os.Getenv("HAKUREI_TEST_SUFFIX") == " as root"
|
||||
|
||||
if len(args) != 1 {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
@@ -437,6 +712,66 @@ func init() {
|
||||
return fmt.Errorf("gid: %d, want %d", gid, tc.gid)
|
||||
}
|
||||
|
||||
// no attack surface increase during as root due to no_new_privs
|
||||
var wantBounding uintptr = 1
|
||||
asRootNot := " not"
|
||||
if !asRoot {
|
||||
wantBounding = 0
|
||||
asRootNot = ""
|
||||
}
|
||||
|
||||
const (
|
||||
PR_CAP_AMBIENT = 0x2f
|
||||
PR_CAP_AMBIENT_IS_SET = 0x1
|
||||
)
|
||||
for i := range container.LastCap(nil) + 1 {
|
||||
r, _, errno := syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
PR_CAP_AMBIENT,
|
||||
PR_CAP_AMBIENT_IS_SET,
|
||||
i,
|
||||
)
|
||||
if errno != 0 {
|
||||
return os.NewSyscallError("prctl", errno)
|
||||
}
|
||||
if r != 0 {
|
||||
return fmt.Errorf("capability %d in ambient set", i)
|
||||
}
|
||||
|
||||
r, _, errno = syscall.Syscall(
|
||||
syscall.SYS_PRCTL,
|
||||
syscall.PR_CAPBSET_READ,
|
||||
i,
|
||||
0,
|
||||
)
|
||||
if errno != 0 {
|
||||
return os.NewSyscallError("prctl", errno)
|
||||
}
|
||||
if r != wantBounding {
|
||||
return fmt.Errorf("capability %d%s in bounding set", i, asRootNot)
|
||||
}
|
||||
}
|
||||
|
||||
const _LINUX_CAPABILITY_VERSION_3 = 0x20080522
|
||||
var capData struct {
|
||||
effective uint32
|
||||
permitted uint32
|
||||
inheritable uint32
|
||||
}
|
||||
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&struct {
|
||||
version uint32
|
||||
pid int32
|
||||
}{_LINUX_CAPABILITY_VERSION_3, 0})), uintptr(unsafe.Pointer(&capData)), 0); errno != 0 {
|
||||
return os.NewSyscallError("capget", errno)
|
||||
}
|
||||
|
||||
if max(capData.effective, capData.permitted, capData.inheritable) != 0 {
|
||||
return fmt.Errorf(
|
||||
"effective = %d, permitted = %d, inheritable = %d",
|
||||
capData.effective, capData.permitted, capData.inheritable,
|
||||
)
|
||||
}
|
||||
|
||||
wantHost := hostnameFromTestCase(tc.name)
|
||||
if host, err := os.Hostname(); err != nil {
|
||||
return fmt.Errorf("cannot get hostname: %v", err)
|
||||
@@ -484,11 +819,22 @@ func init() {
|
||||
return fmt.Errorf("got more than %d entries", len(mnt))
|
||||
}
|
||||
|
||||
// ugly hack but should be reliable and is less likely to false negative than comparing by parsed flags
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",relatime")
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ",noatime")
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",relatime")
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ",noatime")
|
||||
// ugly hack but should be reliable and is less likely to
|
||||
//false negative than comparing by parsed flags
|
||||
for _, s := range []string{
|
||||
"relatime",
|
||||
"noatime",
|
||||
} {
|
||||
cur.VfsOptstr = strings.TrimSuffix(cur.VfsOptstr, ","+s)
|
||||
mnt[i].VfsOptstr = strings.TrimSuffix(mnt[i].VfsOptstr, ","+s)
|
||||
}
|
||||
for _, s := range []string{
|
||||
"seclabel",
|
||||
"inode64",
|
||||
} {
|
||||
cur.FsOptstr = strings.Replace(cur.FsOptstr, ","+s, "", 1)
|
||||
mnt[i].FsOptstr = strings.Replace(mnt[i].FsOptstr, ","+s, "", 1)
|
||||
}
|
||||
|
||||
if !cur.EqualWithIgnore(mnt[i], "\x00") {
|
||||
fail = true
|
||||
@@ -520,18 +866,17 @@ func init() {
|
||||
const (
|
||||
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
||||
|
||||
helperDefaultTimeout = 5 * time.Second
|
||||
helperInnerPath = "/usr/bin/helper"
|
||||
helperInnerPath = "/usr/bin/helper"
|
||||
)
|
||||
|
||||
var (
|
||||
absHelperInnerPath = container.MustAbs(helperInnerPath)
|
||||
absHelperInnerPath = check.MustAbs(helperInnerPath)
|
||||
)
|
||||
|
||||
var helperCommands []func(c command.Command)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
container.TryArgv0(hlog.Output{}, hlog.Prepare, internal.InstallOutput)
|
||||
container.TryArgv0(nil)
|
||||
|
||||
if os.Getenv(envDoCheck) == "1" {
|
||||
c := command.New(os.Stderr, log.Printf, "helper", func(args []string) error {
|
||||
@@ -544,7 +889,7 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
log.Fatal(err)
|
||||
}
|
||||
})
|
||||
return
|
||||
@@ -553,13 +898,16 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Absolute, args ...string) (c *container.Container) {
|
||||
c = container.NewCommand(ctx, absHelperInnerPath, "helper", args...)
|
||||
func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*check.Absolute, args ...string) (c *container.Container) {
|
||||
msg := message.New(nil)
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
|
||||
c = container.NewCommand(ctx, msg, absHelperInnerPath, "helper", args...)
|
||||
c.Env = append(c.Env, envDoCheck+"=1")
|
||||
c.Bind(container.MustAbs(os.Args[0]), absHelperInnerPath, 0)
|
||||
c.Bind(fhs.AbsProcSelfExe, absHelperInnerPath, 0)
|
||||
|
||||
// in case test has cgo enabled
|
||||
if entries, err := ldd.Exec(ctx, os.Args[0]); err != nil {
|
||||
if entries, err := ldd.Resolve(ctx, msg, nil); err != nil {
|
||||
log.Fatalf("ldd: %v", err)
|
||||
} else {
|
||||
*libPaths = ldd.Path(entries)
|
||||
@@ -572,5 +920,5 @@ func helperNewContainerLibPaths(ctx context.Context, libPaths *[]*container.Abso
|
||||
}
|
||||
|
||||
func helperNewContainer(ctx context.Context, args ...string) (c *container.Container) {
|
||||
return helperNewContainerLibPaths(ctx, new([]*container.Absolute), args...)
|
||||
return helperNewContainerLibPaths(ctx, new([]*check.Absolute), args...)
|
||||
}
|
||||
|
||||
+87
-47
@@ -1,9 +1,10 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -12,6 +13,11 @@ import (
|
||||
"syscall"
|
||||
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/netlink"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
type osFile interface {
|
||||
@@ -20,7 +26,8 @@ type osFile interface {
|
||||
fs.File
|
||||
}
|
||||
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour.
|
||||
// syscallDispatcher provides methods that make state-dependent system calls as
|
||||
// part of their behaviour.
|
||||
type syscallDispatcher interface {
|
||||
// new starts a goroutine with a new instance of syscallDispatcher.
|
||||
// A syscallDispatcher must never be used in any goroutine other than the one owning it,
|
||||
@@ -38,7 +45,7 @@ type syscallDispatcher interface {
|
||||
setNoNewPrivs() error
|
||||
|
||||
// lastcap provides [LastCap].
|
||||
lastcap() uintptr
|
||||
lastcap(msg message.Msg) uintptr
|
||||
// capset provides capset.
|
||||
capset(hdrp *capHeader, datap *[2]capData) error
|
||||
// capBoundingSetDrop provides capBoundingSetDrop.
|
||||
@@ -50,19 +57,23 @@ type syscallDispatcher interface {
|
||||
// isatty provides [Isatty].
|
||||
isatty(fd int) bool
|
||||
// receive provides [Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// bindMount provides procPaths.bindMount.
|
||||
bindMount(source, target string, flags uintptr, eq bool) error
|
||||
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
||||
// remount provides procPaths.remount.
|
||||
remount(target string, flags uintptr) error
|
||||
remount(msg message.Msg, target string, flags uintptr) error
|
||||
// mountTmpfs provides mountTmpfs.
|
||||
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
||||
// mountOverlay provides mountOverlay.
|
||||
mountOverlay(target string, options [][2]string) error
|
||||
// ensureFile provides ensureFile.
|
||||
ensureFile(name string, perm, pperm os.FileMode) error
|
||||
// mustLoopback provides mustLoopback.
|
||||
mustLoopback(ctx context.Context, msg message.Msg)
|
||||
|
||||
// seccompLoad provides [seccomp.Load].
|
||||
seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error
|
||||
seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error
|
||||
// notify provides [signal.Notify].
|
||||
notify(c chan<- os.Signal, sig ...os.Signal)
|
||||
// start starts [os/exec.Cmd].
|
||||
@@ -122,24 +133,12 @@ type syscallDispatcher interface {
|
||||
// wait4 provides syscall.Wait4
|
||||
wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error)
|
||||
|
||||
// printf provides [log.Printf].
|
||||
printf(format string, v ...any)
|
||||
// fatal provides [log.Fatal]
|
||||
fatal(v ...any)
|
||||
// fatalf provides [log.Fatalf]
|
||||
fatalf(format string, v ...any)
|
||||
// verbose provides [Msg.Verbose].
|
||||
verbose(v ...any)
|
||||
// verbosef provides [Msg.Verbosef].
|
||||
verbosef(format string, v ...any)
|
||||
// suspend provides [Msg.Suspend].
|
||||
suspend()
|
||||
// resume provides [Msg.Resume].
|
||||
resume() bool
|
||||
// beforeExit provides [Msg.BeforeExit].
|
||||
beforeExit()
|
||||
// printBaseErr provides [Msg.PrintBaseErr].
|
||||
printBaseErr(err error, fallback string)
|
||||
// printf provides the Printf method of [log.Logger].
|
||||
printf(msg message.Msg, format string, v ...any)
|
||||
// fatal provides the Fatal method of [log.Logger]
|
||||
fatal(msg message.Msg, v ...any)
|
||||
// fatalf provides the Fatalf method of [log.Logger]
|
||||
fatalf(msg message.Msg, format string, v ...any)
|
||||
}
|
||||
|
||||
// direct implements syscallDispatcher on the current kernel.
|
||||
@@ -149,34 +148,81 @@ func (k direct) new(f func(k syscallDispatcher)) { go f(k) }
|
||||
|
||||
func (direct) lockOSThread() { runtime.LockOSThread() }
|
||||
|
||||
func (direct) setPtracer(pid uintptr) error { return SetPtracer(pid) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return SetDumpable(dumpable) }
|
||||
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
|
||||
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||
func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
|
||||
|
||||
func (direct) lastcap() uintptr { return LastCap() }
|
||||
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
|
||||
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||
func (direct) capBoundingSetDrop(cap uintptr) error { return capBoundingSetDrop(cap) }
|
||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||
func (direct) isatty(fd int) bool { return Isatty(fd) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return Receive(key, e, fdp)
|
||||
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) bindMount(source, target string, flags uintptr, eq bool) error {
|
||||
return hostProc.bindMount(source, target, flags, eq)
|
||||
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
||||
return hostProc.bindMount(msg, source, target, flags)
|
||||
}
|
||||
func (direct) remount(target string, flags uintptr) error {
|
||||
return hostProc.remount(target, flags)
|
||||
func (direct) remount(msg message.Msg, target string, flags uintptr) error {
|
||||
return hostProc.remount(msg, target, flags)
|
||||
}
|
||||
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||
return mountTmpfs(k, fsname, target, flags, size, perm)
|
||||
}
|
||||
func (k direct) mountOverlay(target string, options [][2]string) error {
|
||||
return mountOverlay(target, options)
|
||||
}
|
||||
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
return ensureFile(name, perm, pperm)
|
||||
}
|
||||
func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
|
||||
var lo int
|
||||
if ifi, err := net.InterfaceByName("lo"); err != nil {
|
||||
msg.GetLogger().Fatalln(err)
|
||||
} else {
|
||||
lo = ifi.Index
|
||||
}
|
||||
|
||||
func (direct) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
|
||||
c, err := netlink.DialRoute(0)
|
||||
if err != nil {
|
||||
msg.GetLogger().Fatalln(err)
|
||||
}
|
||||
|
||||
must := func(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if closeErr := c.Close(); closeErr != nil {
|
||||
msg.Verbosef("cannot close RTNETLINK: %v", closeErr)
|
||||
}
|
||||
|
||||
switch err.(type) {
|
||||
case *os.SyscallError:
|
||||
msg.GetLogger().Fatalf("cannot %v", err)
|
||||
|
||||
case syscall.Errno:
|
||||
msg.GetLogger().Fatalf("RTNETLINK answers: %v", err)
|
||||
|
||||
default:
|
||||
if err == context.DeadlineExceeded || err == context.Canceled {
|
||||
msg.GetLogger().Fatalf("interrupted RTNETLINK operation")
|
||||
}
|
||||
msg.GetLogger().Fatal("RTNETLINK answers with malformed message")
|
||||
}
|
||||
}
|
||||
must(c.SendNewaddrLo(ctx, uint32(lo)))
|
||||
must(c.SendIfInfomsg(ctx, syscall.RTM_NEWLINK, 0, &syscall.IfInfomsg{
|
||||
Family: syscall.AF_UNSPEC,
|
||||
Index: int32(lo),
|
||||
Flags: syscall.IFF_UP,
|
||||
Change: syscall.IFF_UP,
|
||||
}))
|
||||
must(c.Close())
|
||||
}
|
||||
|
||||
func (direct) seccompLoad(rules []std.NativeRule, flags seccomp.ExportFlag) error {
|
||||
return seccomp.Load(rules, flags)
|
||||
}
|
||||
func (direct) notify(c chan<- os.Signal, sig ...os.Signal) { signal.Notify(c, sig...) }
|
||||
@@ -225,7 +271,7 @@ func (direct) pivotRoot(newroot, putold string) (err error) {
|
||||
return syscall.PivotRoot(newroot, putold)
|
||||
}
|
||||
func (direct) mount(source, target, fstype string, flags uintptr, data string) (err error) {
|
||||
return syscall.Mount(source, target, fstype, flags, data)
|
||||
return mount(source, target, fstype, flags, data)
|
||||
}
|
||||
func (direct) unmount(target string, flags int) (err error) {
|
||||
return syscall.Unmount(target, flags)
|
||||
@@ -234,12 +280,6 @@ func (direct) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *s
|
||||
return syscall.Wait4(pid, wstatus, options, rusage)
|
||||
}
|
||||
|
||||
func (direct) printf(format string, v ...any) { log.Printf(format, v...) }
|
||||
func (direct) fatal(v ...any) { log.Fatal(v...) }
|
||||
func (direct) fatalf(format string, v ...any) { log.Fatalf(format, v...) }
|
||||
func (direct) verbose(v ...any) { msg.Verbose(v...) }
|
||||
func (direct) verbosef(format string, v ...any) { msg.Verbosef(format, v...) }
|
||||
func (direct) suspend() { msg.Suspend() }
|
||||
func (direct) resume() bool { return msg.Resume() }
|
||||
func (direct) beforeExit() { msg.BeforeExit() }
|
||||
func (direct) printBaseErr(err error, fallback string) { msg.PrintBaseErr(err, fallback) }
|
||||
func (direct) printf(msg message.Msg, format string, v ...any) { msg.GetLogger().Printf(format, v...) }
|
||||
func (direct) fatal(msg message.Msg, v ...any) { msg.GetLogger().Fatal(v...) }
|
||||
func (direct) fatalf(msg message.Msg, format string, v ...any) { msg.GetLogger().Fatalf(format, v...) }
|
||||
|
||||
+409
-332
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user