Compare commits
879
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
962a2df3f8 | ||
|
|
d67e0b0c23 | ||
|
|
9ab9d01907 | ||
|
|
4abebdf35c | ||
|
|
ced9dc2340 | ||
|
|
8627e4f6a5 | ||
|
|
a056b2c262 | ||
|
|
017a326a13 | ||
|
|
1a9fe0a33b | ||
|
|
8e74619501 | ||
|
|
bb6ed1723f | ||
|
|
435ca204fe | ||
|
|
8640b45e99 | ||
|
|
4219d5ce1d | ||
|
|
85e87cc9f8 | ||
|
|
e577daf6ab | ||
|
|
0d6d1aeb19 | ||
|
|
a58f3516ab | ||
|
|
44103038c0 | ||
|
|
06ad255b95 | ||
|
|
9201d8c4f7 | ||
|
|
ceaf71caf8 | ||
|
|
817e3744ee | ||
|
|
68ead8c7dc | ||
|
|
59f900810e | ||
|
|
0d14613244 | ||
|
|
f5488adab0 | ||
|
|
fd93f39c78 | ||
|
|
1d92591780 | ||
|
|
234c21fb6a | ||
|
|
88801318bb | ||
|
|
9992228dcb | ||
|
|
4e32de8e65 | ||
|
|
a7d2333deb | ||
|
|
5e63bc2a42 | ||
|
|
b44ea19b4b | ||
|
|
d681d5abf0 | ||
|
|
7ea1fa9954 | ||
|
|
372b7d7f65 | ||
|
|
e5e61723d8 | ||
|
|
7fe77a49c9 | ||
|
|
9888820146 | ||
|
|
9a7091b595 | ||
|
|
1041849030 | ||
|
|
eff0446f8d | ||
|
|
e4852c82b4 | ||
|
|
99d52afd3f | ||
|
|
4abeafa20e | ||
|
|
70b7823993 | ||
|
|
46bf5b8541 | ||
|
|
d21af324ac | ||
|
|
f581fa423b | ||
|
|
bff354f9cc | ||
|
|
7593120bd9 | ||
|
|
869421cc05 | ||
|
|
6bd4690c3b | ||
|
|
018f8a20f3 | ||
|
|
13f379621a | ||
|
|
8bacf73e86 | ||
|
|
f327f7f434 | ||
|
|
5e7eb6290f | ||
|
|
bdf0bdf873 | ||
|
|
2f4830a3d0 | ||
|
|
f37ab0824d | ||
|
|
7e526521eb | ||
|
|
88b05c07d9 | ||
|
|
47bf724748 | ||
|
|
2949101ff8 | ||
|
|
c0365ebb38 | ||
|
|
9a13989c7e | ||
|
|
36900780f2 | ||
|
|
6660f59b3f | ||
|
|
f8c299c2e3 | ||
|
|
9296b36366 | ||
|
|
fe6b79127f | ||
|
|
dd81f7a665 | ||
|
|
ce30ec5fe6 | ||
|
|
c3fa6d4db1 | ||
|
|
235c2c4f4d | ||
|
|
9fb939ff24 | ||
|
|
e035f53ab6 | ||
|
|
8b0002e489 | ||
|
|
fcc4a2cf42 | ||
|
|
539f09004c | ||
|
|
c832a5d7a5 | ||
|
|
f09eb81381 | ||
|
|
59b3b9bd34 | ||
|
|
b3d3df5c57 | ||
|
|
e75bd460b8 | ||
|
|
77b43363fb | ||
|
|
199839b273 | ||
|
|
8dd28fb90a | ||
|
|
94a76e569e | ||
|
|
09c7e0c937 | ||
|
|
469f95c596 | ||
|
|
88c2d7a4e3 | ||
|
|
e8713e7a2c | ||
|
|
28c447370a | ||
|
|
f276fda411 | ||
|
|
c26b626c35 | ||
|
|
7bcdccc4d6 | ||
|
|
fc8a06547d | ||
|
|
b1a07df606 | ||
|
|
9d465bce16 | ||
|
|
eda65ece52 | ||
|
|
ae85423e39 | ||
|
|
f3bc5acacc | ||
|
|
556f6e0b9e | ||
|
|
270b9fcf80 | ||
|
|
288b8091ce | ||
|
|
46f6c03147 | ||
|
|
87dd27d022 | ||
|
|
2eade0a7c0 | ||
|
|
0e17756e19 | ||
|
|
5b5221189c | ||
|
|
677f79881c | ||
|
|
3ffacbfe37 | ||
|
|
c3cf86b9e0 | ||
|
|
4a785c2598 | ||
|
|
b7d74d8f87 | ||
|
|
db0d7a1f47 | ||
|
|
9844fdefcb | ||
|
|
a353797edb | ||
|
|
b83a28becc | ||
|
|
a3f004772a | ||
|
|
702389d8d9 | ||
|
|
88004f11ca | ||
|
|
1f6f815917 | ||
|
|
2a7266e17f | ||
|
|
e137778ba2 | ||
|
|
496a3c788e | ||
|
|
772c0728e1 | ||
|
|
b23d5e6047 | ||
|
|
4695fe4371 | ||
|
|
28c951ed9e | ||
|
|
2cc5a6df91 | ||
|
|
10d6ddee38 | ||
|
|
4314dff390 | ||
|
|
c311bb7a1f | ||
|
|
b387c4b127 | ||
|
|
6d6afb4faa | ||
|
|
e996e09f6e | ||
|
|
8d035a065a | ||
|
|
0df339c384 | ||
|
|
c341a9b6cc | ||
|
|
897cc007e1 | ||
|
|
6c9be8b134 | ||
|
|
cfa0419576 | ||
|
|
f8a2ecaabd | ||
|
|
79e37f64f8 | ||
|
|
4d729a2ff0 | ||
|
|
4f96926f6b | ||
|
|
c1da55ad87 | ||
|
|
f7b064a11a | ||
|
|
41f0aa2439 | ||
|
|
1c1b2b2026 | ||
|
|
488577ac0e | ||
|
|
4d40b6f5f7 | ||
|
|
2d8384830d | ||
|
|
0c8e9eef3c | ||
|
|
20b4975d57 | ||
|
|
0f42f92feb | ||
|
|
0b2dac3c5b | ||
|
|
5df6d0c8f3 | ||
|
|
a198cfb9e3 | ||
|
|
83bf76dd41 | ||
|
|
36c096c4e9 | ||
|
|
29d54ed20d | ||
|
|
e59ef240e8 | ||
|
|
27dd9e7b60 | ||
|
|
8185b83eba | ||
|
|
76afc56be8 | ||
|
|
5420a07ffc | ||
|
|
f00d67ca3c | ||
|
|
90dff0b2f2 | ||
|
|
83734c920d | ||
|
|
e1dcee3c86 | ||
|
|
d5114f36b4 | ||
|
|
68a225baec | ||
|
|
ef404da44c | ||
|
|
dbda8a24d7 | ||
|
|
bb462bad69 | ||
|
|
deee734e6c | ||
|
|
8ff4a0bf46 | ||
|
|
d2703b2145 | ||
|
|
9b72a0dadf | ||
|
|
5e3701bb2f | ||
|
|
4bab1f7ea4 | ||
|
|
9d9c82fb03 | ||
|
|
99c1ee3394 | ||
|
|
d88e39fb4e | ||
|
|
f63d640c95 | ||
|
|
86b7f9cf44 | ||
|
|
f331d177ca | ||
|
|
28bbd62d74 | ||
|
|
c1717d4047 | ||
|
|
4bfdb1dcc2 | ||
|
|
6e3be1aaf3 | ||
|
|
eea2b22c4e | ||
|
|
81fc4291db | ||
|
|
1a212489fd | ||
|
|
0d616ce1e7 | ||
|
|
7ff8e1f15a | ||
|
|
40e2fe2e27 | ||
|
|
359a9f3dfc | ||
|
|
68a7695d81 | ||
|
|
6f3d634c77 | ||
|
|
4177ab3536 | ||
|
|
80558f6686 | ||
|
|
da5cf6fc99 | ||
|
|
e6e866a41a | ||
|
|
e1675f6910 | ||
|
|
bb13824ad8 | ||
|
|
857a4cee2f | ||
|
|
a6444725da | ||
|
|
4a00da31da | ||
|
|
af50fde9d9 | ||
|
|
f1b62f9112 | ||
|
|
0b4997c5d5 | ||
|
|
b8dd2b8cc5 | ||
|
|
6cfd88afcf | ||
|
|
8e7fb1f31b | ||
|
|
00a85a1468 | ||
|
|
182fb97970 | ||
|
|
e6a812042f | ||
|
|
2f3cf0933c | ||
|
|
80451c43be | ||
|
|
007f7c8ae8 | ||
|
|
f541ab428b | ||
|
|
7ebfddfc4b | ||
|
|
56d513ed85 | ||
|
|
d1562bdd0e | ||
|
|
145dd1e5ab | ||
|
|
ab5c077600 | ||
|
|
6b413d67a6 | ||
|
|
4363a424d0 | ||
|
|
086eefed55 | ||
|
|
5e8ff95c99 | ||
|
|
21d8b3844e | ||
|
|
a7ffaf497e | ||
|
|
9ce0abe5a9 | ||
|
|
483e89cd79 | ||
|
|
4d9e624f8e | ||
|
|
bafda5ae3a | ||
|
|
09e620e12a | ||
|
|
3bd9b964fa | ||
|
|
a258b77240 | ||
|
|
936bacbe4b | ||
|
|
1227027c2f | ||
|
|
d8089f8d12 | ||
|
|
4862a52684 | ||
|
|
e40d8de1b2 | ||
|
|
dbf6bb759c | ||
|
|
be0bd224fa | ||
|
|
6bf6d99b9c | ||
|
|
7caebc5a11 | ||
|
|
6c13755a5c | ||
|
|
c6f28571b7 | ||
|
|
76ba7aae6a | ||
|
|
066b895055 | ||
|
|
a297d2d649 | ||
|
|
f09d76e3fd | ||
|
|
7737391ded | ||
|
|
dc08256025 | ||
|
|
72d7405a97 | ||
|
|
73096e8e3e | ||
|
|
51c667edd8 | ||
|
|
2edf09fde1 | ||
|
|
6ab2526966 | ||
|
|
c1b0772071 | ||
|
|
159237a70c | ||
|
|
c8051619c1 | ||
|
|
2b74d2bf4f | ||
|
|
35bd19b6c0 | ||
|
|
7e166c650c | ||
|
|
055ffa27b2 | ||
|
|
cd3cf38f06 | ||
|
|
12b0ab437e | ||
|
|
a9bbb470c2 | ||
|
|
6894c66f3a | ||
|
|
a2f9b76d10 | ||
|
|
3d40951e66 | ||
|
|
4350b54769 | ||
|
|
92d52d25a8 | ||
|
|
9e0949434e | ||
|
|
aa7a9b74f4 | ||
|
|
12556344b0 | ||
|
|
2e3ab00ce6 | ||
|
|
f92c78a2e0 | ||
|
|
7c7c316c3f | ||
|
|
94aaf3c8e3 | ||
|
|
c282d03f25 | ||
|
|
f7db49fc97 | ||
|
|
8cc97d2ffe | ||
|
|
f9994e11b5 | ||
|
|
1e0281d4d8 | ||
|
|
42a58881ca | ||
|
|
19c3cc0434 | ||
|
|
81e2a9aa4a | ||
|
|
575a3788e3 | ||
|
|
461417beac | ||
|
|
26b5da6835 | ||
|
|
99608de811 | ||
|
|
5c4207c145 | ||
|
|
a42dc38874 | ||
|
|
83194b19c7 | ||
|
|
14c5ddf864 | ||
|
|
f10a6f70c5 | ||
|
|
e2ee5f2397 | ||
|
|
324b289968 | ||
|
|
5914e26579 | ||
|
|
2894e06014 | ||
|
|
d0edacb7b9 | ||
|
|
1ce285c09b | ||
|
|
8dcd2fac3b | ||
|
|
bb2ef05157 | ||
|
|
c4eddfe50e | ||
|
|
f0629ea679 | ||
|
|
4da9d85545 | ||
|
|
d5444fb9bc | ||
|
|
50da7fe457 | ||
|
|
99be040488 | ||
|
|
902ffa05b4 | ||
|
|
051b13e161 | ||
|
|
04ace670ed | ||
|
|
99dd48480f | ||
|
|
acd9dcd1a8 | ||
|
|
790978458c | ||
|
|
f9a3830416 | ||
|
|
5f51149622 | ||
|
|
0688ffc26d | ||
|
|
ec1fc52da1 | ||
|
|
5e80a9cc6c | ||
|
|
f003aaa92d | ||
|
|
bf03348c64 | ||
|
|
0445c38c31 | ||
|
|
30a69712fc | ||
|
|
27cf6ae365 | ||
|
|
1ad8ff73ad | ||
|
|
0bc0d6cc62 | ||
|
|
23cec465bf | ||
|
|
a4abe30b18 | ||
|
|
34d1dcb7c8 | ||
|
|
93bf846fc4 | ||
|
|
c87cc6e4a2 | ||
|
|
1fbd1e466b | ||
|
|
b4d5da1df2 | ||
|
|
ff74ac8295 | ||
|
|
dcd6cd93c4 | ||
|
|
d439ee7ca7 | ||
|
|
39ee5b392b | ||
|
|
acde6bda9d | ||
|
|
8167e0e5f5 | ||
|
|
5a32d08ffd | ||
|
|
b98b87ea12 | ||
|
|
05d3fa30d9 | ||
|
|
516ed04897 | ||
|
|
1d295c7d3a | ||
|
|
1c6a7e7bf9 | ||
|
|
f1f4976222 | ||
|
|
14515ca446 | ||
|
|
8dd03c2b5c | ||
|
|
412b9bc713 | ||
|
|
ff2860ffbc | ||
|
|
c7a530092d | ||
|
|
deac10e3f6 | ||
|
|
42c4195f2e | ||
|
|
ce3d942efe | ||
|
|
a22a27b7a9 | ||
|
|
7d57b484d8 | ||
|
|
44acb5aecf | ||
|
|
5d11f90cd4 | ||
|
|
4824f3523d | ||
|
|
5a7823e6ef | ||
|
|
5792db996c | ||
|
|
16a33ac9f7 | ||
|
|
3afd31e561 | ||
|
|
2715faffff | ||
|
|
9459b43211 | ||
|
|
964f6cb045 | ||
|
|
bed99abbe9 | ||
|
|
d6eecd6f1c | ||
|
|
ecdd767934 | ||
|
|
1cf2cc10dd | ||
|
|
39f9d6a0e9 | ||
|
|
e84c66a3d3 | ||
|
|
e195894048 | ||
|
|
ee437b9473 | ||
|
|
299c613760 | ||
|
|
74b63cc315 | ||
|
|
032f8e83b2 | ||
|
|
c7d7789a58 | ||
|
|
fb995db489 | ||
|
|
942736d604 | ||
|
|
b8021efed8 | ||
|
|
1260884485 | ||
|
|
52d9077bb5 | ||
|
|
519a2ee242 | ||
|
|
6d6ec7e725 | ||
|
|
d0a168f89e | ||
|
|
5ccd47dd17 | ||
|
|
2f6ff9303e | ||
|
|
d9aede4138 | ||
|
|
0860b1e06d | ||
|
|
10927b1d08 | ||
|
|
3a8cc8a326 | ||
|
|
bf7464a4d9 | ||
|
|
e51badebcb | ||
|
|
d5d103148f | ||
|
|
d5721563fc | ||
|
|
274554f7a5 | ||
|
|
8fe1011da2 | ||
|
|
21a4dd7a70 | ||
|
|
33912355d6 | ||
|
|
5178f7abc6 | ||
|
|
d0ee0341c5 | ||
|
|
a2861b0570 | ||
|
|
a51c080407 | ||
|
|
2ea4fb94ab | ||
|
|
986193055e | ||
|
|
a43d478495 | ||
|
|
bf5a128a05 | ||
|
|
477d8f995b | ||
|
|
03d69cb772 | ||
|
|
f5e4c78b8c | ||
|
|
425e1a527b | ||
|
|
f1e3c2c08a | ||
|
|
bd9f1bb0c9 | ||
|
|
1fb52d3f5b | ||
|
|
4614577d19 | ||
|
|
f37221107d | ||
|
|
42e7b9b1b6 | ||
|
|
7753a0f11e | ||
|
|
4bfec7257f | ||
|
|
2cad81050d | ||
|
|
18f10b23b3 | ||
|
|
a3eeebbc52 | ||
|
|
85c92e35a8 | ||
|
|
be95e93a36 | ||
|
|
7c09bfca7f | ||
|
|
77a737fd71 | ||
|
|
9a9517deab | ||
|
|
b73d6e91e2 | ||
|
|
0987d9dd04 | ||
|
|
ccbd214c97 | ||
|
|
6414cb15ef | ||
|
|
8c3fcfd1c8 | ||
|
|
1b7d314d85 | ||
|
|
ea5a0b7d5a | ||
|
|
6fc964031c | ||
|
|
4bf47ca357 | ||
|
|
ea4a92125b | ||
|
|
5586528555 | ||
|
|
e8d9d683dc | ||
|
|
cc01457957 | ||
|
|
5b8aba9c8b | ||
|
|
f5d67d5240 | ||
|
|
35ef504423 | ||
|
|
a87d3116af | ||
|
|
c5f743a09d | ||
|
|
b167a27e85 | ||
|
|
f00a5bd306 | ||
|
|
608caf01c1 | ||
|
|
e801fce2c6 | ||
|
|
e89948f5ed | ||
|
|
3c7b59ef02 | ||
|
|
54fb4185f1 | ||
|
|
ac264c254c | ||
|
|
5403647aee | ||
|
|
3a007fbb29 | ||
|
|
92e9281623 | ||
|
|
e0f28bf851 | ||
|
|
9dbef48eb3 | ||
|
|
e84e17c396 | ||
|
|
9cc921f615 | ||
|
|
0d5e9332e3 | ||
|
|
b1e94eff34 | ||
|
|
bbe43b9842 | ||
|
|
3bba05d354 | ||
|
|
d43716933f | ||
|
|
3c18e594d1 | ||
|
|
23d426fd0c | ||
|
|
f221fa5a89 | ||
|
|
6e33675503 | ||
|
|
76398d204a | ||
|
|
d2b195473d | ||
|
|
caf37e9541 | ||
|
|
316180c276 | ||
|
|
5f085b2086 | ||
|
|
466a56b499 | ||
|
|
0989eff73d | ||
|
|
187aa02b49 | ||
|
|
0aaf6dfea0 | ||
|
|
9c40b489cf | ||
|
|
ce798a193e | ||
|
|
ddba151aa5 | ||
|
|
2fddf839af | ||
|
|
7bb43c42d6 | ||
|
|
b442d131c9 | ||
|
|
783646a967 | ||
|
|
b74ff2633d | ||
|
|
b97169de19 | ||
|
|
72f48d1f14 | ||
|
|
227104e224 | ||
|
|
d57a729b92 | ||
|
|
3ca9926c48 | ||
|
|
4c9a32d865 | ||
|
|
5e436975c6 | ||
|
|
a4a9ae7b0a | ||
|
|
34a31f63be | ||
|
|
75485bf7bf | ||
|
|
432017cc22 | ||
|
|
147648ffd0 | ||
|
|
1b3c4552fb | ||
|
|
cc510fc74c | ||
|
|
abe3d4bc89 | ||
|
|
7bc2ac5a46 | ||
|
|
b68e0ac3db | ||
|
|
3c44c3d7bc | ||
|
|
e21cd26511 | ||
|
|
7e413db1e1 | ||
|
|
b39a861a57 | ||
|
|
b349d7b7b2 | ||
|
|
c3ac337ef0 | ||
|
|
4aae1f6151 | ||
|
|
259dfa35d9 | ||
|
|
7e0a0905e5 | ||
|
|
34e3d9bba0 | ||
|
|
599d8ad421 | ||
|
|
4035794631 | ||
|
|
5f406c2a41 | ||
|
|
94d6dd05c8 | ||
|
|
67ae4ecf11 | ||
|
|
d0f122f4d8 | ||
|
|
08b6b2e2a1 | ||
|
|
5298fe9f6a | ||
|
|
aefa8ff680 | ||
|
|
0f23076f48 | ||
|
|
92a535e009 | ||
|
|
519d6e4eaf | ||
|
|
716db3a820 | ||
|
|
3d6f855d34 | ||
|
|
229e992695 | ||
|
|
a79c724110 | ||
|
|
6b1298aa62 | ||
|
|
0364abb00e | ||
|
|
f8d532b04d | ||
|
|
4dba742149 | ||
|
|
88eb0f2108 | ||
|
|
ade0e93f7f | ||
|
|
addff8547e | ||
|
|
1c18bdc9ab | ||
|
|
f3008b7aba | ||
|
|
c2f4f048d7 | ||
|
|
3c38072b9b | ||
|
|
4cfd0ec89f | ||
|
|
08c42e62ef | ||
|
|
dd153264e1 | ||
|
|
6f87ea1426 | ||
|
|
6fec5d6d98 | ||
|
|
9b9a9f2e00 | ||
|
|
27a7b7bf25 | ||
|
|
c9ba018a0d | ||
|
|
df5f6263bd | ||
|
|
0b5a892a88 | ||
|
|
076016f34a | ||
|
|
8417b1955c | ||
|
|
67322582f6 | ||
|
|
439361cb52 | ||
|
|
6e350760d1 | ||
|
|
672f04b833 | ||
|
|
f5cab5a14e | ||
|
|
f5f6ff50af | ||
|
|
62f4850968 | ||
|
|
082d9d3960 | ||
|
|
05a5b6c2a7 | ||
|
|
fdcf92be04 | ||
|
|
11aa760d47 | ||
|
|
bc27352710 | ||
|
|
0a00bd1c90 | ||
|
|
e2ee0d81dd | ||
|
|
6afc57569a | ||
|
|
ccb3821e2f | ||
|
|
29f9a97552 | ||
|
|
f37ac2c637 | ||
|
|
0ef0553598 | ||
|
|
f1c88ad375 | ||
|
|
17c92e1745 | ||
|
|
34e346463d | ||
|
|
ef11d3b677 | ||
|
|
2689931826 | ||
|
|
133ea8d120 | ||
|
|
75ff7c4ce3 | ||
|
|
86a0f041c7 | ||
|
|
c1f07ec6ae | ||
|
|
50d5486a9f | ||
|
|
036cf43918 | ||
|
|
fc5b3911f3 | ||
|
|
008f499367 | ||
|
|
8169322d56 | ||
|
|
d84ab69465 | ||
|
|
91bb95a981 | ||
|
|
03f4cd7939 | ||
|
|
71d3b017e6 | ||
|
|
8230eb4de7 | ||
|
|
bc149497f3 | ||
|
|
0bcdb9930a | ||
|
|
f2283eff6b | ||
|
|
a011c57237 | ||
|
|
9e2a1e5208 | ||
|
|
d6d8ffc4a8 | ||
|
|
14b01964b1 | ||
|
|
7fb9b43e9e | ||
|
|
63c0b9a655 | ||
|
|
00502bf4d4 | ||
|
|
d3bc77922b | ||
|
|
b4b11d6086 | ||
|
|
b47a82e3ac | ||
|
|
04dc53672a | ||
|
|
964a938437 | ||
|
|
e8843d9499 | ||
|
|
ecba318dec | ||
|
|
1aec478787 | ||
|
|
8f97cf8070 | ||
|
|
bd0260040c | ||
|
|
3b24e7e207 | ||
|
|
993e22085a | ||
|
|
00e0421e56 | ||
|
|
fabeb770a7 | ||
|
|
47addfeffc | ||
|
|
02e1526115 | ||
|
|
c81cc8b9ca | ||
|
|
99389f4bd3 | ||
|
|
b09a020855 | ||
|
|
090e7db598 | ||
|
|
5215163a37 | ||
|
|
1c1ab24a27 | ||
|
|
353752165c | ||
|
|
b1a3d0c6cd | ||
|
|
16333b989d | ||
|
|
d2bf40e11a | ||
|
|
d0a401e166 | ||
|
|
4edf72ea14 | ||
|
|
9b4974f02d | ||
|
|
f15d53947e | ||
|
|
68129c0151 | ||
|
|
a3a8b5b1be | ||
|
|
55d2f437a0 | ||
|
|
108dfae82b | ||
|
|
3a703d1093 | ||
|
|
433a091db5 | ||
|
|
21ebc832e1 | ||
|
|
a3f3b2fff2 | ||
|
|
093e537fed | ||
|
|
6a14be05f5 | ||
|
|
fd6155aa4a | ||
|
|
f47e80571d | ||
|
|
e02c9f7727 | ||
|
|
05ace58589 | ||
|
|
92c514ce18 | ||
|
|
6267973d62 | ||
|
|
cecbc682a0 | ||
|
|
07cd150b47 | ||
|
|
6a1c289ab2 | ||
|
|
bb380441a3 | ||
|
|
94d1e245a5 | ||
|
|
02265644ce | ||
|
|
c9a3a1334b | ||
|
|
0835c3d3cc | ||
|
|
34959a9657 | ||
|
|
90fab54a4a | ||
|
|
439011ce60 | ||
|
|
ec70908733 | ||
|
|
2b63c680b2 | ||
|
|
44a3b33c3a | ||
|
|
5d10a16baa | ||
|
|
33ccba1f3f | ||
|
|
bebc579180 | ||
|
|
a893f6b4f2 | ||
|
|
ecada765a2 | ||
|
|
b72e51accb | ||
|
|
f123a1bdeb | ||
|
|
3bb8f76b7d | ||
|
|
6b54b63986 | ||
|
|
c08c24140c | ||
|
|
d4efdba44e | ||
|
|
41057ee9d5 | ||
|
|
87724433b1 | ||
|
|
9117813c4d | ||
|
|
15fafc42c9 | ||
|
|
16a5e628fd | ||
|
|
32d80110cb | ||
|
|
b3a223521c | ||
|
|
8cba8ef3e6 | ||
|
|
113f0259ea | ||
|
|
889dfc411a | ||
|
|
ad62eee10c | ||
|
|
d68ab90f23 | ||
|
|
ad0505bd31 | ||
|
|
64519c41a7 | ||
|
|
0d55c98add | ||
|
|
5549a69b1c | ||
|
|
1ceb0beb8d | ||
|
|
386ea85fea | ||
|
|
607de2a074 | ||
|
|
bf3834b95d | ||
|
|
3a00caef50 | ||
|
|
5505afd1f3 | ||
|
|
0e418c7fa9 | ||
|
|
f399ded5ba | ||
|
|
85d96fc857 | ||
|
|
3100985e4c | ||
|
|
6355bd1d73 | ||
|
|
3ed4b73ebf | ||
|
|
41cc04bdab | ||
|
|
d8621c7f27 | ||
|
|
3c49c2a4aa | ||
|
|
633f1d3044 | ||
|
|
34602e3e7e | ||
|
|
330e346a7b | ||
|
|
6d39800351 | ||
|
|
df2f90d1f5 | ||
|
|
1d6596dc61 | ||
|
|
0653121fc4 | ||
|
|
3f603534e9 | ||
|
|
631cc924cf | ||
|
|
9f1825dbf8 | ||
|
|
9163025912 | ||
|
|
5929bb4028 | ||
|
|
fe313cf8f8 | ||
|
|
cb9102f381 | ||
|
|
9a8bc9b461 | ||
|
|
c4bf292085 | ||
|
|
8d9704036f | ||
|
|
fb01a9bb2e | ||
|
|
1d7e12c562 | ||
|
|
7c473dd500 | ||
|
|
1ccb266971 | ||
|
|
7de54ab3e4 | ||
|
|
2c10272e15 | ||
|
|
55cc721c9b | ||
|
|
dea768c21a | ||
|
|
824c663ebd | ||
|
|
80b481827c | ||
|
|
8360e48622 | ||
|
|
41a68b86bc | ||
|
|
cfc0051088 | ||
|
|
cecc5d7d1f | ||
|
|
5e2e6c15cf | ||
|
|
d0c83cfff1 | ||
|
|
9d6d6454b0 | ||
|
|
94fa3207f6 | ||
|
|
5a837f903c | ||
|
|
00b4b92749 | ||
|
|
19a5870d5f | ||
|
|
bfa23afae9 | ||
|
|
909005c1a4 | ||
|
|
ddc163a2b7 | ||
|
|
ca471d777e | ||
|
|
2c4f107e63 | ||
|
|
3d0c4ff0a5 | ||
|
|
a9820bea42 | ||
|
|
52a73ea6a7 | ||
|
|
6e9bacd843 | ||
|
|
fbbd62acc4 | ||
|
|
ef673eb3b7 | ||
|
|
05b0927619 | ||
|
|
e3f2feb480 | ||
|
|
a65b0c8786 | ||
|
|
6c84e680af | ||
|
|
d6eed66926 | ||
|
|
88bf65e07d | ||
|
|
13c1831baa | ||
|
|
5609d57947 | ||
|
|
488be0edd1 | ||
|
|
e620567804 | ||
|
|
fdf2912427 | ||
|
|
51922cd18e | ||
|
|
79946bb07c | ||
|
|
41c32cf063 | ||
|
|
4f627bddff | ||
|
|
2a2a0ee267 | ||
|
|
5de0eb5115 | ||
|
|
cf6c3c99e8 | ||
|
|
c3a7f77b54 | ||
|
|
4f5651906c | ||
|
|
2df21efc22 | ||
|
|
b0f5055c31 | ||
|
|
80eb331d6a | ||
|
|
bb87a0845b | ||
|
|
f7c8b95af6 | ||
|
|
33130c1d74 | ||
|
|
f61c0c3e38 | ||
|
|
2410c39b71 | ||
|
|
5a57b59b50 | ||
|
|
ff3eb02bff | ||
|
|
56d5fcd75a | ||
|
|
484b620016 | ||
|
|
16f81575ce | ||
|
|
10b039686c | ||
|
|
194bf7bb45 | ||
|
|
a288afd8ce | ||
|
|
4f3f6d3e60 | ||
|
|
1c4010ebed | ||
|
|
ace9cb70d0 | ||
|
|
af3c7a4217 | ||
|
|
e403a2f689 | ||
|
|
3944459c29 | ||
|
|
48383ef95e | ||
|
|
f020a6f62b | ||
|
|
d23a96a08e | ||
|
|
dffcd19d21 | ||
|
|
9079ab5c24 | ||
|
|
7e39f16e0f | ||
|
|
00819654cf | ||
|
|
346c06f31d | ||
|
|
ba7c2873c5 | ||
|
|
0e15234ce1 | ||
|
|
e5fafacf99 | ||
|
|
d467d3aa8f | ||
|
|
d5e489598d | ||
|
|
1ff02188ca | ||
|
|
f31b34d0f0 | ||
|
|
ecd5b9fc9d | ||
|
|
aca6364378 | ||
|
|
88e930cc0a | ||
|
|
be235e01aa | ||
|
|
66f004f725 | ||
|
|
cbe22899c2 | ||
|
|
df36e504fc | ||
|
|
7c814f5cb9 | ||
|
|
9ba78c084b | ||
|
|
d644126cac | ||
|
|
f59f64ea48 | ||
|
|
f5f106c682 | ||
|
|
abf342534f | ||
|
|
8e46cf9254 | ||
|
|
15b037aef7 | ||
|
|
69e534304b | ||
|
|
4dbc543cd3 | ||
|
|
4f6808156f | ||
|
|
5870efdf04 | ||
|
|
34db81c5cf | ||
|
|
a31436e421 | ||
|
|
550f384c69 | ||
|
|
a6b909b70e | ||
|
|
fcb0768052 | ||
|
|
5d9da0c6b1 | ||
|
|
e8ee608ba9 | ||
|
|
4e5530aca0 | ||
|
|
4fa3b18cc2 | ||
|
|
50cf61e9cd | ||
|
|
c6ed01110e | ||
|
|
1764603977 | ||
|
|
6c619d5f2a | ||
|
|
58c0fc6bba | ||
|
|
32e648ea9e | ||
|
|
b2efab9806 | ||
|
|
a342316e7a | ||
|
|
c887780022 | ||
|
|
a538f99bf0 | ||
|
|
9fb4823df0 | ||
|
|
a2fb562a22 | ||
|
|
852c05590d | ||
|
|
a54031081b | ||
|
|
23c4e4efbc | ||
|
|
5bc0c1ece4 | ||
|
|
fbab6eae5d | ||
|
|
0d9c728efe | ||
|
|
039c4a9ed4 | ||
|
|
179f248db8 | ||
|
|
589cdcc81f | ||
|
|
6997a82af1 | ||
|
|
c6f7d50838 | ||
|
|
7ce60ca0f2 | ||
|
|
b1112981fa | ||
|
|
a4419b4c89 | ||
|
|
1b036e4ea9 | ||
|
|
f38aa3b08d |
No files matched your search
@@ -299,6 +299,9 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
|
||||
│ ├── call/ # Звонки и общий голосовой стек
|
||||
│ ├── radio/ # Групповая PTT-рация
|
||||
│ ├── stream/ # Прямые трансляции: подписанные RS-блоки, выбор поставщиков, аудио/видео
|
||||
│ ├── member_files/ # Файловое хранилище участников CHAT-группы и асинхронные передачи
|
||||
│ ├── local_transport/ # Обнаружение устройств и доставка приглашений в локальной сети по UDP
|
||||
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
|
||||
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
|
||||
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
|
||||
@@ -336,13 +339,13 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
|
||||
- `tun_route.c/h` - TUN routing table sync
|
||||
- `config_parser.c/h` - INI-style config file parsing
|
||||
- `config_updater.c/h` - Config file modification utilities
|
||||
- `config_updater.c/h` - Проверка и сохранение согласованных X25519-ключей и node_id в INI-конфиге
|
||||
- `control_server.c/h` - Control/monitoring server (etcpmon backend API)
|
||||
- `firewall.c/h` - Firewall rules (per-interface filtering)
|
||||
- `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine
|
||||
- `nat_transport.c/h` - NAT transport layer (packet relay)
|
||||
- `ntp_time.c/h` - NTP time synchronization
|
||||
- `ntp_node_time.c/h` - Inter-node time synchronization
|
||||
- `nat_transport.c/h` - Передача IPv4 между отдельным TUN и удалённым NAT-провайдером через UTUN-router; локальный провайдер использует eim_nat
|
||||
- `ntp_time.c/h` - Асинхронная NTP-синхронизация: поправка времени instance без изменения часов ОС
|
||||
- `ntp_node_time.c/h` - Получение временной поправки от синхронизированных ETCP-пиров и диагностика расхождения часов
|
||||
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
|
||||
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
|
||||
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
|
||||
@@ -353,47 +356,54 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
|
||||
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
|
||||
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
|
||||
- `etcp_debug.c/h` - ETCP packet dump/formatting
|
||||
- `etcp_debug.c/h` - Диагностический разбор секций ETCP_DGRAM и форматирование IPv4-адресов
|
||||
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
|
||||
- `etcp_bbr.c/h` - BBR congestion control for ETCP
|
||||
- `etcp_connect.c/h` - ETCP outbound connections
|
||||
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
|
||||
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
|
||||
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
|
||||
- `packet_dump.c/h` - Packet hex dump utility
|
||||
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
|
||||
- `secure_channel.c/h` - AES-CCM encryption with X25519 key exchange, pubkey obfuscation
|
||||
- `packet_dump.c/h` - Текстовая сводка IPv4-пакета: адреса, поля транспорта и короткий hex-фрагмент
|
||||
- `dummynet.c/h` - Двунаправленный UDP-эмулятор между заданными endpoint: задержка, ограничение скорости и случайные потери
|
||||
- `secure_channel.c/h` - X25519, пакетное AES-128-CCM, потоковое AES-CTR, подписи Ed25519 и обфускация публичного ключа
|
||||
- `etcp_padding.c/h` - Выбор длины случайного padding и периодическое обновление диапазона для instance/соединения
|
||||
- `etcp_tx_stats.c/h` - Общая статистика исходящих очередей, inflight и подтверждённых за секунду байтов ядра
|
||||
- `crc32.c/h` - CRC32 checksums
|
||||
- `stcp.c/h` - STCP протокол (Secure TCP)
|
||||
- `stcp_server.c/h` - STCP сервер
|
||||
- `stcp_client.c/h` - STCP клиент
|
||||
- `stcp_link.c/h` - STCP линк-уровень
|
||||
- `stcp.c/h` - Зашифрованные кадры поверх TCP: согласование ключей/эпох, AES-CTR, CRC, очереди и закрытие соединения
|
||||
- `stcp_server.c/h` - Приём входящих STCP-соединений, handshake и передача готового соединения владельцу
|
||||
- `stcp_client.c/h` - Исходящее STCP-подключение, в том числе через SOCKS5 и REALITY
|
||||
- `stcp_link.c/h` - Подключение STCP к ETCP_LINK: передача пакетов, backpressure и жизненный цикл TCP-линка
|
||||
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
|
||||
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
|
||||
- `socket_monitor.c/h` - Socket state monitoring
|
||||
- `socket_monitor.c/h` - Наблюдение за адресами интерфейсов и маршрутами ОС, обновление сетевого состояния ETCP-сокетов
|
||||
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
|
||||
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
|
||||
- `reality_io.c/h` - Внешний REALITY-handshake и поток защищённых записей STCP, Finished, replay-защита и выбор relay
|
||||
- `reality_record.c/h` - TLS-подобные AES-128-GCM записи: вывод ключей, счётчики направлений, padding и KeyUpdate
|
||||
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
|
||||
- `stun_nat.c/h` - Проверка mapping/filtering NAT через внешние STUN-серверы, RFC 5780 и кеш результатов по IP/интерфейсу
|
||||
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
|
||||
|
||||
**BBR (src/transport_layer/BBR/)**
|
||||
- `bbr_v3.c` - BBR congestion control algorithm v3
|
||||
|
||||
**Routing Layer (src/routing_layer/)**
|
||||
- `routing.c/h` - Routing table (local)
|
||||
- `route_lib.c/h` - Routing library
|
||||
- `route6_lib.c/h` - IPv6 routing
|
||||
- `routing.c/h` - Обмен IPv4-пакетами UTUN между локальным TUN и сервисом DATA ETCP-router
|
||||
- `route_lib.c/h` - Локальная таблица IPv4-подсетей UTUN: добавление/удаление маршрутов и поиск узла назначения
|
||||
- `route6_lib.c/h` - Локальная таблица IPv6-подсетей на radix tree с поиском по самому длинному префиксу
|
||||
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
|
||||
- `route_connectivity.c/h` - Route connectivity checks
|
||||
- `route_connectivity.c/h` - Прямые UDP/TCP-пробы адресов узла для оценки достижимости и RTT при выборе пиров
|
||||
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
|
||||
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
|
||||
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
|
||||
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
|
||||
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
|
||||
- `topo_node.c/h` - Модель TOPO_NODE/TOPO_GROUP_NODE, подписанные снимки узлов, сериализация и общий node_registry
|
||||
- `topo_node_sqlite.c/h` - Хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite
|
||||
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
|
||||
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
|
||||
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
|
||||
- `nat_detection.c/h` - NAT type detection
|
||||
- `etcp_router.c/h` - Надёжные логические каналы сервисов поверх BGP-маршрутов: ACK, окна, транзит и защита от циклов
|
||||
- `route_tx_stats.c/h` - Скользящее окно подтверждённых payload-байтов логического канала для оценки скорости доставки
|
||||
- `multicast.c/h` - Рассылка списку узлов группы с разделением маршрута на ветви, локальными подписками и отчётами о потерях
|
||||
- `nat_detection.c/h` - Проверка mapping и доступности входящих рабочих сокетов через ETCP-пиров с учётом предварительных STUN-измерений
|
||||
- `address_profile.h` - Профиль endpoint: изменение адреса, доступность входящих и источник измерения ETCP/STUN
|
||||
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
|
||||
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
|
||||
|
||||
@@ -408,9 +418,15 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
|
||||
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
|
||||
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
|
||||
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
|
||||
- `chat_profile.c` - Подписанный профиль мембера: имя, аватар и опции сервисов; локальное UI-состояние (часть chat_core)
|
||||
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
|
||||
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
|
||||
- `chat_feedback.c/h` - Подписанные реакции и ответы: версии состояния, сохранение отмен и Merkle-синхронизация между участниками
|
||||
- `chat_poll.c/h` - Создание опросов, голосование, реакции и JSON-снимки результатов для UI/headless
|
||||
- `chat_poll_codec.c` - Проверка и wire-кодеки опросов, ответов и реакций; общий каталог эмодзи (API в chat_poll.h)
|
||||
- `chat_attachment.c/h` - Зашифрованные вложения канала: ключи, запросы доступа, ручное одобрение автора и сохраняемый outbox
|
||||
- `chat_local.c/h` - Обнаружение устройств чата и обмен готовыми invite-ссылками через локальный UDP-транспорт
|
||||
- `group_avatar.c/h` - Подписанная аватарка канала: хранение версии в SQLite, проверка подписи и передача блоков через router
|
||||
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
|
||||
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
|
||||
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
|
||||
@@ -422,7 +438,8 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
**Личные сообщения (src/dm/)**
|
||||
|
||||
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
|
||||
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и потоковое шифрование/проверка медиа
|
||||
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и E2E-контейнеры медиа через media_crypto
|
||||
- `dm_history.c/h` - Локальное удаление истории и скрытие бесед; поколения задач и отпечатки удалённых сообщений против повторов
|
||||
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
|
||||
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
|
||||
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
|
||||
@@ -430,11 +447,25 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
|
||||
**Звонки (src/call/)**
|
||||
|
||||
- `call.c/h` - Сигналинг и медиа P2P-звонка через ETCP-router, состояния звонка и оптимизация пути через conn_mgr
|
||||
- `call_proto.h` - Wire-формат сигналинга и медиа, треки/кодеки, причины завершения
|
||||
- `call.c/h` - P2P аудио/видеозвонок: подписанный E2E-сигналинг, состояния, политика занятости и оптимизация пути через conn_mgr
|
||||
- `call_priv.h` - Общее внутреннее состояние сессии для call.c и call_video_transport.c; доступ только из uasync
|
||||
- `call_proto.h` - Wire-формат сигналинга, аудиокадров и управления видеотреком; треки/кодеки и причины завершения
|
||||
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
|
||||
- `call_jitter.cpp/h` - FIFO кодированных Opus-кадров, адаптивный запас и SoundTouch time-stretch при воспроизведении
|
||||
- `call_jitter.cpp/h` - FIFO Opus, адаптивный сетевой запас, декодирование на pull и восстановление воспроизведения после пауз
|
||||
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
|
||||
- `call_clock.h` - Метки времени захвата и снимок реально проигранного аудио для синхронизации видеотрека
|
||||
- `call_delay.cpp/h` - Уменьшение накопленной задержки PCM прямым ускорением и SoundTouch с сохранением исходной шкалы времени
|
||||
- `call_timeline.cpp/h` - Карта исходного PTS через обработку SoundTouch до позиции аудиоустройства
|
||||
- `call_video.c/h` - Общий формат H.264/H.265 видеотрека, согласование возможностей и сборка кадров из фрагментов
|
||||
- `call_video_bitstream.c/h` - Проверка конфигурации и IDR без reorder, профиля/уровня и геометрии H.264/H.265; KEY-пробы кодеков
|
||||
- `call_video_transport.c/h` - Отдельные router-каналы control/audio/video, backpressure, привязка видео к аудио и запросы KEY
|
||||
- `call_video_rate.c/h` - Регулятор битрейта и допуска новых видеокадров по скорости доставки, RTT и долгу очереди
|
||||
- `call_video_port.c/h` - Потокобезопасный обмен кадрами и командами между платформенными video workers и uasync-транспортом
|
||||
- `call_video_playout.c/h` - Расписание показа по аудиочасам, ожидание/отбрасывание кадров и подтверждение фактического показа
|
||||
- `call_video_fixtures.h` - Встроенные H.264/H.265 KEY-пробы для проверки базовых режимов кодека
|
||||
- `call_video_fast_fixtures.h` - KEY-пробы видеорежимов с частотой 60 fps
|
||||
- `call_video_fullhd_fixtures.h` - KEY-пробы видеорежимов Full HD
|
||||
- `call_video_portrait_fixtures.h` - KEY-пробы видеорежимов в портретной ориентации
|
||||
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
|
||||
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
|
||||
|
||||
@@ -445,14 +476,17 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
|
||||
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
|
||||
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
|
||||
- `radio_auto.h` - Общий автомат автопередачи по VAD/уровню звука, без захвата аудио и сетевого I/O
|
||||
- `radio_call.c/h` - Адресное приглашение участника в рацию канала: сигналинг вызова, занятость, принятие и отмена
|
||||
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
|
||||
|
||||
**Прокси (src/proxy/)**
|
||||
|
||||
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
|
||||
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
|
||||
- `udp_proxy.c/h` - UDP прокси
|
||||
- `icmp_proxy.c/h` - ICMP прокси
|
||||
- `http_proxy.c/h` - Потоковый HTTP/1.x кодек: проверка заголовков, разбор тела/chunked и учёт потреблённых байтов для WINDOW
|
||||
- `udp_proxy.c/h` - Передача UDP из TUN через exit-узел, сопоставление ответов и удаление неактивных потоков
|
||||
- `icmp_proxy.c/h` - Проксирование IPv4 ICMP echo через raw socket exit-узла с восстановлением исходных id/seq
|
||||
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
|
||||
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
|
||||
|
||||
@@ -474,6 +508,45 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
|
||||
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
|
||||
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
|
||||
- `media_crypto.c/h` - Общие AES-256-CCM примитивы и порционное шифрование/проверка файлов для DM и групповых вложений
|
||||
- `encrypted_file.c/h` - Контейнер группового вложения: шифрование метаданных/файла, проверка SHA256 и durable-публикация результата
|
||||
|
||||
**Трансляции (src/stream/)** — live-аудио/видео CHAT-группы с подписью источника, Reed–Solomon и раздачей через участников.
|
||||
|
||||
- `stream.c/h` - Жизненный цикл трансляции, локальные handles издателя/читателей, события и публикация подписок
|
||||
- `stream_priv.h` - Внутренний доступ к сессиям, приём проверенных DATA/END и взаимодействие с планировщиком relay
|
||||
- `stream_proto.h` - Форматы подписанной пачки и RS-частей, дескриптор источника, размеры и коды результата
|
||||
- `stream_block.c/h` - Подпись пачки Ed25519 до RS-кодирования, разбор частей и проверка восстановленных данных
|
||||
- `stream_buffer.c/h` - Ограниченное кольцо RS-частей: дедупликация, восстановление пачек, проверка подписи и вытеснение старых данных
|
||||
- `stream_source.c/h` - Упаковка исходных байтов в подписанные RS-пачки, сброс неполной пачки по таймеру и завершение источника
|
||||
- `stream_member.c/h` - Объявления источников и подписок в опциях подписанного мембера; разбор и проверка принадлежности источника
|
||||
- `stream_wire.c/h` - Протокол выдач и relay: кодограммы управления, пути, метрики и подписанное завершение END
|
||||
- `stream_network.c/h` - Выдача блоков через прямой ETCP, NCD-владение, управляющие очереди, аренда подписок и reverse-запросы
|
||||
- `stream_optimizer.c/h` - Автоматический выбор и перенос поставщиков субпотоков, пробы доступности и привлечение relay
|
||||
- `stream_metrics.c/h` - Оценка относительного отставания субпотока и истории проблем по часам источника без NTP
|
||||
- `stream_load.c/h` - Учёт логической очереди DATA и её роста для обнаружения перегрузки раздающего узла
|
||||
- `stream_media.c/h` - Формат uAV2: упаковка/проверка видеокадров, конфигурации кодека, PTS и Opus внутри FEC-пачек
|
||||
- `stream_media_buffer.c/h` - Медиаиндекс общего RS-кольца и независимые курсоры audio/video; сборка кадров и восстановление с KEY
|
||||
- `stream_publisher.c/h` - Общий потокобезопасный publisher desktop/Android: очередь готовых кадров, публикация и завершение с FIN
|
||||
- `stream_viewer.c/h` - Независимые viewers: запрос ограниченных батчей по бюджетам треков, уведомления готовности и конца audio/video
|
||||
- `stream_playback_audio.cpp/h` - Opus/SoundTouch и очередь готового PCM; ускорение без изменения высоты и привязка к позиции устройства
|
||||
- `stream_playback_clock.c/h` - Общие часы воспроизведения и плавный догон live-потока по непрерывному запасу готового материала
|
||||
|
||||
**Файлы участников (src/member_files/)**
|
||||
|
||||
- `member_files.c/h` - Сервер и асинхронный клиент хранилища CHAT-группы: права доступа, запросы и события через защищённый router
|
||||
- `protocol.c/h` - Wire-кодек файловых команд, снимки версий каталогов/файлов и токены временных загрузок
|
||||
- `storage.c/h` - Серверные каталоги участников, квоты, файловые операции, временные загрузки и их очистка
|
||||
- `transfer.c` - Передача файлов с CM handle, backpressure, дисковым worker, проверкой хеша и публикацией результата
|
||||
- `disk.c/h` - Дисковые примитивы POSIX/Windows: безопасное разрешение путей без ссылок и I/O по 64-битному смещению
|
||||
- `worker.c/h` - Единственный дисковый pthread с ограниченной очередью и возвратом результатов в uasync
|
||||
- `private.h` - Внутреннее состояние запросов, передач, серверного хранилища и дискового worker
|
||||
|
||||
**Локальная сеть (src/local_transport/)**
|
||||
|
||||
- `local_transport.c/h` - Открытый UDP-транспорт LAN: обнаружение устройств, доставка сообщений с ACK/повторами и ограничения входящего потока
|
||||
- `platform.c/h` - Снимок IPv4/IPv6 интерфейсов, broadcast/multicast-адреса, приём метаданных UDP и проверка локальности источника
|
||||
- `protocol.h` - Wire-формат объявлений, сообщений и ACK: ID/эпохи устройств, ID запроса и проверка UTF-8 имени
|
||||
|
||||
**Libraries (lib/)**
|
||||
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
|
||||
@@ -484,6 +557,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
|
||||
- `serialize.c/h` - Binary serialization utilities
|
||||
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
|
||||
- `video_plane.c/h` - Копирование YUV-плоскости с поворотом 0/90/180/270°, учётом stride и проверкой границ без выделения памяти
|
||||
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
|
||||
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
|
||||
- `myip.c` - My IP helper (no header)
|
||||
@@ -509,6 +583,16 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
|
||||
- `wintun.h` - Wintun API header for Windows TUN driver
|
||||
|
||||
**Проверки (tests/)**
|
||||
|
||||
- `Makefile.am` - Состав тестовых целей, зависимости и отдельные проверки runner, service IDs и скрипта сборки
|
||||
- `run_tests.py` - Запуск тестов ограниченным пулом с отдельными логами и последовательными барьерами для CPU/LAN-проверок
|
||||
- `test_run_tests.py` - Проверки пула runner, эксклюзивного запуска, обработки ошибок и сигналов
|
||||
- `test_service_ids.py` - Проверка уникальности ID транспортных и router-сервисов
|
||||
- `test_build_script.py` - Регрессии выбора платформы, конфигурации и инкрементальной работы build.sh
|
||||
- `stream_netns/` - Linux network namespaces: топологии, NAT/STUN-сценарии и проверка трансляций в изолированной сети
|
||||
- `local_transport_ipv6.sh` - Проверка IPv6 link-local обнаружения и доставки в изолированной локальной сети
|
||||
|
||||
**Memory Pools in UTUN_INSTANCE:**
|
||||
- `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами
|
||||
- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём)
|
||||
@@ -685,11 +769,18 @@ cmake .. && make -j4
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `main.cpp` | Точка входа, инициализация QApplication |
|
||||
| `mainwindow.h/cpp` | Главное окно, списки каналов/DM/участников, сообщения, трей, события ядра и окна звонков |
|
||||
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
|
||||
| `main.cpp` | Точка входа, QApplication, язык интерфейса и запуск единственного экземпляра профиля |
|
||||
| `single_instance.h/cpp` | Блокировка каталога профиля и передача запроса показа окна уже работающему экземпляру |
|
||||
| `gui_language.h` | Выбор языка интерфейса независимо от регионального формата дат и чисел |
|
||||
| `mainwindow.h/cpp` | Главное окно, каналы/DM, трей, события ядра, звонки, локальные приглашения и окна трансляций |
|
||||
| `chatview.h/cpp` | Список сообщений: фон, hover, выделение текста и прокрутка истории мышью/клавиатурой |
|
||||
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
|
||||
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
|
||||
| `messagefeedback.cpp` | Действия MessageList: создание опроса, голосование, выбор реакции и показ результатов |
|
||||
| `attachment_key_ui.cpp` | Список запросов на расшифровку групповых вложений и решения автора о выдаче ключа |
|
||||
| `message_links.h/cpp` | Разбор границ utun:// и HTTP/HTTPS ссылок для подсветки и обработки нажатий в сообщении |
|
||||
| `message_notification.h/cpp` | Уведомление о сообщении без захвата фокуса, переход в беседу и скрытие после активности пользователя |
|
||||
| `user_activity_monitor.h/cpp` | Ожидание первой активности через системный idle time либо события приложения |
|
||||
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
|
||||
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
|
||||
| `emojitabbar.h/cpp` | Кастомный QTabBar для эмодзи-панели |
|
||||
@@ -705,11 +796,13 @@ cmake .. && make -j4
|
||||
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
|
||||
| `invite_link.h/cpp` | Логика invite-ссылок |
|
||||
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
|
||||
| `dminvitedialog.h/cpp` | Выбор канала для приглашения собеседника из DM с сопоставлением результата по request_id |
|
||||
| `settingsdialog.h/cpp` | Окно настроек |
|
||||
| `creategroupdialog.h/cpp` | Диалог создания группы |
|
||||
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
|
||||
| `renamedialog.h/cpp` | Диалог переименования |
|
||||
| `networksettingspage.h/cpp` | Страница сетевых настроек |
|
||||
| `addressprofiletext.h` | Читаемое представление mapping, доступности входящих и источника NAT-измерения |
|
||||
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
|
||||
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
|
||||
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
|
||||
@@ -718,14 +811,32 @@ cmake .. && make -j4
|
||||
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
|
||||
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
|
||||
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
|
||||
| `audio_buffer_tuning.h` | Подбор размера аппаратного аудиобуфера по повторным XRUN с сохранением настройки при reopen |
|
||||
| `audio_capture.h/cpp` | Совместное использование одного микрофона трансляцией и рацией, подписки PCM и смена профиля захвата |
|
||||
| `audio_capture_lease.h` | Владение захватом, исключающее одновременную работу publisher, звонка и записи голосового сообщения |
|
||||
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
|
||||
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
|
||||
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
|
||||
| `voice_capture_clock.h` | Непрерывные PTS захваченного PCM по позиции сэмплов с плавной поправкой аппаратного времени |
|
||||
| `voice_playback_buffer.h` | Адаптивный запас готового PCM: увеличение после недостачи и уменьшение при стабильном выводе |
|
||||
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
|
||||
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
|
||||
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
|
||||
| `callwindow.h/cpp` | Окно звонка: состояния, управление, путь соединения и статистика аудио |
|
||||
| `callwindow.h/cpp` | Окно аудио/видеозвонка: управление, выбор камеры, локальное/удалённое видео и статистика |
|
||||
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
|
||||
| `call_video_engine.h/cpp` | Desktop-видеозвонок: камера, TX/RX workers, обмен с ядром и показ кадров по аудиочасам |
|
||||
| `call_video_encoder.h/cpp` | Кодер звонка: согласованный формат, смена битрейта с drain прежнего кодера и подтверждение настройки |
|
||||
| `call_video_decoder.h/cpp` | FFmpeg-декодер H.264/H.265 с выбором GPU/CPU и сохранением PTS при задержанном выходе |
|
||||
| `call_camera_capture.h/cpp` | Управление отдельным процессом камеры, каталог устройств/режимов и получение кадров |
|
||||
| `call_camera_worker.cpp` | Процесс Qt-захвата камеры: настройка устройства, отбор кадров и выдача пикселей через shared memory |
|
||||
| `call_camera_source.h` | Интерфейс backend камеры для процесса захвата и подстановки устройств в тестах |
|
||||
| `call_camera_protocol.h` | Локальный протокол управляющих команд, каталога устройств и FRAME/ACK процесса камеры |
|
||||
| `call_camera_buffer.h` | Владение shared-memory буфером пикселей между процессами захвата и GUI |
|
||||
| `call_camera_rate.h` | Временное отключение снижения FPS из-за автоэкспозиции V4L2 с восстановлением настройки камеры |
|
||||
| `call_video_cadence.h` | Отбор исходных кадров под заданный FPS без накопления ошибки дробного периода |
|
||||
| `call_video_capture_clock.h` | Привязка времени камеры к монотонным часам с коррекцией задержки первого callback |
|
||||
| `call_video_queue.h` | Ограниченная SPSC-очередь кадров между video workers без mutex и выделений памяти |
|
||||
| `call_video_wake.h` | Пробуждение video worker по событию или абсолютному монотонному deadline на POSIX/Windows |
|
||||
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
|
||||
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
|
||||
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
|
||||
@@ -736,6 +847,17 @@ cmake .. && make -j4
|
||||
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
|
||||
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
|
||||
| `videoplayer_window.h/cpp` | Окно просмотра видео |
|
||||
| `live_video_encoder.h/cpp` | Общий FFmpeg-кодер live-видео для звонка/трансляции, GPU/CPU, KEY-конфигурация и capture PTS |
|
||||
| `video_frame_image.h` | Выделение выровненного RGB-буфера FFmpeg и передача владения QImage без копирования |
|
||||
| `stream_publish_engine.h/cpp` | Публикация камеры/экрана и Opus: workers кодирования, локальный preview и завершение через общий publisher |
|
||||
| `stream_publish_window.h/cpp` | Настройки и управление трансляцией; закрытие окна сохраняет работающий engine |
|
||||
| `stream_portal_capture.h/cpp` | Захват экрана Wayland через ScreenCast portal/PipeWire, выбор монитора и режима курсора |
|
||||
| `stream_screen_cursor.h/cpp`, `stream_screen_cursor_win.cpp` | Наложение системного курсора на кадр выбранного экрана средствами X11/Windows |
|
||||
| `stream_video_modes.h` | Выбор размеров/FPS камеры и пропорционального масштабирования экрана для трансляции |
|
||||
| `stream_qp_control.h` | Плавная корректировка потолка QP по фактическому выходному видеобитрейту |
|
||||
| `stream_sources.h` | Разбор снимка активных источников канала из CHAT_EVT_STREAM_SOURCES |
|
||||
| `streamplayer_window.h/cpp` | Просмотр live-потока: отдельные audio/video workers, смешивание PCM и синхронизация по часам устройства |
|
||||
| `streamgrid_window.h/cpp` | Сетка трансляций канала и разворачивание плитки с сохранением плееров; своя плитка использует локальный preview |
|
||||
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
|
||||
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
|
||||
| `debug_ui.h` | Отладочный UI |
|
||||
@@ -745,9 +867,14 @@ cmake .. && make -j4
|
||||
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
|
||||
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
|
||||
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
|
||||
- `src/avatars/` — `avatarmanager.h/cpp`: загрузка/публикация аватаров участников через member_files, подписанные ссылки и очередь передач;
|
||||
`avatarcache.h/cpp`: дисковый кеш, подготовка изображений и очистка; `avatarcrop.h/cpp`: выбор области;
|
||||
`avatargallery.h/cpp`: просмотр аватаров пользователя; `groupavatarmanager.h/cpp`: подготовка и кеш подписанных аватарок каналов
|
||||
- `src/member_files/` — `browser.h/cpp`: браузер серверных файлов группы; `batch.h/cpp`: последовательные операции над файлами/деревьями,
|
||||
фоновый локальный обход, загрузка/скачивание и отмена
|
||||
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
|
||||
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
|
||||
- `tests/` — проверки аудиоустройств и восстановления, голосового тракта, jitter-буферов, PTT, кодирования и GUI-компонентов
|
||||
- `tests/` — проверки аудиотракта, PTT, видеозвонка, захвата камеры/экрана, трансляций, файлового браузера и GUI-компонентов
|
||||
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
|
||||
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
|
||||
|
||||
@@ -761,6 +888,7 @@ cmake .. && make -j4
|
||||
| `config_updater.h/cpp` | Изменение настроек в конфиге |
|
||||
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
|
||||
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
|
||||
| `stream_bridge.h/cpp` | Потокобезопасный мост к общим publisher/viewers: session IDs, каталог источников, запросы батчей и пробуждение workers |
|
||||
|
||||
### Система анимированных эмодзи
|
||||
|
||||
@@ -864,6 +992,13 @@ void lottie_animation_destroy(Lottie_Animation *anim);
|
||||
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
|
||||
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
|
||||
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
|
||||
- `doc/call_video.txt` — видеотрек звонка: возможности кодеков, транспорт, битрейт, аудиочасы и показ кадров
|
||||
- `doc/stream.txt` — трансляции: RS-пачки, выдачи/relay, выбор поставщиков, медиакурсоры и платформенные плееры
|
||||
- `doc/stun_nat.md`, `doc/nat_detection.txt` — STUN-измерения и определение доступности рабочих UDP-сокетов
|
||||
- `doc/member_files.txt` — файловое хранилище участников, права, wire-команды и передачи
|
||||
- `doc/group_avatar.txt` — подписанная аватарка канала, хранение и репликация
|
||||
- `doc/encrypted_attachments.md` — контейнер группового вложения, запросы ключа и ручная выдача доступа
|
||||
- `doc/pm_invites.md`, `doc/message_links.md` — приглашения из личной беседы и ссылки внутри сообщений
|
||||
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
|
||||
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
|
||||
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
|
||||
@@ -906,7 +1041,11 @@ C-библиотека `utun_lite` собирает `lib/` и `src/` по `libut
|
||||
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
|
||||
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
|
||||
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
|
||||
| `libutun_lite/stream_bridge.c/h` | Android-мост к publisher/viewers: session IDs, готовые access units/Opus и ограниченные батчи треков |
|
||||
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
|
||||
| `jni_bridge/android_call_output_jni.c` | Вывод PCM звонка через AAudio с аппаратным тактированием и снимками позиции устройства |
|
||||
| `jni_bridge/android_call_video_jni.c` | JNI видеозвонка: capabilities/формат, обмен кадрами с call_video_port, playout и поворот YUV |
|
||||
| `jni_bridge/android_stream_jni.c` | JNI трансляций: publisher/viewer API, Opus/SoundTouch, готовый PCM и часы воспроизведения |
|
||||
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
|
||||
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
|
||||
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
|
||||
|
||||
+9
-3
@@ -153,10 +153,14 @@ general=info # общая информация
|
||||
```ini
|
||||
[firewall]
|
||||
allow=all # разрешить всё (bypass)
|
||||
allow=192.168.1.0/24 # разрешить подсеть
|
||||
allow=192.168.1.10 # разрешить весь IP
|
||||
allow=10.0.0.1:80 # разрешить конкретный IP:порт
|
||||
```
|
||||
|
||||
Фильтр основного TUN проверяет IP и TCP/UDP-порт назначения одинаково в обоих направлениях.
|
||||
Без разрешений весь трафик этого интерфейса запрещён; ответы требуют отдельных разрешений.
|
||||
CIDR-подсети не поддерживаются. Ошибочное правило отменяет загрузку нового конфига.
|
||||
|
||||
### 3.7. `[nat]` — EIM NAT
|
||||
|
||||
```ini
|
||||
@@ -553,11 +557,13 @@ dump=trace
|
||||
```ini
|
||||
[firewall]
|
||||
allow=all # всё разрешено
|
||||
allow=10.0.0.0/8 # разрешить подсеть
|
||||
allow=10.0.0.5 # разрешить весь IP
|
||||
allow=192.168.1.5:22 # разрешить IP:порт
|
||||
```
|
||||
|
||||
По умолчанию без секции `[firewall]` — фильтрация выключена.
|
||||
Без секции `[firewall]` весь входящий и исходящий трафик основного TUN запрещён.
|
||||
`allow=IP` и `allow=IP:0` разрешают все протоколы IP; `allow=IP:port` — TCP/UDP указанного порта.
|
||||
Для фрагментов IPv4 нужен весь IP либо `allow=all`. NAT и прокси используют отдельные TUN интерфейсы.
|
||||
|
||||
### 7.3. Проброс портов
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
Chat: исправление приглашений и проверка контрактов — 26.10.2026
|
||||
|
||||
Граница работы
|
||||
|
||||
В начале аудита по /home/user/utun-doc-audit-checklist.txt установлены два дефекта:
|
||||
успех invite_link_decode без узла подключения и разыменование непроверенного
|
||||
выделения cm_invite_wrap. После согласования архитектуры исправлен весь путь
|
||||
команды JOIN; добавлены тесты контрактов, отказов и жизненного цикла.
|
||||
Полный аудит всех модулей src/chat не завершён. chat_sync.h остаётся [~].
|
||||
|
||||
Реализация
|
||||
|
||||
1. invite_link.c/.h — единый C-кодек для headless, Qt и Kotlin/JNI.
|
||||
Только версия 0x03, строгий канонический base64 с padding, полный разбор,
|
||||
одинаковый ключ во всех блоках, 1..32 адреса, family=4/6, proto=1/2/6,
|
||||
ненулевой порт, согласованный REALITY. Канал — 1..INT64_MAX, join_key —
|
||||
любой ненулевой uint64. Нулевой первый байт публичного ключа допустим.
|
||||
Строки ограничены, превышение длины отклоняется; скрытый NUL не принимается.
|
||||
Успех decode полностью определяет InviteData; ошибка оставляет out прежним.
|
||||
Encode и сериализация адресов также не меняют выход при отказе.
|
||||
Максимум с NUL — INVITE_LINK_MAX=1552: 32 IPv6, REALITY и пароль127 байт.
|
||||
Из frontend удалены отдельные реализации декодирования; Qt/Kotlin только
|
||||
преобразуют результат C-кодека в своё представление.
|
||||
|
||||
2. chat_sync_join_link/offer_link выполняются только в uasync и принимают ссылку.
|
||||
Промежуточные chat_invite/addrs_data/cm_invite_wrap удалены. NCD bootstrap
|
||||
строится целиком, включая REALITY; ошибка освобождает все временные адреса.
|
||||
Общий объект локальной операции хранит роль, фазу, request/attempt, NCD и deadline.
|
||||
PREPARE не реагирует на UP до установки handle; затем уже работающий транспорт
|
||||
запускает обмен без ожидания повторного UP. Другой владелец сохраняет свой NCD.
|
||||
BUSY относится к локальной операции; чужие JOIN обслуживаются независимо.
|
||||
|
||||
3. INFO_REQ/RESP, REQUEST, READY и ERROR несут attempt_id:8BE сразу после type.
|
||||
JOIN_READY содержит также ровно8 байт ожидаемого group_id. Ответ сопоставляется
|
||||
с peer/group/attempt/phase; повторное использование ссылки создаёт новый attempt.
|
||||
Connection-узел хранит отдельные waiters попыток; повторный запрос не затирает
|
||||
токен прежнего waiter. Есть deadline и ограничение8 waiters на peer; просроченные
|
||||
удаляются при новых запросах и штатном обновлении кеша.
|
||||
CHANNEL_INVITE переносит предложение и join_key; принимающий создаёт свой attempt.
|
||||
Регистрация ключа A↔C и подписанные записи участников не используют attempt_id.
|
||||
Сетевой формат JOIN изменён без поддержки старых узлов, согласно devel-контракту.
|
||||
|
||||
4. chat_join_port — отдельная удерживаемая ссылка frontend на вход команд.
|
||||
Submit копирует ссылку, резервирует весь posted_task до принятия команды и
|
||||
публикует без дополнительного выделения памяти. Закрытие и публикация защищены
|
||||
mutex. Закрытый порт не содержит instance/uasync и не оживает при restart.
|
||||
Close отменяет ожидающие команды; уже извлечённый uasync batch исполнит только
|
||||
освобождение. Arg встроен в выделение task, повторного освобождения нет.
|
||||
Qt и instance_lite публикуют/удерживают порт под собственным mutex; GUI/JNI
|
||||
не читают chat_sync из внешнего потока. Chat stop закрывает приём до workers.
|
||||
Request интерфейса не равен сетевому attempt. Headless использует request=0,
|
||||
сохраняя собственную корреляцию JSON-команд отдельно от GUI.
|
||||
|
||||
5. Завершение: отделение локальной операции, снятие таймеров, освобождение своего
|
||||
handle, затем единое событие peer:8/result:4/channel:8/request:8 (native).
|
||||
Обработчик события может сразу начать новую попытку. Ранний отказ core не даёт
|
||||
события; мост завершает свою уже принятую команду этим отказом. Принятая core
|
||||
попытка получает один результат, включая cancel/delete/stop.
|
||||
Cancel не откатывает членство, которое уже сохранил другой узел, и не удаляет
|
||||
принятый NCD snapshot или рабочие линки других владельцев.
|
||||
|
||||
6. JOIN_INFO_RESP: подписанное описание и member/Merkle записываются в одной SQL
|
||||
транзакции. Проверяется привязка connection-member к транспортному peer и
|
||||
неизменность уже известных канальных ключей. Ошибка откатывает SQL и не создаёт
|
||||
CHAT-группу. После commit публикуются runtime-ресурсы и уведомления member_sync.
|
||||
Предыдущая семантика отложенной проверки неизвестного родителя сохранена;
|
||||
успешный apply не называется подтверждением всей цепочки приглашений.
|
||||
Ensure_channel_ready возвращает ошибку и сохраняет необходимый порядок:
|
||||
группа → подписка → db_sync. При отказе убираются только новые runtime-ресурсы.
|
||||
Проверенные данные после commit сохраняются для повторной подготовки.
|
||||
Ключи приглашения, пароль и исходная ссылка не печатаются в новых логах.
|
||||
|
||||
Тесты
|
||||
|
||||
- test_invite_codec: 116342 проверки. Roundtrip1..32 адресов IPv4/IPv6/mixed/REALITY,
|
||||
пароль127, SNI63, максимальная ссылка1551 байт; точный буфер и буфер на байт меньше,
|
||||
сохранность выходов, усечения, алфавит всех256 байтов, старые версии, конфликт
|
||||
ключей блоков, неизвестные/отсутствующие поля и границы значений.
|
||||
- test_chat_join_faults: настоящий controller/NCD/SQLite, перехват только доставки
|
||||
пакетов и точек отказа. Каждая аллокация подготовки, включая REALITY, отказы
|
||||
transport/info/ready deadlines, send и NCD; повтор после отказа и чужое владение.
|
||||
Запоздавшие ERROR/READY, чужой group/короткий READY, повтор результата, BUSY,
|
||||
вложенный UP во время PREPARE и новый JOIN из обработчика завершения.
|
||||
SQL-trigger и неверная подпись member откатывают канал; отказы group/db_sync/
|
||||
подписки после commit сохраняют проверенные данные и убирают runtime-ресурсы.
|
||||
Узел обслуживает чужой JOIN во время своей попытки. Проверены queued/active
|
||||
дубликаты request, producer другого потока, старый порт после restart,
|
||||
stop между копированием аргументов и публикацией (condvar-барьер), stop внутри
|
||||
уже извлечённого batch. После teardown число u_malloc-блоков возвращается к исходному.
|
||||
- test_chat_join_e2e: настоящий обмен A!=C, A==C, неверный ключ.
|
||||
- test_dm_invite: приглашение из DM и реальное вступление.
|
||||
- test_services: независимый lifecycle сервисов.
|
||||
- test_chat_sync_schedule: фикстуры приведены к наличию активной попытки/waiter;
|
||||
подтверждается синхронизация пары после JOIN_READY без нового транспортного UP.
|
||||
- Qt test_invite_links/test_local_invite_ui: ссылки и UI на общем кодеке.
|
||||
|
||||
Результаты окончательных прогонов и ограничения приводятся ниже.
|
||||
|
||||
Итоговые результаты
|
||||
|
||||
Linux check.sh: 196/196 PASS, 0 failed, 0 skipped; proxy9/9, burst1/1,
|
||||
load8/8, 134217728 проверенных байт, 190 секунд.
|
||||
Лог: /tmp/chat-join-complete-check.log.
|
||||
Отдельный финальный test_chat_join_faults с condvar-гонкой и вложенным UP PASS;
|
||||
лог: /tmp/chat-join-race-final.log. Тот же вариант включён в окончательный check.sh.
|
||||
|
||||
ASAN/UBSan/LSan: 6/6 PASS, detect_leaks=1, halt_on_error=1.
|
||||
Кодек, отказы JOIN, настоящий E2E, DM invite, services и планировщик.
|
||||
В изолированной копии /tmp/utun-chat-join-sanitizers согласованно инструментированы
|
||||
lib/src/Opus. Правка флагов Opus выполнена только в копии: рабочая сборка не смешивается
|
||||
с sanitizer-объектами. Сводка: /tmp/chat-join-sanitizer-results.log;
|
||||
по каждому набору: /tmp/chat-join-sanitizer-test_*.log.
|
||||
|
||||
Desktop CMake build PASS: /tmp/chat-join-desktop-verified-build.log.
|
||||
Qt invite/local_invite: 2/2 PASS, /tmp/chat-join-desktop-verified-tests.log.
|
||||
Android assembleDebug --offline PASS (JNI, Kotlin и APK; без установки на устройства):
|
||||
/tmp/chat-join-android-verified-build.log.
|
||||
Дополнительный старый Linux CMake-каркас Android headless не собрался из-за отсутствия
|
||||
include speex/speex_echo.h в его собственном списке путей; этот отдельный каркас
|
||||
не заменяет успешную APK-сборку и native Linux-фикстуры основного набора.
|
||||
Windows/FreeBSD native build/runtime не выполнялись. git diff --check PASS.
|
||||
|
||||
Промежуточные отказы проверок устранены: размер size_t и порядок _GNU_SOURCE в
|
||||
instance_lite, старое объявление InviteDataC в JNI; затем фикстура планировщика
|
||||
обновлена под обязательные waiter/активную попытку. Тесты runtime-отката выявили
|
||||
зависимость db_sync от группы; требуемый порядок восстановлен, добавлена очистка
|
||||
новой группы при отказе истории/подписки. Это покрыто отрицательными сценариями.
|
||||
Исходные изменения аудита и итоговые правки не коммитились и не публиковались.
|
||||
+79
-27
@@ -1,17 +1,18 @@
|
||||
# P2P аудио-звонок пользователю — архитектура
|
||||
# P2P аудио- и видеозвонок пользователю — архитектура
|
||||
|
||||
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
|
||||
|
||||
## 1. Область
|
||||
|
||||
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
|
||||
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
|
||||
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
|
||||
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
|
||||
Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**.
|
||||
Модуль оперирует `group_id` + `peer_node_id`. Для `dm:<conv_id>` функция `dm_call_route`
|
||||
выбирает общую CHAT-группу с живым BGP-маршрутом к собеседнику; отдельная DM-группа не создаётся.
|
||||
Аудио и видео используют отдельные каналы `etcp_router`; возможности видеокодеков
|
||||
согласуются в активном звонке (см. [call_video.txt](call_video.txt)). Работает desktop↔desktop и desktop↔android
|
||||
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
|
||||
|
||||
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
|
||||
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
|
||||
аудиопоток и опциональное видео через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
|
||||
|
||||
### Входящие вызовы и телефонная занятость
|
||||
|
||||
@@ -42,22 +43,41 @@ Android обычного звонка показывает своё уведом
|
||||
|
||||
| Файл | Язык | Сборки | Назначение |
|
||||
|---|---|---|---|
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x36` |
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd |
|
||||
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
|
||||
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
|
||||
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
|
||||
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
|
||||
| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами |
|
||||
|
||||
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
|
||||
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
|
||||
|
||||
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
|
||||
Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`).
|
||||
Начальная проверка codec пишет краткий итог на info, детали и ожидаемые отказы — на debug:
|
||||
`[gui] debug_categories=call=debug,video=debug`. Повторы FFmpeg внутри пробы сворачиваются со счётчиком;
|
||||
рабочие ошибки параллельного звонка сохраняют error/warn. Информационный stderr libva отключён по умолчанию;
|
||||
для диагностики драйвера можно явно запустить с `LIBVA_MESSAGING_LEVEL=2`.
|
||||
Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal.
|
||||
|
||||
## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd)
|
||||
|
||||
Формат приёма: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
|
||||
## 3. Протокол
|
||||
|
||||
Каналы роутера независимы:
|
||||
|
||||
| svc_id | Константа | Назначение |
|
||||
|---|---|---|
|
||||
| `0x36` | `ETCP_RT_ID_CALL` | сигналинг звонка |
|
||||
| `0x3D` | `ETCP_RT_ID_CALL_VIDEO_CONTROL` | согласование и восстановление видеотрека |
|
||||
| `0x3E` | `ETCP_RT_ID_CALL_VIDEO` | видеокадры |
|
||||
| `0x3F` | `ETCP_RT_ID_CALL_AUDIO` | аудиокадры `CALL_MEDIA` |
|
||||
|
||||
Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
|
||||
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
|
||||
|
||||
Сигналинг (`0x36`):
|
||||
|
||||
```
|
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
|
||||
CALL_RINGING 0x02 {call_id:8}
|
||||
@@ -68,14 +88,27 @@ CALL_CANCEL 0x06 {call_id:8}
|
||||
CALL_HANGUP 0x07 {call_id:8, reason:1}
|
||||
CALL_END_ACK 0x08 {call_id:8}
|
||||
CALL_PHONE_BUSY 0x09 {call_id:8}
|
||||
```
|
||||
|
||||
Аудио (`0x3F`):
|
||||
|
||||
```
|
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
|
||||
```
|
||||
|
||||
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
|
||||
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
|
||||
- `group_id` — CHAT-группа; для DM-звонка выбирается общая группа участников с живым BGP-маршрутом.
|
||||
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
|
||||
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
|
||||
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
|
||||
- Сигналинг, аудио и видео: обязательные `ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT` между конечными узлами.
|
||||
- AES-128-CCM шифрует тело, Ed25519 подписывает заголовок роутера и шифртекст. Посредники не имеют ключа для расшифровки.
|
||||
- Ключ получается из постоянных X25519-идентичностей собеседников; forward secrecy отсутствует.
|
||||
- Маска защиты задаётся через `etcp_router_require_crypto`: открытые/частично защищённые данные отбрасываются
|
||||
на конечном узле до изменения состояния приёма, очередей и `receive_ready`; отправка без требуемых флагов возвращает ошибку.
|
||||
Пустые служебные пакеты роутера не являются медиаданными. Перехода на открытое медиа при ошибках нет.
|
||||
- Каждый пакет получает случайный nonce(13) от OpenSSL CSPRNG; ошибка RNG прекращает шифрование.
|
||||
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
|
||||
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
|
||||
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
|
||||
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
|
||||
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
|
||||
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
|
||||
@@ -361,27 +394,45 @@ CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
|
||||
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
|
||||
```
|
||||
|
||||
## 10. Публичный API (call.h)
|
||||
## 10. Основные функции публичного API
|
||||
|
||||
```
|
||||
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
|
||||
void call_accept(call_id); // callee
|
||||
void call_decline(call_id);
|
||||
void call_hangup(call_id);
|
||||
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
|
||||
// аудио-поток (общий процессинг в C-ядре):
|
||||
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
|
||||
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
|
||||
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
|
||||
void call_release(call_id); // post → uasync: освободить аудио-объекты
|
||||
`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке.
|
||||
Из других потоков команды передаются через `uasync_post`/мост GUI и соответствующие `call_*_trampoline`.
|
||||
`ch_id` — идентификатор CHAT-группы либо `dm:<conv_id>`; `out_call_id` — значение, заранее сгенерированное вызывающим.
|
||||
|
||||
```c
|
||||
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
|
||||
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
|
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
|
||||
const struct call_capture_time* timing);
|
||||
```
|
||||
|
||||
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
|
||||
`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync.
|
||||
Захват и воспроизведение используют по одному аудиопотоку на направление.
|
||||
|
||||
```c
|
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us);
|
||||
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us);
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
|
||||
void call_audio_release(uint64_t call_id);
|
||||
```
|
||||
|
||||
`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC
|
||||
и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O.
|
||||
Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`.
|
||||
Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame`
|
||||
в uasync-потоке; полный контракт — в `call_video_transport.h`.
|
||||
|
||||
`call_init / call_destroy` вызываются из `chat_service_start / chat_service_stop` в `utun_instance.c`:
|
||||
инициализация после `dm_core_init`, освобождение перед `dm_core_destroy`.
|
||||
|
||||
## 11. Платформенный I/O
|
||||
|
||||
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
|
||||
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
|
||||
(capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI)
|
||||
+ события в `gui_bridge_impl.cpp`.
|
||||
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
|
||||
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
|
||||
@@ -398,7 +449,8 @@ void call_release(call_id); // post → uasync: о
|
||||
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
|
||||
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
|
||||
`LANGUAGES C CXX`, те же файлы.
|
||||
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
|
||||
- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`,
|
||||
`ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`.
|
||||
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
|
||||
|
||||
## 13. Порядок задач
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
Аудит config_parser — 26.10.2026
|
||||
|
||||
Полностью прочитаны config_parser.c/.h, config_parser_doc.md и общий config_ini.h.
|
||||
Для согласования строгого разбора также повторно сверены config_updater.c/.h/_doc.md.
|
||||
Разбор сохраняет модель и владение free_config; запуск ресурсов остаётся у instance.
|
||||
|
||||
Исправления
|
||||
- IPv6 /32 больше не подменяется /128; префиксы проверяются до сужения к uint8_t.
|
||||
Control allow=/0 задаёт нулевую маску без сдвига на 32; IPv6 control allow отклоняется.
|
||||
- Ошибки известных обработчиков и выделения памяти отклоняют всю модель.
|
||||
Все частичные списки освобождаются, включая client links и не завершённую секцию.
|
||||
- Числа проверяются целиком до преобразования: знаки, хвост, диапазон и overflow.
|
||||
Keepalive interval помещается в uint16_t; таймеры ограничены до умножения.
|
||||
Bool унифицирован: 0/1, no/yes, false/true. Версия — ровно три десятичных октета.
|
||||
- Адресные порты проверяются как decimal 0..65535, SOCKS — 1..65535.
|
||||
Control ip/port даёт одинаковый результат в обоих порядках; ошибки getaddrinfo не теряются.
|
||||
- Storage sizes проверяют UINT64_MAX, множитель M/G и полный хвост; -1 не становится безлимитом,
|
||||
переполнение не становится нулевым лимитом. Слишком длинные строковые настройки отклоняются.
|
||||
- Network ID ограничен 56 битами; требуются оба 32-byte HEX ключа.
|
||||
Предоставленные ключи идентичности/пиров проверяются целиком; пустые необязательные остаются незаданными.
|
||||
- CFG_CLIENT_LINK сохраняет имя local_srv. Повторный обычный addr сбрасывает режим dynamic IPv6.
|
||||
- Флаг tcp_proxy_client enabled=no не включается снова следующей опцией той же секции.
|
||||
Наличие service-секции задаёт её default enable и при отсутствии опций.
|
||||
- Print_config не печатает private/signing keys; Networks выведены за пределы firewall-цикла.
|
||||
Устаревший append ключей проверяет вход, запись и fclose; не объявляется атомарным updater.
|
||||
- Updater выбирает итоговую идентичность/REALITY по последнему вхождению, но исправляет также
|
||||
синтаксически плохие ранние дубли выбранными значениями. Из-за такого дубля ключи не генерируются заново.
|
||||
Это нужно, поскольку строгий парсер проверяет каждое вхождение. Булевы REALITY enabled согласованы.
|
||||
|
||||
Контракты и ограничения
|
||||
Обновлены заголовок и документ: порядок списков, необъединяемые named-секции, remote-only links,
|
||||
синхронный DNS и первый getaddrinfo-адрес, Windows socket init, default-значения и границы,
|
||||
повторные опции, поведение неизвестных секций/debug/gui и независимость parser/updater/start.
|
||||
Bootstrap invite printer использует конфигурационные адреса и нулевые channel_id/join_key;
|
||||
это не снимок фактических bind/NAT-адресов и не invite конкретного канала.
|
||||
Нативные сборки/runtime Windows и FreeBSD в этом этапе не выполнялись.
|
||||
|
||||
Проверки
|
||||
- Полный check.sh: 195/195 PASS, 0 failed/0 skipped, proxy9/9, burst1/1, load8/8,
|
||||
134217728 байт, 169 секунд; /tmp/config-instance-audit-first-check.log.
|
||||
- Профиль 6/6 PASS: config_parser, config_updater, config_debug, reality_config, ncd_config, services.
|
||||
- Test_config_parser: 53 ошибочных конфигурации отвергнуты одинаково из файла/буфера;
|
||||
10 отказов аллокаций с полным rollback, IPv4/IPv6 CIDR и control order, предельные размеры/порты,
|
||||
скрытие секретов и /dev/full при append; баланс памяти сохранён.
|
||||
- ASAN/UBSan/LSan: те же 6/6 PASS с detect_leaks=1/ halt_on_error=1.
|
||||
Все встроенные lib/src/Opus инструментированы согласованно в отдельной копии
|
||||
/tmp/utun-config-instance-asan-20261026; рабочие объекты не смешивались с sanitizer-объектами.
|
||||
Первый updater sanitizer-прогон обнаружил обход test mock через glibc __fread_chk;
|
||||
checked wrapper добавлен, повторный прогон всех файловых и allocation faults PASS.
|
||||
- Логи: /tmp/config-parser-audit-contract.log, /tmp/config-parser-audit-updater.log,
|
||||
/tmp/config-parser-audit-debug.log, /tmp/config-parser-audit-reality.log,
|
||||
/tmp/config-parser-audit-ncd.log, /tmp/config-parser-audit-services.log.
|
||||
Санитайзеры: соответствующие /tmp/config-parser-audit-asan-*.log,
|
||||
сборка /tmp/config-parser-audit-asan-build.log и /tmp/config-parser-audit-asan-tests-build.log.
|
||||
Общая сборка: /tmp/config-instance-audit-full-build.log.
|
||||
+8
-2
@@ -19,12 +19,17 @@
|
||||
## Идентичность и шифрование
|
||||
|
||||
```text
|
||||
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
|
||||
conv_hash = SHA256("utun_dm_v" || min(node_a,node_b) || max(node_a,node_b))
|
||||
conv_id = native_u64(conv_hash[0:8]) & 0x7FFFFFFFFFFFFFFF
|
||||
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
|
||||
msg_nonce = SHA256(conv_id || author || seq)[..13]
|
||||
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
|
||||
```
|
||||
|
||||
Префикс conv_hash — 9 ASCII-байт без NUL. ID узлов подаются в хеш в порядке
|
||||
байтов хоста; native_u64 читает первые 8 байт хеша в том же порядке.
|
||||
Нулевой conv_id заменяется на 1.
|
||||
|
||||
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
|
||||
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
|
||||
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
|
||||
@@ -62,7 +67,8 @@ tags, точный размер и хеш; владелец публикует
|
||||
|
||||
- `dm_core`: беседы, история, outbox и подтверждение получателя.
|
||||
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
|
||||
- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.
|
||||
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
|
||||
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
|
||||
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
|
||||
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
|
||||
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
|
||||
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
|
||||
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
|
||||
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
|
||||
название «Зашифрованное вложение» вместо manifest.
|
||||
нельзя открыть или сохранить как готовое вложение. До расшифровки цитата показывает
|
||||
название «Зашифрованное вложение», после — обычный preview исходного вложения.
|
||||
Список чатов сохраняет общее название зашифрованного вложения вместо manifest.
|
||||
|
||||
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
||||
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
||||
@@ -38,6 +39,13 @@ encrypted_file_body:variable
|
||||
```
|
||||
|
||||
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
|
||||
MIME — непустая NUL-строка длиной1..31 байт: ASCII33..126 без двойной кавычки
|
||||
и обратной косой черты. Общий `encrypted_file_validate_type` применяется в send,
|
||||
write и read; это проверка представления поля, без полного разбора MIME-грамматики.
|
||||
Send отклоняет неверное значение до создания задачи/ключа/worker, write — до файловых
|
||||
операций. Read проверяет расшифрованное аутентифицированное поле и при ошибке сохраняет
|
||||
прежние info/type. Сырые неверные строки в диагностику не выводятся.
|
||||
|
||||
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
|
||||
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
|
||||
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
|
||||
@@ -45,8 +53,26 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
|
||||
|
||||
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
|
||||
все tags файла, точный размер и hash тела. Результат пишется во временный файл
|
||||
и публикуется после успешной проверки и durable rename. Все длительные файловые
|
||||
операции и криптография выполняются в media_async.
|
||||
и публикуется после успешной проверки и durable rename. Подготовка файлов
|
||||
и криптография выполняются в media_async worker.
|
||||
|
||||
`encrypted_file_write/read` возвращают закрытый проверенный `file_io_temp`,
|
||||
не изменяя конечное назначение. Задача владеет им до publish/discard; запись идёт
|
||||
в эксклюзивно созданный соседний файл с правами 0600 на POSIX. Одинаковые файлы,
|
||||
включая `/./`, hardlink и конечную ссылку назначения, отклоняются до подготовки;
|
||||
публикация повторно проверяет идентичность. При ошибке read не выдаёт новые метаданные.
|
||||
|
||||
Режим `FILE_IO_DURABLE` действует весь срок temp: flush/sync открытого файла/close,
|
||||
затем rename и сохранение каталога. После успешного rename ошибка directory sync
|
||||
не откатывает установленный результат; temp уже отсоединён от владельца.
|
||||
Completion не удаляет файлы по заранее вычисленному имени: очистка учитывает,
|
||||
какие артефакты задача действительно создала или опубликовала.
|
||||
|
||||
Реестр `file_io` защищает активные temp от очистки кеша, включая время после close.
|
||||
Восстановление до запуска workers и очистка после их завершения удаляют оставшиеся
|
||||
`<UUID>.uenc.tmp.<UUID>` / `<UUID>.file.tmp.<UUID>` в `media/decrypted`.
|
||||
Конечные файлы и имена других форматов этот проход сохраняет; активные temp другого
|
||||
instance того же процесса остаются защищены общим реестром.
|
||||
|
||||
## Хранение и разрешения
|
||||
|
||||
@@ -69,14 +95,15 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
|
||||
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
|
||||
Сетевая классификация `peers.storage` для этого не используется.
|
||||
|
||||
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
|
||||
В шапке desktop-клиента кнопка «Запросы расшифровки» видна при наличии запросов без решения
|
||||
и показывает их число.
|
||||
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
|
||||
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
|
||||
и подтверждённая доставка ключа; отказ также сохраняется.
|
||||
|
||||
## Адресный протокол
|
||||
|
||||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь
|
||||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
|
||||
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
|
||||
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
|
||||
|
||||
@@ -110,9 +137,18 @@ backpressure и повтором через 5 секунд; restart продол
|
||||
|
||||
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
|
||||
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
|
||||
лишние байты и отсутствие частичного plaintext после отказа.
|
||||
лишние байты, alias обоих API, сохранность существующего назначения/метаданных,
|
||||
23 сценария отказов I/O с контролем fd/temp, включая ошибку после rename.
|
||||
Для MIME проверяются все256 значений первого байта, NULL и границы поля,
|
||||
восемь ранних отказов writer с сохранением файлов/output, три допустимых roundtrip
|
||||
и семь аутентифицированных некорректных wire-полей с сохранением info/type/target.
|
||||
`test_media_primitives` проверяет оба режима temp и границу durable-публикации;
|
||||
`test_attachment_send` проверяет восемь отказов MIME до ключей/worker/файлов,
|
||||
затем останавливает сервис после настоящего crypto rename до completion.
|
||||
|
||||
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
|
||||
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
|
||||
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
|
||||
восстановление удалённого plaintext без нового согласия и удаление канала.
|
||||
При startup/restart дополнительно проверяются оба дерева: stale temp удаляются,
|
||||
открытый и закрытый активные temp, конечные файлы и чужие имена сохраняются.
|
||||
+5
-1
@@ -1,4 +1,8 @@
|
||||
Архитектура etcp:
|
||||
> Исторический документ: описания NAT, модулей и ограничения обмена маршрутами ниже не отражают текущую реализацию.
|
||||
> Актуальные правила группового обмена маршрутами — в [topo_group.h](../src/routing_layer/topo_group.h),
|
||||
> модель узла — в [node_snapshot.md](node_snapshot.md), протокол роутера — в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
Архитектура etcp:
|
||||
|
||||
ETCP устанавливает только одно подключение между двумя узлами. Это подключение может добавлять-убирать каналы связи (динамически) без разрыва соединения.
|
||||
Также каналы связи мониторятся и трафик распределяется между доступными каналами. Если канал определяется как недоступен - он исключается из обмена.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+289
-101
@@ -1,81 +1,193 @@
|
||||
Формат кодограммы:
|
||||
------------------
|
||||
пакеты идут по следующему пути:
|
||||
пакет кем-то помещается в входную очередь (ETCP input_queue).
|
||||
Когда load balancer готов отправить следующий пакет он делает вызов etcp_request_pkt в etcp.c/h и etcp либо формирует следующую кодограмму или встаёт в состояние async ожидания (возвращая null):
|
||||
- либо wait ETCP input_queue (если нечего передавать)
|
||||
- либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия".
|
||||
из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку.
|
||||
как формируется пакет:
|
||||
1. выбираем канал передачи (etcp_loadbalancer_select_link)
|
||||
2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем
|
||||
3. если пакет найден или есть неподтверждённые ACK:
|
||||
1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало
|
||||
2. в конец добавляем секцию 0x00 с данными пакета
|
||||
при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен
|
||||
Функция прикрепления опциональных секций:
|
||||
- запрашивает выбор канала передачи.
|
||||
- добавляет накопившиеся ACK
|
||||
Кодограмма сервиса через etcp_send попадает в normalizer; его фрагменты поступают в
|
||||
input_queue, затем в indexed input_send_q с назначенным seq.
|
||||
Очереди, ACK/RTO, изменение окна и готовность линка вызывают etcp_conn_wake_sender.
|
||||
Один call_soon обслуживает накопленные события без рекурсивного входа и задержки таймером.
|
||||
При OOM выделения сетевого пакета очередь сохраняется; retry назначается через 10ms.
|
||||
close/reset отменяет ожидающий запуск. Не подтверждённая сессия не передаёт DATA/ACK;
|
||||
HELLO/CHALLENGE/CONFIRM и keepalive передаются своим независимым путём.
|
||||
|
||||
Формирование пакета:
|
||||
1. Просмотреть input_send_q без извлечения: первый готовый повтор, иначе первый готовый новый DATA.
|
||||
Заблокированная запись пропускается; порядок остальных записей не меняется. Поиск не резервирует окно,
|
||||
не выдаёт номер попытки и не меняет историю/RR. Если кандидата нет, можно подгрузить один новый фрагмент
|
||||
и повторить просмотр. Общий бюджет учитывает DATA-байты send_q+wait_ack; приёмное окно по till сохраняется.
|
||||
После callback подгрузки проверяются reset_count/close/сессия, включая удаление прежнего burst-линка.
|
||||
2. Выбрать инициализированный UP-линк с готовым shaper и местом для целого DATA:
|
||||
inflight_bytes + длина DATA <= inflight_lim_bytes. Проверка выполняется без переполнения.
|
||||
Среди допустимых выбирается минимум inflight_bytes, при равенстве — round-robin.
|
||||
Для повтора обход начинается после last_link самого фрагмента: сначала неиспользованные допустимые пути.
|
||||
История — битовая карта256 локальных слотов,32 байта на логический пакет; wire-формат не меняется.
|
||||
Пока остался неиспользованный путь, занятый по cwnd/pacing, этот пакет ждёт, а send_q просматривается дальше.
|
||||
Когда все допустимые пути использованы, разрешён следующий обход. Очистка истории и новый бит фиксируются
|
||||
только после принятия send; отказ send не расходует обход. Перед удалением линка очищаются его курсор/бит
|
||||
во всех логических записях до освобождения слота и callbacks, которые могут повторно выдать тот же local_link_id.
|
||||
При наличии незастрявшего initialized+UP пути застрявшие исключены из повторов, даже если альтернатива занята.
|
||||
Если застряли все, доступны все UP-пути. Прежний путь разрешён лишь при отсутствии других допустимых;
|
||||
иначе ждём альтернативу. DOWN последней альтернативы пробуждает отправителя.
|
||||
3. Наступивший по сроку или срочный ACK обслуживает тот же линк, который принял DATA,
|
||||
без ограничения DATA-cwnd/pacing. Готовый DATA другого линка уступает этому ACK;
|
||||
DATA того же линка забирает ACK piggyback. Если DATA не помещается, он остаётся в очереди.
|
||||
Кумулятивный till можно отправить по любому рабочему пути. ACK-only не увеличивает нагрузку DATA-shaper;
|
||||
физическая очередь канала остаётся общей, её накопление ограничивают DATA-cwnd/pacing BBR.
|
||||
4. Выделить ETCP_DGRAM до удаления DATA из очереди и изменения счётчиков.
|
||||
Выделить физическую попытку с новым attempt_no для этого seq и зарезервировать окно выбранного линка.
|
||||
Только успешное принятие транспортом фиксирует время, last_link, историю/RR и перенос DATA в input_wait_ack.
|
||||
Ошибка send снимает резерв; логический DATA остаётся готовым без сетевого loss sample.
|
||||
Пакет содержит ACK, опциональные timestamp/измерительные секции и последнюю секцию PAYLOAD.
|
||||
5. В активной ограниченной burst-серии pacing обходится; DATA всё равно должен
|
||||
помещаться в окно, иначе серия отправляет FILLER. ACK/FILLER окно DATA не занимают.
|
||||
Если готовый повтор относится к другому пути, серия идёт FILLER: новые DATA его не обгоняют.
|
||||
При приёме пакета (от etcp_connections):
|
||||
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
|
||||
- ack (0x01): помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также снимаем блокировку отправки
|
||||
- timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма
|
||||
- [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). также добавляем ACK запись в ack_q для отправки.
|
||||
- ack (0x01): пара seq+attempt_no подтверждает физическую попытку только принявшего ACK линка;
|
||||
seq отдельно подтверждает логическую доставку. till освобождает логические записи, без BBR/снятия чужого flight.
|
||||
- timestamp (0x06): обновляем RTT и EWMA |ΔRTT| пути; recv_dt_avg хранит поправки времени TX/RX в Q16.
|
||||
У conn rtt_last — среднее RTT линков, rtt_avg_10 — их максимум (имя историческое).
|
||||
BBR использует отдельные DATA RTT/jitter по ACK физических попыток; RTO также учитывает rtt_last своего линка.
|
||||
- [0x00] payload: сохраняем фрагмент в recv_q либо распознаём уже сохранённый/доставленный seq.
|
||||
Только после этого ставим ACK с seq+attempt_no в ack_q принявшего линка; OOM до сохранения не подтверждается.
|
||||
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
|
||||
|
||||
пересканирование inflight:
|
||||
- запускается при добавлении пакета в очередь ожидания ACK (wait_ack_cb). устанавливается таймер на первый неистекший пакет.
|
||||
- если время последней отправки > RTT_avg10*K1+jitter*K2 (коэффициенты K1=32, K2=32, деление на 16) то пакет заново отправляется
|
||||
- очередь FIFO (сортирована по timestamp добавления = времени отправки), сканирование прекращается на первом неистекшем пакете
|
||||
- по истечению таймера (ack_timeout_cb) снова вызывается проверка очереди
|
||||
Таймер попыток каждого линка (etcp_tx.c):
|
||||
- RTO = clamp(ceil(1.2*RTT_for_RTO + 1.5*data_jitter_us + 3000), 10000, 1000000) мкс,
|
||||
RTT_for_RTO=max(data_rtt_us,100*rtt_last): DATA EWMA и последний timestamp/keepalive RTT своего линка.
|
||||
Целочисленный расчёт использует uint64 и округляет вверх до целой микросекунды.
|
||||
RTT использует собственный DATA ACK минус receiver_delay; EWMA RTT=1/8, jitter=1/4 |RTT-EWMA|.
|
||||
Первый DATA RTT задаёт jitter=RTT/2. Timestamp-RTT задаёт нижнюю границу срока, чтобы после роста задержки
|
||||
попытки доживали до ACK и DATA-измерения продолжались; BBR RTT/min_rtt по этому сигналу не обновляются.
|
||||
Без обоих измерений RTO=1с.
|
||||
- FIFO содержит физические попытки в порядке принятия транспортом. Таймер назначается по старейшей;
|
||||
при изменении RTO проверяется новый срок. Timestamp handler пересчитывает таймер сразу после обновления RTT,
|
||||
включая сокращение без новых DATA/ACK. Допускается оставить уже запланированную более раннюю проверку.
|
||||
uasync округляет задержку вверх до миллисекунды; callback может выполниться позже расчётного срока.
|
||||
- Истечение снимает только собственный физический flight и сообщает loss собственному BBR.
|
||||
Если логический seq ещё не доставлен и других активных копий нет, он возвращается в input_send_q.
|
||||
Уже доставленный seq не возвращается, даже если его физическая копия истекла позже.
|
||||
|
||||
Обычный ACK-only ждёт received_at_us старейшей записи своего пути + 2000мкс.
|
||||
Piggyback на готовом DATA передаёт ACK сразу. Готовый ACK может включать более свежие записи:
|
||||
срок проверяется по голове пакета, а не отдельно для каждой записи внутри него.
|
||||
Среди готовых обычных ACK выбирается самый старый срок; срочный ответ на ACK_REQUEST имеет приоритет.
|
||||
Короткая ограниченная измерительная burst-серия завершается до перехода к ACK другого пути.
|
||||
Продвижение логической границы также задаёт срок 2мс от первого неподтверждённого продвижения;
|
||||
следующие DATA его не отодвигают. Любой принятый пакет с актуальным till снимает этот запрос.
|
||||
Один ack_resp_timer соединения обслуживает минимум сроков очередей рабочих путей и логической границы.
|
||||
После piggyback/частичной отправки срок пересчитывается по оставшимся головам. Новый RX между
|
||||
timer и отложенным sender не задерживает старые ACK: готовность проверяется по абсолютному времени.
|
||||
При DOWN очередь пути не создаёт таймер; после UP её старый наступивший срок действует сразу.
|
||||
OOM ACK-таймера логируется и включает существующий срочный ответ, сохраняя received_at_us для RTT.
|
||||
Reset/close отменяют таймер и очищают сроки; DATA/RTO/BBR и wire-формат этим механизмом не меняются.
|
||||
|
||||
inflight - две очереди (ll_queue):
|
||||
- input_send_q: список пакетов ожидающих отправку
|
||||
- input_wait_ack: список неподтвержденных пакетов ожидающих ack
|
||||
При отправке пакет перемещается из input_send_q в input_wait_ack.
|
||||
При ретрансмиссии пакет перемещается из input_wait_ack обратно в input_send_q.
|
||||
При принятии отправки транспортом пакет перемещается из input_send_q в input_wait_ack.
|
||||
При необходимости повтора пакет перемещается из input_wait_ack обратно в input_send_q.
|
||||
доп. свойства пакетов (struct INFLIGHT_PACKET):
|
||||
- указатель на ETC_LINK последней отправки (last_link)
|
||||
- timestamp последней отправки (last_timestamp)
|
||||
- число отправок (send_count)
|
||||
- send_hist[8] - история через какие линки отправлялся (индекс линка)
|
||||
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
|
||||
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
|
||||
- send_hist[8] — local_link_id последних попыток, только диагностика
|
||||
- last_attempt_id — внутренний составной ключ (seq<<16)|attempt_no последней принятой отправки
|
||||
- attempt_count — выданные номера попыток данного seq, общие для всех каналов, включая неудачные send
|
||||
- active_attempts — число ещё учитываемых физических копий
|
||||
- Payload принадлежит только логической записи. Попытка ETCP_TX_ATTEMPT хранит seq и неизменные снимки, без payload.
|
||||
inflight_bytes/inflight_packets линка равны сумме DATA-длин/числу записей в его attempts, включая резерв send.
|
||||
ACK/till может уже удалить логический объект, пока другая физическая копия продолжает занимать своё окно.
|
||||
Повторный ACK неизвестной/истёкшей попытки не вычитает flight и не создаёт BBR sample.
|
||||
ACK известного ID с другим seq отклоняется целиком; неизвестный ID допускает только логическое подтверждение seq.
|
||||
Удаление линка обнуляет last_link в обеих очередях до освобождения его памяти.
|
||||
Общий optimal_inflight равен сумме лимитов всех линков, с насыщением UINT32_MAX;
|
||||
это дополнительный backpressure подачи новых фрагментов, не замена индивидуальных окон.
|
||||
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
|
||||
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
|
||||
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
|
||||
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
|
||||
input_queue даже если подтверждённые записи уже удалены по selective ACK.
|
||||
При заполненном sequence-окне собственный таймер запрашивает актуальный till секцией ACK_REQUEST.
|
||||
Получатель отвечает через запрашивающий путь, даже без ожидающих DATA/ACK-элементов.
|
||||
При продвижении frontier, reset/close таймер отменяется. Это восстанавливает поток при потере последнего till.
|
||||
Память физических попыток ограничена: 16384 на линк, 32768 на соединение; исчерпание блокирует DATA до освобождения.
|
||||
Очередь ACK каждого линка ограничена 16384 записями; переполнение логируется, DATA остаётся сохранённым для повтора/till.
|
||||
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
|
||||
optimal_inflight расчитывается из RTT и bandwidth линка.
|
||||
|
||||
bandwidth по каждому линку адаптивно подстраиваем:
|
||||
Периодически в stats_timer_cb собираем статистику окна (rtt_avg10, ретрансмиссии, переданные пакеты).
|
||||
BW рассчитывается как: inflight_lim_bytes * 8 / rtt_min (в секундах) * 8 / 1024 * 1.3 (Kbits/sec).
|
||||
Размер inflight_lim_bytes плавно подстраивается вверх/вниз в зависимости от того, растёт ли RTT.
|
||||
Burst-измерения bandwidth (0x07, 0x08) зарезервированы, но на данный момент не реализованы.
|
||||
BBR каждого линка получает ACK/loss samples и возвращает cwnd и pacing rate.
|
||||
Скорость — доставленные байты за max(интервал отправки, интервал доставки), по снимкам при отправке.
|
||||
Интервал между соседними ACK не используется: ACK одной пачки могут обрабатываться в один tick.
|
||||
ACK с подтверждениями также возвращает точку приёма своего пути: накопленные DATA-байты и монотонное время
|
||||
последнего увеличения счётчика. В счётчик входят допустимые сохранённые/уже доставленные DATA, включая копии;
|
||||
OOM до сохранения и отвергнутые DATA его не меняют. Считаются байты полезной нагрузки секции, без заголовков.
|
||||
При отправке попытка запоминает известную точку. ACK действующей попытки продвигает точку пути;
|
||||
повторный/чужой ID её не меняет, переставленные ACK не откатывают. Противоречивая пара логируется и игнорируется.
|
||||
При положительных дельтах interval=max(send_us,ceil(delivered*delta_rx_us/delta_rx_bytes)).
|
||||
Приёмный интервал заменяет ACK clock: обратная очередь больше не завышает BW сжатием ACK
|
||||
и не занижает его растяжением. Send-интервал сохраняет защиту от сжатия приёмного времени.
|
||||
Без достоверной приёмной базы/положительных дельт используется max(send_us,ack_us).
|
||||
Счётчики и времена — uint64, часы двух сторон не сравниваются; нужны только дельты одних часов.
|
||||
Если интервал не представим в uint32, sample не используется для BW вместо насыщения, завышающего скорость.
|
||||
После пустого физического flight база начинается заново, чтобы простой не занижал скорость следующей передачи.
|
||||
Reset потока обнуляет точки приёма; исчерпание локального 64-битного счётчика закрывает соединение с ERROR.
|
||||
RTT/min_rtt, формула RTO, политика окна и дедлайн ACK не корректируются приёмной точкой.
|
||||
Внутренняя полоса — байты/мкс Q16; pacing — байты/с, BDP — байты, без повторного множителя MSS.
|
||||
Минимальное окно BBR — четыре DATA-фрагмента normalizer; максимальное — bbr_max_cwnd (по умолчанию 1048576).
|
||||
8192 — нижняя допустимая граница настройки bbr_max_cwnd; транспорт не повышает до неё текущее окно.
|
||||
PROBE_RTT осушает flight до max(4*MSS, 0.5*BDP), выдерживает >=200мс и раунд ACK, затем восстанавливает окно.
|
||||
RTT-фильтры обновляются при обработке ACK. Короткий минимум принимает меньший достоверный RTT
|
||||
либо текущее измерение после истечения 5с; RTT=0 означает отсутствие измерения.
|
||||
Долгий min_rtt принимает короткий минимум, если он не больше прежнего, либо истекли его 10с.
|
||||
Вместе со значением копируется метка короткого фильтра: повторное использование кеша не задаёт текущее время.
|
||||
Завершение PROBE_RTT возобновляет короткое окно. Окна истекают строго после своей границы;
|
||||
hold завершается при достижении срока и завершённом ACK-раунде. Метки RTT и срок hold — полные uint64 TB (100мкс).
|
||||
MSS — размер DATA-фрагмента normalizer без заголовков; начальное окно UDP и STCP равно 4*MSS с верхним лимитом конфига.
|
||||
В PROBE_BW действует дополнительный целевой бюджет очереди 20мс: всё окно, включая резервы ACK и PROBE_UP,
|
||||
ограничивается max(4*MSS, BDP + BW*20мс) по min_rtt. STARTUP/DRAIN не ограничивают окно этой формулой,
|
||||
но целый раунд RTT выше min_rtt+бюджет завершает STARTUP и переводит его в DRAIN.
|
||||
Shaper списывает только DATA-байты: единицы совпадают с delivery sample и pacing BBR.
|
||||
Это цель по оценкам BBR; минимум окна, изменение полосы и очереди обратного пути могут давать большую задержку.
|
||||
Отказ из-за нехватки окна сохраняет cwnd_limited до следующего ACK: вычитание ACK
|
||||
не должно скрывать этот факт от BBR. app_limited учитывает ожидающие очереди и
|
||||
pending/частичный буфер normalizer; ограничение окном не означает отсутствие данных.
|
||||
Потеря для BBR учитывается при RTO, фактическая повторная передача — при новой попытке.
|
||||
Три RTO физических DATA-попыток без ACK-прогресса ставят локальный флаг tx_stalled, в том числе в одном вызове таймера.
|
||||
До установки флага действующий ACK своего пути сбрасывает серию RTO. После установки сброс возможен только по ACK
|
||||
попытки, принятой транспортом после момента установки флага; старый/чужой/уже снятый attempt не доказывает восстановление.
|
||||
tx_stalled ограничивает выбор повторов при наличии альтернативы; UP, новые DATA, ACK/keepalive и формула RTO сохраняются.
|
||||
Reset линка снимает флаг и серию RTO. Новых очередей, копий payload и таймеров детектор не создаёт.
|
||||
При уменьшении окна уже отправленные пакеты остаются в учёте; новые DATA ждут ACK/RTO.
|
||||
Измерительные секции burst 0x07/0x08 и FILLER поддерживаются реализацией.
|
||||
|
||||
|
||||
**** Формат кодограмм для etcp.c/h ****
|
||||
Каждая кодограмма состоит из обязательного заголовка и опциональных секций:
|
||||
1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах:
|
||||
[Timestamp high][Timestamp low][flags/flag_up]
|
||||
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется
|
||||
[Timestamp:2][flags/flag_up:1]
|
||||
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое), при повторе обновляется.
|
||||
Копируется из памяти в порядке байтов хоста; на используемых little-endian платформах передаётся LE.
|
||||
- flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны
|
||||
|
||||
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
|
||||
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
|
||||
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
|
||||
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
|
||||
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
|
||||
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
|
||||
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
|
||||
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
|
||||
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
|
||||
а) Подтверждения (ACK) - заголовок 0x01:
|
||||
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
|
||||
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
|
||||
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][attempt_no(2 LE)][delay_us(4 LE)] × count]
|
||||
- count: количество ACK записей (1 байт; до 255, дополнительно ограничено MTU и оставшимся местом)
|
||||
- last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд)
|
||||
- rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне)
|
||||
- seq: uint32_t — номер подтверждаемого пакета
|
||||
- recv_ts: uint16_t — локальный timestamp приёма пакета (в единицах 0.1 мс)
|
||||
- dly: uint16_t — задержка между приёмом пакета и отправкой ACK (для расчёта RTT)
|
||||
Каждая запись = 8 байт.
|
||||
- attempt_no: uint16_t — номер физической попытки данного seq, 1..65535; возвращается только тем же путём
|
||||
- delay_us: uint32_t — задержка между приёмом DATA и формированием ACK, мкс; UINT32_MAX исключает RTT sample
|
||||
При count>0 после всех записей обязательно добавляется [rx_data_bytes(8 LE)][rx_data_time_us(8 LE)].
|
||||
Это точка приёма всего пути на момент формирования ACK, а не отдельной подтверждаемой попытки.
|
||||
Время — монотонные микросекунды получателя при последнем положительном приросте DATA-байтов; 0/0 — точки нет.
|
||||
Каждая запись = 10 байт, база ACK = 8 байт; полный размер = 8+10*count+(count>0 ? 16 : 0).
|
||||
count=0 допустим для передачи till, точки приёма не содержит. До обработки проверяется вся длина ACK.
|
||||
ACK_REQUEST: [0x0a], одна секция длиной 1 байт, запрос повторной отправки актуального till.
|
||||
в) channel timestamp (добавляется после выбора канала передачи, если есть свежие данные):
|
||||
[0x06] [RET_Timestamp(2 байта LE)] [RECV_Rel_Timestamp(2 байта LE)]
|
||||
- RET_Timestamp: uint16_t = last_recv_timestamp + (now - last_recv_local_time)
|
||||
@@ -83,12 +195,20 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
- RECV_Rel_Timestamp: uint16_t = last_recv_local_time - last_recv_timestamp
|
||||
(относительное смещение между локальным и удалённым временем)
|
||||
Всего 5 байт. Добавляется только если last_recv_updated=1 (был принят пакет с момента последней отправки)
|
||||
и если прошло не более ~100ms (dt < 1000000 таймбаз).
|
||||
и если прошло менее 100с (dt < 1000000 таймбаз). Результат rtt_last задаёт нижнюю границу RTT для RTO;
|
||||
DATA RTT и min_rtt BBR обновляются только ACK действующих физических попыток.
|
||||
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
|
||||
[0x00] [ID 4 байта LE] [данные...]
|
||||
- ID: uint32_t, 32-битный циклический номер пакета
|
||||
[0x00] [ID 4 байта LE] [attempt_no 2 байта LE] [данные...]
|
||||
- ID: uint32_t, номер фрагмента 1..UINT32_MAX в пределах одной эпохи потока.
|
||||
- attempt_no: uint16_t, номер 1..65535, общий для всех каналов одного ID.
|
||||
Номер выдаётся при создании попытки и не возвращается при отказе send; исходный seq при повторе сохраняется.
|
||||
После номера65535 следующая попытка аварийно меняет эпоху через REINIT; счётчик не оборачивается.
|
||||
Перед выдачей seq=0 после UINT32_MAX поток также меняет эпоху: пара seq/attempt_no не повторяется внутри эпохи.
|
||||
Смена эпохи отбрасывает прежний поток, владельцы восстанавливают свои операции по REINIT, как при фатальном OOM.
|
||||
Старые DATA/ACK отклоняются заголовком сессии до разбора. Заголовок PAYLOAD занимает7 байт.
|
||||
Внутренние хеш-очереди сохраняют64-битный ключ (seq<<16)|attempt_no; он не передаётся по сети целиком.
|
||||
Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные),
|
||||
всегда в порядке: ACK, channel_timestamp, payload; payload всегда последний.
|
||||
Порядок: ACK, возможный ACK_REQUEST, измерительные/временные секции и FILLER, затем payload последним.
|
||||
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции).
|
||||
|
||||
цель протокола:
|
||||
@@ -99,71 +219,100 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
|
||||
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
|
||||
- RTT последнего пакета и время отправки последнего пакета
|
||||
- общее количество отправленных/принятых байт
|
||||
- количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
|
||||
- число фактических повторных отправок; потери для BBR учитываются отдельно при истечении физических попыток.
|
||||
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
|
||||
Планировщик/балансир каналов (etcp_loadbalancer.c/h):
|
||||
для каждого канала ограничение полосы с тестированием отклика:
|
||||
- полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков)
|
||||
Ограничение реализуем следующим образом:
|
||||
- в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности.
|
||||
Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время)
|
||||
счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи");
|
||||
когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения.
|
||||
также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client.
|
||||
Окно и pacing проверяются до извлечения DATA из очереди; без допустимого UP-линка фрагмент остаётся в очереди.
|
||||
У каждого линка собственный shaper по точной bbr_pacing_rate в байтах/с:
|
||||
- После успешного send списываются только payload_len DATA-байтов, без заголовков и ACK.
|
||||
- Основной счётчик времени имеет единицу 0.1 мс; sub-разряды — 0.1 нс, модуль 1000000.
|
||||
Прибавка равна ceil(payload_len * 10000000000 / bbr_pacing_rate), вычисляется целочисленно в uint64.
|
||||
- Накопленный кредит после простоя ограничивается 1 мс. При смене ставки пересчитывается оставшийся долг;
|
||||
таймер снимается и при необходимости назначается заново. При нулевой ставке pacing не ограничивает отправку.
|
||||
- Положительный долг должен быть строго меньше 1 мс. Таймер ждёт первого допустимого TB:
|
||||
shaper_load_time_tb - SHAPER_BURST_DELAY_TB + 1; дробь меньше TB не меняет эту строгую границу.
|
||||
uasync округляет срок вверх до миллисекунды; callback повторно проверяет готовность перед уведомлением.
|
||||
- ACK/control и измерительная burst-серия не списывают DATA-pacing. Окно DATA проверяется и в burst.
|
||||
Конструктор исходящего UDP-линка запускает INIT сразу; TCP использует отдельный STCP connect/handshake.
|
||||
процесс установки соединения (рукопожатие):
|
||||
- клиент отправляет init запрос и выставляет таймаут
|
||||
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
|
||||
- по таймауту повторяет INIT: начальный интервал 500 TB = 50 мс, максимум 50000 TB = 5 с;
|
||||
каждые 10 отправок интервал увеличивается на interval/4 + 1 TB. TB здесь равен 0.1 мс.
|
||||
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
|
||||
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03).
|
||||
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04).
|
||||
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
|
||||
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
|
||||
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
|
||||
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
|
||||
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
|
||||
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
|
||||
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
|
||||
**** Формат кодограмм для etcp_connections.c/h ****
|
||||
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется.
|
||||
|
||||
Типы пакетов:
|
||||
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
|
||||
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
|
||||
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
|
||||
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
|
||||
0x02 — ETCP_INIT_REQUEST (начальный handshake линка)
|
||||
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
|
||||
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
|
||||
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
|
||||
0x06 — ETCP_PING (проверка доступности)
|
||||
0x07 — ETCP_PONG (ответ на пинг)
|
||||
0x08 — ETCP_KEEPALIVE
|
||||
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
|
||||
0x0a — ETCP_SESSION_HELLO
|
||||
0x0b — ETCP_SESSION_CHALLENGE
|
||||
0x0c — ETCP_SESSION_CONFIRM
|
||||
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
|
||||
|
||||
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
|
||||
|
||||
1) Init запрос (0x02 / 0x04):
|
||||
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)]
|
||||
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)]
|
||||
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
|
||||
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
|
||||
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
|
||||
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
- code: 0x02 (со сбросом) или 0x04 (без сброса)
|
||||
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
|
||||
- code: 0x02 или 0x04, вариант handshake линка
|
||||
- node_id: 64 бита, идентификатор узла (big-endian)
|
||||
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian)
|
||||
- mtu_local: uint16_t локальный MTU
|
||||
- keepalive_interval: uint16_t интервал keepalive в мс
|
||||
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100)
|
||||
- local_link_id: uint8_t (0-255), назначается отправителем
|
||||
- sock_id: uint8_t, уникальный ID сокета отправителя
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
|
||||
- mtu: uint16_t локальный MTU
|
||||
- keepalive: uint16_t интервал keepalive в мс
|
||||
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
|
||||
- link_id: uint8_t (1-255), назначается отправителем; ноль запрещён
|
||||
- socket_id: uint8_t, уникальный ID сокета отправителя
|
||||
- only_local: uint8_t, флаг "только локальные подключения"
|
||||
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
|
||||
- type: uint8_t, конфигурационный тип сокета CFG_SERVER_TYPE_*
|
||||
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
|
||||
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
|
||||
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
|
||||
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
|
||||
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
|
||||
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
|
||||
- salt + obfuscated_pubkey: ключ отправителя XOR SHA256(salt || публичный ключ получателя).
|
||||
Этот блок не шифруется AES, передаётся в конце пакета
|
||||
в noencrypt_len байтах (не попадает под AES-шифрование)
|
||||
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт.
|
||||
Между нодами только одно подключение, но можно добавлять каналы.
|
||||
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
|
||||
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
|
||||
|
||||
2) Init подтверждение (0x03 / 0x05):
|
||||
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)]
|
||||
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)]
|
||||
[random_padding...]
|
||||
- code: 0x03 (со сбросом) или 0x05 (без сброса)
|
||||
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
|
||||
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
|
||||
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
|
||||
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
|
||||
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
|
||||
- node_id: 64 бита, big-endian
|
||||
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
|
||||
- mtu_local: uint16_t локальный MTU сервера
|
||||
- local_link_id: uint8_t идентификатор канала сервера
|
||||
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
|
||||
- mtu: uint16_t локальный MTU сервера
|
||||
- link_id: uint8_t идентификатор канала сервера, 1..255
|
||||
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
|
||||
- only_local: uint8_t флаг сервера
|
||||
- type: uint8_t тип сокета сервера
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
|
||||
- peer_port: 2 байта (порт клиента как его видит сервер)
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
|
||||
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
|
||||
При IPv6 оба поля нулевые.
|
||||
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
|
||||
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
||||
- keepalive: uint16_t интервал keepalive сервера в мс
|
||||
- random_padding: случайные байты
|
||||
|
||||
3) PING (0x06):
|
||||
@@ -183,16 +332,34 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
|
||||
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
|
||||
|
||||
При получении init получатель пакета должен:
|
||||
- reset ETCP_LINK с этим ip_port если он есть
|
||||
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
|
||||
приём кодограмм из UDP выглядит так:
|
||||
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
|
||||
До изменения транспорта INIT_REQUEST проверяет полное 67-байтовое тело и соответствие node_id
|
||||
аутентифицированному X25519-ключу. Оба направления INIT отклоняют link_id=0 и MTU меньше
|
||||
ETCP_MIN_UDP_MTU(family): 191 байт для IPv4, 211 для IPv6. MTU больше 1500 ограничивается сверху.
|
||||
Получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
|
||||
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
|
||||
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
|
||||
Принятый INIT обновляет recv_keepalive, время последнего RX и входной remote_keepalive=flag_up
|
||||
до переиспользования буфера для ответа. INIT с flag_up=0 снимает прежний UP UDP-линка:
|
||||
получение запроса доказывает только входящую достижимость, обратный путь ещё не подтверждён.
|
||||
Ответ и keepalive отправляются и при DOWN, чтобы холодное подключение и восстановление
|
||||
могли завершить двустороннюю проверку. При DOWN одного линка соединение остаётся UP,
|
||||
если есть другой работоспособный линк. Повтор неизменного состояния не создаёт нового DOWN/UP.
|
||||
INIT_RESPONSE должен содержать полное обязательное тело (64 байта без timestamp/flag_up)
|
||||
и node_id, соответствующий аутентифицированному ключу пира. Усечённый ответ или чужой node_id
|
||||
отклоняются до изменения живости, эпохи и параметров линка. Дополнительный padding разрешён.
|
||||
Входящий conn с известным ключом индексируется по node_id до callback создания линка.
|
||||
Отмена входящего линка из callback не закрывает conn, если у него появился другой линк или NCD-владелец.
|
||||
Close немедленно отключает линк и ресурсы, но память удерживается active_users до выхода внутренних стеков RX/переходов/callbacks.
|
||||
Приём UDP: uasync -> etcp_connections_read_callback_socket -> etcp_process_packet ->
|
||||
etcp_packet_decrypted (проверки и снятие SESSION_DATA) -> etcp_conn_input -> normalizer/output_queue.
|
||||
|
||||
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
|
||||
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
|
||||
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
|
||||
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
|
||||
Обычные keepalive периодически шлют обе стороны; полезный TX сбрасывает адаптивный период к базовому.
|
||||
Сон одной из сторон останавливает обычный таймер. UDP-анонс сна подтверждается флагом SLEEP_ACK,
|
||||
повторяется до ACK/исчерпания попыток; TCP отдельного подтверждения сна не требует.
|
||||
RX watchdog работает отдельно в normal и standby и не продлевается собственными отправками.
|
||||
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
|
||||
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
|
||||
|
||||
@@ -206,7 +373,8 @@ Padding keepalive, PING/PONG и STCP-handshake:
|
||||
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
|
||||
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
|
||||
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
|
||||
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
|
||||
padding_size=0 сразу завершает handshake. При ETCP_CONN клиент читает актуальный диапазон при отправке;
|
||||
без ETCP_CONN сохраняет выбранную длину при запуске. Сервер выбирает длину независимо по пиру/instance.
|
||||
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
|
||||
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
|
||||
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
|
||||
@@ -217,14 +385,14 @@ Padding keepalive, PING/PONG и STCP-handshake:
|
||||
|
||||
Клиент: если все линки =down то начинается процедура восстановления связи:
|
||||
- клиент посылает init без сброса (0x04), link_state=2
|
||||
- сервер как получает init проверяет session_id:
|
||||
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
|
||||
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
|
||||
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
|
||||
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
|
||||
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
|
||||
|
||||
**** Connection session confirmation (2026-09) ****
|
||||
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
|
||||
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
|
||||
uses the same control protocol on every initialized UDP or TCP link, bypassing the
|
||||
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
|
||||
normalizer and reliable DATA/ACK queues. A stream reset does not generate physical DOWN/UP.
|
||||
|
||||
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
|
||||
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
|
||||
@@ -237,7 +405,8 @@ Control frames (all 25 bytes after timestamp/flag):
|
||||
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
|
||||
|
||||
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
|
||||
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
|
||||
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
|
||||
HELLO and a change to an already known epoch advertised by INIT only request a challenge. Only a
|
||||
CONFIRM for the current local epoch and pending random challenge commits a peer
|
||||
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
|
||||
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
|
||||
@@ -251,3 +420,22 @@ only after STCP handshake. It completes the same session confirmation before INI
|
||||
REINIT notifies stream consumers without tearing down physical ownership. The
|
||||
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
|
||||
its previous control messages may have been discarded by the stream reset.
|
||||
|
||||
Отказы памяти и прерывание потока:
|
||||
- OOM до сохранения RX DATA в recv_q не создаёт ACK; отправитель повторяет DATA.
|
||||
- ACK означает принятие в текущий транспортный поток, а успешный etcp_send —
|
||||
передачу владения транспорту. Оба не гарантируют завершение операции сервиса
|
||||
после прерывания сессии. Надёжность операции через REINIT обеспечивает её владелец.
|
||||
- OOM после принятия данных в normalizer или при подготовке TX inflight вызывает
|
||||
fatal REINIT: новая локальная эпоха, блокировка DATA, отбрасывание старых очередей,
|
||||
частичных кодограмм и физических попыток. Старые payload не воспроизводятся.
|
||||
Физические линки сохраняются; очистка очередей/reset не выделяет память.
|
||||
- Активный обработчик освобождает собственный извлечённый фрагмент ровно один раз;
|
||||
после reset/close из callback он прекращает разбор старой кодограммы/пакета.
|
||||
- Согласование новой эпохи выполняется после очистки. OOM control-пакета оставляет
|
||||
повтор активным, без продления исходного срока 10с. Отказ обязательного session-
|
||||
таймера или генерации локальной эпохи закрывает соединение с диагностикой.
|
||||
Если невозможно выделить даже обязательный deferred-close token внутри callback,
|
||||
процесс завершается через abort: соединение не оставляется в зависшем состоянии.
|
||||
- После подтверждения эпох владельцы получают REINIT и восстанавливают операции
|
||||
своего протокола; при невозможности согласования соединение закрывается.
|
||||
+125
-305
@@ -1,329 +1,149 @@
|
||||
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
|
||||
# Архитектура ETCP Router
|
||||
|
||||
# ETCP Router Architecture
|
||||
Маршрутизатор связывает сервис с удалённым узлом через группу, не привязывая логическую
|
||||
сессию к одному физическому ETCP. Ключ канала — `(group_id, remote_node_id, svc_id)`.
|
||||
Wire-формат и сервисный API подробно описаны в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md),
|
||||
объявления — в [etcp_router.h](../src/routing_layer/etcp_router.h).
|
||||
|
||||
## Структуры данных
|
||||
## Отправка и приём
|
||||
|
||||
```
|
||||
ETCP_ROUTER_CONN (один на пару remote_node_id + svc_id)
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ ll_entry (хеш-индекс: remote_node_id[8] + svc_id[1]) │
|
||||
│ remote_node_id: u64 │
|
||||
│ svc_id: u8 │
|
||||
│ last_dgram_ts: u16 (timestamp последней датаграммы) │
|
||||
│ │
|
||||
│ tx_seq: u32 следующий seq для отправки │
|
||||
│ rx_seq: u32 ожидаемый seq для сборки │
|
||||
│ tx_acked: u32 сколько наших пакетов подтвердил remote │
|
||||
│ last_sent_ack_seq: u32 последний отправленный ACK │
|
||||
│ │
|
||||
│ recv_q: ll_queue* очередь reorder (хеш по seq[4]) │
|
||||
│ send_q: ll_queue* очередь при переполнении inflight │
|
||||
│ │
|
||||
│ ack_timer: void* периодический 100ms │
|
||||
│ idle_ack_timer: void* idle таймаут 500ms │
|
||||
│ send_waiter: queue_waiter_handle waiter на send_input_q │
|
||||
│ watchdog_timer: void* проверка инварианта 500ms │
|
||||
│ send_blocked: u8 inflight полон │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
SVC_ROUTE_HDR (22 байта)
|
||||
┌──────┬────────────────┬────────────────┬───────┬────────┬──────────┐
|
||||
│ cmd │ dst_node_id │ src_node_id │ seq │ svc_id │ payload │
|
||||
│ u8 │ u64 │ u64 │ u32 │ u8 │ ... │
|
||||
└──────┴────────────────┴────────────────┴───────┴────────┴──────────┘
|
||||
|
||||
seq в data-пакетах = tx_seq (счетчик отправленных)
|
||||
seq в ACK-пакетах = rx_seq (ожидаемый следующий)
|
||||
CLOSE-флаг = 0x80000000 в seq, RST-флаг = 0x40000000
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Producer["Сервис: payload + mode"] --> Send["send_q: plaintext, seq, mode"]
|
||||
Send --> Session{"peer_reset_id подтверждён?"}
|
||||
Session -- нет --> Hello["HELLO / challenge / confirm"]
|
||||
Hello --> Session
|
||||
Session -- да --> Route["Текущий маршрут: conn_mgr → BGP, либо прямой глобальный ETCP"]
|
||||
Route --> Wait["Waiter физической send_input_q"]
|
||||
Wait --> Encode["route_crypto_encode: encrypt → sign"]
|
||||
Encode --> Inflight["inflight_q: неизменяемое тело без visited-list"]
|
||||
Inflight --> Path["Добавить source ID"]
|
||||
Path --> ETCP["etcp_send / normalizer"]
|
||||
Inflight -- RTO 300 ms --> Wait
|
||||
ETCP --> Network["Сеть / транзитные узлы"]
|
||||
Network --> Check["Проверить visited-list"]
|
||||
Check --> Destination{"dst=self?"}
|
||||
Destination -- нет --> Transit["Добавить self один раз, BGP next hop / transit FIFO"]
|
||||
Transit --> Network
|
||||
Destination -- да --> Decode["Crypto policy + verify/decrypt + проверка эпох"]
|
||||
Decode --> Incoming["incoming_q: plaintext"]
|
||||
Incoming --> Reorder["recv_q: reorder по seq"]
|
||||
Reorder --> Ready{"receive_ready разрешает?"}
|
||||
Ready -- нет --> Busy["Сохранить пакет, ACK RECEIVE_BUSY"]
|
||||
Busy --> Resume["resume_receive: отложенное продолжение"]
|
||||
Resume --> Reorder
|
||||
Ready -- да --> Callback["Callback сервиса получает entry"]
|
||||
Callback --> Ack["rx_seq++ → ACK текущей границы"]
|
||||
Ack --> Release["Отправитель снимает подтверждённый inflight"]
|
||||
```
|
||||
|
||||
## Send Path (отправка данных)
|
||||
Каждая кодограмма имеет самостоятельный seq; это не байтовый поток.
|
||||
send_q ограничена 64 пакетами при обычной отправке, `force` обходит этот порог.
|
||||
Inflight по умолчанию — 256, при minRTT probe — 4. Получатель ограничивает reorder
|
||||
окном max_inflight и суммарным размером очередей max_inflight+1 записей.
|
||||
|
||||
```
|
||||
etcp_route_send(inst, dst, entry)
|
||||
etcp_router_conn_send(rconn, data, len)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ rconn = find/get │ поиск по (dst, svc_id)
|
||||
│ авто-создание если │ или etcp_router_conn_get()
|
||||
│ ещё нет │
|
||||
└────────┬────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ router_enqueue_send(rconn) │ всегда через send_q (FIFO)
|
||||
│ if send_q >= 64 && !force: │ → вернуть -1 (backpressure)
|
||||
│ return -1 │
|
||||
└──────┬───────────────────────┘
|
||||
│ копия в send_q + router_send_kick()
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_send_kick(rconn) │
|
||||
│ closed/no_route → return │
|
||||
│ send_q пуст → return │
|
||||
│ inflight >= limit → send_blocked=1, ret │
|
||||
│ waiter уже ждёт → return │
|
||||
│ нет conn → set_no_route │
|
||||
│ queue_waiter_wait(send_input_q, │
|
||||
│ &send_waiter, drain_cb) │
|
||||
└──────┬───────────────────────────────────┘
|
||||
│ send_input_q опустел (round-robin: FIFO waiter-ов)
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ router_send_drain_cb(rconn) │ шлём ОДИН пакет
|
||||
│ e = send_q.pop() │
|
||||
│ router_send_one_flags(e) │
|
||||
│ → tx_seq++ │
|
||||
│ → etcp_send(conn, entry) │
|
||||
│ если send_q ещё непуст: │
|
||||
│ inflight полон → blocked=1│
|
||||
│ иначе → queue_waiter_wait │ (в хвост → round-robin)
|
||||
│ если пуст → send_blocked=0 │
|
||||
└──────────────────────────────┘
|
||||
Все события выполняются в uasync-потоке. Штатный normalizer откладывает physical waiters,
|
||||
а callbacks производителей send_q могут выполняться синхронно при освобождении места.
|
||||
Поэтому код отправки повторно проверяет состояние/эпохи после callback.
|
||||
|
||||
Повторный запуск drain:
|
||||
• приход ACK → router_handle_ack → kick
|
||||
• маршрут появился → router_no_route_retry_cb → kick
|
||||
• рост inflight-лимита → router_update_inflight_limit → kick
|
||||
• страховка → router_send_watchdog_cb (500ms) → DEBUG_ERROR + kick
|
||||
## Эпохи логического канала
|
||||
|
||||
Полный сетевой пакет:
|
||||
|
||||
`[57-byte SVC_ROUTE_HDR][payload][signature?][visited_ids:8*N][count:1]`
|
||||
|
||||
Числа в заголовке — native endian. flags — отдельный байт; биты seq не заняты флагами.
|
||||
Заголовок включает group/src/dst/service, seq/timestamp и поля reset_id/peer_reset_id/challenge.
|
||||
visited-list содержит 1..64 ID, находится вне подписи/шифрования и проверяется на каждом переходе.
|
||||
|
||||
Первичное согласование двух сторон имеет вид:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Узел A, local=a
|
||||
participant B as Узел B, local=b
|
||||
A->>B: HELLO(reset=a, peer=0)
|
||||
B->>A: CHALLENGE(reset=b, peer=a, cB)
|
||||
A->>B: CONFIRM(reset=a, peer=b, cB)
|
||||
A->>B: CHALLENGE(reset=a, peer=b, cA)
|
||||
B->>A: CONFIRM(reset=b, peer=a, cA)
|
||||
Note over A,B: Каждый узел принимает peer ID только после ответа на свой pending challenge
|
||||
```
|
||||
|
||||
## Receive Path (приём данных)
|
||||
Получение HELLO/CHALLENGE с неизвестной эпохой само по себе не сбрасывает работающий канал.
|
||||
Переставленные/старые CONFIRM не совпадают с pending ID/challenge и игнорируются.
|
||||
Новые challenge ограничены интервалом 300 ms; первичный HELLO повторяется без отдельного timeout.
|
||||
|
||||
```
|
||||
etcp_router_recv_cb(conn, entry)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ dst == node_id? │
|
||||
└──┬───────────────┬───┘
|
||||
│ YES │ NO (транзит)
|
||||
▼ ▼
|
||||
┌────────────────────┐ ┌──────────────────────────┐
|
||||
│ Мы — целевая нода │ │ Транзит: найти next hop │
|
||||
└────────┬───────────┘ │ route_bgp_find_conn_for_ │
|
||||
│ │ node(bgp, hdr.dst) │
|
||||
▼ │ etcp_send(next, entry) │
|
||||
┌────────────────────┐ └──────────────────────────┘
|
||||
│ pl_len == 0? │
|
||||
└──┬───────────┬─────┘
|
||||
│ YES │ NO (данные)
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────────────────────┐
|
||||
│ CLOSE/ │ │ rconn = find/get │
|
||||
│ RST? │ │ (авто-создание если нет) │
|
||||
└──┬──┬───┘ └───────────┬─────────────┘
|
||||
│ │ │
|
||||
│ │ NO (ACK) ▼
|
||||
│ ▼ ┌───────────────────────────────────────┐
|
||||
│ ┌──────────┐│ Проверка границ seq: │
|
||||
│ │ tx_acked ││ d = seq - rx_seq │
|
||||
│ │ = hdr.seq││ |d| < ROUTER_MAX_INFLIGHT? │
|
||||
│ │ (только ││ │
|
||||
│ │ вперёд) ││ Дубликат? │
|
||||
│ │ kick ││ rx_seq - seq > 0 ИЛИ │
|
||||
│ └──────────┘│ seq уже в recv_q? │
|
||||
│ └──────┬─────────────┬──────────────────┘
|
||||
│ │ OK │ out-of-bounds / dup
|
||||
│ ▼ ▼
|
||||
│ ┌──────────────┐ ┌─────────┐
|
||||
│ │ recv_q.put() │ │ DROP │
|
||||
│ │ хеш по seq │ └─────────┘
|
||||
│ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────────────┐
|
||||
│ │ seq == rx_seq? │
|
||||
│ └──┬───────────────┬───┘
|
||||
│ │ YES │ NO
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ try_assembly │ │
|
||||
│ │ while next в │ │
|
||||
│ │ recv_q: │ │
|
||||
│ │ deliver() │ │
|
||||
│ │ rx_seq++ │ │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┴──┐
|
||||
│ │ router_deliver() │
|
||||
│ │ вызывает callback │
|
||||
│ │ сервиса (svc_id) │
|
||||
│ │ entry: [svc_id|payload] │
|
||||
│ └──────────────┬──────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────────────────────────┐
|
||||
│ router_schedule_ack(rconn) │
|
||||
│ запускает ack_timer (100ms) если │
|
||||
│ ещё не запущен │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
| Событие | Что сохраняется | Что меняется |
|
||||
| --- | --- | --- |
|
||||
| Смена ETCP/next hop | Логические эпохи, seq, очереди | Физический waiter/маршрут |
|
||||
| Первое подтверждение peer | Накопленная send_q | Заполняется peer_reset_id, начинается отправка |
|
||||
| Подтверждённый рестарт peer | Собственный reset_id | Очереди, seq, RTT/метрики; уведомление сервиса |
|
||||
| Явный conn_restart | Ключ group/peer/service и адрес rconn | Новый reset_id, пустое состояние; затем callback и HELLO |
|
||||
| conn_close | Только данные, переданные во владение callback | Запись снимается с реестра, ресурсы освобождаются |
|
||||
|
||||
```
|
||||
CLOSE / RST ветка:
|
||||
┌────────────────────────────────────────┐
|
||||
│ pl_len == 0 && seq & CLOSE/RST │
|
||||
│ → router_close_and_notify(rconn) │
|
||||
│ ├─ router_send_close_to_service() │
|
||||
│ │ (callback с NULL conn) │
|
||||
│ ├─ отправить CLOSE удалённой стороне│
|
||||
│ ├─ отмена всех таймеров │
|
||||
│ ├─ очистка recv_q + send_q │
|
||||
│ └─ удаление из inst->router_conns │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
Сервис возобновляет прикладную синхронизацию после сброса; старые гарантии доставки через reset не продолжаются.
|
||||
Готовность router означает наличие подтверждённого peer ID. Это отдельное состояние
|
||||
от физического UP и READY BGP-группы.
|
||||
|
||||
## ACK Mechanism
|
||||
## Подтверждение и пауза потребителя
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_schedule_ack(rconn) │
|
||||
│ (вызывается при получении данных) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
┌────────────────▼────────────────────────┐
|
||||
│ если idle_ack_timer активен — отменить │
|
||||
│ если ack_timer не активен — запустить │
|
||||
│ ROUTER_ACK_INTERVAL_TB = 1000 (100ms) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼ (через 100ms)
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_ack_timer_cb() │
|
||||
│ │
|
||||
│ if rx_seq != last_sent_ack_seq: │
|
||||
│ router_send_ack(rconn) │
|
||||
│ last_sent_ack_seq = rx_seq │
|
||||
│ перезапустить ack_timer │
|
||||
│ else: │
|
||||
│ запустить idle_ack_timer (500ms) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│
|
||||
▼ (через 500ms)
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_idle_ack_timer_cb() │
|
||||
│ │
|
||||
│ if rx_seq != last_sent_ack_seq: │
|
||||
│ router_send_ack(rconn) (досылка) │
|
||||
│ last_sent_ack_seq = rx_seq │
|
||||
└─────────────────────────────────────────┘
|
||||
DATA проходят crypto policy и verify/decrypt до изменения reorder/rx_seq.
|
||||
Сервис получает `[svc:1][src:8][dst:8][rx_flags:1][group:8][payload]`, всего 26 байт заголовка.
|
||||
Его callback владеет entry, ACK продвигается после успешной передачи callback.
|
||||
|
||||
router_send_ack(rconn):
|
||||
hdr.seq = rx_seq
|
||||
hdr.svc_id = svc_id
|
||||
pl_len = 0 → это ACK-пакет
|
||||
etcp_send(conn, entry)
|
||||
```
|
||||
ACK подтверждает только диапазон до tx_sent, в модульном порядке uint32.
|
||||
При продвижении снимаются существующие inflight-записи и один раз учитываются исходные payload bytes.
|
||||
|
||||
```
|
||||
Обработка входящего ACK:
|
||||
┌────────────────────────────────────────┐
|
||||
│ pl_len == 0 && нет CLOSE/RST флагов │
|
||||
│ │
|
||||
│ if seq >= tx_acked: // только вперёд │
|
||||
│ tx_acked = seq // обновить │
|
||||
│ else: │
|
||||
│ DEBUG_WARN "stale ACK" │
|
||||
│ │
|
||||
│ if send_blocked: │
|
||||
│ send_blocked = 0 │
|
||||
│ router_send_kick(rconn) │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
RECEIVE_BUSY не подтверждает отложенные DATA, но поддерживает живость ожидающего диска/буфера сервиса.
|
||||
Даже при прежнем seq допустимый BUSY обновляет отсчёт RTO и останавливает новую отправку/проход ретрансмитов.
|
||||
Обычный повторный ACK без продвижения этого не делает. Снятие BUSY возобновляет send_q.
|
||||
|
||||
## Inflight Control
|
||||
receive_ready — предикат без I/O и изменения/закрытия роутера. При 0 пакет сохраняется в recv_q;
|
||||
resume_receive планирует продолжение. Управляющий CLOSE не проходит через этот предикат.
|
||||
|
||||
```
|
||||
ROUTER_MAX_INFLIGHT = 256
|
||||
| Механизм | Интервал/условие |
|
||||
| --- | --- |
|
||||
| ACK | Не чаще 10 ms; текущая граница повторяется при дубликате DATA после потери ACK |
|
||||
| RECEIVE_BUSY | Около 100 ms, пока сервис остановлен |
|
||||
| Idle ACK | 500 ms, только если rx_seq ещё не был отправлен в ACK |
|
||||
| Handshake/RTO | 300 ms |
|
||||
| Нет маршрута после handshake | Повторный поиск через 20 ms |
|
||||
| Транзит при backpressure | Waiter + повтор выбора маршрута через 20 ms |
|
||||
| Watchdog send_q | 500 ms, пока очередь непуста |
|
||||
| Нет прогресса/BUSY | Закрытие после 17 циклов, для сервиса CALL — после 67 |
|
||||
|
||||
Отправка возможна когда: tx_seq - tx_acked < 256
|
||||
Отправка заблокирована: tx_seq - tx_acked >= 256
|
||||
Транзитные очереди принадлежат физическому ETCP, сгруппированы по group/src/dst и не имеют своих seq.
|
||||
При смене маршрута пакет переносится на актуальный next hop без ожидания освобождения старой очереди.
|
||||
Отсутствие пути приводит к потере транзитной копии; её восстанавливает end-to-end inflight источника.
|
||||
|
||||
При блокировке:
|
||||
данные → send_q (FIFO, без дропов)
|
||||
send_blocked = 1
|
||||
## Владение и локальная доставка
|
||||
|
||||
Разблокировка:
|
||||
приход ACK от remote → tx_acked обновляется
|
||||
→ router_send_kick() → waiter → drain_cb разгребает send_q
|
||||
или маршрут появился / inflight-лимит вырос → kick
|
||||
страховка: router_send_watchdog_cb (500ms) → force kick
|
||||
| Объект | Владение |
|
||||
| --- | --- |
|
||||
| Входной entry etcp_route_send | Всегда поглощается, включая ошибку |
|
||||
| Буфер etcp_router_conn_send | Заимствован на время копирования |
|
||||
| ETCP_ROUTER_CONN из conn_get | Заимствован до close/destroy |
|
||||
| Сервисный entry | Передаётся callback; освободить dgram и entry |
|
||||
| Физический conn в callback | Заимствован, может обозначать транзитного пира |
|
||||
| Синтетический conn | Только на время callback, instance/peer=self; без сетевого send/ref_take |
|
||||
| Send-ready handle/arg | У производителя до callback/отмены; перед освобождением отменить |
|
||||
|
||||
send_q полностью разобран → send_blocked = 0
|
||||
```
|
||||
Self-доставка через etcp_route_send синхронна и не создаёт канал. Она обходит сетевые crypto policy
|
||||
и receive_ready, выдаёт rx_flags=0; режим проверяется на допустимые биты.
|
||||
Payload должен быть непустым и помещаться вместе с 26-байтным заголовком в uint16 длину.
|
||||
Нет обработчика, переполнение размера или отказ памяти возвращают -1.
|
||||
Отдельный синтетический conn каждого вызова сохраняет контекст при вложенных доставках разных ядер.
|
||||
|
||||
## Полная схема потоков
|
||||
Закрываемый канал удаляется из реестра до callback. Callback может закрывать группу/другие каналы
|
||||
или открыть замену с тем же ключом; старая запись не возвращается повторным поиском.
|
||||
Память канала и send_q освобождается отложенно за пределами текущего callback.
|
||||
До массовой остановки сервисы отменяют producers/waiters; уничтожение ядра из callback останавливаемого сервиса запрещено.
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
│ Сервис │ (routing, proxy, NAT, ...)
|
||||
│ (svc_id)│
|
||||
└────┬─────┘
|
||||
│
|
||||
etcp_route_send() │ callback(svc_id, entry)
|
||||
▼ ▲
|
||||
┌─────────┐ send ┌─────────────────────────────────┐ recv ┌─────────┐
|
||||
│ TUN │─────────▶│ ETCP ROUTER │─────────▶│ TUN │
|
||||
│ (выход) │ │ │ │ (вход) │
|
||||
└─────────┘ │ ┌──────────┐ ┌──────────┐ │ └─────────┘
|
||||
│ │ send_q │ │ recv_q │ │
|
||||
│ │ (FIFO) │ │ (хеш seq)│ │
|
||||
│ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ Inflight / Assembly │ │
|
||||
│ │ tx_seq - tx_acked │ │
|
||||
│ │ rx_seq → deliver │ │
|
||||
│ └───────────┬─────────────┘ │
|
||||
│ │ │
|
||||
└──────────────┼──────────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ BGP │
|
||||
│ (нахождение│
|
||||
│ next hop) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ ETCP conn │──▶ сеть
|
||||
└─────────────┘
|
||||
close_group удаляет логические каналы и pending transit указанной группы.
|
||||
destroy снимает wire callback, закрывает все логические каналы и очищает транзитные очереди,
|
||||
таймеры и waiters на всех живых ETCP. Уничтожать сам ETCP для очистки роутера не требуется.
|
||||
|
||||
──▶ send (исходящие данные)
|
||||
──▶ recv (входящие данные, callback сервису)
|
||||
──▶ транзит (через BGP к следующему hop)
|
||||
```
|
||||
|
||||
## Таймеры (один rconn)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ ack_timer ──────── 100ms ─────▶ router_ack_timer_cb│
|
||||
│ (периодический) │
|
||||
│ │
|
||||
│ idle_ack_timer ─── 500ms ─────▶ router_idle_ack_cb │
|
||||
│ (досылка последнего ACK) │
|
||||
│ │
|
||||
│ watchdog_timer ── 500ms ─────▶ router_send_watchdog│
|
||||
│ (страховка инварианта drain, force kick) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Флаги seq
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ CLOSE: 0x80000000 нормальное закрытие conn │
|
||||
│ RST: 0x40000000 conn не найден (reset) │
|
||||
│ │
|
||||
│ CLOSE/RST-пакеты: seq=0 | флаг, pl_len=0 │
|
||||
│ Data-пакеты: seq=tx_seq, pl_len>0 │
|
||||
│ ACK-пакеты: seq=rx_seq, pl_len=0 │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
Метрики delivery считают первые подтверждения исходного payload, без заголовков/crypto/ретрансмитов.
|
||||
RTT — 0.1 ms, snapshot API выдаёт микросекунды; jitter — unsigned 16.16 EWMA,
|
||||
монитор публикует удвоенный EWMA. Очереди, метрики и параметры minRTT сбрасываются вместе с логической сессией.
|
||||
@@ -0,0 +1,69 @@
|
||||
Аудит документации Local_transport — 26.10.2026
|
||||
|
||||
Объём и статус
|
||||
|
||||
Полностью прочитаны src/local_transport/local_transport.c и platform.c,
|
||||
сверены все объявления и комментарии трёх заголовков local_transport.h,
|
||||
platform.h и protocol.h. Все три заголовка готовы к отметке [V] в чек-листе.
|
||||
Для проверки интеграции прочитан chat_local.c; прочитаны три профильных теста
|
||||
и local_transport_ipv6.sh. Полный аудит src/chat этой записью не заявляется.
|
||||
LAN-раздел doc/node_snapshot.md уточнён; общая отметка документа остаётся [~].
|
||||
|
||||
Исправленные описания
|
||||
|
||||
1. Доставка и дедупликация. Убрано безусловное обещание отсутствия дублей:
|
||||
кеш хранит последние 128 успешно принятых (peer, epoch, request) до 30с,
|
||||
без продления повтором. Отказы received не кешируются. Request уникален
|
||||
среди pending, а повторное использование ID также ограничено кешем получателя.
|
||||
FAILED и отсутствие ACK не доказывают, что приложение не получило данные.
|
||||
Описаны 8 pending, немедленная отправка и до двух повторов по секундному tick.
|
||||
|
||||
2. Активность и callbacks. Уточнены синхронные уведомления из API, порядок
|
||||
activity_changed перед открытием/закрытием сокетов, завершение pending
|
||||
через FAILED и сохранение эпохи/дедупликации при паузе того же handle.
|
||||
Отказы открытия сокетов не означают отказ set_active: повтор при сверке;
|
||||
невозможность создать tick-таймер выключает транспорт. peers() очищает TTL
|
||||
и может вызвать routes_changed; route() callbacks не вызывает.
|
||||
|
||||
3. Broadcast и TTL. Изменение ника рассылается только при active+broadcasts;
|
||||
тот же ник не вызывает отправку. Отключение broadcasts сохраняет unicast,
|
||||
приём, сверку интерфейсов и TTL. Только обычный ANNOUNCE продлевает endpoint.
|
||||
Жизнь кандидата дополнительно ограничена его собственным TTL, интерфейсом
|
||||
и discovery-endpoint. Offline-gate применяется к подписанному входящему
|
||||
обмену, но не повторяется при route() и не нужен для ответа на CHALLENGE.
|
||||
|
||||
4. Wire. Описаны все смещения 60-байтного заголовка, NUL в 16-байтном magic,
|
||||
длина тела, BE, типы сообщений, неподписанные MESSAGE/ACK и ограничения
|
||||
discovery-порта. Уточнены подпись/nonce, scope, допустимые protocol,
|
||||
уникальные socket_id, ненулевые port/generation и пустой PROOF.
|
||||
|
||||
5. Платформа. Зафиксированы различия отбора POSIX/Windows, дедупликация
|
||||
адресов, усечение снимка, ответственность за ifindex/scope, допустимый
|
||||
источник и результаты local_receive (включая пустой UDP/would-block/-2).
|
||||
Добавлены предусловия встроенных BE/UTF-8 helpers и категория discovery.
|
||||
|
||||
Проверки
|
||||
|
||||
make -C src -j4 libutun.a и сборка трёх профильных тестов — PASS.
|
||||
Логи /tmp/local-transport-audit-build.log и
|
||||
/tmp/local-transport-audit-tests-build.log. Существующие предупреждения
|
||||
db_sync/etcp_connections/dm_media не относятся к изменённым комментариям.
|
||||
|
||||
Пять сценариев PASS, последовательно вне sandbox из-за запрета сокетов:
|
||||
- test_local_transport: UDP discovery, ACK retry/dedup, TTL, настройки,
|
||||
остановка/повторная активация; /tmp/local-transport-audit-discovery.log.
|
||||
- test_local_transport_packets: миллион некорректных пакетов, ограничения
|
||||
flood/памяти/events и восстановление; /tmp/local-transport-audit-packets.log.
|
||||
- test_local_routes: подписи, nonce/replay, BGP gate, поколения сокетов,
|
||||
TTL, NCD UDP/STCP и сохранность snapshot; /tmp/local-transport-audit-routes.log.
|
||||
- sh tests/local_transport_ipv6.sh: изолированная IPv6-only LAN;
|
||||
/tmp/local-transport-audit-ipv6-discovery.log.
|
||||
- sh tests/local_transport_ipv6.sh test_local_routes: IPv6 scope и LAN/NCD;
|
||||
/tmp/local-transport-audit-ipv6-routes.log.
|
||||
|
||||
В заголовках изменены только комментарии; логика, ABI и wire не менялись.
|
||||
Новых подтверждённых дефектов реализации в этом объёме не обнаружено.
|
||||
Прежняя задача о нестабильном ACK в test_local_transport остаётся открытой:
|
||||
успешный отдельный прогон не подтверждает устранение флака под общей нагрузкой.
|
||||
Полный check.sh, санитайзеры, native Windows/FreeBSD и GUI/Android runtime
|
||||
не запускались. [V] означает завершённую сверку документации по исходникам.
|
||||
@@ -0,0 +1,530 @@
|
||||
Аудит media_async / media_delivery — 2026-10-24
|
||||
Статус: alias encrypted_file, граница voice_file, переанонс SQLite и расхождение MIME write/read исправлены.
|
||||
Сверка двух каталогов завершена: все11 заголовков, sync.md, _desc.txt и encrypted_attachments.md.
|
||||
Общий service_lifecycle.md проверен в media-части; разделы других сервисов не считаются закрытыми.
|
||||
Ниже сохранена хронология воспроизведений и исправлений; прежние отметки незавершённости относятся к этапам.
|
||||
Последние проверки MIME и итоговая архитектурная сверка — в конце.
|
||||
|
||||
Подтверждённый дефект: исходник уничтожается через другое имя того же файла
|
||||
|
||||
encrypted_file_write, src/media_async/encrypted_file.c:79-87, сравнивает только строки
|
||||
source/destination. Разные строки могут обозначать один inode. fopen(destination,"wb")
|
||||
усекает уже открытый source. Криптография затем успешно обрабатывает пустой файл.
|
||||
Контейнер корректен для нулевого содержимого, но исходные данные потеряны; возврат равен 0.
|
||||
|
||||
Воспроизведено на настоящих функциях из src/libutun.a:
|
||||
- source и source с компонентом /./: исходные30 байт заменены контейнером152 байта,
|
||||
plaintext_size=0, rc=0, SHA256 исходника изменился;
|
||||
- source и hardlink: тот же результат;
|
||||
- encrypted_file_read тоже пропускает оба вида alias: контейнер182 байта заменяется
|
||||
plaintext30 байт, rc=0, исходный ciphertext больше не существует в прежнем виде.
|
||||
|
||||
У read такая же проверка strcmp в строке118 и разрушающий fopen в строке142.
|
||||
В текущем групповом pipeline вызывающий выбирает отдельный .part; воспроизведение
|
||||
доказывает дефект файлового API, а не наблюдавшуюся потерю реального пользовательского вложения.
|
||||
Ошибку нельзя замаскировать описанием одинаковых файлов как успешного no-op:
|
||||
шифрование и расшифровка должны создавать другое содержимое.
|
||||
|
||||
Диагностика:
|
||||
- /tmp/media-encrypted-alias-repro.c — четыре проверки, сверяются inode, размеры и SHA256;
|
||||
- /tmp/media-encrypted-alias-repro.mk — использует цели/библиотеки/флаги tests/Makefile;
|
||||
- /tmp/media-encrypted-alias-repro.log — полный журнал с media=debug;
|
||||
- /tmp/media-encrypted-alias-lvVS77/ — сохранённые входы и результаты опыта.
|
||||
Проектные .c/.h для воспроизведения не менялись.
|
||||
|
||||
Согласованная архитектура исправления
|
||||
|
||||
Одной проверки file_io_paths перед fopen(wb) недостаточно для полного исправления
|
||||
контракта владения. Сейчас encrypted_file удаляет destination при ошибке, а
|
||||
ak_file_done повторно удаляет заранее вычисленный .part даже при раннем отказе.
|
||||
Нужно убрать открытие произвольного назначения с усечением и очистку по одному имени.
|
||||
|
||||
Граница модулей
|
||||
|
||||
- lib/file_io: UTF-8/regular-file проверки, идентичность, эксклюзивный temp,
|
||||
flush/sync/close, atomic rename, владение temp и защита от orphan cleanup.
|
||||
- encrypted_file: создание/проверка контейнера в собственном temp; без публикации
|
||||
конечного имени, SQLite и состояния async-задачи.
|
||||
- chat_attachment: задача владеет temp, выбирает конечное имя и выполняет одну
|
||||
публикацию; completion меняет состояние БД/UI и освобождает только свои ресурсы.
|
||||
Подготовка, sync и rename остаются в worker, как сейчас; новые файловые I/O в uasync не добавляются.
|
||||
|
||||
Предлагаемый контракт write/read
|
||||
|
||||
1. Функция получает source, конечный target_path и указатель на обнулённый file_io_temp.
|
||||
target_path задаёт место будущего результата; он не открывается с усечением.
|
||||
До начала работы file_io_paths(source,target_path) обязан вернуть 0.
|
||||
Совпадение строки, /./, hardlink и конечная ссылка назначения отклоняются без изменений.
|
||||
2. Источник открывается общим file_io_open_read. Только после успешного открытия
|
||||
создаётся уникальный temp рядом с target_path через file_io_temp_open.
|
||||
На POSIX он имеет права 0600; создание эксклюзивное, владелец сразу учтён в общем реестре.
|
||||
3. Контейнер пишется/проверяется через уже открытый FILE этого temp. Нет второго .part,
|
||||
второго открытия выхода по пути или дополнительного слоя публикации внутри codec.
|
||||
4. Полностью проверяются данные, закрытие входа и durable завершение выхода.
|
||||
При ошибке удаляется только созданный этой операцией temp; существующие source/target сохраняются.
|
||||
При успехе возвращается закрытый, синхронизированный temp, который ещё не опубликован.
|
||||
Результат info/type у read присваивается только после полного успешного завершения подготовки.
|
||||
5. Владелец file_io_temp размещается прямо в ak_file и имеет стабильный адрес до done;
|
||||
копировать его структуру нельзя: реестр хранит адрес владельца.
|
||||
Функции не принимают уже активный output и не уничтожают его при отказе аргументов.
|
||||
|
||||
Единая политика durability
|
||||
|
||||
В file_io_temp_open добавить flags, в file_io_temp хранить выбранный режим на весь
|
||||
срок операции. FILE_IO_DURABLE включает обе части: finish и publish.
|
||||
Обычные пользователи передают 0 и сохраняют свою семантику; отдельные флаги в каждом
|
||||
finish/publish не нужны. Это предотвращает случайное сохранение файла без сохранения имени.
|
||||
|
||||
Durable finish: fflush -> fsync/_commit того же открытого fd -> fclose. Ошибка любой
|
||||
фазы сохраняет первый код ошибки; close выполняется и при отказе sync. Открывать путь
|
||||
заново ради fsync не нужно. После отказа temp остаётся у владельца до discard.
|
||||
|
||||
Publish повторно проверяет идентичность source/target и принимает только закрытый temp.
|
||||
На POSIX: rename и fsync каталога назначения; соседний temp находится в том же каталоге.
|
||||
На Windows: общий wide API и MOVEFILE_WRITE_THROUGH. Родительский каталог вычисляется
|
||||
без обрезания: имя без '/' означает '.', '/file' означает '/'.
|
||||
Политика sync принадлежит file_io, а не формату зашифрованного контейнера.
|
||||
|
||||
Точка фиксации — успешный rename. До неё ошибка сохраняет старый target.
|
||||
После rename ошибка fsync каталога означает: результат уже установлен, долговечность
|
||||
имени не подтверждена. Нельзя обещать откат, удалять новый файл вслепую или восстанавливать
|
||||
старое содержимое. В этот момент temp уже отсоединён от реестра и его имя в owner пусто.
|
||||
Этот переход должен быть явно описан в file_io.h и проверен тестом.
|
||||
Длительный fsync выполняется после снятия блокировки реестра временных файлов.
|
||||
|
||||
Владение задачи и отмена
|
||||
|
||||
ak_file хранит file_io_temp вместо temporary[...]. Worker после успешной подготовки
|
||||
один раз вызывает file_io_temp_publish с тем же target и источником.
|
||||
До publish поле temporary непустое; после успешного rename оно пустое даже при
|
||||
ошибке directory sync. По этому переходу задача фиксирует факт своей публикации.
|
||||
Completion всегда может вызвать discard: после rename он ничего не удаляет.
|
||||
Cleanup при отмене/удалении канала учитывает отдельно действительно созданные/
|
||||
заменённые артефакты. Вычисленный путь не является доказательством владения файлом.
|
||||
При отказе до создания выхода нельзя удалять существующий target или его alias.
|
||||
|
||||
Отдельных async owners, refcount и фоновых потоков не требуется. Ключ по-прежнему сохраняется
|
||||
до шифрования; ready получателя/публикация сообщения подтверждаются только после
|
||||
успешного worker и проверки актуальности канала в completion. Авторская строка ключа
|
||||
сохраняется до worker по прежним правилам. Общей транзакции ФС/SQLite нет: после rename
|
||||
путь остаётся известен attachment_keys для последующей очистки/повторной операции.
|
||||
|
||||
Кеш и восстановление после crash
|
||||
|
||||
Активный temp защищён существующим file_io-реестром, в том числе после finish.
|
||||
В chat_msg убрать специальное сохранение вычисленного cipher_path.part; конечный
|
||||
cipher_path по-прежнему защищается attachment_keys, временный файл — своим owner.
|
||||
После crash реестра нет: такие temp допустимо удалить.
|
||||
|
||||
Нельзя ограничиться нынешним orphan walk дерева media: plaintext-temp находится
|
||||
в decrypted, а ak_recover_preparations знает только фиксированный .part.
|
||||
В init/очистке после завершения workers нужно удалять оставшиеся временные файлы
|
||||
строгого собственного формата <UUID>.uenc.tmp.<32hex> / <UUID>.file.tmp.<32hex>
|
||||
в управляемых каталогах media/decrypted. Использовать file_io_remove_orphan,
|
||||
чтобы сохранить активные temp другого instance того же процесса. Конечные файлы,
|
||||
ключи и данные иных форматов такой проход не удаляет; ошибка обхода логируется.
|
||||
|
||||
Область изменений
|
||||
|
||||
Основные файлы: lib/file_io.c/h, media_async/encrypted_file.c/h,
|
||||
chat/chat_attachment.c, chat/chat_msg.c, test_encrypted_file/test_media_primitives
|
||||
и сетевые проверки зашифрованных вложений. Остальные вызовы file_io_temp_open
|
||||
в media_async/media_delivery/video переводятся на явные flags=0.
|
||||
DM не вызывает encrypted_file_write/read: он использует media_crypto streams
|
||||
и общие sync/publish helper-функции из encrypted_file.h. При правке общего слоя
|
||||
необходимо сохранить их durable контракт; перенос общих операций в file_io должен
|
||||
быть прямым, без совместимых обёрток и без изменения DM state machine.
|
||||
Формат контейнера, nonce, протокол ключей и схема БД для этого фикса не меняются.
|
||||
|
||||
Регрессии и критерии готовности
|
||||
|
||||
- Обе операции: одинаковая строка, /./, hardlink, symlink, отсутствующий/необычный source;
|
||||
source и прежний target не меняются, alias не удаляется, чужой temp не освобождается.
|
||||
- Обычные файлы размером 0/1/32767/32768/32769 байт, большой файл, UTF-8 пути;
|
||||
успешная подготовка не меняет target до publish. Read не публикует info/type при ошибке.
|
||||
- Внедрение ошибок open/fdopen/read/write/seek/flush/fsync/input-close/output-close/
|
||||
rename/directory-sync/RNG; отсутствие утечек fd/памяти/регистраций temp.
|
||||
- Подмена target на hardlink между подготовкой и publish: повторная проверка
|
||||
отклоняет публикацию, исходник сохраняется, удаляется только собственный temp.
|
||||
- Отказ directory sync после rename: новый файл цел, temp-owner отсоединён,
|
||||
ошибка не представляется как откат; повторная операция корректна.
|
||||
- Orphan cleanup во время work, после finish, после publish; crash/restart
|
||||
в обоих деревьях, чужие имена сохраняются. Отмена, stop и удаление канала
|
||||
до work, после подготовки и после rename: один done, нет публикации сообщения/ready.
|
||||
- Профильные 20 тестов, ASAN/UBSan/LSan и полный набор. Изменение file_io требует
|
||||
также проверки его прежних пользователей: copy, video и media blocks/download.
|
||||
|
||||
Этот проект реализован. Успешные 20 тестов ниже относятся к исходной реализации;
|
||||
проверки исправленного кода приведены в конце.
|
||||
|
||||
Что проверено и что осталось
|
||||
|
||||
Прочитаны все шесть пар .c/.h media_async, целиком file_transfer.c/h, заголовки
|
||||
media_delivery/download/index/proto, sync.md и doc/encrypted_attachments.md.
|
||||
Полная повторная сверка остальных реализаций media_delivery и исправление контрактов
|
||||
ещё не выполнены. Прежние [V] в общем checklist относятся к предыдущим аудитам;
|
||||
новые незавершённые пункты отмечены [~].
|
||||
|
||||
Linux: ./build.sh --no-gui -j4 — PASS;20/20 профильных тестов — PASS:
|
||||
media_primitives, attachment, attachment_send, encrypted_file, encrypted_attachment,
|
||||
media_async, media_index, media_index_regressions, media_delivery_sql,
|
||||
media_delivery_download, media_download_timeout, media_query, media_blocks,
|
||||
media_replication, media_delivery_integration, media_delivery_full,
|
||||
media_delivery_chat, dm_media, dm_worker, services.
|
||||
Логи: /tmp/media-audit-build.log, /tmp/media-doc-audit-results.log,
|
||||
/tmp/media-doc-audit-logs/. Штатный test_encrypted_file не покрывает alias.
|
||||
Старые сбои encrypted_attachment/media_delivery_chat в этом прогоне не воспроизвелись;
|
||||
один PASS не закрывает задачи о нестабильности.
|
||||
На исходном этапе полный check.sh и новые sanitizer-прогоны не запускались.
|
||||
|
||||
Результаты исправления — 2026-10-24
|
||||
|
||||
- file_io_temp получил единый FILE_IO_DURABLE: sync открытого fd при finish,
|
||||
write-through rename на Windows и directory sync после rename на POSIX.
|
||||
После rename temporary пуст, включая отказ directory sync; discard не удаляет результат.
|
||||
Общие sync/publish функции перенесены в file_io; DM вызывает их напрямую.
|
||||
- Write/read готовят отдельный закрытый temp. Source/target не открываются с усечением;
|
||||
совпадающая строка, /./, hardlink и конечная ссылка отклоняются до создания выхода.
|
||||
Read выдаёт info/type только после успешного закрытия/сохранения проверенного результата.
|
||||
- ak_file хранит один output, учитывает подготовленный plaintext snapshot и выполненный rename.
|
||||
Worker публикует один раз; completion очищает только собственные артефакты.
|
||||
Кеш использует реестр владельцев; recovery очищает строгие temp в media и decrypted.
|
||||
Построение путей и SQL recovery используют одну каноническую media_base.
|
||||
- test_encrypted_file: прежние шесть размеров, восемь alias-сценариев обоих API,
|
||||
активный output, alias перед publish и 23 отказа I/O с контролем fd/temp/метаданных.
|
||||
До rename прежний target сохраняется; после отказа directory sync новый файл установлен.
|
||||
- test_media_primitives: durable и обычный режим, sync failure до rename, failure после
|
||||
rename, относительный/корневой каталог, защита активного temp в worker и после close.
|
||||
- test_attachment_send детерминированно ждёт настоящего crypto rename без обработки
|
||||
completion, затем останавливает сервис: нет сообщения/ключа/оставшихся файлов/temp.
|
||||
- test_encrypted_attachment при каждом startup/restart проверяет оба дерева:
|
||||
stale temp удалены; открытый/закрытый активные owners, конечные файлы и чужие имена сохранены.
|
||||
- Профильный Linux-набор: 21/21 PASS, включая video/no_ffmpeg, DM, services и media delivery.
|
||||
Основной make check: 185/185 PASS. TUN в отдельном network namespace с тем же бинарником:
|
||||
proxy9/9, burst1/1, load8/8 PASS; 134217728 проверенных байт.
|
||||
- ASAN/UBSan/LSan: 21/21 PASS с detect_leaks=1. Все встроенные зависимости собраны
|
||||
с sanitizer-флагами в /tmp/utun-media-fix-sanitizers; обычные объекты проекта не смешивались.
|
||||
Windows/FreeBSD build/runtime не проверялись.
|
||||
|
||||
Логи: /tmp/media-fix-profile-results.log, /tmp/media-fix-full-check.log,
|
||||
/tmp/media-fix-network-check.log, /tmp/media-fix-sanitizer-results.log,
|
||||
/tmp/media-fix-cancel-after-rename.log, /tmp/media-fix-encrypted-file.log.
|
||||
Проверка реализации не выявила противоречий владения в перечисленных сценариях.
|
||||
Частичные отметки остальных файлов аудита сохраняются; этот фикс не означает полного
|
||||
завершения проверки media_async/media_delivery или закрытия старых нестабильных тестов.
|
||||
|
||||
Продолжение аудита — 2026-10-24: граница длительности voice_file
|
||||
|
||||
voice_file.h обещает максимум24ч; attachment_validate (attachment.c:35) отвергает
|
||||
duration_ms >86400000. Однако voice_file.c:16 проверяет samples/channels/rate >86400:
|
||||
целочисленное деление отбрасывает долю секунды. Записи с полными кадрами на24ч+20..980мс
|
||||
проходят проверку аргументов. Затем voice_file.c:37 вычисляет точную длительность
|
||||
в миллисекундах по числу20мс кадров, уже выходящую за предел метаданных.
|
||||
attachment_send.c вызывает voice_file_encode в worker, а attachment_validate —
|
||||
при публикации результата. При успешном полном кодировании такой результат будет
|
||||
отклонён после подготовки файла. Это дефект границы записей длиннее суток;
|
||||
сбой обычных коротких записей или потеря данных этим опытом не установлены.
|
||||
|
||||
Диагностическое воспроизведение на настоящем voice_file_encode из src/libutun.a:
|
||||
- /tmp/media-voice-duration-repro.c и .mk; сборка через tests/Makefile и его библиотеки/флаги.
|
||||
- Linux64: виртуальный read-only PCM достаточного размера, без чтения или заполнения.
|
||||
Linker wrapper opus_codec_encoder_create фиксирует достижение кодировщика
|
||||
и возвращает NULL до чтения PCM/открытия output. Реальная проверка аргументов не подменена.
|
||||
- Частоты8000/12000/16000/24000/48000, mono/stereo; для каждой пары проверены
|
||||
24ч-20мс, ровно24ч, +19мс хвоста, +20мс, +980мс и +1000мс: всего60 случаев.
|
||||
- Ровно24ч и +19мс хвоста проходят, так как хвост короче полного кадра отбрасывается;
|
||||
+1000мс отвергается до encoder. На +20/+980мс во всех10 сочетаниях codec достигнут,
|
||||
тогда как attachment_validate соответствующей длительности возвращает-1:
|
||||
duration_guard_mismatches=20, confirmed=1.
|
||||
- Все60 вызовов возвращают-1: для допустивших encoder это намеренная инъекция отказа,
|
||||
для +1000мс — штатная валидация. Опыт доказывает ошибку допуска к encoder,
|
||||
но не является24-часовым кодированием или сквозной проверкой отправки такого файла.
|
||||
- Полный журнал с media=info/debug=info: /tmp/media-voice-duration-repro.log;
|
||||
сборка: /tmp/media-voice-duration-build.log. Проектные .c/.h не менялись.
|
||||
|
||||
Проект простого исправления:
|
||||
1. Задать один ATTACHMENT_DURATION_MAX_MS=86400000 в attachment.h; использовать
|
||||
в валидаторе voice/video и проверке video completion вместо отдельных литералов.
|
||||
2. После проверки rate/channels считать frame_samples=(rate/50)*channels,
|
||||
frames=count/frame_samples. До создания encoder/открытия файла отклонять
|
||||
frames<25 либо frames>ATTACHMENT_DURATION_MAX_MS/20 (4320000).
|
||||
Проверяется длительность записываемых полных кадров, хвост <20мс по-прежнему отбрасывается.
|
||||
3. Только после ограничения frames считать samples=frames*frame_samples
|
||||
и duration_ms=frames*20. Размер samples не превосходит count, длительность
|
||||
помещается в uint32_t; большие count не требуют потенциально переполняющего умножения на20.
|
||||
4. Сообщение раннего отказа различает неверные параметры и недопустимую длительность;
|
||||
для длительности выводит count/rate/channels/frames и разрешённые границы.
|
||||
Дополнительный helper/API или изменение pipeline/формата не нужны.
|
||||
5. В test_attachment добавить проверку границ500мс/24ч, хвоста <20мс,
|
||||
превышения на полный кадр, разных rate/channels и раннего отказа без изменения файла/метаданных.
|
||||
Верхние границы проверять диагностической точкой до encoder без реального кодирования суток;
|
||||
сохранить существующий настоящий Opus roundtrip короткой записи.
|
||||
|
||||
На этапе подтверждения аудит был остановлен по AGENTS.md:10, без изменения рабочего кода.
|
||||
Пользователь согласовал исправление; результаты приведены ниже.
|
||||
|
||||
Результаты исправления voice_file — 2026-10-24
|
||||
|
||||
- ATTACHMENT_DURATION_MAX_MS в attachment.h задаёт один предел86400000мс:
|
||||
его используют валидатор voice/video, video completion и кодировщик голоса.
|
||||
- voice_file считает frames=count/frame и проверяет25..4320000 до создания Opus
|
||||
и открытия выходного файла. Метаданные при раннем отказе не изменяются.
|
||||
Хвост <20мс сохраняет прежнее поведение: отбрасывается, включая хвост после ровно24ч.
|
||||
- Проверка архитектуры: frame всегда положителен и не больше1920 для разрешённых rate/channels;
|
||||
деление count/frame не переполняется. После ограничения frames произведение frames*frame
|
||||
не превосходит исходного count; frames*20 не превосходит86400000 и помещается в uint32_t.
|
||||
Подготовка, публикация, владение PCM/файлом и формат контейнера не меняются.
|
||||
- Лог раннего отказа содержит count/rate/channels/frames и разрешённые границы;
|
||||
debug принятой записи показывает число кадров, длительность и отброшенный хвост.
|
||||
- test_attachment: 120 граничных вызовов для пяти частот и mono/stereo.
|
||||
Все70 недопустимых длительностей отвергнуты до encoder; все50 допустимых
|
||||
дошли до диагностического encoder с инъекцией отказа. Во всех120 случаях
|
||||
побайтно сохранены исходные metadata и содержимое существующего output.
|
||||
Linux64 использует виртуальный read-only PCM; суточное кодирование не выполняется.
|
||||
- Настоящий Opus encode/decode: прежняя1с запись и десять записей на500мс с хвостом
|
||||
почти20мс, все rate/channels, все три preset. Проверены header, декодирование,
|
||||
число20мс кадров, EOF и валидность метаданных результата.
|
||||
- validate/encode/decode voice/video принимают86400000мс и отвергают86400001мс.
|
||||
- ./build.sh --no-gui -j4 — PASS. В изменённых C-файлах/тесте предупреждений сборки нет;
|
||||
общие прежние предупреждения остальных модулей остаются в полном логе.
|
||||
- Профильные тесты10/10 PASS: attachment, attachment_send, encrypted_file,
|
||||
encrypted_attachment, media_async, dm_media, dm_worker, services, video, video_no_ffmpeg.
|
||||
Первый запуск attachment внутри sandbox выдал запреты создания stream fd;
|
||||
весь профильный набор, включая attachment, успешно повторён вне sandbox.
|
||||
|
||||
Логи: /tmp/media-voice-fix-build.log, /tmp/media-voice-fix-profile-results.log,
|
||||
/tmp/media-voice-fix-profile-logs/test_attachment.log и остальные логи этого каталога.
|
||||
Новый полный make check и ASAN/UBSan/LSan не запускались; прежние результаты encrypted_file
|
||||
выше не относятся к этому изменению. Windows/FreeBSD не проверялись.
|
||||
voice_file.h и attachment.h сохраняют [~]: завершение этого фикса не закрывает
|
||||
оставшуюся сверку контрактов всего аудита. Повторная проверка media_delivery ещё не завершена.
|
||||
|
||||
Продолжение аудита — 2026-10-24: переанонс записей SQLite
|
||||
|
||||
Полностью прочитаны media_index.c/h, media_download.c/h, media_delivery.c/h,
|
||||
file_transfer.c/h, media_delivery_proto.h и sync.md. Повторно сверены media_crypto.c/h.
|
||||
В media_crypto.h уточнены native байты nonce, границы CCM/буферов, пустой поток,
|
||||
момент выдачи размеров/хеша и отсутствие close/fsync/rename; неверный аргумент не обнуляет выходы.
|
||||
В file_transfer.h уточнены lookup/авторизация, 16 операций, размер1..INT64_MAX,
|
||||
усечение своего временного назначения даже при отказе запуска, таймаут и отмена/владение callback.
|
||||
Изменения только описаний сохранены коммитом5dbca4a2.
|
||||
|
||||
Подтверждённый дефект
|
||||
|
||||
media_delivery_announce_local_blocks (media_delivery.c:1610) читает media_id/block_id
|
||||
через sqlite3_column_blob, но проверяет только ненулевой указатель. Затем
|
||||
media_delivery_send_have_block копирует по16 байт в пакет и подписывает объявление.
|
||||
media_files имеет BLOB NOT NULL без CHECK длины: такие строки допустимы текущей схемой.
|
||||
Обычные media_index writers пишут правильные длины; опыт моделирует повреждённые
|
||||
локальные данные, а не доказанный путь внесения повреждения удалённым участником.
|
||||
|
||||
Настоящие SQLite, схема, обработчик, выбор группы/суперузла и Ed25519; перехвачена
|
||||
только транспортная отправка в существующем tests/media_delivery_fixture.h:
|
||||
- Контрольные UUID16/16, chunk0, chat_id111: один корректный HAVE_BLOCK.
|
||||
- media_id длиной1 байт: один HAVE_BLOCK с16-байтным media_id, из них15 байт вне BLOB.
|
||||
В журнале вместо ожидаемого единственного7a видны посторонние ненулевые байты памяти.
|
||||
- block_id длиной1 байт: аналогичное чтение15 лишних байт в сетевой block_id.
|
||||
- media_id или block_id длиной17 байт: отправка с молчаливым усечением до16.
|
||||
- chunk=-1: wire_chunk=4294967295; chunk=4294967296: wire_chunk=0.
|
||||
Причина — sqlite3_column_int с последующим приведением к uint32_t без диапазона.
|
||||
- chat_id111junk: strtoull без endptr принимает префикс111 и направляет объявление в группу111.
|
||||
- Итого8 случаев,7 повреждённых строк из7 отправлены; invalid_records_sent=7, confirmed=1.
|
||||
Ошибки соответствующих полей в модуле не логируются. Пакеты сохранены только стендом,
|
||||
реальные удалённые узлы в опыте не участвовали. Ошибка ASAN или падение не воспроизводились.
|
||||
|
||||
Артефакты: /tmp/media-announcement-audit.c, /tmp/media-announcement-audit.mk,
|
||||
/tmp/media-announcement-audit-build.log, /tmp/media-announcement-audit.log.
|
||||
Сборка через tests/Makefile с настоящими src/libutun.a/libuasync.a и штатными флагами;
|
||||
проектные .c для диагностики не менялись. Очистка собственного стенда исправлена и повторно проверена.
|
||||
Отдельно по коду видно: результаты bind/завершающего step/finalize игнорируются;
|
||||
SQL-ошибку окончания обхода нельзя выдавать как обычное «done». Инъекция SQL-ошибок здесь не выполнялась.
|
||||
|
||||
Проект простого исправления
|
||||
|
||||
1. Проверять строку на границе чтения SQLite до доступа как к фиксированным структурам:
|
||||
media_id/block_id имеют тип BLOB и ровно16 байт; проверить полученные указатели.
|
||||
Скопировать прошедшие проверку UUID в локальные массивы до вложенных SQL/отправок.
|
||||
2. chunk читать как int64: тип INTEGER, 0..UINT32_MAX, только затем cast.
|
||||
chat_id — TEXT, длина1..20 без embedded NUL, каноническое ненулевое uint64 decimal.
|
||||
Полный разбор должен отвергать суффиксы, знаки, пробелы, leading zero и переполнение;
|
||||
строка канала должна сохранять свой смысл при переводе в router group_id.
|
||||
3. Плохую строку пропускать с WARN, указав rowid/типы/длины/некорректное поле,
|
||||
без чтения за BLOB и без анонса. Корректные соседние строки продолжать обрабатывать.
|
||||
Никакого общего выделяемого снимка или отката уже отправленных корректных анонсов не требуется.
|
||||
4. Проверять bind, terminal step и finalize; SQL-ошибки логировать с фазой/кодами
|
||||
и количеством обработанных строк. Не превращать их в обычный успешный финальный лог.
|
||||
Сохранить существующий void API, поштучные анонсы и wire-формат.
|
||||
5. Регрессии в штатной media-цели: valid row, длины UUID0/1/15/16/17 и неверные SQL-типы,
|
||||
chunk=-1/0/UINT32_MAX/UINT32_MAX+1, неверные/граничные chat_id, embedded NUL;
|
||||
смесь корректных/плохих записей и ошибки SQL. Перехват HAVE_BLOCK обязан доказать,
|
||||
что наружу ушли только исходные16 байт проверенных UUID и правильный номер группы/блока.
|
||||
|
||||
Аудит остановлен по AGENTS.md:10 после подтверждения дефекта и подготовки проекта исправления.
|
||||
Новые полные build/check/sanitizer-прогоны не запускались: изменения в репозитории только документальные.
|
||||
media_crypto.h и file_transfer.h можно отметить [V]; media_delivery.h/proto.h остаются [~]:
|
||||
нужно исправление этого пути и завершение сверки описаний. Остальные прежние отметки не закрывают
|
||||
весь аудит. Остались контракты attachment/attachment_send/voice, связанные encrypted/lifecycle документы
|
||||
и устаревший _desc.txt (RTT-подбор, direct-only репликация и её интервалы расходятся с текущим кодом).
|
||||
|
||||
Результаты исправления SQLite-переанонса — 2026-10-24
|
||||
|
||||
- Проверка остаётся внутри существующего обхода media_files. UUID допускаются только
|
||||
как BLOB ровно16 байт; chunk — INTEGER в0..UINT32_MAX, читается через int64.
|
||||
Исходные SQL-типы получены до преобразований. Проверенные UUID копируются
|
||||
в локальные16-байтные массивы до вложенных запросов/отправок.
|
||||
- chat_id имеет тип TEXT, длину1..20; разбор по объявленной длине отвергает embedded NUL,
|
||||
знаки/пробелы/суффиксы, ноль/leading zero и переполнение. Допускается весь ненулевой uint64,
|
||||
включая UINT64_MAX. Условие gid<=(UINT64_MAX-digit)/10 до операции исключает переполнение.
|
||||
- Непроверенные фиксированные memcpy и сужающие cast устранены. Некорректная строка
|
||||
пропускается с WARN: id/поле, типы/длины, исходный chunk. Хорошие соседние строки продолжают обрабатываться.
|
||||
- prepare/bind/завершающий step/finalize проверяются. Отказ SQL логирует фазу/коды
|
||||
и неполный обход; обычный complete-лог не выдаётся. Счётчики rows/eligible/skipped
|
||||
описывают обработку, а не гарантированную доставку. Уже корректно отправленные пакеты не откатываются.
|
||||
- Архитектура сохраняет существующие void API, wire-формат, источник ролей и единичные анонсы;
|
||||
нет общего снимка/новых ресурсов, транзакций или состояния. Statement закрывается и при ошибке bind/step.
|
||||
- test_media_delivery_sql: 36 случаев полей,32 недопустимых строки не отправляются,
|
||||
четыре допустимые сохраняют точные UUID/group/chunk/timestamp. Проверены границы
|
||||
UINT32_MAX/UINT64_MAX, все заявленные длины UUID и неверные SQL-типы, embedded NUL.
|
||||
В смеси valid/invalid/valid отправлены только два корректных анонса.
|
||||
- Linux: пять внедрений prepare/bind/first-step/step после первой строки/finalize.
|
||||
До отказа сохранены0/0/0/1/2 корректных отправки, statement не остаётся активным;
|
||||
проверяется ERROR с фазой и отсутствие ложного complete. После каждого отказа
|
||||
обычный повтор успешно отправляет обе строки с исходными UUID и номерами блоков.
|
||||
После захвата файлового лога тест явно восстанавливает console_enabled.
|
||||
- ./build.sh --no-gui -j4 PASS; в изменённых C-файлах предупреждений нет.
|
||||
Профиль12/12 PASS: delivery_sql, query, blocks, replication, index/index_regressions,
|
||||
delivery_download, download_timeout, delivery_integration/full/chat, services.
|
||||
После изменения только вывода диагностики SQL-тест повторно прошёл отдельно.
|
||||
- ASAN/UBSan/LSan4/4 PASS: delivery_sql/query/blocks/replication, detect_leaks=1.
|
||||
Свежая копия /tmp/utun-media-announcement-sanitizers содержит текущий код;
|
||||
CPPFLAGS=-fsanitize=address,undefined применён и к встроенным зависимостям.
|
||||
Обычные объекты проекта не смешивались с sanitizer-объектами.
|
||||
Первый configure внутри sandbox отклонён LeakSanitizer из-за ptrace;
|
||||
сборка и тесты успешно выполнены вне sandbox.
|
||||
|
||||
Логи: /tmp/media-announcement-fix-build.log, /tmp/media-announcement-fix-profile-results.log,
|
||||
/tmp/media-announcement-fix-sql-final.log, /tmp/media-announcement-sanitizer-build.log,
|
||||
/tmp/media-announcement-sanitizer-tests-build.log, /tmp/media-announcement-sanitizer-results.log.
|
||||
Полный make check и Windows/FreeBSD не запускались. Завершение фикса не означает завершения
|
||||
сверки оставшихся заголовков и документов двух каталогов.
|
||||
|
||||
Продолжение сверки описаний — 2026-10-24: MIME контейнера
|
||||
|
||||
Сверены и уточнены attachment.h, attachment_send.h, voice_file.h, media_delivery_proto.h
|
||||
и комментарий неиспользуемого HAVE_BLOCK timeout в media_delivery.h:
|
||||
- attachment: UTF-8 и только ASCII C0/DEL, границы полей по kind, состояние выходов
|
||||
при ошибке encode/decode, content_type не является валидатором kind.
|
||||
- voice_file: собственное перезаписываемое назначение, поздняя ошибка close может
|
||||
изменить info; native контейнер и отсутствие fsync/атомарной публикации.
|
||||
- attachment_send: владение начинается при вызове trampoline, при отказе самого post
|
||||
остаётся у caller; PCM/compressor, контекст освобождения, DM generation, канальные
|
||||
request_id/reply, внутренний UUID подготовки и события подготовки вместо обещания доставки.
|
||||
- proto: вне SYNC числа native-endian; SERVE_REG/LEAVE только вспомогательный учёт,
|
||||
загрузчик их не посылает. SERVE_ACK не отправляется; приём SERVE_ACK/HAVE_BLOCK_ACK игнорируется,
|
||||
ожидания ACK/его таймера нет. Не менялись команды, wire или runtime-поведение.
|
||||
Коммиты описаний:08692fa4 и4e4c60ee. Из общего service_lifecycle повторно сверена
|
||||
только media-секция111..196 по текущему порядку start/quiesce/destroy и прочитанным модулям;
|
||||
за пределами media документ не считается полностью проверенным.
|
||||
|
||||
Подтверждённый дефект MIME
|
||||
|
||||
encrypted_file_write проверяет только ненулевой type и strlen(type)<32, затем шифрует строку.
|
||||
encrypted_file_read требует непустую NUL-терминированную строку в32-байтном поле,
|
||||
ASCII33..126 без '"' и '\\'. Поэтому write может успешно подготовить непригодный для read контейнер.
|
||||
chat_attachment_send (chat_attachment.c:198..225) имеет такую же слабую входную проверку:
|
||||
до сохранения ключа/старта worker проверяется лишь наличие указателя и длина.
|
||||
|
||||
Реальный roundtrip, без подмены криптографии или файловых функций:
|
||||
- Контроль application/octet-stream: write=0/read=0, hash расшифрованного temp равен исходному.
|
||||
- Пустой MIME, перевод строки, двойная кавычка, обратная косая черта и не-ASCII UTF-8:
|
||||
все пять write=0, после publish и вычисления точного внешнего hash соответствующий read=-1.
|
||||
Ключ, author, UUID и контейнер при чтении те же. Ошибка — invalid metadata/type,
|
||||
а не аутентификация или порча контейнера в опыте.
|
||||
- При ошибке read прежние info/type сохранены, temp очищен; SHA256 исходника неизменен во всех6 случаях.
|
||||
Это подтверждённый дефект принятия аргумента в API. Сквозная отправка/открытие карточки GUI не моделировалась.
|
||||
- /tmp/media-content-type-audit.c и .mk, сборка через tests/Makefile и настоящие библиотеки;
|
||||
/tmp/media-content-type-audit-build.log, /tmp/media-content-type-audit.log.
|
||||
Артефакты: /tmp/media-content-type-audit-FdtMC3/. Лог содержит MIME как hex, без управляющих символов в тексте.
|
||||
|
||||
Проект простого исправления
|
||||
|
||||
1. Экспортировать encrypted_file_validate_type в encrypted_file.h: 0/-1, bounded strnlen(type,32),
|
||||
непустая строка длиной1..31, ASCII33..126 кроме двойной кавычки и обратной косой черты.
|
||||
Это ограничение существующего контейнера, не новый полноценный MIME parser и не правило для всех сообщений чата.
|
||||
Диагностика показывает длину/позицию/hex ошибочного байта, не выводит сырую непроверенную строку.
|
||||
2. Вызывать один валидатор в chat_attachment_send до выделения задачи/генерации ключа/записи в БД,
|
||||
в encrypted_file_write до открытия source/temp и в read для расшифрованного32-байтного поля.
|
||||
Существующие проверки длины расшифрованного блока сохраняются; правило MIME не дублируется в трёх местах.
|
||||
3. Не менять формат, nonce, crypto, DB state machine или файловое владение. Reader остаётся строгим;
|
||||
writer не должен выдавать успех для метаданных, которые этот же reader не принимает.
|
||||
4. Регрессии: NULL/пустая строка/31/32 байта, каждый запрещённый символ, ASCII-границы
|
||||
и валидные значения; writer на раннем отказе сохраняет source/target/output.
|
||||
Для reader плохие metadata шифруются тестом с известным ключом и пересчитанным внешним hash:
|
||||
отказ не теряет прежние info/type и не оставляет temp. Проверить send-отказ без строки ключа/worker/файлов.
|
||||
|
||||
На этапе воспроизведения аудит был остановлен по AGENTS.md:10. Затем пользователь разрешил продолжать исправления.
|
||||
|
||||
Реализация и проверка MIME-фикса — 2026-10-24
|
||||
|
||||
- encrypted_file_validate_type использует bounded strnlen(type,32) и одно правило для send/write/read.
|
||||
NULL, пустая/нетерминированная строка и недопустимый байт отклоняются с WARN: длина/позиция/hex.
|
||||
Успешная проверка имеет DEBUG-лог; сырые непроверенные строки в лог не попадают.
|
||||
- send проверяет type до выделения задачи, генерации/сохранения ключа и старта worker.
|
||||
write отказывает до файловых операций и изменения output. read проверяет уже аутентифицированное
|
||||
поле после проверки длины metadata и сохраняет атомарное присваивание info/type только при успехе.
|
||||
Валидатор не выполняет I/O и разрешён в uasync; файловые write/read остаются worker-only.
|
||||
- Правило сохраняет действующий набор reader, включая короткое значение "a"; это не полный MIME parser.
|
||||
Не добавлены новый wire, изменение nonce/crypto, состояние БД или второй владелец файлов.
|
||||
Код:90964e7a/f4cc6920, регрессии:aace975a.
|
||||
- test_encrypted_file: все256 значений первого байта,92 допустимых; NULL отдельно.
|
||||
Восемь ранних отказов сохраняют output побайтно, source/cipher/target по SHA256.
|
||||
Три настоящих roundtrip проверяют длины1/24/31, metadata, SHA256 plaintext и сохранность target до publish.
|
||||
Семь плохих wire-полей зашифрованы настоящим CCM с пересчитанным внешним SHA256:
|
||||
read отказывает, сохраняя info/type/старый target/source; temp и fd не остаются.
|
||||
- test_attachment_send: восемь отказов до ключей/worker, число файлов и исходный байт сохранены.
|
||||
Сохранён прежний сценарий реального encrypted worker, отмены после публикации и recovery.
|
||||
- ./build.sh --no-gui -j4 PASS; в изменённых C-файлах предупреждений нет.
|
||||
Профиль10/10 PASS: encrypted_file, attachment_send, encrypted_attachment, attachment, media_async,
|
||||
services, media_delivery_chat/full, dm_media, dm_worker.
|
||||
- ASAN/UBSan/LSan4/4 PASS: encrypted_file, attachment_send, encrypted_attachment, services.
|
||||
detect_leaks=1, halt_on_error=1; встроенные зависимости также инструментированы через CPPFLAGS.
|
||||
Отдельная копия /tmp/utun-media-mime-sanitizers исключает смешение обычных/sanitizer-объектов.
|
||||
Первый build остановился из-за нехватки /tmp; очищены воспроизводимые объекты прежних копий,
|
||||
затем повторная сборка и весь прогон завершились успешно. Исходники и журналы сохранены.
|
||||
|
||||
Логи: /tmp/media-mime-fix-build.log, /tmp/media-mime-fix-profile-results.log,
|
||||
/tmp/media-mime-fix-profile-logs/, /tmp/media-mime-sanitizer-build-retry.log,
|
||||
/tmp/media-mime-sanitizer-tests-build.log, /tmp/media-mime-sanitizer-results.log,
|
||||
/tmp/media-mime-sanitizer-test_*.log. Полный check и Windows/FreeBSD для MIME-фикса не запускались.
|
||||
encrypted_file.h теперь согласован с реализацией. Последующая сверка общих описаний завершена ниже.
|
||||
|
||||
Завершение аудита media_async/media_delivery — 2026-10-24
|
||||
|
||||
- Все11 публичных заголовков двух каталогов сверены по полным функциям и структурам реализаций:
|
||||
media_async/crypto, attachment/send, encrypted_file, voice_file, media_index/download/delivery/proto,
|
||||
file_transfer. Уточнены назначения, входы/выходы, владение и потоки, ошибки и wire-ограничения.
|
||||
- _desc.txt заменён актуальным описанием подготовки, поиска/загрузки, relay, каталога и репликации.
|
||||
Убраны прежние предположения о RTT-выборе, только прямых соединениях, часовых повторах и ACK-окне.
|
||||
Отражены фактические QUERY/CM/transfer UUID, пределы выдач, per-group RX-курсор и атомарный приём страницы.
|
||||
sync.md уточняет отсутствие ожидания HAVE ACK и проверки SQLite при локальном переанонсе.
|
||||
- encrypted_attachments.md целиком сопоставлен с chat_attachment.c и источниками desktop/Android UI:
|
||||
inputbar, MessageList/attachment_key_ui, JNI snapshot, ChatRepository, MessageBubble/Reply/ChannelList.
|
||||
Уточнены MIME, видимость desktop-кнопки при наличии pending requests и preview цитаты после расшифровки.
|
||||
Контейнер не выдаётся UI как готовый файл; путь/метаданные заменяются после готовности plaintext.
|
||||
Это сверка исходников, без нового интерактивного GUI/Android runtime-прогона.
|
||||
- media-раздел service_lifecycle.md сверён по текущему start/quiesce/join/done/destroy и владельцам
|
||||
worker/temp/CM/relay. Дописано, что ранний MIME-валидатор допустим в uasync и не выполняет I/O.
|
||||
Общий документ сохраняет частичную отметку: голос, звонки и другие немедийные владельцы вне этой проверки.
|
||||
- Архитектура исправлений остаётся простой: формат/валидация контейнера принадлежат encrypted_file,
|
||||
temp/durability — file_io, публикация и БД/UI — задаче chat_attachment; один владелец на ресурс.
|
||||
Media_index регистрирует подготовленное, delivery проверяет передачу и ведёт каталог своей группы.
|
||||
Синхронные приём/проверка/сборка delivery в uasync и отсутствие общей транзакции ФС/SQLite
|
||||
явно описаны как действующие границы. File_transfer оставляет ключи/авторизацию/проверку caller.
|
||||
- Все четыре воспроизведённых дефекта исправлены и имеют регрессии. Неразобранных подтверждённых
|
||||
дефектов в рамках этого аудита не осталось. Последние изменения после MIME-прогона — только описания;
|
||||
git diff --check проходит. Для MIME действуют профиль10/10 и ASAN/UBSan/LSan4/4 выше;
|
||||
прежние полные185/186 и sanitizer21/21 не приписываются последующим исправлениям.
|
||||
|
||||
Коммиты общей сверки:91fed902 (delivery),8d175049 (encrypted/UI/lifecycle).
|
||||
Чек-лист: все media-заголовки и encrypted_attachments.md — [V], общий service_lifecycle.md — [~].
|
||||
Windows/FreeBSD, новый полный check и интерактивные GUI/Android проверки не выполнялись.
|
||||
+41
-12
@@ -25,7 +25,8 @@
|
||||
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
|
||||
|
||||
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
|
||||
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
|
||||
enabled проверяется на каждом серверном запросе; клиент доступа к другим storage остаётся доступен.
|
||||
Лимит одинаков для каждого UID отдельно,
|
||||
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
|
||||
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
|
||||
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
|
||||
@@ -86,7 +87,7 @@ Wire, версия 4
|
||||
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
|
||||
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
|
||||
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
|
||||
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
|
||||
компоненты с ведущей '.', завершающей точкой/пробелом, ASCII C0/DEL,
|
||||
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
|
||||
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
|
||||
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
|
||||
@@ -136,6 +137,7 @@ Windows удерживает родительские каталоги без SH
|
||||
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
|
||||
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
|
||||
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
|
||||
Непустой каталог возвращает DIRECTORY_NOT_EMPTY.
|
||||
|
||||
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
|
||||
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
|
||||
@@ -164,13 +166,22 @@ Windows удерживает родительские каталоги без SH
|
||||
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
|
||||
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
|
||||
|
||||
Статусы (0..13): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
|
||||
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT.
|
||||
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
|
||||
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
|
||||
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
|
||||
DIRECTORY_NOT_EMPTY.
|
||||
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ до истечения 120 секунд от начала обработки
|
||||
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
|
||||
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
|
||||
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
|
||||
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
|
||||
мутация возвращает BUSY до истечения старых. В таком ответе cursor содержит
|
||||
остаток до ближайшего освобождения квитанции в timebase 0.1мс (1..1200000).
|
||||
Нулевой cursor означает BUSY другой причины с обычной задержкой 5 секунд.
|
||||
Клиент COMMIT/MKDIR/RENAME/REMOVE сохраняет pending и прежний request_id/параметры,
|
||||
ставит отменяемый таймер на этот срок и повторяет команду; промежуточный BUSY не завершает callback.
|
||||
При потере ответа обычный повтор идёт через 5 секунд. Общий срок от создания
|
||||
запроса остаётся 120 секунд и не продлевается даже при BUSY; pump не отправляет
|
||||
команду после этого срока. Поэтому повторы остаются внутри серверного окна
|
||||
квитанции, не требуя её досрочного вытеснения. Остальные команды возвращают BUSY caller.
|
||||
Повторы после этого срока
|
||||
не дают гарантии того же результата; перед новой мутацией требуется STAT.
|
||||
|
||||
Потоки, таймауты и остановка
|
||||
@@ -222,6 +233,8 @@ quiesce запрещает новые команды, отменяет тайм
|
||||
вне callbacks дожидается только своих работ/completions и закрывает дисковый
|
||||
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
|
||||
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
|
||||
Немедленная отмена после start снимает starting: поздний CM UP не запускает STAT/подготовку заново.
|
||||
После перехода к финализации публикации повторный cancel не меняет уже выбранный результат.
|
||||
|
||||
Desktop
|
||||
-------
|
||||
@@ -260,7 +273,14 @@ Stop сервиса/группы завершает все принадлежа
|
||||
|
||||
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
|
||||
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
|
||||
Один локальный файл назначения не может одновременно принадлежать двум download.
|
||||
Два download с одной строкой local одновременно не допускаются; физические aliases
|
||||
и подмену локальных путей исключает вызывающий. Upload читает стабильный regular source;
|
||||
изменение снимка возвращает STALE. На POSIX O_NONBLOCK позволяет отвергнуть FIFO
|
||||
до блокирующего open/read единственного дискового worker.
|
||||
Download создаёт соседний temp эксклюзивно и получает право cleanup только после успеха open.
|
||||
Отказ из-за существующего temp, max_bytes, STALE или ранняя отмена не удаляют чужой путь.
|
||||
После проверки SHA256 выполняются sync/close и заменяющий rename; до публикации прежний local сохраняется.
|
||||
POSIX-публикация локального download не делает fsync родительского каталога, в отличие от серверного COMMIT.
|
||||
|
||||
Аватарки пользователей
|
||||
---------------------
|
||||
@@ -348,18 +368,27 @@ tests/test_member_files: wire/UTF-8, произвольные блоки, пов
|
||||
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
|
||||
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
|
||||
fd idle и принадлежность work/done потокам.
|
||||
На POSIX отдельный процесс с границей 2с проверяет неблокирующее открытие FIFO
|
||||
и последующий отказ проверки типа; fork выполняется до создания pthread.
|
||||
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
|
||||
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
|
||||
проверка, отмена одного handle, restart router и destroy с активным server I/O.
|
||||
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
|
||||
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
|
||||
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
|
||||
Проверены сохранность чужого temp/прежнего назначения при EXISTS, max_bytes и немедленном cancel,
|
||||
поздний CM UP после отмены, 76 созданий и 76 удалений с исчерпанием квитанций.
|
||||
Один успешный ответ отброшен перед клиентским handler: повтор с прежним ID возвращает квитанцию,
|
||||
счётчики остаются точными (76 каталогов после создания, 0 после удаления).
|
||||
test_services — stop/start общего чат-сервиса.
|
||||
Текущая диагностика выделена в категорию debug. Настройка для полного трасса:
|
||||
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
|
||||
Настройка для полного трасса этих модулей:
|
||||
[debug]
|
||||
debug=trace
|
||||
debug=debug показывает запрос/ответ, ACL, очереди, маршрут, ACK, crypto-решения,
|
||||
I/O, fd idle и состояние браузера. trace дополнительно показывает каждый
|
||||
media=trace
|
||||
etcp_route=trace
|
||||
media=debug показывает запрос/ответ, ACL, очереди файлового сервиса, I/O и fd idle.
|
||||
etcp_route=debug добавляет выбор маршрута, состояния канала и ретрансмиты.
|
||||
Уровень trace дополнительно показывает каждый
|
||||
read/write и передачу задач/завершений между дисковым потоком и uasync.
|
||||
stream progress раз в секунду показывает ID/направление, done/total, число блоков
|
||||
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
Аудит C-модуля member_files — 2026-10-24
|
||||
|
||||
Статус: полностью прочитаны шесть реализаций и шесть заголовков src/member_files/.
|
||||
Заголовки сверены; четыре подтверждённых дефекта исправлены и проверены.
|
||||
Серверная/клиентская части doc/member_files.txt согласованы с кодом. Общий документ
|
||||
остаётся [~]: его desktop/AvatarManager/Android-подсистемы целиком в этот аудит не входили.
|
||||
Ниже результаты относятся к C-сервису, а не к полному аудиту всех файлов GUI/аватарок.
|
||||
|
||||
1. Удаление чужого локального temp
|
||||
|
||||
До исправления TRANSFER_FINISH безусловно удалял вычисленный путь temp при ошибке,
|
||||
включая отказ эксклюзивного open из-за существующего файла и отказ до открытия.
|
||||
Настоящий test_member_files_network: preexisting target/temp по37 проверенных байт,
|
||||
скачивание с подписанного storage, MF_EXISTS; target=1, foreign_temp=0, FAIL phase23.
|
||||
Корректное воспроизведение: /tmp/member-files-temp-repro5.log, строки391..395.
|
||||
|
||||
Исправление: один temp_owned у handle, устанавливается только после успешного
|
||||
эксклюзивного создания. Finish закрывает fd, публикует/удаляет только свой temp.
|
||||
После успешного rename владение именем temp снято; source/target не подменяются caller.
|
||||
Новый FILE-слой или второй файловый владелец не добавлены: disk API работает с native fd.
|
||||
Обычный и sanitizer-прогоны: EXISTS, max_bytes, немедленная отмена сохраняют
|
||||
прежние37 байт target и чужого temp, finished ровно один раз.
|
||||
|
||||
2. Ранняя отмена возобновляла передачу и приводила к UAF
|
||||
|
||||
После cancel сразу вслед за start поздний CM UP вызывал wake. Starting оставался1,
|
||||
поэтому pump после начала cleanup снова посылал STAT. Finish освобождал handle,
|
||||
а ответ STAT вызывал transfer_response с уже освобождённым arg.
|
||||
Лог: /tmp/member-files-temp-fixed.log; GDB: /tmp/member-files-cancel-gdb.log,
|
||||
SIGSEGV в transfer_response → mf_pending_finish → mf_dispatch.
|
||||
|
||||
Переход stopping теперь снимает starting; wake без готового финального ответа
|
||||
ничего не планирует. Обязательная cleanup-задача остаётся единственным продолжением,
|
||||
затем отправляется финальный CANCEL/END и освобождается handle до callback.
|
||||
Не добавлены новые ссылки/таймеры или зависимость от скорости ответа CM.
|
||||
Регрессия early cancel прошла вместе с обычной отменой, параллельными потоками,
|
||||
router restart и destroy при активном server I/O.
|
||||
|
||||
3. Заполнение64 квитанций обрывало массовые мутации
|
||||
|
||||
Прежнее воспроизведение в doc/tasks.md: из76 удалений выполнены64, затем MF_BUSY.
|
||||
Серверный предел/окно нужны для идемпотентности и остаются: до64 успешных
|
||||
COMMIT/MKDIR/RENAME/REMOVE, MF_REPLAY_TB=1200000 (120с) от начала обработки.
|
||||
Квитанции не вытесняются досрочно. Полный кеш возвращает BUSY с cursor=остаток
|
||||
до ближайшего освобождения в timebase0.1мс; это значение кодируется обычным LE-полем.
|
||||
Нулевой/недопустимый hint использует обычную задержку5с; недопустимый логируется.
|
||||
|
||||
Клиент этих четырёх команд сохраняет тот же pending/ID/параметры и ожидает
|
||||
отменяемый retry timer. Остальные BUSY возвращаются callback как раньше.
|
||||
Общий срок запроса остаётся120с от создания; ни BUSY, ни потеря ответа его не продлевают.
|
||||
Перед повторной отправкой pump также проверяет срок. Сервер начинает обработку
|
||||
не раньше создания клиентского запроса, поэтому допустимые повторы не выходят
|
||||
за окно первой успешной квитанции. Ошибка/timeout не обещают отката серверной мутации.
|
||||
При постоянной конкуренции запрос может завершиться TIMEOUT; unbounded waiting нет.
|
||||
|
||||
Вариант с продлением срока клиента отклонён при архитектурной сверке до итогового
|
||||
прогона: поздний BUSY и потеря результата могли вывести повтор за жизнь квитанции.
|
||||
Один секундный tick также недостаточен для точного освобождения; введён отдельный
|
||||
retry timer, который снимается до free/cancel/quiesce, с tick fallback при OOM.
|
||||
Формат125-байтного заголовка не менялся; смысл mutation BUSY.cursor описан в protocol.h.
|
||||
|
||||
Новая сетевая регрессия проходит настоящий клиентский API/router/storage:
|
||||
-76 MKDIR, включая исчерпание кеша; QUOTA подтверждает ровно76 каталогов.
|
||||
-Первый успешный ответ намеренно отброшен до клиентского handler; повтор с прежним
|
||||
ID возвращает успех, не создавая второй объект и не выдавая EXISTS.
|
||||
-76 STAT/REMOVE, включая новое исчерпание квитанций; QUOTA подтверждает0 объектов/байт/резерва.
|
||||
Qt recursiveDeletion(server-busy) вызывает storage напрямую через stub bridge:
|
||||
его сырой MF_BUSY остаётся правильным серверным ответом. Поведение полного
|
||||
клиентского ожидания и массового удаления проверено C-сетевой регрессией, не этим stub.
|
||||
|
||||
4. FIFO блокировал общий disk worker до проверки типа
|
||||
|
||||
POSIX mf_disk_local_open(source,0) использовал блокирующий open. FIFO без writer
|
||||
зависал ещё до mf_disk_info, который должен отвергнуть не-regular источник.
|
||||
Регрессия запускает отдельный процесс до создания pthread, alarm2с ограничивает
|
||||
только диагностический child. До фикса он завершился SIGALRM (status14), failures=1:
|
||||
/tmp/member-files-fifo-repro.log. Добавлен O_NONBLOCK, как у серверного open:
|
||||
FIFO открывается без ожидания, проверка типа даёт INVALID_PATH, fd закрывается.
|
||||
Обычный и sanitizer-прогоны: child status0, failures0; invalid mode/links имеют WARN.
|
||||
Произвольный уже выполняющийся системный I/O/хеширование всё ещё не прерывается cancel.
|
||||
|
||||
Сверка архитектуры и описаний
|
||||
|
||||
-Uasync владеет запросами, streams, local FIFO, ACL, CM handles, waiter/pump/timers.
|
||||
Единственный worker выполняет файловое I/O последовательно; work FIFO, порядок
|
||||
done при синхронном drain не обещается. Данные живут до completion.
|
||||
-Обычных работ до64; заранее зарезервированные cleanup jobs обходят лимит.
|
||||
Quiesce запрещает producers; drain завершает I/O и cleanup, затем storage destroy
|
||||
выполняется в worker, после него join/free, пока ядро/группы ещё живы.
|
||||
-Storage принимает только уже проверенные членство/роли. Сам проверяет UID мутации
|
||||
и токены temp. Квота общая для UID во всех группах; обычные операции не делают rescan.
|
||||
-Server COMMIT и local download имеют разные гарантии: локальный POSIX rename
|
||||
не синхронизирует каталог. Ошибка server sync после rename не откатывает публикацию.
|
||||
-Пути disk API доверенные и уже проверены protocol/storage. Callback LIST получает
|
||||
заимствованные name/info; порядок файловой системы, не сортировка. Source/target
|
||||
aliases локальных download исключает caller; одинаковая строка local запрещена.
|
||||
-Codec проверяет формат и размеры, не ACL и не UTF-8 policy пути. Ошибка decode
|
||||
может изменить output; encode не проверяет семантику команды. Эти границы описаны.
|
||||
-Все поля/функции шести .h получили соответствующие контракты и комментарии.
|
||||
|
||||
Проверки и границы
|
||||
|
||||
./build.sh --no-gui -j4 PASS: /tmp/member-files-audit-final-build.log.
|
||||
Профиль7/7 PASS: member_files, member_files_network, services, encrypted_attachment,
|
||||
dm_media, attachment_send, member_adapter. Результат: /tmp/member-files-audit-profile-results.log,
|
||||
подробности: /tmp/member-files-audit-profile-logs/.
|
||||
|
||||
ASAN/UBSan/LSan4/4 PASS, detect_leaks=1, halt_on_error=1: member_files,
|
||||
member_files_network, services, encrypted_attachment. Отдельная копия:
|
||||
/home/user/utun4/.cache/member-files-asan-b87xwg24, все встроенные зависимости
|
||||
инструментированы через CPPFLAGS; системные OpenSSL/FFmpeg не пересобирались.
|
||||
Логи: /tmp/member-files-sanitizer-build.log, /tmp/member-files-sanitizer-tests-build.log,
|
||||
/tmp/member-files-sanitizer-results.log, /tmp/member-files-sanitizer-test_*.log.
|
||||
После этого прогона добавлены только уточнения описаний и WARN без изменения логики.
|
||||
|
||||
В sanitizer-сборке остаётся прежнее предупреждение snprintf в disk_parent_open:
|
||||
strlen(component)<=MF_NAME_MAX проверяется до записи в leaf[MF_NAME_MAX+1];
|
||||
воспроизведённого усечения здесь нет. Остальные предупреждения сборки относятся
|
||||
к существующему коду других модулей/зависимостей, не скрывались фильтрами компилятора.
|
||||
|
||||
Первые диагностические прогоны до repro5 недействительны для temp: тест был
|
||||
собран с новым utun_instance.h и старой libutun.a, в startup читались0xaaaaaaaa.
|
||||
Общая сборка устранила несовпадение; ненужное изменение маршрутного условия отменено.
|
||||
Один промежуточный старт остановился из-за полного /tmp; очищены только
|
||||
воспроизводимые объекты предыдущей sanitizer-копии, исходники/журналы сохранены.
|
||||
|
||||
Новый полный make check, Windows/FreeBSD runtime и GUI/Android проверки не выполнялись.
|
||||
Четыре подтверждённых дефекта закрыты; новых подтверждённых незакрытых в этом объёме нет.
|
||||
@@ -107,3 +107,40 @@ IP совпали — трансляция IP не обнаружена; отл
|
||||
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
|
||||
Через SOCKS внешний IP прокси
|
||||
не записывается как собственный адрес.
|
||||
|
||||
Управляющий обмен и ресурсы
|
||||
--------------------------
|
||||
|
||||
Команды идут через сервис NAT_DETECTION ETCP-router с подписью и шифрованием.
|
||||
Используются узлы группы с рабочим маршрутом через READY-соседа; это не требует
|
||||
прямого UDP-соединения A с C. CHAT-допуск требует полной неудалённой записи участника.
|
||||
Источник и группа берутся из проверенного заголовка router, а не из next hop.
|
||||
|
||||
Версия управляющего формата — 4. Основное сообщение занимает 117 байт:
|
||||
10 однобайтовых полей команды/сокетов/этапа/результата, шесть uint64
|
||||
(owner, generation, round, token, observer, helper), uint16 timeout_ms
|
||||
и три адреса по 19 байт (target, primary, observer_address).
|
||||
RECHECK занимает 23 байта: cmd, command, version, socket_id и адрес 19 байт.
|
||||
Многобайтовые числа кодируются вручную в big-endian; адреса — route_ping_addr_encode.
|
||||
Это размеры сообщения после служебного заголовка router. Приём требует точного
|
||||
размера; старые generation/round/token и чужие делегирования результата отклоняются.
|
||||
|
||||
Детектор работает с шагом 100 мс. Помощник отправляет до трёх проб на этап;
|
||||
таймаут одной пробы — не меньше секунды и не меньше четырёх RTT наблюдателя.
|
||||
Общий бюджет обхода — 30 секунд; незавершённый обход может продолжиться после паузы.
|
||||
Полный результат перепроверяется через 30 минут, неполный — через 30 секунд.
|
||||
Повтор по неудаче подключения ограничен одним запросом за пять минут на endpoint.
|
||||
Сохранённый успешный ETCP-профиль не истекает по таймеру; изменение endpoint его сбрасывает.
|
||||
|
||||
История исходящих хранит до 256 IP для каждого локального endpoint (IP:порт).
|
||||
Она сохраняется в SQLite. Переполнение или ошибка загрузки/записи помечают историю
|
||||
неполной: UNKNOWN сохраняется до достоверного измерения, OPEN по незнакомому входу
|
||||
с неполной историей не подтверждается. Кеш наблюдений ограничен 128 записями,
|
||||
одновременно выполняется не более 64 задач помощи.
|
||||
|
||||
Детектор владеет таймерами, подписками и изолированными probe_only UDP-сокетами.
|
||||
Эти сокеты не публикуются как рабочие адреса. Их отдельное удаление снимает ссылку
|
||||
в состоянии родителя, отменяет зависимые пробы и допускает обычный последующий teardown.
|
||||
Ошибка регистрации любой подписки или начального таймера отменяет весь запуск.
|
||||
Ядро сохраняет контекст в instance->nat_det; останавливает его до ETCP/router
|
||||
и обнуляет поле после destroy. Уничтожать ядро из callback детектора нельзя.
|
||||
+75
-2
@@ -6,6 +6,14 @@
|
||||
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
|
||||
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
|
||||
|
||||
В NODEINFO секции подсетей присутствуют только при `TOPO_FLAG_SEND_SUBNETS`.
|
||||
Без флага оба счётчика подсетей равны нулю; противоречащий этому заголовок отклоняется.
|
||||
Parser проверяет точную длину пакета и проходит все присутствующие секции независимо
|
||||
от запрошенных `out_*`: например, `out_subnets=NULL` отключает создание объектов подсетей,
|
||||
но сохраняет проверку префиксов и правильное положение следующего hop-list.
|
||||
Выходы выдаются только после полного успеха; при ошибке они обнулены, частичные объекты
|
||||
освобождены. Подпись и допустимость пути проверяются после структурного разбора.
|
||||
|
||||
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
|
||||
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
|
||||
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
|
||||
@@ -13,6 +21,12 @@
|
||||
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
|
||||
только timestamp.
|
||||
|
||||
Локальный узел группы готовится до удаления прежнего `local_node`: сравниваются сами адреса
|
||||
и префиксы подсетей, включая замену при прежнем количестве. При отказе памяти, подписи или
|
||||
конфликте IPv4-маршрутов прежние узел и маршруты сохраняются. Подсети CHAT не публикуются.
|
||||
`topo_group_update_my_nodeinfo` возвращает число IPv4-подсетей, для CHAT — 0, при ошибке — -1;
|
||||
рассылку подготовленного NODEINFO выполняет вызывающий.
|
||||
|
||||
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
|
||||
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
|
||||
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
|
||||
@@ -50,6 +64,8 @@ snapshot загрузка использует его. Локальная пуб
|
||||
Уникальность определяется endpoint и видом адреса,
|
||||
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
|
||||
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
|
||||
Такая загрузка требует хотя бы одного адреса; отказ SQL/памяти освобождает все частичные списки
|
||||
и возвращает NULL. Наличие валидного snapshot с пустым списком адресов, напротив, допустимо.
|
||||
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
|
||||
старые профили сбрасываются, сериализованные snapshots инвалидируются.
|
||||
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
|
||||
@@ -62,14 +78,71 @@ Timestamp 0 обозначает bootstrap-сведения без подпис
|
||||
|
||||
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
|
||||
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
|
||||
Путь подготавливается до изменения общего реестра и групповых подсетей: отказ
|
||||
памяти сохраняет прежнюю запись, ссылки и пути. После commit IPv4-таблица
|
||||
перестраивается для этого узла, включая удаление старых подсетей при пустом
|
||||
новом наборе. Конфликт подсетей или OOM установки маршрутов логируется,
|
||||
новый набор не устанавливается, прежние маршруты удаляются; ошибка запрещает
|
||||
READY сессии. Принятая подписанная запись и пути при этом могут сохраниться.
|
||||
Линки дополняются как при UP, так и до установления соединения; повторная
|
||||
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
|
||||
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
|
||||
обслуживает соединение, и использует её при восстановлении.
|
||||
Для обхода адресов NCD делает отдельную копию: callbacks транспорта могут заменить
|
||||
списки в общем реестре до завершения обхода. Удержание ссылки на запись сохраняет
|
||||
сам объект, но не старые списки. Обновление, открытие и добавление нового сокета
|
||||
используют общий подбор разрешённых пар: family, UDP/TCP, локальная политика,
|
||||
NAT-профиль и обязательные метаданные REALITY. IPv6 link-local требует netif_index.
|
||||
Новый сокет обслуживает NCD-транспорты вне FIN_WAIT; случайный выбор одного адреса не используется.
|
||||
|
||||
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
|
||||
свежая подписанная запись принимается целиком, старая уступает уже известной.
|
||||
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
|
||||
ID узла проверяется по public_key. Ошибка запуска возвращает NULL handle и не снимает
|
||||
прежний FIN_WAIT. Отдельно принятый snapshot и уже добавленные линки при частичной
|
||||
ошибке могут сохраниться; новый handle публикуется после подготовки ресурсов.
|
||||
Явный specific_sock задаёт локальную политику, сохраняя проверки family/протокола/цели/REALITY;
|
||||
автоматическое появление сокета использует обычную политику подбора.
|
||||
|
||||
LAN discovery предоставляет отдельные адреса подключения и не меняет подписанный снимок.
|
||||
Открытое объявление рабочих сокетов и ответ на проверку имеют Ed25519-подпись;
|
||||
публичные ключи в этих пакетах отсутствуют. Получатель использует известный ключ
|
||||
из registry/SQLite, а неизвестное устройство остаётся только в списке discovery.
|
||||
Обычные объявления имени, сообщения и ACK не подписаны и не подтверждают личность.
|
||||
Общий UDP-порт 45731 принимает только объявления; unicast-обмен использует отдельные
|
||||
ephemeral-порты. Формат и API описаны в `src/local_transport/protocol.h` и `local_transport.h`.
|
||||
|
||||
Подписываются весь заголовок local_transport (magic/version/type, длина, IDs/эпохи,
|
||||
request=0) и тело: nonce(16), family(4/6), IP(16), количество сокетов,
|
||||
затем socket_id/protocol/port(BE)/generation(BE) каждого сокета. IPv4 дополнен
|
||||
нулевыми байтами. Подписанный IP обязан совпасть с источником UDP; рабочий порт
|
||||
отдельный от discovery-порта. Публикуются принимающие IP/interface обычные сокеты,
|
||||
без PRIVATE, probe и SOCKS UDP; до 32 endpoint на один IP, без фрагментации.
|
||||
|
||||
Объявление само не разрешает NCD использовать адрес. Только для узла без прямого UP
|
||||
и живого BGP-пути во всех группах получатель отправляет случайный nonce;
|
||||
подписанный ответ связывает его с обоими node_id и эпохами discovery. Ответ принимается
|
||||
однократно в течение 3 секунд. Это проверка свежести без часов/NTP: старые подписи,
|
||||
эпохи или случайные поколения сокетов сами по себе свежесть не доказывают.
|
||||
После проверки адрес действует 30 секунд по монотонным часам и только на интерфейсе
|
||||
приёма (включая IPv6 scope). Повтор объявления/ответа не продлевает срок без нового nonce.
|
||||
Дополнительно должен оставаться живым discovery-endpoint: только обычный ANNOUNCE
|
||||
продлевает его собственный TTL 30 секунд; MESSAGE/ACK и подписанный обмен этого не делают.
|
||||
Смена discovery-эпохи очищает endpoints и кандидаты. Проверка offline выполняется
|
||||
при обработке подписанного объявления/ответа; чтение кандидатов её не повторяет
|
||||
и само по себе не удаляет ранее проверенные адреса при появлении рабочего пути.
|
||||
Сверка интерфейсов и периодические объявления выполняются каждые 10 секунд,
|
||||
очистка TTL — по секундному tick или при запросе списка устройств/отправке сообщения.
|
||||
Отключение broadcasts сохраняет unicast-объявления и ответы, приём и сроки жизни;
|
||||
полная остановка discovery закрывает сокеты, завершает pending-доставки и очищает кандидаты.
|
||||
|
||||
NCD использует эти кандидаты при проверке совместимости, открытии/обновлении
|
||||
и появлении локального сокета. Если сохранены только ключи, идентичность загружается
|
||||
из nodes без требования старого адреса. LAN не создаёт NCD handle автоматически.
|
||||
Неподтверждённые ETCP-пробы очищаются после истечения кандидата/остановки discovery;
|
||||
после успешного handshake линк живёт по обычным правилам транспорта. REALITY требует
|
||||
метаданных из известного снимка; прямые LAN TCP-пробы при глобальном SOCKS выключены.
|
||||
Диагностика: discovery для подписи/nonce/адресов, etcp для NCD-проб и их удаления.
|
||||
|
||||
Конфигурационные подключения UTUN используют один адаптер
|
||||
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
|
||||
@@ -78,8 +151,8 @@ Bootstrap-адреса не перекрывают подписанную зап
|
||||
удерживает один handle на клиента, независимо от количества его адресов.
|
||||
|
||||
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
|
||||
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
|
||||
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
|
||||
Само событие транспортного UP не включает соединение в UTUN-группу автоматически.
|
||||
Входящий JOIN остаётся явным запросом
|
||||
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
|
||||
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
|
||||
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
|
||||
|
||||
+5
-2
@@ -14,8 +14,11 @@ GUI добавляет заголовок «Приглашение в групп
|
||||
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
|
||||
После успешной отправки открывается личная беседа с получателем.
|
||||
|
||||
Срок действия ключа — **10 минут с момента создания**, а не получения PM.
|
||||
Он указан в диалогах отправки и подключения. Поздняя offline-доставка
|
||||
Срок действия ключа — **10 минут с момента последней успешной регистрации на каждом узле**.
|
||||
Повторная регистрация того же ключа для того же канала тем же инвайтером продлевает срок.
|
||||
Connection-узел сохраняет ключ при обработке регистрации, инвайтер — после её подтверждения;
|
||||
при совпадении этих ролей запись одна. Для JOIN ключ должен действовать у обоих.
|
||||
Получение PM срок не продлевает. Длительность указана в диалогах отправки и подключения. Поздняя offline-доставка
|
||||
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
|
||||
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
|
||||
без обратного отсчёта и вычисления оставшегося времени.
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
Proxy: исправления и проверка архитектуры
|
||||
Дата: 2026-10-24. Статус: реализовано и проверено на Linux.
|
||||
Ниже сохранён согласованный проект; фактические изменения и результаты — в конце.
|
||||
|
||||
Подтверждённые факты до исправлений
|
||||
|
||||
1. udp_proxy.c не проверяет bind/sendto, результат socket_set_nonblocking,
|
||||
часть выделений/регистраций/таймеров и отправку ответа через router.
|
||||
Неудачный запуск flow может оставлять ресурс без рабочего таймера очистки.
|
||||
2. server_init и client_create оба запускают общие UDP/ICMP-контексты.
|
||||
client_destroy сохраняет их по server.enabled, server_destroy уничтожает всегда.
|
||||
Простой rollback через destroy одного модуля не выражает владение общими ресурсами.
|
||||
3. ICMP init публикует контекст и сообщает успех без raw socket. При этом тот же
|
||||
контекст нужен для клиентских REPLY: отсутствие возможности exit не означает,
|
||||
что клиентская сторона ICMP не может работать через другой exit.
|
||||
4. В диагностическом прогоне tcp_io error_cb при HUP отменяет read_waiter,
|
||||
включает чтение и напрямую вызывает read_cb вопреки read_paused.
|
||||
Для fd=15 порог чтения 4; журнал фиксирует read_paused=1 и рост rq до 53.
|
||||
Это нарушение ограничения очереди, а не только избыточная диагностика.
|
||||
5. EPIPE/ENOTCONN в HTTPS CONNECT воспроизведены. Системная трасса четырёх TLS-потоков:
|
||||
recv(destination)=0 -> поздняя TLS-запись 24 байта от клиента -> send(destination)=24
|
||||
-> SO_ERROR=EPIPE либо shutdown(SHUT_WR)=ENOTCONN. Повторный shutdown не обнаружен.
|
||||
Определение содержимого зашифрованной TLS-записи по трассе невозможно.
|
||||
Сам по себе этот сценарий не доказывает дефект half-close в proxy.
|
||||
6. После удаления exit-потока приходят поздние WINDOW: диспетчер пишет unhandled,
|
||||
хотя это известная команда уже завершённого потока. Создавать поток по WINDOW нельзя.
|
||||
7. enabled=yes в серверных тестовых конфигах не поддержан парсером. Сейчас сервер
|
||||
включается при обработке любого параметра секции, включая неизвестный.
|
||||
|
||||
Предлагаемая архитектура
|
||||
|
||||
Владелец общих UDP/ICMP-контекстов — UTUN_INSTANCE. Их жизненный цикл координируется
|
||||
в utun_instance.c после router_init и до router_destroy. TCP client/server управляют
|
||||
только своими listeners, TCP-потоками и bindings. Общих счётчиков ссылок не требуется:
|
||||
при существующем lifecycle они живут до остановки instance.
|
||||
|
||||
Контексты нужны при включённом exit или клиентском TUN; SOCKS/HTTP без TUN их не требуют.
|
||||
server.enabled обозначает действующий TCP exit. Роль при запуске ICMP exit должна
|
||||
передаваться явно, а не определяться случайным порядком вызовов init.
|
||||
При отказе создания клиента rollback освобождает только его ресурсы и собственный binding.
|
||||
Повторное создание клиента/сервера поверх активного состояния отклоняется.
|
||||
Результат client_create публикуется в inst->tcp_proxy_client только после успеха.
|
||||
|
||||
Порядок остановки: клиент и exit-потоки -> общие UDP/ICMP -> router -> drain deferred free.
|
||||
При destroy всего instance остановку proxy перенести перед остановкой UTUN-группы,
|
||||
разборкой topo_groups и ETCP-сокетов/соединений. Сейчас proxy уничтожается в phase M,
|
||||
после topo_groups и транспорта: для нового rollback нельзя сохранять этот поздний порядок.
|
||||
Общий instance_init_common уже вызывает instance_destroy при отказе создания;
|
||||
нужно использовать этот путь, не добавляя параллельную систему частичной очистки.
|
||||
При create_from_config сохранить существующее правило: отказ возвращает config вызывающему коду.
|
||||
Ресурсы закрываются в одном uasync-потоке; stop/destroy не вызывается из активного
|
||||
callback уничтожаемого владельца. Таймеры/waiters отменяются до освобождения памяти.
|
||||
|
||||
Инициализация и ICMP
|
||||
|
||||
Разделить создание общего ICMP-контекста/регистрацию REPLY и запуск raw exit socket.
|
||||
Это две разные операции: при отказе raw нельзя уничтожать работающую клиентскую сторону.
|
||||
Например, icmp_proxy_init создаёт общий контекст, отдельный enable_exit открывает
|
||||
raw socket и регистрирует чтение. Новый raw socket публикуется только после полного успеха.
|
||||
Обе операции возвращают ошибки непосредственно своему вызывающему коду.
|
||||
|
||||
Рекомендуемая политика: EPERM/EACCES/отсутствие поддержки raw явно отключают только ICMP exit,
|
||||
с WARN о причине и итогом запуска tcp=on/udp=on/icmp_exit=off. TCP/UDP продолжают работать.
|
||||
Ошибки выделения/регистрации обязательных ресурсов прерывают запуск proxy с полной очисткой.
|
||||
Если продукт требует обязательного ICMP, отсутствие raw должно быть фатальным на уровне
|
||||
конфигурации; это отдельная политика, а не скрытый fallback внутри init.
|
||||
В production виртуальные echo-ответы недопустимы. Тестовый loopback выбирается явно
|
||||
и должен работать одинаково с правами raw и без них.
|
||||
|
||||
Заданные SOCKS/HTTP listeners обязательны: enabled с пустым/неверным адресом либо
|
||||
неудачный bind/listen проваливают create. Нельзя вернуть готовый клиент без выбранного входа.
|
||||
Проверки router bind оставляем, но при живом inst нынешний bind не выделяет память
|
||||
и возвращает 0; не нужно изображать реальные OOM-пути там, где их нет.
|
||||
|
||||
UDP и ограниченные ресурсы
|
||||
|
||||
Новый flow сначала полностью создаётся локально: socket -> nonblocking -> bind ->
|
||||
read registration -> обеспечение expire timer. Только затем добавляется в список.
|
||||
Любой отказ снимает уже созданные регистрации/таймеры и закрывает именно новый сокет.
|
||||
Если общий таймер не удаётся продлить, оставшиеся flows закрываются с диагностикой:
|
||||
состояние без гарантированной очистки не сохраняется.
|
||||
|
||||
sendto проверяется по точному размеру. Для нулевой датаграммы n=0 означает успех.
|
||||
EINTR допускает повтор операции; EAGAIN/ENOBUFS означает явное отбрасывание датаграммы
|
||||
с диагностикой. Не добавляем неограниченную очередь и TCP-подобные повторы UDP.
|
||||
Остальные ошибки также логируются с peer, tuple, длиной и errno; packet-specific ошибка
|
||||
не должна автоматически уничтожать рабочий flow. Невалидный/сломанный сокет удаляется.
|
||||
Проверяются ошибки router send, выделения ответов и доставки в TUN.
|
||||
ICMP request тоже не публикуется без рабочего механизма истечения срока ожидания.
|
||||
|
||||
TCP HUP, FIN и границы ответственности
|
||||
|
||||
FIN относится только к одному направлению и следует после его DATA. Ошибка реальной
|
||||
записи остаётся ошибкой даже после FIN противоположной стороны; ENOTCONN не превращается
|
||||
в успешный on_fin_sent. Существующая регрессия test_fin_shutdown_error сохраняет этот контракт.
|
||||
|
||||
HUP — повод обработать завершение сокета, но не разрешение обойти high_water или явную pause_read.
|
||||
При достижении high_water чтение останавливается до low_water. Если level-triggered HUP
|
||||
не даёт event loop уснуть, регистрацию сокета нужно временно снять и восстановить по
|
||||
queue waiter; простой return с оставленным HUP в epoll/poll создаёт busy loop.
|
||||
Нужен один механизм восстановления регистрации в tcp_io, учитывающий connected,
|
||||
fin_remote, write_buf, write_queue и уничтожение владельца. Состояние fd/пулы сохраняются
|
||||
до окончательного destroy; запланированная запись не должна зависеть от паузы чтения.
|
||||
Повторное добавление использует новую generation uasync, старые события игнорируются.
|
||||
Явную паузу владельца следует отличать от паузы по high_water; HUP не снимает ни одну.
|
||||
|
||||
Это наиболее чувствительный этап: перед реализацией проверить переходы регистрации
|
||||
при HUP с ожидающей записью, low_water, обеих FIN и destroy из callback.
|
||||
Не менять глобальную семантику всех регистраций uasync ради одного tcp_io-сценария.
|
||||
|
||||
Поздние WINDOW/FIN/CLOSE/ERROR для отсутствующего потока игнорируются как события
|
||||
завершённого sid с debug-диагностикой. DATA не воскрешает поток, неверное окно живого
|
||||
потока по-прежнему является ошибкой. HTTP не обязан ждать FIN для ответа с известной длиной.
|
||||
Проверить синхронный loopback: состояние перед отправкой control резервируется заранее;
|
||||
после callback с возможным destroy нельзя обращаться к владельцу без проверки его жизни.
|
||||
|
||||
HTTPS-тест разделить на нормальное двустороннее TLS shutdown и явный abrupt close.
|
||||
В первом случае ожидать отсутствие EPIPE/ENOTCONN; во втором — корректную ошибку,
|
||||
сохранность уже доставленного ответа и освобождение ресурсов. Proxy не разбирает TLS.
|
||||
Сначала проверить эту гипотезу управляемым тестовым сервером, затем менять fixture.
|
||||
|
||||
Проверка после реализации
|
||||
|
||||
- Fault injection: socket/nonblocking/bind/read registration/expire timer/sendto/OOM.
|
||||
Проверять отсутствие fd/памяти/таймеров/handlers после каждого отказа и успешный повторный запуск.
|
||||
- client only, exit only, обе роли на одном instance, два независимых instance;
|
||||
отказ raw сохраняет ICMP REPLY клиента, остановка одного TCP-модуля не ломает общие контексты.
|
||||
- UDP payload=0, краткая/ошибочная отправка, transient error и следующий успешный пакет.
|
||||
- HUP при полном proxy window и остановленном consumer: очередь остаётся ограниченной,
|
||||
event loop не крутится, после снятия блокировки все байты и FIN доходят в правильном порядке.
|
||||
- pending write при HUP, manual pause, low_water/re-register, destroy в callback и stale events.
|
||||
- FIN в обе стороны при backpressure, RST до/после ответа, нормальный/оборванный TLS,
|
||||
поздний WINDOW, синхронный loopback и HTTP keep-alive со сменой sid.
|
||||
- Текущие proxy9, test_tcp_io, тесты uasync/queue, затем ASAN/UBSan и полный check.sh.
|
||||
Windows/FreeBSD требуют отдельной проверки poll/select; Linux PASS её не заменяет.
|
||||
|
||||
Доказательства исходного анализа
|
||||
|
||||
Диагностический test_socks_http_proxy под gdb: PASS, 16 byte-exact загрузок.
|
||||
Включены существующие proxy/socket debug через gdb, проектные файлы не менялись.
|
||||
Полный журнал: /tmp/proxy-design-debug.log; события lifecycle: /tmp/proxy-design-lifecycle.log.
|
||||
Системные вызовы TLS: /tmp/proxy-design-close.744427,
|
||||
сводка четырёх соединений: /tmp/proxy-design-close-summary.txt.
|
||||
Эти журналы относятся к реализации до исправлений.
|
||||
|
||||
Реализация и проверка архитектуры
|
||||
|
||||
1. UTUN_INSTANCE единолично создаёт и уничтожает общие UDP/ICMP-контексты. TCP-модули
|
||||
владеют только своими bindings/listeners/потоками. Их rollback не затрагивает общие
|
||||
контексты. Занятой binding не перезаписывается, повторный init TCP отклоняется.
|
||||
Proxy закрывается и deferred free обрабатываются до остановки групп/транспорта.
|
||||
2. Все заданные listeners и TUN mappings обязательны. Частичный отказ возвращает NULL
|
||||
и освобождает созданные ресурсы; create_from_config сохраняет config у вызывающего.
|
||||
3. ICMP init создаёт контекст/binding, enable_exit атомарно создаёт raw socket/регистрацию.
|
||||
Отсутствие прав/поддержки отключает только ICMP exit с явным WARN. Клиентские REPLY
|
||||
сохраняются; остальные ошибки запуска фатальны. Test loopback включается явно.
|
||||
4. UDP flow публикуется после всех обязательных шагов. Отказы создания/продления таймера
|
||||
не оставляют бессрочных flows. Проверяются socket/nonblocking/bind/registration,
|
||||
sendto, ответные выделения/router/TUN. EINTR повторяется, нулевая датаграмма допустима;
|
||||
ошибки пакета логируются без очереди повторов, сломанный сокет закрывается.
|
||||
5. TCP HUP сохраняет high/low-water и ручную остановку чтения. Без доступного I/O
|
||||
регистрация временно снимается; low_water восстанавливает её с новой generation.
|
||||
fd/очереди сохраняются; запись обслуживается независимо. Ошибка восстановления
|
||||
регистрации корректно вызывает on_error, включая destroy владельца из callback.
|
||||
6. Поздний WINDOW отсутствующего sid отбрасывается с DEBUG. Формат протокола и машина
|
||||
CONNECTED/WINDOW/FIN сохранены. Реальные ошибки записи/SHUT_WR не превращаются в успех.
|
||||
HTTPS fixture теперь завершает TLS двусторонне; abrupt close — отдельный режим.
|
||||
7. Неподдержанный enabled в серверных конфигурациях профильных и TUN-тестов заменён
|
||||
поддержанным connect_timeout. Контракты заголовков и документы модулей обновлены.
|
||||
|
||||
Проверено:
|
||||
- test_proxy_lifecycle: 25/25 изолированных сценариев, контроль fd/памяти/регистраций/таймеров.
|
||||
Покрыты socket/nonblock/bind/registration/timer/OOM, отправка/EINTR/нулевой UDP,
|
||||
частичный отказ instance, повторный запуск, ICMP REPLY после EPERM, HUP re-register,
|
||||
ожидающая запись и destroy из error callback. /tmp/proxy-fix-lifecycle2.log.
|
||||
- test_tcp_io: 19/19, HUP high_water=4 на epoll/poll, парковка без busy loop,
|
||||
точная доставка данных и FIN после low_water, ручная пауза и независимая запись.
|
||||
- SOCKS/HTTP/HTTPS: оба режима по 16 byte-exact загрузок. Штатный TLS shutdown без
|
||||
EPIPE/ENOTCONN; abrupt сохраняет ошибки TCP без повреждения доставленных ответов.
|
||||
/tmp/proxy-fix-check1/test_socks_http_proxy.log, /tmp/proxy-fix-tls-abrupt.log.
|
||||
- Основной make check: 185/185 PASS. /tmp/proxy-fix-full-check2.log.
|
||||
Первый check.sh остановлен общим лимитом 141с; local_transport_packets отдельно PASS.
|
||||
Повтор завершил основной набор, но TUN-этапу помешал чужой процесс на UDP 15002.
|
||||
- Те же TUN fixtures и идентичный src/utun выполнены в отдельном network namespace:
|
||||
proxy9/9, burst1/1, load8/8 PASS; load проверил134217728 байт.
|
||||
/tmp/proxy-fix-network-check.log; копия fixtures/логов: /tmp/utun-proxy-fix-network/.
|
||||
- Настоящий raw ICMP echo до127.0.0.1 через два ETCP-instance и явный loopback PASS.
|
||||
UTUN_TEST_ICMP_RAW=1 требует raw socket и не переключается на виртуальный ответ.
|
||||
/tmp/proxy-fix-icmp-raw.log, /tmp/proxy-fix-icmp-loopback.log.
|
||||
- ASAN/UBSan/LSan: 16/16 PASS с detect_leaks=1, включая оба TLS-режима, lifecycle,
|
||||
tcp_io/flush, пакеты, regressions, HTTP, TCP/UDP/ICMP, services, uasync/queue и STCP.
|
||||
Настоящий raw ICMP дополнительно PASS под теми же санитайзерами с root.
|
||||
Все встроенные зависимости собраны с sanitizer-флагами в отдельной копии текущих
|
||||
исходников /tmp/utun-proxy-fix-sanitizers; обычные объекты проекта не менялись.
|
||||
/tmp/proxy-fix-sanitizer-results.log, /tmp/proxy-fix-sanitizer-icmp-raw.log.
|
||||
|
||||
В проверенных Linux-сценариях противоречий владения/rollback/half-close не выявлено.
|
||||
Сборка и runtime Windows/FreeBSD не проверялись; Linux epoll/poll не заменяют эту проверку.
|
||||
+185
-46
@@ -1,61 +1,200 @@
|
||||
# Proxy: потоки TCP и датаграммы
|
||||
# Proxy: TCP, UDP и ICMP
|
||||
|
||||
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
|
||||
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
|
||||
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
|
||||
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
|
||||
Клиент принимает IPv4 из отдельного TUN через lwIP либо TCP от локальных SOCKS5/HTTP
|
||||
listeners. Exit открывает обычный IPv4 TCP/UDP или raw ICMP socket. Вся передача proxy
|
||||
идёт через `etcp_route_send` в `TOPO_GROUP_UTUN`, mode=0. Протокол не имеет слоя
|
||||
совместимости с прежними форматами.
|
||||
|
||||
## TCP
|
||||
## Сообщения для router и приёмные метаданные
|
||||
|
||||
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
|
||||
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
|
||||
Это форматы **сервисного API**, а не полные ETCP wire packets. На отправке первый байт —
|
||||
svc_id, остальное — payload router. Router callback получает такой заголовок:
|
||||
|
||||
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
|
||||
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
|
||||
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
|
||||
- DATA (3): от 1 до 4096 байт.
|
||||
- CLOSE (4), ERROR (5): прекращение потока.
|
||||
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
|
||||
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
|
||||
| Смещение | Размер | Поле |
|
||||
|---|---|---|
|
||||
| 0 | 1 | svc_id |
|
||||
| 1 | 8 | src_node_id |
|
||||
| 9 | 8 | dst_node_id |
|
||||
| 17 | 1 | rx_flags |
|
||||
| 18 | 8 | group_id |
|
||||
| 26 | N | service payload |
|
||||
|
||||
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
|
||||
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
|
||||
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
|
||||
не является подтверждением потребления данных конечным TCP.
|
||||
Смещения определены `ROUTER_SVC_*_OFF` в `src/routing_layer/etcp_router.h`.
|
||||
Заголовок callback имеет размер 26 байт. Сервис освобождает полученные entry/dgram
|
||||
при любом исходе. На отправке router принимает владение entry/dgram даже при ошибке;
|
||||
0 означает принятие сообщения router, а не доставку конечному TCP/UDP.
|
||||
|
||||
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
|
||||
или backpressure роутера. Закрытие одного направления не прекращает другое.
|
||||
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
|
||||
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
|
||||
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
|
||||
TCP-клиент проверяет src_node_id==via_node_id и group_id==TOPO_GROUP_UTUN, включая
|
||||
пустые уведомления. UDP/ICMP допускают REQUEST при включённом exit, REPLY — только при
|
||||
наличии клиента и совпадении src_node_id с его via_node_id; отдельной проверки group
|
||||
в их callback нет. IP, TCP/UDP-порты и ICMP id/seq в proxy API передаются в network
|
||||
byte order. TCP stream_id и WINDOW credit копируются как host-order uint32_t:
|
||||
формат не нормализован между архитектурами с разным byte order.
|
||||
|
||||
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
|
||||
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
|
||||
накопленные HTTP headers/body передаются частями размером DATA.
|
||||
## TCP и управление окном
|
||||
|
||||
Определения — `src/proxy/proxy_protocol.h`.
|
||||
На отправке: `svc:1, cmd:1, stream_id:4, data` (заголовок 6 байт).
|
||||
На приёме: router header(26), cmd(1), stream_id(4), data (заголовок 31 байт).
|
||||
Клиент отправляет в `ETCP_RT_ID_TCP_PROXY_SERVER=0x04`, exit отвечает в
|
||||
`ETCP_RT_ID_TCP_PROXY_CLIENT=0x08`. На exit поток определяется парой peer_node_id/stream_id;
|
||||
повторный CONNECT этой пары отклоняется с ERROR.
|
||||
|
||||
| Команда | Код | Содержимое после stream_id |
|
||||
|---|---|---|
|
||||
| CONNECT | 1 | IPv4:4, порт:2 |
|
||||
| CONNECTED | 2 | Пусто; exit завершил реальный TCP connect |
|
||||
| DATA | 3 | 1..4096 байт |
|
||||
| CLOSE | 4 | Пусто; прекращение потока с дренажом принятой записи на exit |
|
||||
| ERROR | 5 | Пусто; отмена, на exit немедленно уничтожает TCP и очереди |
|
||||
| FIN | 6 | Пусто; конец одного направления после всех его DATA |
|
||||
| WINDOW | 7 | uint32_t дополнительного кредита, ровно 4 байта |
|
||||
|
||||
Каждое направление начинает с окна 65536 байт. proxy_flow init оставляет ready=0.
|
||||
DATA ждёт CONNECTED; SOCKS success и HTTP CONNECT 200 также отправляются только после него.
|
||||
Локальный TUN TCP handshake выполняет lwIP и сам по себе connect назначения не подтверждает.
|
||||
При отправке DATA списывается tx_credit, при приёме — rx_credit.
|
||||
WINDOW допускает только ненулевой кредит без увеличения tx_credit сверх 65536.
|
||||
|
||||
Кредит возвращается после потребления локальным TCP: на OS-сокетах после опустошения
|
||||
write_queue **и write_buf**, в lwIP после принятия байтов ограниченным send-buffer
|
||||
через tcp_write. Это не подтверждение чтения удалённым приложением.
|
||||
Для HTTP учитывается число исходных upstream байтов независимо от преобразования
|
||||
headers/chunks/trailers; кредит возвращается после доставки соответствующего результата в OS TCP.
|
||||
|
||||
Backpressure router и окно потока независимы. При ошибке DATA остаётся в очереди/буфере
|
||||
владельца, регистрируется send-ready waiter и safety-net на 500 мс.
|
||||
У TCP-клиента с lwIP timer повторяет DATA с force=1, у exit и SOCKS/HTTP — обычный force=0.
|
||||
Force обходит лимит send_q router, но никогда не окно proxy.
|
||||
При отсутствии CONNECTED/кредита (-2) отправка ждёт ready/WINDOW, а не только router waiter.
|
||||
|
||||
OS TCP сохраняет ограничение read_high_water и явную остановку чтения при HUP.
|
||||
Если HUP не оставляет работы для чтения/записи, tcp_io временно снимает регистрацию fd;
|
||||
low_water восстанавливает её с новой generation. Сокет и очереди сохраняются, запись
|
||||
обслуживается независимо от чтения. Ошибка повторной регистрации закрывает соединение.
|
||||
|
||||
## Half-close и удаление потока
|
||||
|
||||
Владелец выставляет fin_pending только после отправки всех исходящих DATA.
|
||||
proxy_flow не видит очередей владельца и сам это условие не проверяет.
|
||||
FIN не должен ждать записи противоположного направления: обе половины независимы.
|
||||
Приём FIN ставит FIN-маркер после ранее принятых DATA в OS write_queue; в lwIP FIN
|
||||
откладывается до дренажа to_lwip и успешного tcp_shutdown.
|
||||
|
||||
Окончательное закрытие сырого TCP наступает после обоих FIN и необходимых локальных
|
||||
очередей/shutdown. Дополнительный обмен CLOSE при штатном half-close не требуется.
|
||||
На exit CLOSE паузит чтение, отбрасывает непрочитанный read_queue и закрывает сокет
|
||||
после принятой write_queue; ERROR уничтожает поток сразу. На клиенте CLOSE/ERROR
|
||||
прекращают поток; HTTP вне туннеля дополнительно проверяет завершённость ответа.
|
||||
|
||||
Общий retry повторяет pending CONNECTED → WINDOW → FIN через 5000 tb (500 мс),
|
||||
используя force=1. CONNECT/CLOSE/ERROR в эту очередь не входят.
|
||||
Клиентский close_pending может повторяться в poll/free пока поток жив; сервер после
|
||||
ошибки TCP удаляет поток и делает попытку ERROR. Гарантированного повторения CLOSE/ERROR
|
||||
после освобождения владельца нет.
|
||||
Поздний WINDOW отсутствующего потока отбрасывается с debug-диагностикой, без создания потока.
|
||||
|
||||
Router close/restart уведомляет сервис entry длиной 26 байт без payload.
|
||||
Proxy называет это CLOSE_ALL: exit удаляет потоки указанного peer, клиент — все свои
|
||||
TUN/SOCKS/HTTP потоки к настроенному exit. Это не самостоятельная подкоманда или
|
||||
9-байтовый пакет на проводе.
|
||||
|
||||
## SOCKS5 и HTTP
|
||||
|
||||
SOCKS реализует CONNECT к IPv4 или DNS-имени, метод аутентификации 0 (no auth).
|
||||
IPv6 отклоняется с REP=8; BIND и UDP ASSOCIATE не реализованы.
|
||||
DNS выполняется асинхронно на ingress, выбирается первая A-запись.
|
||||
Совмещённые и разделённые greeting/request/data сохраняют непрочитанный хвост.
|
||||
Ошибка DNS/connect до успеха даёт SOCKS REP=4 либо HTTP 502.
|
||||
|
||||
HTTP/1.0–1.1 forward proxy требует absolute http:// URL; upstream получает origin-form,
|
||||
Host из URL и Via. Обычный запрос отправляется как HTTP/1.1 с Connection: close,
|
||||
кроме Upgrade. Frontend HTTP/1.1 может сохранять keep-alive: каждый запрос получает
|
||||
новый stream_id/upstream, pipelining обслуживается последовательно, включая разные цели.
|
||||
Новый stream начинается после доставки полного ответа и отправки CLOSE/ERROR старого.
|
||||
|
||||
Codec проверяет headers до пересылки, поддерживает Content-Length, chunked
|
||||
с extensions/trailers, промежуточные ответы, HEAD/204/304 и тело до EOF.
|
||||
Hop-by-hop и proxy authentication поля удаляются, framing/Connection строятся заново.
|
||||
HTTP/1.0 получает декодированное chunked тело и закрытие. CONNECT ждёт полного блока
|
||||
headers и реального CONNECTED; согласованный 101 переводит соединение в сырой туннель.
|
||||
Ранний final response прекращает upload и требует закрытия клиента.
|
||||
Ошибка до final headers может дать собственный HTTP error; после них закрывает оборванный ответ.
|
||||
|
||||
Лимиты: headers/trailers 32 KiB, start line и chunk/trailer line 8 KiB, 512 header fields.
|
||||
CL+TE, разные Content-Length, obs-fold, управляющие байты и неверные порты отклоняются.
|
||||
Поддержан только transfer coding chunked; HTTP IPv6-назначения отклоняются с 502.
|
||||
Headers имеют deadline 30 с, DNS/connect — 15 с, передача/idle/дренаж закрытия — 60 с.
|
||||
Deadline headers не продлевается каждым байтом. Установленные CONNECT/Upgrade-туннели
|
||||
используют TCP lifecycle без HTTP deadline. Детали codec — в `src/proxy/http_proxy.h`.
|
||||
|
||||
## UDP и ICMP
|
||||
|
||||
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
|
||||
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
|
||||
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
|
||||
Контексты принадлежат конкретному UTUN_INSTANCE. REQUEST=1, REPLY=2.
|
||||
На отправке UDP: `svc:1, cmd:1, src_ip:4, src_port:2, dst_ip:4, dst_port:2, payload`.
|
||||
ICMP: `svc:1, cmd:1, dst_ip:4, orig_src_ip:4, echo_id:2, echo_seq:2, payload`.
|
||||
Заголовки отправки — 14 байт, приёма — 39. Отдельного sender_node_id в payload нет.
|
||||
|
||||
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
|
||||
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
|
||||
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
|
||||
UDP exit создаёт сокет на tuple client_node_id/src_ip/src_port/dst_ip/dst_port,
|
||||
проверяет источник ответа, меняет src/dst местами в REPLY. Удаляет неактивные flows
|
||||
при очередной периодической проверке с порогом >60 с. Клиент восстанавливает IPv4 checksum,
|
||||
UDP checksum=0 (допустимо для IPv4).
|
||||
Flow публикуется только после socket/nonblocking/bind/read registration и создания
|
||||
таймера очистки. Ошибка продления общего таймера закрывает оставшиеся flows.
|
||||
sendto проверяется по точной длине, включая успешный нулевой payload; EINTR повторяется.
|
||||
EAGAIN/ENOBUFS и ошибки отдельного пакета логируются без очереди повторов; сломанный
|
||||
сокет удаляется. Ошибки выделения ответа, router send и доставки в TUN диагностируются.
|
||||
|
||||
## Проверки и ограничения
|
||||
ICMP exit использует один raw socket; заменяет id/seq token и сопоставляет
|
||||
wire_id/wire_seq/IP цели с pending. Исходные id/seq восстанавливаются для клиента.
|
||||
Порог ожидания >5 с проверяется периодически. Raw reply проходит проверку ICMP checksum.
|
||||
Нечётный хвост checksum обрабатывается без чтения за буфером.
|
||||
`icmp_proxy_init` создаёт общий контекст и binding; `icmp_proxy_enable_exit` отдельно
|
||||
открывает raw socket. EPERM/EACCES/отсутствие поддержки raw отключают только ICMP exit
|
||||
с WARN; клиентские REPLY сохраняются. Прочие ошибки запуска прерывают создание instance.
|
||||
Тестовый `test_loopback` включается явно, закрывает raw socket и даёт виртуальные ответы
|
||||
независимо от прав процесса. Production его не включает.
|
||||
Pending raw request создаётся только с рабочим таймером истечения срока ожидания.
|
||||
|
||||
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
|
||||
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
|
||||
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
|
||||
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
|
||||
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
|
||||
Send API принимает payload 0..65507, TUN delivery — 0..65506 из-за однобайтового
|
||||
префикса в uint16_t длине entry. UDP send требует локальный UDP-контекст, ICMP send
|
||||
его не требует. Доставка ответа требует клиентский TUN; своего окна/повтора датаграмм нет.
|
||||
|
||||
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
|
||||
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
|
||||
Windows/FreeBSD в этом прогоне не проверялись.
|
||||
## Жизненный цикл, диагностика и проверки
|
||||
|
||||
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
|
||||
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
|
||||
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
|
||||
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
|
||||
Все операции и callbacks — в uasync владельца. Proxy init запрашивает binding
|
||||
после router init; client/server имеют разные service IDs. Общие UDP/ICMP создаёт
|
||||
и уничтожает UTUN_INSTANCE; TCP-модули освобождают только собственные ресурсы.
|
||||
Контексты нужны для включённого exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
|
||||
Заданные listeners, TUN mappings и bindings обязательны: ошибка откатывает создание.
|
||||
Повторный init активного модуля и попытка занять чужой binding отклоняются.
|
||||
При уничтожении instance TCP-клиент/сервер и общие контексты закрываются до остановки
|
||||
групп и транспорта; ua обрабатывает отложенные free. Точные контракты — в .h.
|
||||
|
||||
Категория proxy: info — подключения/закрытия/транзакции, debug — окна/паузы/повторы,
|
||||
trace — трафик и секундная диагностика exit TCP-очередей/пулов/OS buffers.
|
||||
Для поиска проблемы также полезны socket и etcp_route.
|
||||
Регрессионный тест включает proxy=debug через UTUN_TEST_DEBUG=1.
|
||||
|
||||
| Тест | Что проверяет |
|
||||
|---|---|
|
||||
| test_http_proxy | HTTP codec, разбиения headers/chunks, framing, лимиты, source credit, Upgrade |
|
||||
| test_proxy_packets | IPv4/UDP/ICMP, checksum и payload длиной 0..1501 |
|
||||
| test_proxy_regressions | Handshake/CONNECTED, HTTP pipeline/EOF/deadlines, окна/FIN, peer+sid, reentrancy, TUN options/fragment validation, per-instance UDP/ICMP |
|
||||
| test_proxy_lifecycle | 25 сценариев отказов/rollback, fd/память/таймеры, владение bindings, ICMP без raw, HUP re-register и destroy из callback |
|
||||
| test_tcp_proxy_client | Create/destroy с тестовым TUN, без утечек |
|
||||
| test_tcp_proxy_server | TCP echo через tcp_conn; сам по себе не проверяет протокол exit |
|
||||
| test_socks_http_proxy | Реальные SOCKS IPv4/domain, HTTP и HTTPS CONNECT, byte-exact загрузки; штатный TLS shutdown либо UTUN_TEST_TLS_ABRUPT=1 |
|
||||
| test_udp_proxy / test_icmp_proxy | Два instance через ETCP; ICMP: явный test_loopback, UTUN_TEST_ICMP_RAW=1 требует настоящий raw socket |
|
||||
| test_tcp_io | 19 сценариев, включая HUP high/low-water на epoll/poll, ручную паузу и независимую запись |
|
||||
| test_tcp_io_flush | Callback после последнего синхронного send |
|
||||
|
||||
Проверка 2026-10-24: профильные тесты проходят на Linux. ICMP проверен и через явный
|
||||
test_loopback, и настоящим raw echo до 127.0.0.1 с правами root. Штатный TLS shutdown проходит без EPIPE/ENOTCONN;
|
||||
аварийный сохраняет реальные ошибки записи, все 16 загрузок остаются byte-exact.
|
||||
Тестовые конфиги используют поддержанный connect_timeout. Итоги полного прогона
|
||||
и sanitizer-проверок — в [proxy_fix_plan.txt](proxy_fix_plan.txt).
|
||||
Windows/FreeBSD runtime в этом окружении не проверены.
|
||||
Создание proxy из конфигурации в utun_instance.c исключено под _WIN32.
|
||||
IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет.
|
||||
Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией.
|
||||
+201
-15
@@ -22,17 +22,30 @@ utun_instance_destroy(inst);
|
||||
```
|
||||
|
||||
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
|
||||
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
|
||||
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
|
||||
в конфигурации. Desktop и Android GUI по умолчанию запускают ядро и чат.
|
||||
`[gui] utun_enabled=yes` явно добавляет UTUN-сервис; `[global] tun_enabled=yes`
|
||||
управляет только созданием TUN внутри включённого UTUN. Отсутствующий GUI-флаг означает no.
|
||||
Без TUN и NAT запуск чата не требует прав администратора.
|
||||
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
|
||||
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
|
||||
ошибка запрошенного TUN откатывает запуск и возвращается в GUI. Android сохраняет тот же выбор при рестарте.
|
||||
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
|
||||
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
|
||||
UTUN/чата откатывает созданные им ресурсы; ошибка core start оставляет подготовленные
|
||||
ресурсы для retry/destroy. `utun_instance_init()` сохраняет ранее запущенные сервисы
|
||||
при отказе последующего. `utun_instance_stop()` останавливает цикл
|
||||
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
|
||||
|
||||
Create уже подготавливает listeners, топологию/SQLite, router/proxy и мониторинг;
|
||||
core/UTUN/chat запускаются отдельно. `create_from_str` использует временный файл
|
||||
в `/tmp`, updater и parse. `create_from_config` передаёт владение config только
|
||||
при успехе; при отказе config остаётся caller, но node_id мог быть исправлен.
|
||||
Logger общий для процесса, его изменения не откатываются при отказе создания.
|
||||
|
||||
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
|
||||
|
||||
```ini
|
||||
[gui]
|
||||
utun_enabled=yes
|
||||
|
||||
[global]
|
||||
tun_enabled=yes
|
||||
tun_ifname=vibechat
|
||||
@@ -65,15 +78,60 @@ ConnectivityManager до запроса запуска и передаёт ко
|
||||
|
||||
| Владелец | Ресурсы |
|
||||
|---|---|
|
||||
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
|
||||
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
|
||||
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, сокеты, NCD и постоянные транспорты `[client]`, маршрутизатор, репликация, время, мониторинг |
|
||||
| UTUN | Группа UTUN, независимые запросы участия к настроенным пирам, DATA binding, TUN, системные маршруты, NAT transport |
|
||||
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
|
||||
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
|
||||
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
|
||||
|
||||
`[client]` открывает транспорт без вступления в UTUN. CHAT согласуется только при наличии
|
||||
членства в соответствующем канале. Входящий JOIN отсутствующей группы отклоняется.
|
||||
UTUN stop закрывает свои групповые запросы, но сохраняет постоянные транспорты ядра.
|
||||
Запись настроенного пира владеет копией параметров и адресов; DOWN/TIMEOUT сохраняет NCD,
|
||||
окончательный CLOSED запускает повтор через секунду. Удаление записи отменяет её повтор
|
||||
и освобождает только её handles. Ошибка подготовки нового списка сохраняет прежний список.
|
||||
Изменение автосокетов переприменяет сохранённые endpoints также для bootstrap без подписанного NODEINFO.
|
||||
Полный reload сохраняет набор запущенных сервисов и running; частичный обновляет `[client]` также без UTUN.
|
||||
Restart нужен при смене негорячих global-настроек (в том числе db_path, control,
|
||||
TUN/NAT/proxy), подсетей либо server-конфига в automatic режиме. В ручном режиме
|
||||
сравниваются все параметры endpoint и IPv6 scope; неизменённый port=0 сохраняет
|
||||
порт ОС. TCP endpoint закрывается вместе с listener и своей TCP-моделью.
|
||||
Ошибка подготовки сохраняет прежний instance; ошибка создания после полного destroy
|
||||
возвращает NULL. Частичный reload не атомарен: отказ нового endpoint логируется,
|
||||
новый config сохраняется для повторной попытки. Caller всегда использует результат reload.
|
||||
|
||||
Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами;
|
||||
TCP client/server владеют только своими bindings, listeners и TCP-потоками.
|
||||
Общие контексты нужны для exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
|
||||
Ошибка обязательного listener, TUN mapping или регистрации откатывает создание instance.
|
||||
Недоступность raw ICMP по правам или поддержке отключает только ICMP exit с WARN;
|
||||
клиентские ICMP REPLY и TCP/UDP продолжают работать.
|
||||
`utun_instance_destroy()` закрывает proxy и обрабатывает отложенные free до остановки
|
||||
UTUN/CHAT-групп и транспорта. Waiters/таймеры снимаются до освобождения их владельцев.
|
||||
Остановка отдельного TCP proxy-модуля не уничтожает общие UDP/ICMP-контексты.
|
||||
|
||||
Команды JOIN из Qt/JNI используют удерживаемый `chat_join_port`: frontend копирует ссылку,
|
||||
а состояние чата читает только callback в uasync. Close закрывает приём под mutex, отменяет
|
||||
ожидающие команды и отделяет порт от instance. Ссылка frontend остаётся безопасной после stop;
|
||||
новый запуск публикует новый порт. Команды старого запуска не обращаются к новому ядру.
|
||||
При stop чата порт и активная попытка закрываются до ожидания media workers. В принятой команде
|
||||
есть отдельный request_id интерфейса; сетевой JOIN сопоставляется по peer/group/attempt_id/phase.
|
||||
Результат публикуется после очистки состояния и собственного NCD handle, поэтому callback может
|
||||
начать следующий JOIN. Отмена ожидания не откатывает уже сохранённое удалённое членство.
|
||||
|
||||
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
|
||||
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
|
||||
`:memory:` данные сохраняются только до уничтожения ядра.
|
||||
Caller завершает заимствованные SQLite statements/backups до destroy. Ошибка close
|
||||
логируется; close_v2 при BUSY откладывает освобождение БД до завершения этих объектов.
|
||||
UASYNC живёт до конца destroy. Отменённые heap-таймеры могут оставаться в нём
|
||||
до следующего poll; это не разрешает callback обращаться к освобождённому instance.
|
||||
|
||||
Headless chat/call/radio bind задаёт IPv4 literal:port, decimal 0..65535;
|
||||
port=0 выбирает ОС. Ошибка bind/listener не останавливает чат и не открывает
|
||||
порт по умолчанию. Stop снимает клиентов с UASYNC, отменяет cleanup/mix-таймеры,
|
||||
освобождает headless-контексты и кодеки. PCM после 11-байтового заголовка
|
||||
копируется в выровненный буфер перед Opus.
|
||||
|
||||
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
|
||||
не разрывает соединение, нужное другой группе. При остановке группы отменяются
|
||||
@@ -97,6 +155,21 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
|
||||
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
|
||||
завершает ранее запрошенное мягкое закрытие.
|
||||
|
||||
NCD возвращает новый handle после подготовки подписки на все события ETCP и нужных
|
||||
таймеров. До возврата open новый владелец событий не получает; уже работающий
|
||||
транспорт доставляет начальный UP через call_soon. Отказ подписки, таймера или
|
||||
планирования UP возвращает ошибку и сохраняет прежних владельцев и FIN_WAIT.
|
||||
TIMEOUT не закрывает удерживаемый транспорт: возможен поздний UP, новое open может
|
||||
начать новый бюджет подключения. CLOSED отменяет таймеры, отсоединяет conn и требует
|
||||
закрыть оставшийся handle; его можно закрыть после уничтожения ядра.
|
||||
Операции открытия, обновления линков и закрытия удерживают NCD/ETCP через вложенные
|
||||
callbacks, включая синхронный callback исходящей очереди при отправке CLOSE.
|
||||
Более новый вложенный переход прекращает оставшуюся рассылку прежнего события.
|
||||
Управляющий пакет NCD занимает ровно 10 байт: cmd=0x12, subcmd и node_id little-endian.
|
||||
Приём проверяет длину, команду и принадлежность отправителя тому же транспорту.
|
||||
Отказы ресурсов, вложенные события, обновление snapshot и удаление соседних линков
|
||||
проверяет `test_ncd_faults`; готовый handle, TIMEOUT и сетевой CLOSE/KEEP_ALIVE — `test_node_conn_direct`.
|
||||
|
||||
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
|
||||
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
|
||||
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
|
||||
@@ -113,10 +186,92 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
|
||||
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
|
||||
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
|
||||
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
|
||||
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
|
||||
Подготовленные блоки отменённых задач не регистрируются; учёт назначений подготовки
|
||||
сохраняется для последующей очистки. Это синхронное ожидание: длительное
|
||||
транскодирование или распознавание речи может задержать stop; принудительного
|
||||
прерывания этих библиотек нет.
|
||||
|
||||
Владелец принимает не более 16 задач, включая уже завершённые workers до доставки
|
||||
completion. Отказ запуска/неверные аргументы дают синхронный done с -1, остановка
|
||||
владельца — с `MEDIA_ASYNC_CANCELLED`; при NULL done только логируется ошибка.
|
||||
Примитивы `media_async.h` синхронные: отмена задачи не откатывает выполненные ими
|
||||
действия. `ma_copy_file()` публикует копию внутри worker после проверенных записи и
|
||||
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
|
||||
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
|
||||
|
||||
Групповой crypto worker хранит один `file_io_temp` прямо в задаче вложения.
|
||||
Общий валидатор MIME не выполняет I/O: send вызывает его в uasync до создания
|
||||
задачи/ключа, файловый codec повторяет проверку в worker до открытия файлов.
|
||||
`encrypted_file_write/read` только готовят закрытый durable temp; worker публикует
|
||||
его одним rename, completion подтверждает результат в БД/UI. Отмена до completion
|
||||
не публикует сообщение; cleanup учитывает реально созданные/заменённые файлы.
|
||||
Ошибка синхронизации каталога после rename означает установленное имя без подтверждённой
|
||||
durability; отложенный discard уже не удаляет результат. Общей транзакции ФС/БД нет.
|
||||
До запуска workers и после их завершения recovery очищает собственные stale temp
|
||||
в `media/decrypted`; общий file_io-реестр сохраняет активные temp всех instance.
|
||||
|
||||
`media_delivery_init()` вызывается после подготовки chat core и media_index, пока
|
||||
общая БД и группы живы; подписка на роли обязательна. Репликация хранит отдельный
|
||||
контекст и CM handle для каждой пары «CHAT-группа–пир». DOWN отменяет текущий RPC,
|
||||
сохраняя handle для восстановления CM; TIMEOUT закрывает его перед повторным открытием.
|
||||
Отдельный ETCP DOWN не означает потерю всех путей CM. До free контекста отменяются
|
||||
его waiter, pump, watchdog и собственный handle; destroy отменяет также сверку ролей.
|
||||
Обычный stop сохраняет каталог, его эпоху и RX-курсоры. Потеря локальной роли удаляет
|
||||
чужие объявления вместе с RX-курсорами только этой группы одной транзакцией;
|
||||
удаление канала удаляет также собственные объявления. Страница репликации и её
|
||||
RX-курсор фиксируются одним самостоятельным commit; вложенная внешняя транзакция
|
||||
не считается сохранённым прогрессом. Wire-контракт находится в `media_delivery_proto.h`.
|
||||
|
||||
Перед файловой подготовкой назначение записывается в `media_preparations`. Успешный
|
||||
`media_index_commit()` регистрирует весь файл и снимает эту запись в одном savepoint;
|
||||
после ошибки/отмены путь остаётся известен, даже если worker уже опубликовал копию.
|
||||
Это гарантия записи БД; файловые изменения и внешний SQL COMMIT ей не охватываются.
|
||||
При обычной ошибке откатывается только регистрация; SQLite может сама отменить всю
|
||||
транзакцию при тяжёлой ошибке. Общая база берётся из `ui_state.media_base` и повторно
|
||||
используется после stop/start. Загрузчик не меняет существующую базу.
|
||||
|
||||
Входящая загрузка и исходящая подготовка резервируют нормализованный путь в общем
|
||||
media_delivery до начала записи. Отмена снимает загрузку с индекса, закрывает CM handles
|
||||
и таймеры, освобождает резервацию; выполняющийся обработчик удерживает сам контекст.
|
||||
Поэтому отмена и повторный start из progress callback допустимы, а done старой загрузки
|
||||
вызывается после возврата из progress. Каждая попытка блока имеет отдельные UUID и temp:
|
||||
поздние пакеты старого держателя/попытки не изменяют новый файл.
|
||||
|
||||
Producer и relay-выдачи независимо удерживают файл блока. После отмены producer
|
||||
непроверенный источник завершается с ошибкой; проверенный может обслужить оставшихся
|
||||
читателей. Последний владелец удаляет temp. При удалении канала отменяются и producer,
|
||||
и все его выдачи; перед освобождением выдачи отменяются её waiter, pump и таймер.
|
||||
DONE выдачи следует только после проверки блока и принятия всех его данных router.
|
||||
|
||||
После проверки всех блоков загрузчик собирает отдельный temp, проверяет операции
|
||||
чтения/записи/close, полный размер и SHA-256, затем атомарно заменяет имя назначения.
|
||||
Ошибка до публикации сохраняет прежний файл. Затем выполняется регистрация в SQLite:
|
||||
при её ошибке опубликованный файл остаётся учтён в media_preparations для очистки.
|
||||
Проверка и сборка блоков пока синхронны в uasync; контракт workers относится к исходящей подготовке.
|
||||
|
||||
Локальное удаление канала ждёт его исходящие video/register задачи. Последний
|
||||
completion продолжает удаление; новые исходящие медиа канала в это время отклоняются,
|
||||
подготовленные результаты ожидающих отправок не публикуются как новые сообщения.
|
||||
После завершения задач отменяются загрузки/раздача и удаляются учтённые файлы, включая
|
||||
назначения неуспешных подготовок. Общие файлы других каналов сохраняются. Очистка файлов
|
||||
выполняется вне внешней транзакции: SQL-откат не восстанавливает удалённый файл.
|
||||
При ошибке очистки записи оставшихся файлов сохраняются для повторного вызова;
|
||||
удаление метаданных канала и неполная файловая очистка диагностируются отдельно.
|
||||
Низкоуровневый `media_index_delete_channel()` сам workers не останавливает.
|
||||
Точные ограничения путей и контракт повторной регистрации находятся в `media_index.h`.
|
||||
|
||||
Подготовка файлов в `src/video/video.h` выполняет remux подходящих H.264/AAC либо
|
||||
перекодирование в MP4 H.264/YUV420P и AAC. Worker пишет уникальный соседний временный
|
||||
файл, завершает декодеры/ресемплер/FIFO/кодеры, проверяет trailer, закрытие и метаданные.
|
||||
Конечное имя заменяется только успешным completion в uasync. При отмене, включая
|
||||
отмену уже подготовленного результата до доставки completion, временный файл удаляется,
|
||||
старое назначение сохраняется. Совпадающие исходник/назначение и файловые алиасы
|
||||
отклоняются; исходный файл должен оставаться неизменным до done. В процессе занята
|
||||
одна задача до completion; атомарный захват исключает гонку между экземплярами ядра.
|
||||
Перед done занятость освобождается, поэтому callback может запустить следующую задачу.
|
||||
JPEG использует те же правила файлов, но извлекается синхронно в отдельном frontend worker.
|
||||
Probe также синхронен и не обещает ограниченного времени чтения.
|
||||
|
||||
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
|
||||
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
|
||||
части против вытеснения. После FIN publisher может снять своё владение, сохранив
|
||||
@@ -136,9 +291,24 @@ destroy отсоединённого cursor не обращается к store.
|
||||
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
|
||||
|
||||
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
|
||||
PTS показа; audio cursor сохраняет временной lookahead. Bridges готовят один ответ
|
||||
на незавершённый запрос и не читают следующий кадр после получения ответа автоматически.
|
||||
Android worker держит один рабочий input и один готовый output MediaCodec; свободный
|
||||
PTS показа; audio cursor сохраняет временной lookahead. Viewers готовят ограниченный батч
|
||||
на запрос: отдельно задаются число видеокадров, audio lookahead и число всех записей.
|
||||
Desktop и Android принимают до 16 записей и обрабатывают треки по их готовности.
|
||||
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека, не ожидая другого трека;
|
||||
общий END завершает вход, а окно ждёт опустошения обоих выходов. GAP/CLOSED отменяют старый батч.
|
||||
Получение батча погашает запрос, включая повторные обращения до pull; скрытого чтения следующего батча нет.
|
||||
Callback готовности только будит desktop worker и не вызывает API ядра/bridge. Stop снимает callback
|
||||
под mutex viewer до освобождения его контекста; detach будит ожидающие workers до уничтожения manager.
|
||||
На Android signal принадлежит audio worker и живёт до stream_stop, join video worker и close mixer input;
|
||||
ядро только уведомляет его. Surface освобождается после join video worker.
|
||||
В wait не удерживаются mutex bridge/ядра. Расход PCM будит тот же signal, уведомления сливаются без потери пробуждения.
|
||||
GUI и audio callback будят worker при движении часов/освобождении места; сна между готовыми пакетами нет.
|
||||
Каждый reader имеет отдельные audio/control и video workers. Audio worker читает батчи и готовит
|
||||
до 250ms PCM через Opus/SoundTouch, не удерживая блокировку consumer. Запас пополняется при наличии
|
||||
данных без обязательного prefill. SPSC передаёт PCM вместе с отображением исходных PTS; mixer/audio
|
||||
callback только копирует и смешивает. Короткая блокировка защищает output map/clock и lifetime,
|
||||
в ней нет декодирования, time-stretch или MediaCodec/FFmpeg. EOF продолжает prepare до опустошения DSP.
|
||||
Android video worker получает ограниченный mailbox (два кадра) и держит один рабочий input и один готовый output MediaCodec; свободный
|
||||
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
|
||||
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
|
||||
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
|
||||
@@ -146,8 +316,11 @@ input buffer получает лишь для уже готовых данных
|
||||
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
|
||||
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
|
||||
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
|
||||
Смена revision заменяет только video decoder и его измеренный защитный guard;
|
||||
Смена revision заменяет только video decoder и его диагностические измерения;
|
||||
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
|
||||
Reset увеличивает video generation; результаты прежнего поколения не публикуются. Video worker
|
||||
после EOF остаётся доступен для reset до явного stop. Audio clock после старта не ждёт видео:
|
||||
просроченные изображения декодируются для сохранения references, но не показываются.
|
||||
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
|
||||
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
|
||||
конец GUI требует также показа оставшихся изображений и завершения audio tail.
|
||||
@@ -187,10 +360,16 @@ Microphone lease публикации исключает call/recorder; раци
|
||||
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
|
||||
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
|
||||
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
|
||||
На Android `MicrophoneCapture` владеет AudioRecord и reader-потоком, подписчики — ограниченными очередями.
|
||||
Stop публикации удаляет её подписку до join Opus worker и FIN; AudioRecord остаётся живым для рации.
|
||||
PTT и PCM рации проходят через одну очередь. Последний подписчик делает stop/read join/release;
|
||||
приоритет глобального AudioManager сохраняется до освобождения трансляционного захвата.
|
||||
На Android `MediaAudioController` владеет общим выбором устройства, AudioManager и одним аппаратным reader.
|
||||
Маршрутизация использует `CallAudioRouter`; AudioTrack рации и микшера просмотра регистрируются у одного владельца.
|
||||
`MicrophoneCapture` раздаёт независимые ограниченные очереди постоянного PCM 48k stereo; моно апмиксуется.
|
||||
После подключения публикации встроенный микрофон использует CAMCORDER, сохраняемый до последней подписки.
|
||||
Смена профиля заменяет только аппаратный AudioRecord: камера, видеокодер и source_epoch продолжают работать.
|
||||
Общая сетка по capture timestamps имеет запас 200ms; пропуски заполняются тишиной, старый PCM не переносится вперёд.
|
||||
Stop публикации удаляет её подписку до join Opus worker и FIN, сохраняя захват для оставшейся рации.
|
||||
PTT и PCM рации проходят через одну очередь. Последняя подписка завершает общий reader;
|
||||
контроллер закрывает аппаратный вход, сохраняя выходы. Перед Telecom все владельцы снимаются,
|
||||
а `MediaAudioController.awaitIdle()` дожидается освобождения входа и глобального маршрута.
|
||||
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
|
||||
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
|
||||
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
|
||||
@@ -291,3 +470,10 @@ standby даже при начальном `client_activity=STANDBY`. Keepalive
|
||||
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
|
||||
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
|
||||
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
|
||||
|
||||
Activity/peer-sleep подписки уникальны по fn+arg и вызываются синхронно в обратном
|
||||
порядке добавления. Remove себя/соседа безопасен: память записи освобождается
|
||||
после внешней рассылки. Новые подписки могут получить вложенное событие;
|
||||
более новая эффективная activity прерывает остаток прежней рассылки.
|
||||
Destroy/reload из этих callbacks запрещён. `test_instance_contract` проверяет
|
||||
вложенные подписки, отказы ресурсов, manual/full reload, TCP listener и headless stop/start.
|
||||
+260
-60
@@ -1,5 +1,17 @@
|
||||
Модуль стриминга. Техническое задание, этап 1.
|
||||
|
||||
Статус аудита 26.10.2026: сверка завершена только для stream_proto.h,
|
||||
stream_block.h, stream_source.h и stream_media.h; остальные разделы проверены частично.
|
||||
Расхождение допуска deleted устранено: stream_member_load читает принятый флаг
|
||||
в том же SELECT, что и профиль, и возвращает STREAM_ERR_NOT_MEMBER до crypto.
|
||||
Штатный test_stream_member проверяет подписи/версии, source/supplier/self,
|
||||
закрытие handles/выдач, изоляцию каналов и повторное разрешение участия.
|
||||
Полный аудит оставшихся разделов ещё не завершён. Подтверждения, архитектура
|
||||
исправления и результаты проверок — doc/stream_audit_findings.txt.
|
||||
Дефект publisher_block устранён: помещающийся Opus уходит в отдельную аудиопачку,
|
||||
ожидающее видео получает следующий слот actor. Штатный test_stream_publisher
|
||||
проверяет 23 сценария границ, продвижения треков и OPENING/FIN; раздел 7 отчёта.
|
||||
|
||||
1. Цель и принятые решения
|
||||
|
||||
Модуль размещается в src/stream. Узлы получают поток данных от источника через других подписчиков группы,
|
||||
@@ -8,7 +20,7 @@
|
||||
|
||||
Принятые решения:
|
||||
- Транспорт — существующий ETCP с быстрыми переповторами, мультилинком, шифрованием и управлением нагрузкой.
|
||||
- Параметры FEC задаёт источник; 6+2 — значение по умолчанию, а не ограничение протокола.
|
||||
- Параметры FEC задаёт источник; 8+0 — значение по умолчанию, а не ограничение протокола.
|
||||
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
|
||||
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
|
||||
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
|
||||
@@ -68,7 +80,7 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
||||
|
||||
Источник формирует пачки и кодирует их lib/reed_solomon:
|
||||
- n — полное число субпотоков, r — число проверочных, k = n-r — число исходных.
|
||||
- По умолчанию n=8, r=2, k=6. Любые k кодограмм одной пачки восстанавливают её содержимое.
|
||||
- По умолчанию n=8, r=0, k=8. Любые k кодограмм одной пачки восстанавливают её содержимое.
|
||||
- subblock от 0 до n-1 определяет субпоток; первые k блоков систематические, остальные проверочные.
|
||||
- batch_seq — общий 64-битный индекс пачки; блок субпотока X определяется парой (batch_seq, X).
|
||||
Позиция N в запросе — batch_seq, а не байтовое смещение или отдельный счётчик субпотока.
|
||||
@@ -78,7 +90,7 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
||||
Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size.
|
||||
Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск.
|
||||
В настройках desktop/Android раздел «Трансляция» сохраняет общее число субблоков и число избыточных RS.
|
||||
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 2).
|
||||
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 0).
|
||||
Медиапубликатор читает параметры при запуске и один раз выделяет рабочий буфер по ёмкости пачки,
|
||||
но не больше предела медиаблока 65536 байт. Изменение настроек действует только со следующего запуска.
|
||||
Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку.
|
||||
@@ -87,9 +99,9 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
|
||||
|
||||
Максимальный размер части субпотока по умолчанию — 1300 байт: внешние метаданные 47 и RS до 1253 байт.
|
||||
Внутри RS находятся подписанные метаданные 55 байт, содержимое и одна 64-байтная подпись.
|
||||
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 6+2 — 7345 байт.
|
||||
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 8+0 — 9833 байт.
|
||||
При длине содержимого L фактический RS-size = 9 + ceil((55 + L + 64)/k), полный размер части = 47 + RS-size.
|
||||
Например, L=1000 даёт восемь частей по 243 байта; L=7345 даёт восемь частей по 1300 байт.
|
||||
Например, при 8+0 L=1000 даёт восемь частей по 196 байт; L=9833 даёт восемь частей по 1300 байт.
|
||||
Один контекст RS принимает пачки разной длины без пересоздания матрицы; все части одной пачки равны по размеру.
|
||||
Handle получателя, поколение stream-сессии, изменяемый путь и служебные заголовки ETCP идут сверх этого бюджета.
|
||||
Не обещаем соответствия одной кодограммы одному UDP-пакету: необходимую фрагментацию выполняет ETCP normalizer.
|
||||
@@ -396,21 +408,25 @@ A — поступившая нагрузка, D — передано в ETCP, X
|
||||
в интервале без смены выдач g = max(0, (A-D)/elapsed_seconds).
|
||||
Rate выдачи — её средний вклад в поступающую нагрузку, а не урезанная скорость выхода из перегруженного канала.
|
||||
Для разгрузки нужно снять суммарный rate больше g, чтобы очередь начала убывать.
|
||||
Число выдач выбирается по их rates: закрываем достаточно выдач для требуемого сокращения;
|
||||
при сильном росте это сразу несколько, не фиксированная одна и не случайное постоянное число.
|
||||
Число выдач оценивается по их rates, но закрывать можно только избыточные части сверх k.
|
||||
Недостаточная полоса без избыточных частей требует пропуска устаревших данных, а не разрушения FEC-набора.
|
||||
|
||||
OVERLOAD содержит request_id, расчётное число выдач, требуемое сокращение rate и список handles с фиксированными rates.
|
||||
Получатель выбирает handles с учётом FEC и альтернативных поставщиков и отправляет OVERLOAD_SELECT.
|
||||
Получатель выбирает handles только при сохранении k различных работающих частей и отправляет OVERLOAD_SELECT.
|
||||
Альтернатива должна уже получать DATA с действительными метриками без остановки; ожидающий INFO не является заменой.
|
||||
Одновременный выбор для других перегруженных пиров тоже исключается из доступных альтернатив.
|
||||
Отправитель исполняет выбор, только пока актуален исходный request_id, и сообщает CLOSED.
|
||||
Получатель не отсоединяет выбранные handles до CLOSED: поздний SELECT после принудительного закрытия не режет дополнительные выдачи.
|
||||
При разных rates одного количества закрытий недостаточно: суммарный снятый rate должен покрыть требование.
|
||||
Получатель не отсоединяет выбранные handles до CLOSED; поздний SELECT после завершения запроса не меняет выдачи.
|
||||
При разных rates снятая нагрузка считается по сумме rates; безопасность FEC важнее полного исполнения требования.
|
||||
Rate закрываемых выдач фиксируется по оценке на момент запроса, чтобы условие исполнения не менялось на каждом тике.
|
||||
Срок реакции — 2 секунды с первого запроса; повторы не сбрасывают срок и не начинают новое требование.
|
||||
Новые выдачи, компенсирующие запрошенное сокращение, до разгрузки не принимаются.
|
||||
Если требование не выполнено, отправитель сам закрывает достаточный остаток выдач и сообщает handles/причину.
|
||||
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку, а при равенстве допустима случайность.
|
||||
Если очередь опустела, запрос завершается сразу. По истечении 2 секунд отправитель завершает запрос без слепых закрытий.
|
||||
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку.
|
||||
При глобальном отставании сокращение выбирается по всем пирам модуля.
|
||||
Память и возраст очереди остаются ограниченными во время ожидания; просроченные задания удаляются с GAP/EXPIRED.
|
||||
Возраст ожидающих DATA используется для диагностики перегрузки, но не разрешает пропуск данных.
|
||||
Backpressure сохраняет курсоры и непрерывную цепочку кадров. GAP обозначает фактическое вытеснение/утрату диапазона.
|
||||
Принятые ETCP пакеты и подписки сохраняются. Завершённый источник дочитывается до подписанного END.
|
||||
|
||||
13. Статусы через опции мембера и Merkle
|
||||
|
||||
@@ -502,7 +518,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
|
||||
- Перекрытие поставщиков без дублирования потребителю, rollback, одновременное восстановление нескольких субпотоков.
|
||||
- Цикл по устаревшим метрикам, одновременная перестройка и выдача сохранённых путей: DATA не циркулирует.
|
||||
- Глитч короче подтверждения, устойчивое отставание 500 мс, нулевой выход и локальная перегрузка uasync.
|
||||
- Выбор числа закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и закрытие остатка через 2 секунды.
|
||||
- Выбор безопасных закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и сохранение k после 2 секунд.
|
||||
- 32 совместимых кандидата на старте, далее 1/с, маленький/большой список и запрет повторного полного цикла раньше 30 с.
|
||||
- Merkle-опции до/после BGP-события, офлайн-опция не кандидат, поздний участник, перезапуск и сохранение других полей userinfo.
|
||||
- Мобильный источник/получатель и исключение мобильных из фоновых проб ретрансляторов.
|
||||
@@ -592,13 +608,14 @@ Stop/destroy выполняются по правилам ядра, вне callb
|
||||
Rates фиксируются при создании OVERLOAD. Расчётное число — подсказка, исполнение проверяется по суммарному rate.
|
||||
Получатель предпочитает закрытия, сохраняющие k с учётом других поставщиков, затем parity при равных условиях.
|
||||
OVERLOAD_SELECT предлагает handles; source закрывает только актуальное требование, поздний/повторный выбор безопасен.
|
||||
Срок 2с начинается с первого запроса и не меняется при повторе. Невыполненный остаток source закрывает сам.
|
||||
Срок 2с начинается с первого запроса и не меняется при повторе. С 25.10 остаток без безопасного SELECT не закрывается.
|
||||
Новые SUBSCRIBE не компенсируют разгрузку: запрет действует до исполнения и 500мс снятого давления;
|
||||
stream_peer_get_load.accepting сообщает этот статус отдельно от request_active.
|
||||
Глобальный бюджет включается только для backlog нескольких пиров вместе с CPU/опозданием callbacks;
|
||||
CPU отдельно не отключает выдачи, уже запрошенное сокращение повторно не учитывается.
|
||||
Control FIFO ограничена 128 кодограммами по 1094 байта, возрастом 4с и отменой неподтверждённых запросов.
|
||||
Просроченные ожидающие DATA при активном OVERLOAD учитываются X и пропускаются явным GAP, кольцо не стирается.
|
||||
Удаление DATA только по возрасту отменено: оно разрезало KEY и вызывало постоянные resync при недостаточной полосе.
|
||||
GAP сообщает фактическую потерю диапазона; после END ожидающие DATA дочитываются.
|
||||
Диагностика сообщает A/D/X/Q, age, input/output/g, длительность интервала, cpu/late callbacks,
|
||||
frozen/released rates, request/deadline и нижние очереди/inflight; D остаётся принятием ETCP, а не доставкой.
|
||||
test_stream_core: баланс, X отдельно от D, граница 500мс, глитч, смена состава, 10-секундное окно и широкие wire-метрики.
|
||||
@@ -666,26 +683,30 @@ Stop/destroy выполняются по правилам ядра, вне callb
|
||||
|
||||
Все stream-числа на wire — big-endian, без packed-структур и зависимости от host byte order.
|
||||
Версия stream wire — 4; обратная совместимость не поддерживается.
|
||||
Заголовок raw (52 байта, version=5): cmd:1, version:1, command:1, subblock:1,
|
||||
Заголовок raw (52 байта, version=7): cmd:1, version:1, command:1, subblock:1,
|
||||
group_id:8, source_node_id:8, source_epoch:8, sender_epoch:8, receiver_epoch:8, handle:8.
|
||||
INFO_REQ/RESP согласуют sender/receiver epochs; receiver_epoch=0 используется INFO_REQ для обнаружения поколения и REVERSE_REQ.
|
||||
Подписанный routed payload содержит тот же заголовок; его group_id сверяется с реальной группой router,
|
||||
sender берётся из end-to-end router metadata. DATA через routed service отвергается.
|
||||
|
||||
INFO_REQ: probe_nonce:8, не резервирует номер SUBSCRIBE.
|
||||
INFO_REQ: probe_nonce:8, want_media:1 (0/1), не резервирует номер SUBSCRIBE.
|
||||
want_media=1 просит поставщика подготовить общий медиаиндекс; поиск KEY выполняется отложенно с ограниченным бюджетом.
|
||||
INFO_RESP/ACCEPT: probe_nonce:8 (0 в ACCEPT), actual_seq:8, first_available:8, last_available:8, n:1, r:1, block_size:2,
|
||||
metric_flags:1, lag_ms:8, source_rate:8, receive_rate:8, blocks:8, late_blocks:8, stops:8,
|
||||
offer_flags:1, free_slots:2, cpu_basis_points:2, queue_bytes:8, progress_age_ms:8, path_rtt_ms:4,
|
||||
path_count:1, node_ids:8*path_count — фиксированная часть 111 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
|
||||
key_seq:8, key_last_seq:8, path_count:1, node_ids:8*path_count — фиксированная часть 127 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
|
||||
metric_flags: bit0 — измерение известно, bit1 — source_rate известен, bit2 — receive_rate известен, bit3 — stopped.
|
||||
offer_flags: bit0 — принимает новые выдачи, bit1 — RTT цепочки известен, bit2 — возраст прогресса известен,
|
||||
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока. CPU — 0..10000 (сотые доли процента).
|
||||
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока, bit5 — полный KEY сохранён для выдачи этой части.
|
||||
При bit5=0 обе позиции KEY равны UINT64_MAX; при bit5=1 first_available <= key_seq <= key_last_seq <= last_available.
|
||||
Каждая часть диапазона KEY проверяется на наличие; свежий незавершённый KEY не заменяет предыдущий полный.
|
||||
CPU — 0..10000 (сотые доли процента).
|
||||
Пустой источник: first/last=UINT64_MAX, bit4=0, INFO actual_seq=UINT64_MAX, ACCEPT actual_seq=0, путь {source}.
|
||||
Курсор last+1 допустим только в ACCEPT непустого кольца; DATA/END проверяются по точной позиции и подписи источника.
|
||||
SUBSCRIBE: requested_seq:8; REJECT: reason:1, для SUBSCRIBE дополнительно first_available:8,last_available:8,
|
||||
для INFO дополнительно probe_nonce:8. Старый nonce не подтверждает новую пробу.
|
||||
RENEW: один header handle без payload (каждая кодограмма продлевает один элемент списка).
|
||||
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,неизменяемая часть RS-пачки переменной длины.
|
||||
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,frame_timing:48,неизменяемая часть RS-пачки переменной длины.
|
||||
GAP: old_cursor:8,next_cursor:8, уведомление включает диапазон [old_cursor,next_cursor).
|
||||
END: "stream_end":10,group_id:8,source_node_id:8,source_epoch:8,last_seq:8,Ed25519:64 — всего 106 байт.
|
||||
UINT64_MAX в END означает пустой источник; принимается только выдачей с next=0 и без ранее принятых DATA.
|
||||
@@ -743,7 +764,7 @@ Restart в той же source_epoch сохраняет consumer cursor: bootstra
|
||||
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника.
|
||||
|
||||
Медиаблок uAV2 находится целиком внутри входа RS, поэтому header восстанавливается вместе с данными.
|
||||
При штатных 6+2 полный медиаблок <=7345 байт. Раскладка big-endian, без packed-структур:
|
||||
При штатных 8+0 полный медиаблок <=9833 байт. Раскладка big-endian, без packed-структур:
|
||||
|
||||
fixed header (32 байта):
|
||||
magic="uAV2":4, video_codec:1, audio_codec:1, video_count:1, audio_count:1,
|
||||
@@ -811,13 +832,15 @@ Android:
|
||||
Разделённые CODEC_CONFIG buffers собираются отдельно с пределом 64KiB до разбора конфигурации.
|
||||
- Camera MediaCodec Surface timestamps и AudioTimestamp.TIMEBASE_MONOTONIC приводятся к общему
|
||||
origin System.nanoTime; время callback/получения блока не заменяет capture PTS.
|
||||
- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo. Перед Opus AUDIO/128kbps
|
||||
- Встроенный микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo (mono fallback при отказе формата).
|
||||
Гарнитуры используют разговорный тракт; mono приводится к stereo перед Opus AUDIO/128kbps. Перед кодированием
|
||||
применяется тот же audio_compressor, что в TX рации: общие настройки включения, max_gain и rise_rate,
|
||||
lookback=200ms, lookahead=0, release=rise*5, target=0.25. История и gain принадлежат только audio worker
|
||||
трансляции; оба канала используют общий gain, сохраняющий стереобаланс. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона,
|
||||
Opus или source_epoch; compressor не добавляет задержку и не меняет capture PTS.
|
||||
- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread, decoder — отдельный viewer worker,
|
||||
общий AudioTrack — worker StreamAudioMixer. Перекладка плиток сохраняет TextureView/decoder.
|
||||
- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread. Каждый viewer имеет
|
||||
audio/control worker и независимый video worker; общий AudioTrack — worker StreamAudioMixer.
|
||||
Перекладка плиток сохраняет TextureView/decoder.
|
||||
- Native bridge использует общий stream_publisher для TX и stream_viewers для RX.
|
||||
stream_publisher держит pthread mutex, ограниченные очереди и один pending post либо media timer;
|
||||
статистика обслуживается отдельным таймером 100ms. Тот же модуль используется desktop.
|
||||
@@ -826,22 +849,39 @@ Android:
|
||||
UI не сохраняет borrowed inst/ua. Idle без pending data не будит мобильное ядро.
|
||||
- TX<=2MiB/128 records, не более 250ms ожидания. Actor публикует <=2 пачек за вызов,
|
||||
неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms.
|
||||
Opus целиком занимает header32 + audio descriptor16 + payload; точное заполнение допустимо.
|
||||
Без ожидающего видео резерв под video descriptor отсутствует. Обычная смешанная пачка
|
||||
сохраняет место под описание и непустой фрагмент видео. Если первый Opus помещается только отдельно,
|
||||
второй слот того же actor отдаётся видео без ожидания сборки неполной пачки.
|
||||
feed=1 означает копирование в очередь. Actor после определения FEC-ёмкости при open удаляет Opus,
|
||||
не помещающийся даже отдельно, с WARN (ID, PTS, bytes, max, capacity), не блокируя следующие записи.
|
||||
При video pressure отбрасываются целые ожидающие кадры, P-кадры запрещены до нового KEY.
|
||||
Кодировщик получает отказ admission и запрашивает KEY с ограничением частоты 500ms.
|
||||
- RX: stream_viewers держит отдельный handle/cursor, demand, status и один owned ответ на session ID.
|
||||
- RX: stream_viewers держит отдельный handle/cursor, demand, status и ограниченный owned батч на session ID.
|
||||
Android принимает до 16 записей: максимум один новый видеокадр и звук в пределах собственного lookahead.
|
||||
Общей RX FIFO нет: остановка/отставание одного reader не задерживает остальных.
|
||||
Получение ответа и сетевые AVAILABLE не запускают автоматическое чтение следующего кадра.
|
||||
Запас остаётся в RS-кольце. want_video запрашивает следующий полный видеокадр независимо от часов показа;
|
||||
audio_ahead ограничивает только звук относительно PTS потребителя. Управление/звук обслуживаются независимо от video pressure.
|
||||
StreamVideoPlayback владеет MediaCodec и принимает mailbox до двух кадров от audio/control worker.
|
||||
StreamDecodePump держит один рабочий кодированный кадр и один ожидающий output MediaCodec.
|
||||
Decoder заполняет свой внутренний конвейер до первого выхода без ограничений по числу поданных кадров или video lookahead.
|
||||
Перед каждым входом проверяется выход; свободный input buffer берётся только для уже готового рабочего кадра.
|
||||
Будущий output ждёт своего PTS и приостанавливает новые video requests; уже запрошенный ответ помещается в рабочий input.
|
||||
Низкая задержка запрашивается через KEY_LOW_LATENCY при заявленной поддержке, но корректность не зависит от этого режима.
|
||||
За проход обслуживаются до четырёх media replies, до восьми output buffers за один вызов pump.
|
||||
За проход обрабатываются до 32 готовых записей, до восьми output buffers за один вызов pump.
|
||||
Готовый батч и расход PCM будят worker через отдельную condition variable, без JVM callbacks из ядра.
|
||||
Video worker проверяет MediaCodec каждые 5ms при незавершённой работе, иначе ждёт до 50ms;
|
||||
audio worker будят media, расход PCM и изменение video state. Без звука его wall clock обновляется каждые 5ms.
|
||||
Между готовыми записями sleep нет. Audio worker владеет signal до stream_stop, join video worker
|
||||
и отсоединения своего mixer input; detach ядра будит signal, не освобождая его.
|
||||
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY.
|
||||
Просроченный кадр при нормальной последовательности декодируется без показа.
|
||||
- Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра.
|
||||
- Первый декодированный кадр разрешает audio output; общий PCM-модуль desktop/Android перед первым звуком
|
||||
и после underrun накапливает 250ms готового PCM. Это порог готового звука, без аппаратной задержки и lookahead DSP.
|
||||
Во время накопления backend выдаёт silence с неподвижным исходным PTS; decoder references сохраняются.
|
||||
Уже переданный устройству PCM продолжает воспроизводиться. До возобновления регулятор держит темп 1x;
|
||||
затем избыток сокращается по обычной истории минимального запаса. EOF снимает порог и выпускает весь короткий хвост.
|
||||
AudioTrack задаёт время показа видео по реально услышанному PCM;
|
||||
без аудио используются монотонные часы с остановкой на границе готовых кадров.
|
||||
Android StreamAudioOutput принадлежит StreamAudioMixer и хранит один недописанный общий PCM (до 480 stereo frames).
|
||||
@@ -851,13 +891,14 @@ Android:
|
||||
Заполнение поддерживается до фактического bufferSizeInFrames устройства; API>=31 задаёт start threshold=1.
|
||||
При EOF недописанный PCM передаётся полностью; mixer выдаёт тишину после последнего кадра источника
|
||||
до его физического воспроизведения. END одного input не закрывает AudioTrack остальных.
|
||||
Hold/starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS.
|
||||
Starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS.
|
||||
Видео не удерживает звук после старта; накопившийся из-за тишины избыток погашается ускорением.
|
||||
GAP создаёт новый input/origin, сохраняя общее устройство и часы остальных трансляций.
|
||||
Watchdog 2s проверяет отсутствие движения hardware head только при достаточном заполнении либо заблокированной записи;
|
||||
намеренная пауза и ожидание данных до порога запуска не считаются зависанием. Ошибка содержит состояние и счётчики устройства.
|
||||
Защитный guard минутного контроллера учитывает audio device lead и наблюдённое опережение decoder
|
||||
(максимум last_input_PTS-output_PTS текущей revision плюс период кадра). Drain не обнуляет этот guard;
|
||||
новый decoder начинает измерение заново. Отдельного контроллера задержки декодера нет.
|
||||
Защитный guard аудиоконтроллера — максимум 40ms и audio device lead + 20ms, плюс
|
||||
внутренний lookahead SoundTouch (SETTING_INITIAL_LATENCY при текущем темпе).
|
||||
Наблюдённое опережение decoder остаётся диагностикой и не увеличивает задержку звука.
|
||||
Отсутствие output progress 10s при наличии работы даёт ERROR; сетевые паузы и ожидание PTS допустимы.
|
||||
Состояния ожидания KEY и decoder разделены; логи показывают inputs/decoded/shown, рабочие slots, inflight и decoderLeadUs.
|
||||
Короткие audio gaps заполняются Opus PLC, большой разрыв сбрасывает timeline.
|
||||
@@ -925,17 +966,20 @@ Android:
|
||||
Это ограниченный drain, не подтверждение воспроизведения всеми viewers. Abort/core restart очищает очередь сразу.
|
||||
Завершение логирует срок drain, оставшиеся RS-байты выдач и локальных readers; RS=0 не означает подтверждение доставки ETCP.
|
||||
Поздние команды старого session ID не меняют новый запуск.
|
||||
- Рация работает одновременно с публикацией. При совпадении выбранного входа она получает копию
|
||||
PCM CAMCORDER 48kHz stereo с исходными timestamp; её source/stereo предпочтения не меняют трансляцию.
|
||||
Другой явно выбранный вход открывается отдельно; отказ не заменяет его системным default.
|
||||
Трансляция имеет приоритет глобального AudioManager: рация не включает разговорный режим и не меняет SCO
|
||||
общего тракта. Перед запуском трансляции ранее работавшая рация освобождает capture и затем подключается
|
||||
к фактическому входу трансляции либо повторно открывает свой отдельный вход.
|
||||
Очереди потребителей ограничены; PTT упорядочен с PCM, разрыв сбрасывает историю детектора рации.
|
||||
Последняя подписка закрывает AudioRecord; stop рации/трансляции не закрывает захват другого потребителя.
|
||||
Проверяется фактический Android route и системное silencing: конфликтующий отдельный capture рации
|
||||
завершается с диагностикой, приём рации сохраняется. Окно VAD/экран/разрешения не обходятся общим захватом.
|
||||
- Рация, публикация и просмотр трансляций используют общий профиль MediaAudioController: AUTO/PHONE/WIRED/BLUETOOTH.
|
||||
Выбор из любого экрана меняет общий вход и маршруты всех активных AudioTrack; настройка сохраняется одна.
|
||||
Аппаратный микрофон один, очереди потребителей и обработка PCM независимы. PTT упорядочен с PCM.
|
||||
Подключение публикации переводит встроенный вход в CAMCORDER stereo; профиль сохраняется до последней подписки.
|
||||
Смена устройства сохраняет подписки, камеру, видеокодер, source_epoch и временную шкалу эфира.
|
||||
MediaCaptureTimeline выдаёт 20ms PCM в общей сетке с запасом 200ms; аппаратные timestamps задают положение звука.
|
||||
Отсутствующий PCM заменяется тишиной. Запоздавшие данные отбрасываются, а не получают новые PTS.
|
||||
Подтверждаются фактический маршрут, свежий PCM и отсутствие системного silencing. При ошибке — диагностика
|
||||
и резервный телефонный профиль; при полном отказе входа публикация продолжает видео с тишиной.
|
||||
Последний потребитель закрывает вход; выходы живут до удаления своих AudioTrack. Просмотр не открывает микрофон.
|
||||
Окно VAD/экран/разрешения не обходятся общим захватом. Ошибки видны в общем меню выбора устройства.
|
||||
Звонок и voice recording по-прежнему исключают публикацию в обе стороны.
|
||||
Аппаратная проверка (debug APK, RECORD_AUDIO разрешён):
|
||||
adb shell am instrument -w com.utun.chat/com.utun.chat.data.MediaAudioProbe
|
||||
|
||||
Звук трансляции и рация (Android и desktop):
|
||||
- Во время публикации с микрофоном звук можно выключить и включить. Заглушается только копия PCM
|
||||
@@ -1026,23 +1070,41 @@ Desktop:
|
||||
в uasync. Протокол uAV2 и wire v5 при добавлении desktop publisher не меняются.
|
||||
- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник.
|
||||
Player содержит имя, состояние и громкость; fullscreen/close в сетке принадлежат общему окну.
|
||||
- transport/stream_bridge владеет stream_viewers: независимый cursor и один ближайший ответ на reader.
|
||||
- transport/stream_bridge владеет stream_viewers: независимый cursor и ближайший батч на reader.
|
||||
Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex.
|
||||
Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers.
|
||||
Запрос задаёт число видеокадров, audio lookahead и максимум 16 записей. Проход reader ограничен 2ms/256KiB;
|
||||
последняя целая запись может пересечь порог байтов, но один кадр не превышает 2MiB.
|
||||
При WAIT/MORE готовая часть отдаётся сразу. Получение батча погашает запрос, включая его повторы до pull.
|
||||
Callback готовности будит worker через condition variable; GUI/audio также будят его при освобождении места.
|
||||
Stop снимает callback до уничтожения worker; detach будит ожидание перед освобождением manager.
|
||||
- stream_media_cursor собирает ближайший access unit выбранного кодека и выдаёт Opus 48kHz mono/stereo/20ms из RS-кольца.
|
||||
Android AudioTrack и desktop output работают в stereo; mono-пакеты upmix выполняет libopus.
|
||||
FFmpeg/Opus декодируются отдельным worker; Qt-очередь не накапливает кадры.
|
||||
Worker держит один рабочий AVPacket и не более двух готовых изображений. Следующий video request не ограничен часами показа.
|
||||
Opus/SoundTouch и FFmpeg декодируются разными workers каждого player; Qt-очередь не накапливает кадры.
|
||||
Video worker держит один рабочий AVPacket и не более двух готовых изображений; audio worker передаёт ему
|
||||
ближайшие кодированные кадры через ограниченную очередь. Следующий video request не ограничен часами показа.
|
||||
Бюджет нового видео учитывает AVPacket, готовые изображения и оставшиеся кодированные кадры батча.
|
||||
Записи аудио/видео расходуются независимо по готовности потребителя; заполненное видео не блокирует запрос звука.
|
||||
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека и не ждут расходования другого трека.
|
||||
END подтверждает выдачу обоих треков. Нулевые бюджеты media также разрешают получение управляющих событий.
|
||||
GAP/CLOSED отменяют прежний батч и сбрасывают frontend сразу; старые media перед ними не доигрываются.
|
||||
Между готовыми пакетами нет sleep или нового round trip к ядру.
|
||||
FFmpeg output обслуживается перед input; EAGAIN сохраняет рабочий packet и означает backpressure, не GAP.
|
||||
Дальние кадры и звук не копируются из кольца. SoundTouch и устройство держат только рабочую порцию PCM.
|
||||
Кадры сохраняются до достижения их PTS аудиочасами; задержка показа удерживает clock только этого player.
|
||||
Видео декодируется без задержки frame-threading. При GAP сбрасываются decoder refs и timeline до KEY.
|
||||
- Первый decoded frame разрешает движение clock/audio, защитный guard учитывает наблюдённый decoder lead.
|
||||
FIN отправляет decoder EOS; выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
|
||||
Кадры сохраняются до достижения их PTS аудиочасами; опоздавшие более чем на 100ms отбрасываются до RGB conversion.
|
||||
FFmpeg использует четыре slice threads без frame-threading. При GAP сбрасываются decoder refs и timeline до KEY;
|
||||
поколение video worker исключает публикацию результатов прежнего decoder.
|
||||
- Первый decoded frame разрешает движение clock/audio; затем позднее видео не удерживает звук.
|
||||
VIDEO_END отправляет decoder EOS, AUDIO_END дренирует SoundTouch независимо от видеодекодера.
|
||||
Общий END не управляет сбросом отдельных декодеров: иначе заполненный PCM, ожидание первой картинки и decoder EOS образуют цикл.
|
||||
Выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
|
||||
После физического воспроизведения последнего PCM оставшееся видео использует монотонные часы;
|
||||
EOF видеодекодера не влияет на независимое воспроизведение оставшегося звука.
|
||||
Ожидающий кадр не теряется при заполненной очереди изображений. Отсутствие прогресса при наличии работы логируется как ошибка.
|
||||
- При начальном подключении desktop/Android прогревают Opus из кольца до первого KEY (до 500ms истории).
|
||||
Старый звук обрезается по PTS видео, включая начало stereo PCM-пакета; декодер не сбрасывается из-за preroll.
|
||||
Фиксированного prebuffer нет: показ следует физическим audio callback timestamps;
|
||||
Звук начинается после накопления 250ms готового PCM; после underrun применяется тот же порог без сброса decoder/PTS.
|
||||
EOF выпускает хвост любой длины. Показ следует физическим audio callback timestamps;
|
||||
без звука — монотонным часам с ограничением готовым PTS. END до KEY завершает пустой reader.
|
||||
Короткие пропуски аудио заполняются Opus PLC; большой разрыв/pressure сбрасывает timeline.
|
||||
Закрытие окна снимает только reader, собственная публикация сохраняется.
|
||||
@@ -1104,7 +1166,15 @@ StreamDecodePumpTest проверяет конвейеры 4/8 кадров пр
|
||||
test_stream_playback проверяет восемь будущих кадров из кольца без video lookahead; test_android_stream — один ответ
|
||||
при повторных запросах, отсутствие скрытого prefetch и возобновление с прежней позиции.
|
||||
test_stream_view дополнительно задерживает четыре настоящих FFmpeg decoded frames: запуск требует кадра после 80ms,
|
||||
а короткий поток из трёх кадров получает все изображения после FIN, без потери decoder tail.
|
||||
а короткий поток из трёх кадров получает все изображения после FIN, в том числе при заполненном аудиобуфере.
|
||||
Проверяются также 8 кадров, отсутствие звука и аудиотрек короче/длиннее видео; завершение ждёт оба выходных хвоста.
|
||||
StreamReadQueueTest проверяет независимые EOF, отмену старых записей по GAP/CLOSED и границы записей батча.
|
||||
test_android_stream проверяет батч из 3 video + 3 audio, уведомления без потерь и пробуждение ожидающего worker при stop.
|
||||
Батчевый сценарий проверяет накопленный поток, раздельные бюджеты треков, независимые readers, уведомления и остановку:
|
||||
28 video + 50 audio записей проходят через 16 батчей без пропусков/повторов, END следует за последними данными.
|
||||
Диагностика debug: viewer buffer выводит число/максимальный размер батчей и prepare/handoff (среднее/максимум);
|
||||
desktop viewer work — обработанные записи, batch_max, pending_records, data_signals и wait_us (сумма/максимум).
|
||||
wait_us включает полезное ожидание новых данных/места; задержка уже готового батча измеряется handoff_us.
|
||||
test_stream_publish проверяет реальные encoder/decoder H.264/H.265/VP9/AV1, обновление libx264 bitrate
|
||||
с прежним decoder, общую публикацию/RS, смену revision, pressure/KEY recovery, FIN и concurrent detach
|
||||
с балансом tracked allocations. Дополнительный Xvfb/private D-Bus сценарий проверяет реальный экран/окно,
|
||||
@@ -1147,23 +1217,31 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
|
||||
- AVAILABLE сообщает готовность индекса, DATA/GAP/END callbacks описывают live ingest.
|
||||
media_read выдаёт END после обоих треков конкретного reader. MORE продолжает ограниченную сборку
|
||||
отдельным callback без ожидания новых пакетов; WAIT ожидает новые данные или изменение позиции.
|
||||
- Общий C-контроллер хранит 60 секундных минимумов запаса: ready_PTS - playback_PTS.
|
||||
ready_PTS — меньшая из границ полного видео и звука, для preview без звука — граница видео.
|
||||
Каждое обновление учитывается в минимуме текущей секунды, включая короткий простой.
|
||||
- Декодированный PCM пополняется до 250ms при наличии данных; перед стартом и после underrun consumer ждёт этот порог.
|
||||
При EOF остаток выпускается без порога. Во время накопления PTS удерживается silence span; Opus/SoundTouch не сбрасываются.
|
||||
Это запас отдельного audio worker, размер аппаратного буфера не увеличивается. Audio lookahead — device lead + 500ms,
|
||||
достаточный для 250ms PCM даже при x1.6. SPSC имеет 128 слотов по 960 stereo frames с собственными PTS spans.
|
||||
Mixer/callback не вызывает Opus/SoundTouch; Android переиспользует PCM-массив каждого источника.
|
||||
- Общий C-контроллер хранит четыре минимума за интервалы по 50ms: ready_PTS - playback_PTS.
|
||||
ready_PTS — граница готового звука, пока аудиотрек активен; без звука/после AUDIO_END — максимум границ треков.
|
||||
Каждое обновление учитывается в минимуме текущего интервала, включая короткий простой.
|
||||
История нормализована через correction += wall_elapsed - actually_played_media_elapsed.
|
||||
Старые минимумы автоматически увеличиваются на длительность паузы и уменьшаются на реальный расход при догоне.
|
||||
Измерения опираются на PTS фактически услышанного PCM, включая аппаратную очередь и периоды тишины.
|
||||
- После полной здоровой минуты темп плавно повышается до x1.1; SoundTouch сохраняет тон и стерео.
|
||||
Ускорение расходует только минимальный запас за минуту сверх безопасной границы (не менее 33ms,
|
||||
с учётом аппаратной задержки), уже поставленного ускоренного звука и плавного торможения.
|
||||
- После 200ms без фактической нехватки темп плавно повышается до x1.6; SoundTouch сохраняет тон и стерео.
|
||||
Темп пропорционален избытку (масштаб 2s), изменение ограничено x0.6 за 500ms.
|
||||
Ускорение расходует только минимальный запас сверх защитной границы, уже подготовленного/поставленного
|
||||
ускоренного звука и плавного торможения. Каждый источник имеет собственный controller и отображение PTS.
|
||||
При нехватке данных темп возвращается к x1.0, часы ждут, сетевой приём продолжает накапливать запас.
|
||||
После возобновления отсчёт здоровой минуты начинается заново. Разрыв/GAP/новая эпоха сбрасывают историю.
|
||||
После возобновления начинается новое окно 200ms. Разрыв/GAP/новая эпоха сбрасывают историю.
|
||||
- Диагностика stream: PTS, ready, текущий/минимальный запас, correction, healthy interval, tempo,
|
||||
pending gain, wait/resume, generation, причины resync и размеры рабочих очередей. DEBUG — раз в секунду.
|
||||
test_stream_playback проверяет историю, плавный догон с отложенной аппаратной очередью,
|
||||
независимые readers, позднюю повреждённую RS-часть, вытеснение, GAP, FIN и большой многопачечный кадр.
|
||||
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1, сохранение тона/баланса,
|
||||
отображение аппаратного frame counter в исходный PTS и остановку PTS на тишине.
|
||||
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1/x1.6, сохранение тона/баланса,
|
||||
конкурентные producer/consumer, запас 250ms, аппаратный frame counter и остановку PTS на тишине.
|
||||
test_stream_playback моделирует паузы 200ms/4s с отложенным выходом 350ms и проверяет возврат к guard без перерасхода.
|
||||
test_stream_view задерживает видеодекодер на 500ms и проверяет непрерывность аудио.
|
||||
|
||||
21. Суперузлы: помощь в раздаче по запросу
|
||||
|
||||
@@ -1248,3 +1326,125 @@ callback обнаружения поставщика без секундной
|
||||
раздачу нескольким зрителям, общий owner с локальным viewer, остановку при уходе последнего,
|
||||
поздний повтор запроса до/после удаления tombstone, CANCEL до REQUEST, истечение READY без SUBSCRIBE,
|
||||
откат неуспешного добавления viewer, исключение relay-only кандидатов и передачу пустого подписанного END.
|
||||
|
||||
Диагностика доставки и индикатор буфера
|
||||
|
||||
[debug] video=debug включает две сводки с периодом не меньше 500 мс и фактическим dt_ms.
|
||||
stream NET — одна строка на соседа для всех его трансляций и субпотоков вместе:
|
||||
- TX: sent/drop/gap за интервал, remain до головы локального кольца, возраст очереди и скорость её роста.
|
||||
- ETCP: очередь до нормализации/отправки, уникальные unacked DATA, физический flight и сумма окон линков,
|
||||
уникальные acked_B, принятые транспортом повторы и число физических RTO за интервал; очередь пересборки RX.
|
||||
Все поля ETCP относятся к общему соединению с пиром, включая другие сервисы. unacked и flight не складываются.
|
||||
errors_total — суммы счётчиков send/recv/crypto существующих линков; удаление/сброс линка меняет базу этих сумм.
|
||||
- PATH: рабочие UDP/TCP, stalled, заполненные окна для первого ожидающего DATA, pacing, RTT/jitter/RTO и причина ожидания.
|
||||
- RX: received/gap/dup/expired за интервал, время без DATA, максимальное относительное отставание и число stopped.
|
||||
worst=group/source/epoch/sub обозначает самый отставший измеренный субпоток. connecting — все ещё открывающиеся выдачи пира.
|
||||
Счётчики блоков/байтов записаны как blocks/bytes. sent означает принятие ETCP, не ACK.
|
||||
na — нет измерения. После смены эпохи changed=1, транспортные дельты na; следующая сводка имеет новую базу.
|
||||
ack_idle_ms начинается только после реального логического ACK, не после отправки/keepalive.
|
||||
Метрика lag относительна исходной базе timestamp, не является абсолютной задержкой от источника.
|
||||
|
||||
stream PLAY — одна строка на плеер/источник: TIME (позиция, непрерывная готовая граница, запас, минимум, guard,
|
||||
темп и причина решения), VIDEO (показ/отброс за интервал, PTS показанного/переданного поверхности кадра,
|
||||
av_ms=video PTS минус media clock, очередь и ожидание KEY), AUDIO (PCM, устройство, underruns/тишина за интервал).
|
||||
Длительности — мс, tempo=1000 означает 1x. generation/epoch отделяют новые временные шкалы.
|
||||
GAP, перегрузка, смена поставщика и restart выводятся отдельными событиями. Подробности кодеков/кадров/GUI — video=trace.
|
||||
Kotlin videoDebug/videoTrace используют native-фильтр и общий log hook, включая файловый/UDP вывод.
|
||||
|
||||
Индикатор публикации обновляется раз в 500 мс независимо от уровня логирования:
|
||||
Android — 96x48dp слева от Start/Stop в шапке, desktop — рядом с этой кнопкой над preview.
|
||||
Горизонталь [first, head] показывает сохранённое окно, где head — следующий seq после последнего блока.
|
||||
Красная/жёлтая/зелёная трети обозначают близость к вытеснению/середину окна/близость к голове.
|
||||
Линия объединяет субпотоки одного получателя и отмечает худшую и лучшую позиции передачи в ETCP.
|
||||
Показываются до 16 наиболее отставших получателей, порядок при равенстве — ID. Позиции вне окна прижимаются к краям.
|
||||
Индикатор отражает чтение кольца для отправки; неподтверждённые транспортные данные учитываются отдельно в inflight.
|
||||
|
||||
Восстановление доставки и старт с полного KEY — 25.10.2026
|
||||
|
||||
Отправитель не пропускает очередь по времени: 1-секундный catchup удалён. Возраст и рост очереди остаются метриками.
|
||||
Последняя полная проверенная RS-пачка обновляет progress_ms общей сессии. Приход отдельных частей/дубликатов,
|
||||
INFO/RENEW и уже объявленные GAP не являются таким прогрессом. Отсутствие полной пачки 5с запускает перезапрос, в том числе до первого DATA;
|
||||
следующая попытка не раньше чем через 5с. Сохраняются сессия, принятые части, consumer/media cursors и decoder references.
|
||||
Переоткрываются лишь зависшие выдачи, свежие INFO обновляют диапазоны и точки KEY поставщиков.
|
||||
Счётчик selection.restarts означает попытки перезапроса, а не сбросы медиабуфера.
|
||||
|
||||
INFO и ACCEPT сообщают last_available и [key_seq,key_last_seq] последнего полного сохранённого KEY.
|
||||
Медиаиндекс обновляет KEY только после всех фрагментов; позднее создание индекса ищет начало назад с бюджетом
|
||||
и индексирует вперёд. Снимок использует уже известную точку, без повторного сканирования всего 32-МБ кольца.
|
||||
INFO_REQ.want_media позволяет реле подготовить индекс без локального плеера; частичный relay не объявляет непроверенный KEY.
|
||||
Начальная media-подписка выбирает KEY из INFO и открывает части параллельно с одной явной позиции.
|
||||
Обычный непрозрачный stream сохраняет прежний старт с последней доступной пачки.
|
||||
|
||||
При восстановлении сначала запрашивается текущая недостающая пачка. Если свежие диапазоны/отказы поставщиков
|
||||
и локальные части не позволяют собрать k, выбирается полный доступный KEY не ниже уже пройденной границы.
|
||||
Отправитель не заменяет согласованную позицию самостоятельно; вытеснение между INFO и SUBSCRIBE возвращает EXPIRED.
|
||||
Данные старых handles не примешиваются к новой попытке. Пропущенный диапазон выдаётся одним явным GAP;
|
||||
новая цепочка начинает декодирование с KEY, уже выданные DATA/PTS не повторяются.
|
||||
|
||||
Desktop/Android не закрывают соединение по отдельному 10-секундному ожиданию KEY: отсутствие DATA обслуживает ядро.
|
||||
Ошибка декодера, смена эпохи/членства, остановка сервиса и подписанный END сохраняют собственную обработку.
|
||||
Новый KEY может занимать несколько пачек; продвижение этих пачек предотвращает ложный перезапрос во время сборки кадра.
|
||||
Автор публикует все n частей вместе: один INFO разрешает параллельный выбор, каждое открытие проходит собственный INFO/ACCEPT.
|
||||
Реле проверяются отдельно для каждой части. Подтверждённая поломка уже работавшего транспорта допускает повтор через 250мс.
|
||||
|
||||
Диагностика video:
|
||||
- stream source offers ready — first/last, key/key_last, valid;
|
||||
- stream recovery request — epoch, недостающий next, idle_ms, число переоткрытых выдач, preserve_media=1;
|
||||
- stream recovery resumed — продвижение после попытки без сброса медиа;
|
||||
- stream recovery KEY — пройденная граница, выбранный KEY и причина недоступности прежнего диапазона;
|
||||
- stream GAP — точные first/next и направление; stream NET/PLAY — сводки каждые >=500мс.
|
||||
- stream BUFFER — худшая цепочка, сумма её буферов, число устаревших/недоступных/неизвестных узлов, максимум возраста раз в секунду.
|
||||
|
||||
Буферы цепочки воспроизведения (raw protocol v7):
|
||||
|
||||
TELEMETRY=18 идёт через общий raw ETCP отдельной служебной кодограммой. В очередь видео DATA он не ставится:
|
||||
перед очередной DATA pump сначала отправляет просроченную/обновлённую телеметрию. Снимок строится в момент отправки,
|
||||
не копится в control FIFO; при backpressure сохраняется только dirty-флаг. Уже принятые ETCP данные он не обгоняет.
|
||||
Источник и relay обновляют собственные измерения каждые 200ms; получение нового upstream сразу будит downstream.
|
||||
Если upstream не обновлялся 400ms, его значения сохраняются с STALE. Подтверждённый разрыв помечает последний переход DOWN,
|
||||
предыдущую часть цепочки — STALE. Отсутствовавшее измерение отличается от известного нулевого буфера.
|
||||
|
||||
Payload TELEMETRY: count:1, count записей по 33 байта:
|
||||
node_id:8, buffered_us:8, sample:8, frames:4, age_ms:4, flags:1. Все числа big-endian.
|
||||
Флаги KNOWN=1, STALE=2, DOWN=4. Путь начинается источником, заканчивается отправителем и не содержит повторов/получателя.
|
||||
sample — монотонный номер измерения узла; возраст увеличивается при каждом хранении/пересылке, без синхронизации часов.
|
||||
Потеря выдачи не удаляет последний снимок из общего кольца; новая эпоха получает отдельное кольцо.
|
||||
|
||||
Подсказка frame_timing в DATA содержит first_frame:8,last_frame:8,first_pts:8,last_pts:8,period_us:4,
|
||||
started:4,audio_us:4,flags:1,reserved:3. Флаги: valid=1,video=2,continuation=4; reserved=0.
|
||||
Она формируется из uAV2 у источника и неизменно пересылается relay. Это диагностическая подсказка непосредственного пира,
|
||||
а не часть подписанного RS-тела: на проверку подписи, выбор частей и декодирование видео она не влияет.
|
||||
Считаются только части >= cursor конкретной выдачи, ещё не переданные в ETCP. История кольца и другие получатели не прибавляются.
|
||||
Начала кадров плюс одно продолжение в начале очереди дают число уникальных кадров, независимо от FEC и дробления большого KEY.
|
||||
Длительность определяется их PTS и последним измеренным периодом кадра; при постоянном FPS соответствует frames/FPS.
|
||||
До измерения периода первого кадра время неизвестно. Аудио считается по сэмплам 48kHz; параллельные треки не суммируются.
|
||||
Источник также учитывает ещё не упакованную publisher-очередь, объединяя границы частично упакованного кадра без дублирования.
|
||||
|
||||
Для каждого viewer выбирается целый фактический входящий путь с максимальной суммой buffered_us.
|
||||
Desktop/Android показывают две строки: «0.6S → 0.5S → 2S 1.1x», под каждым временем имя узла.
|
||||
Имя длиннее восьми Unicode code points сокращается до первых восьми и одного символа «…».
|
||||
Последняя колонка — собственный непрерывный запас до фактической playback-позиции, включая decoder/PCM/device;
|
||||
перенос между локальными очередями не уменьшает запас повторно. Темп берётся из общего playback clock и виден только при ускорении.
|
||||
STALE отмечается приглушением и «?», DOWN — приглушением и «×»; последнее известное время сохраняется.
|
||||
|
||||
Регрессии test_stream_service --recovery-only используют настоящий CHAT/ETCP, подписи и RS 8+0.
|
||||
Медиатест создаёт 50 кадров/с по 6000 байт, KEY 55000 байт на нескольких пачках раз в 50 кадров.
|
||||
Проверяются порядок, возрастающий PTS, длины и каждый байт после штатного media cursor; H.264-декодер здесь не запускается.
|
||||
Сценарии: блокировка отправки 850мс и 2600мс без GAP; REINIT с задержкой stream-управления 100мс;
|
||||
10с выдачи 60КБ/с без потери KEY; остановка 5600мс с одной попыткой после 5с; поздний viewer с общим KEY-стартом;
|
||||
настоящее вытеснение 32-МБ source-кольца с переходом на KEY; зависание одной части при продолжающейся доставке семи других;
|
||||
вытеснение KEY между INFO и SUBSCRIBE ещё до первого DATA с обновлением стартовой точки через 5с.
|
||||
После коротких задержек сохраняются все кадры и состояние media; при фактической утрате диапазона требуется явный GAP.
|
||||
test_stream_media отдельно проверяет пустой индекс, неполный KEY, сохранение предыдущего KEY, завершение, вытеснение и GAP.
|
||||
|
||||
Предыдущая проверка 25.10 до этого изменения: ./check.sh — 187/187 и proxy/burst/load;
|
||||
Qt stream_view/audio/publish — 3/3, Android assembleDebug — PASS.
|
||||
Предыдущий изолированный стенд с 16 узлами: baseline 10с, WAN 2Мбит/с + RTT 300мс + loss 5% на 10с,
|
||||
восстановление, закрытие двух подписок и END — PASS. Результат: /tmp/stream-recovery-netns/result.json.
|
||||
|
||||
Новая реализация использует stream wire v6: обновляются отправитель, получатели и задействованные реле.
|
||||
Обратная совместимость v5 не поддерживается. Desktop и Android собраны из utun2.
|
||||
|
||||
Проверка реализации c3cf86b9: ./check.sh — 187/187, proxy/burst/load — PASS; Android assembleDebug — PASS.
|
||||
Девять сетевых регрессий входят в test_stream_service; лог полного прогона: /tmp/utun2-key-check-final-pass.log.
|
||||
Известный нестабильный Qt audio-gap тест описан в doc/tasks.md; результаты отдельного Qt-прогона: /tmp/utun2-key-qt-isolated.log.
|
||||
@@ -0,0 +1,332 @@
|
||||
Аудит документации src/stream — 26.10.2026, частично завершён
|
||||
|
||||
Текущий статус: deleted исправлен и покрыт штатным test_stream_member; сборка,
|
||||
профиль7/7 и check.sh195/195 PASS относятся к внедрению этого исправления (раздел 5).
|
||||
При продолжении аудита подтверждена потеря помещающегося Opus в publisher_block (раздел 6).
|
||||
Исправление внедрено: сборка и штатный test_stream_publisher23/23 PASS; после
|
||||
объединения origin/master профиль8/8 и check.sh200/200 PASS (раздел 7).
|
||||
Остальные частичные/непроверенные контракты каталога не закрыты. Разделы 1–4
|
||||
описывают первоначальную диагностику и проверку архитектуры до внедрения deleted.
|
||||
|
||||
1. Подтверждённый дефект: deleted не учитывается stream_member_load
|
||||
|
||||
src/stream/stream_member.c:166–167 выбирает строку только по node_id. Условие
|
||||
source=0 проверяется ниже, но столбец deleted не выбирается и не проверяется.
|
||||
Проверка дерева приглашений и подписи профиля не заменяет проверку блока владельца:
|
||||
после принятого deleted=yes эти подписи участника остаются корректными.
|
||||
Поэтому loader возвращает STREAM_OK и публикует прежние stream-опции/ключ.
|
||||
|
||||
Это расходится с допуском CHAT (source=0, deleted=0), закреплённым в topo_group.h,
|
||||
и с результатом topo_node_sqlite_member_allowed. stream_member_supernode уже
|
||||
отбирает deleted=0. Существующий тест deleted проверяет только supernode lookup.
|
||||
Обработчик member_changed в stream.c:539–546 использует stream_member_load:
|
||||
данный результат не запускает ветку инвалидирования STREAM_CLOSE_MEMBER.
|
||||
Для исходной реализации полный сетевой сценарий продолжения выдачи после удаления не проверялся;
|
||||
обход сетевой авторизации этим воспроизведением не заявляется.
|
||||
|
||||
Изолированная проверка
|
||||
|
||||
Настоящее ядро с SQLite :memory:, отдельные ключи канала и тестового участника.
|
||||
Join/update/tree подписи создаются штатными функциями и принимаются member_sync_put.
|
||||
В профиле корректные stream_source/subscriptions. Участник отличается от локального
|
||||
узла; никаких удалённых узлов или пользовательской БД проверка не затрагивает.
|
||||
Затем через member_sync_put применяется подписанный ключом владельца блок
|
||||
{"ver":"1","deleted":"yes"}; прямой UPDATE deleted для воспроизведения не используется.
|
||||
|
||||
Фактический лог:
|
||||
before load=0 allowed=1; after load=0 allowed=0 published=1; expected load=-9
|
||||
|
||||
Тест завершился exit=1 на несовпадении с ожидаемым STREAM_ERR_NOT_MEMBER.
|
||||
Стенд: /tmp/stream-audit-member-deleted.c.
|
||||
Сборка через существующие Makefile/LIBUTUN/COMMON_LIBS и дополнительную цель:
|
||||
make -C tests -f Makefile -f /tmp/stream-audit-member.mk /tmp/stream-audit-member-deleted
|
||||
Логи: /tmp/stream-audit-member-build.log, /tmp/stream-audit-member-deleted.log.
|
||||
Runtime выполнен вне sandbox из-за ограничений сокетов uasync.
|
||||
|
||||
Предлагаемый вариант исправления
|
||||
|
||||
Добавить deleted в список полей этого же SELECT и до проверки профиля вернуть
|
||||
STREAM_ERR_NOT_MEMBER для удалённого участника, с отдельным логом причины и ID.
|
||||
Вторая SQL-проверка и новый API не требуются. Вариант WHERE deleted=0 тоже
|
||||
отсечёт запись, но существующий лог "member absent" не отличит удаление от отсутствия.
|
||||
Сохранить различие отсутствующего/удалённого участника и неполного профиля (WAIT),
|
||||
атомарность state/public_key при отказе.
|
||||
Регрессия должна проверить подписанное удаление источника/локального участника/
|
||||
поставщика, закрытие соответствующих handles и отсутствие выдачи новых DATA.
|
||||
После исправления продолжить сверку оставшихся заголовков и сетевых контрактов.
|
||||
Реализация не исправлялась: аудит остановлен по правилу AGENTS.md:10.
|
||||
|
||||
2. Выполненная сверка
|
||||
|
||||
Все 18 заголовков src/stream прочитаны предварительно. Полностью прочитаны
|
||||
10 реализаций: stream_block/source/buffer/media/media_buffer/wire/member/metrics/
|
||||
load/playback_clock. Другие реализации stream, включая playback_audio.cpp,
|
||||
network/optimizer/publisher/viewer и stream.c целиком, ещё не проверены.
|
||||
В stream.c просмотрена только затронутая ветка member_changed; полный аудит
|
||||
жизненного цикла сервиса этим не заявляется.
|
||||
|
||||
Завершены и готовы к [V]: stream_proto.h, stream_block.h, stream_source.h,
|
||||
stream_media.h. Исправлены описание полного размера части и области подписи,
|
||||
предусловия BE helpers, Ed25519 seed/ключ, владение и ошибки codec/source,
|
||||
нулевой write/accepted, отсутствие END в source_finish, ограничения uAV2,
|
||||
пересчёт first_frame_offset и атомарность views/выходов при ошибке.
|
||||
В этих четырёх заголовках изменены комментарии/оформление, C-токены сохранены.
|
||||
|
||||
Частично проверены [~]: stream_buffer.h, stream_load.h, stream_media_buffer.h,
|
||||
stream_member.h, stream_metrics.h, stream_playback_clock.h, stream_wire.h.
|
||||
Их реализации прочитаны, но оставшиеся уточнения контрактов не внесены после
|
||||
остановки. В stream_member.h добавлено предупреждение о подтверждённом дефекте.
|
||||
|
||||
Остальные семь заголовков остаются [ ]: stream.h, stream_network.h,
|
||||
stream_optimizer.h, stream_playback_audio.h, stream_priv.h, stream_publisher.h,
|
||||
stream_viewer.h. Прочтение одного заголовка без реализации не закрывает аудит.
|
||||
doc/stream.txt остаётся [~]: просмотрены начальные разделы о FEC, подписи,
|
||||
кольце и выдачах; последующие сетевые/media/desktop/Android разделы не сверены.
|
||||
Добавлен статус аудита.
|
||||
Полные .h/doc контракты всего модуля ещё не закрыты.
|
||||
|
||||
3. Проверки и ограничения
|
||||
|
||||
Сборка диагностического стенда PASS; отрицательная проверка deleted воспроизвела дефект.
|
||||
Профильные наборы stream, полный check.sh, санитайзеры, Windows/FreeBSD и
|
||||
GUI/Android runtime не запускались. Неизменность C-токенов заголовков и
|
||||
git diff --check — PASS; успешные сетевые/медиа тесты не заявляются.
|
||||
Предыдущие изменения Local_transport и чужие записи чек-листа сохранены.
|
||||
|
||||
4. Архитектурная проверка предлагаемого исправления — 26.10.2026
|
||||
|
||||
Вывод: проверка deleted в stream_member_load архитектурно корректна. На этом
|
||||
этапе проверка выполнялась на временной копии модуля без изменений .c проекта;
|
||||
внедрение описано в разделе 5. Статусы полного аудита каталога этим не закрываются.
|
||||
|
||||
Граница ответственности
|
||||
|
||||
Блок A участника описывает личность/stream-опции. Блок B владельца определяет
|
||||
в том числе исключение из канала. Корректный A не отменяет запрет из B.
|
||||
member_apply_record проверяет подпись B и его версию; owner_put обновляет
|
||||
adm_tags/adm_tags_sig/storage/deleted одним UPDATE. После успешного commit
|
||||
member_committed посылает props-событие; Merkle-page уведомляет после commit
|
||||
всей страницы. Поэтому достаточно прочитать принятую проекцию deleted в том же
|
||||
SELECT, не вводя вторую криптопроверку блока B или отдельный SQL-запрос допуска.
|
||||
|
||||
STREAM_ERR_NOT_MEMBER уже обработан в stream_open_check, каталоге источников,
|
||||
member_changed/service_reconcile, authorized и network_membership_changed.
|
||||
Событие источника/self немедленно ставит invalidated, затем reconcile закрывает
|
||||
сессию. session_find/handle_active/session_deliver учитывают invalidated до
|
||||
отложенного освобождения. Закрытие сохраняет владение handles у caller до close.
|
||||
Для исключённого поставщика закрываются его flow, а локальная сессия источника
|
||||
может продолжать получать от другого поставщика. Сфера запрета — конкретный
|
||||
канал; общий ETCP/NCD и владельцы других каналов не должны закрываться.
|
||||
|
||||
Результаты реального стенда
|
||||
|
||||
В /tmp/stream-arch-member.c добавлены только поле deleted в существующий SELECT
|
||||
и ветка NOT_MEMBER с причиной в логе до разбора профиля. Копия связана перед
|
||||
штатной libutun.a, остальные модули взяты из существующей сборки.
|
||||
Стенд использует fixture test_stream_service: четыре живых ядра, два канала,
|
||||
подписанные owner/member записи, настоящий Merkle, UDP/TCP и NCD.
|
||||
|
||||
Все четыре группы проверок PASS:
|
||||
- Неполный профиль/placeholder остаётся WAIT; отсутствие и deleted дают
|
||||
NOT_MEMBER; state/public_key не меняются на отказе. Неверная подпись владельца
|
||||
отвергается, старая версия не снимает запрет, более свежая разрешает участие.
|
||||
- Исключение источника сразу закрывает receive с MEMBER и инвалидирует reader;
|
||||
новых DATA callback нет. Запрет через Merkle останавливает сам publisher.
|
||||
Handle другого канала и транспорт с дополнительным NCD-владельцем сохраняются.
|
||||
- Исключение поставщика закрывает только его локальную выдачу; reader источника,
|
||||
другой канал и upstream другого ядра в этот момент остаются активными.
|
||||
- Исключение self закрывает handles данного канала и запрещает новые publish/
|
||||
subscribe. Другой канал остаётся активен. После разрешения новый open работает,
|
||||
прежние отсоединённые handles не оживают. Новая публикация получает новую эпоху.
|
||||
|
||||
Итоговый exit=0, teardown flows=0 и ноль оставшихся tracked allocations.
|
||||
Сборка: /tmp/stream-arch.mk, /tmp/stream-arch-build-final.log.
|
||||
Кейсы: /tmp/stream-arch-cases.h; harness: /tmp/stream-arch-service.c.
|
||||
Результат: /tmp/stream-arch-results-final.log.
|
||||
Запуск: /tmp/stream-arch-service --architecture-only, вне sandbox.
|
||||
|
||||
Первый прогон /tmp/stream-arch-results.log неверно пытался повторно использовать
|
||||
publisher после распространённого через Merkle запрета: старый publisher уже
|
||||
правильно закрыт. Ожидание стенда исправлено на новый запуск после разрешения,
|
||||
сама временная проверка deleted при повторе не менялась.
|
||||
|
||||
Гарантии и ограничения
|
||||
|
||||
Уведомления/освобождение остаются отложенными; нужен обычный uasync loop.
|
||||
Проверено отсутствие новых DATA callback у исключённого reader. Уже принятые
|
||||
ETCP пакеты не отзываются, удалённые узлы узнают запрет при его доставке через
|
||||
Merkle. Архитектурная проверка не обещает мгновенного глобального отзыва данных.
|
||||
Полный check.sh, санитайзеры и платформенные/GUI runtime не запускались.
|
||||
На этапе архитектурной проверки C-код репозитория не менялся; последующее
|
||||
внедрение и постоянная регрессия описаны ниже.
|
||||
|
||||
5. Внедрение согласованного исправления — 26.10.2026
|
||||
|
||||
В stream_member_load существующий SELECT дополнен deleted. Для удалённой записи
|
||||
до crypto возвращается STREAM_ERR_NOT_MEMBER, с отдельным DEBUG_WARN о запрете
|
||||
владельца, group_id/member_id. Другого SQL-запроса, нового API или формата нет.
|
||||
Остальные результаты loader, включая WAIT, и атомарность выходов сохранены.
|
||||
Предупреждение о неисправленном дефекте в stream_member.h заменено действующим
|
||||
контрактом. Задача deleted в doc/tasks.md закрыта, текущий статус stream.txt уточнён.
|
||||
|
||||
Постоянная регрессия — test_stream_member в check_PROGRAMS. Тест использует
|
||||
test_stream_service.c с теми же четырьмя ядрами/подписанными записями/сетью,
|
||||
а STREAM_MEMBER_TEST_ONLY выбирает изолированный сценарий в отдельном процессе.
|
||||
Fixture не дублируется, её изменённые member-версии/активность не влияют на
|
||||
обычный test_stream_service. Сценарий доступен также через --member-only.
|
||||
|
||||
Проверены подписи/версии владельца, WAIT/NOT_MEMBER и сохранность выходов,
|
||||
удаление источника/поставщика/self, причины закрытия, отсутствие новых DATA
|
||||
callback после запрета, распространение через Merkle до остановки publisher,
|
||||
сохранность чужого NCD-владения и работоспособность другого канала после
|
||||
исключения self. Разрешение не оживляет старые handles; новая публикация
|
||||
проверяется на новую эпоху. Ожидание удалённой остановки зависит от состояния,
|
||||
а не фиксированного sleep; teardown подтверждает отсутствие tracked allocations.
|
||||
|
||||
./build.sh --no-gui -j4 PASS: /tmp/stream-member-fix-build.log.
|
||||
Профиль7/7 PASS: stream_member, stream_service, stream_core, stream_media,
|
||||
stream_playback, stream_tx_stats, android_stream.
|
||||
Сводка /tmp/stream-member-fix-profile-results.log,
|
||||
подробности /tmp/stream-member-fix-test_*.log.
|
||||
Последующий перенос длинной строки лога не меняет содержимое сообщения;
|
||||
обе test-stream цели пересобраны, test_stream_member повторно PASS:
|
||||
/tmp/stream-member-fix-test-rebuild.log, /tmp/stream-member-fix-test_stream_member-final.log.
|
||||
|
||||
Полный Linux check.sh:195/195 PASS,0 failed/0 skipped; proxy9/9,burst PASS,
|
||||
load8/8,134217728 байт,167 секунд. Лог /tmp/stream-member-fix-full-check.log.
|
||||
git diff --check PASS. Sanitizers и нативные Windows/FreeBSD/GUI/Android runtime
|
||||
в этом этапе не запускались; test_android_stream — тест общего C bridge на Linux.
|
||||
Отметки полного аудита заголовков не расширены: устранение одного дефекта
|
||||
не завершает оставшуюся сверку src/stream и всего doc/stream.txt.
|
||||
|
||||
6. Продолжение аудита: помещающийся Opus не упаковывается — 26.10.2026
|
||||
|
||||
Полностью прочитаны stream_playback_audio.cpp, stream_viewer.c, stream_publisher.c
|
||||
и их заголовки. Всего прочитаны13 реализаций; полные stream.c/network/optimizer
|
||||
пока не сверены. До исправления документов трёх новых модулей подтверждён дефект,
|
||||
поэтому publisher.h/viewer.h/playback_audio.h отмечены[~], не[V].
|
||||
Теперь4 заголовка[V],10[~],4[ ]; doc/stream.txt[~] сохраняется.
|
||||
|
||||
Причина
|
||||
|
||||
В publisher_block (stream_publisher.c:145) перед каждым Opus проверяется
|
||||
used + STREAM_MEDIA_AUDIO_SIZE + packet_length + STREAM_MEDIA_VIDEO_SIZE >= capacity.
|
||||
Резерв video descriptor добавляется всегда, включая полностью пустую video-очередь.
|
||||
В результате допустимая audio-only запись, которая целиком помещается в uAV2,
|
||||
не попадает в block. При отсутствии видео publisher_block возвращает WAIT,
|
||||
actor повторяет попытки и после MEDIA_AGE_US=250000 удаляет принятый пакет.
|
||||
Штатный лог говорит audio expired, не объясняя блокировку из-за лишнего резерва.
|
||||
|
||||
Фактическое воспроизведение
|
||||
|
||||
Один настоящий локальный publisher, отдельный автоматически созданный канал,
|
||||
SQLite в памяти, stream_subblocks=1/stream_rs_redundant=0. Ёмкость содержимого
|
||||
FEC-пачки1125 байт. Видео в очереди нет. Codec Opus48kHz stereo/20мс:
|
||||
штатный opus_codec_encode плюс opus_packet_pad; оба тестовых пакета дают
|
||||
opus_packet_get_nb_samples=960 и действительно декодируются в960 frames/channel.
|
||||
Самостоятельный stream_media_encode успешно помещает оба блока в ту же ёмкость.
|
||||
После publisher feed принимаются настоящие RS-пачки, проверяется подпись,
|
||||
содержимое декодируется штатным stream_media_parse в DATA callback.
|
||||
|
||||
packet=1044 valid_samples=960 media_bytes=1092 capacity=1125 feed=1 delivered=1 queued=0 state=LIVE
|
||||
packet=1045 valid_samples=960 media_bytes=1093 capacity=1125 feed=1 delivered=0 queued=0 state=LIVE
|
||||
В логе между этими строками: publisher audio expired pts=1020000.
|
||||
Результат: expected=2 received=1, exit=1; после destroy нет оставшихся tracked allocations.
|
||||
Для каждой проверки отсчитано500мс: больше TTL и периода обновления TX snapshot.
|
||||
Это не переполнение TX, не испорченный Opus и не нехватка места в полной пачке.
|
||||
|
||||
Стенд: /tmp/stream-audit-audio-pack.c, дополнительная цель /tmp/stream-audit-audio-pack.mk.
|
||||
Сборка через штатные Makefile/LIBUTUN/COMMON_LIBS/test_clock.o PASS:
|
||||
/tmp/stream-audit-audio-pack-build.log. Полный лог /tmp/stream-audit-audio-pack.log.
|
||||
Первый диагностический запуск остановился на неверной константе capacity1133
|
||||
в стенде; она заменена вычислением границы из фактической ёмкости1125.
|
||||
Реализация проекта при диагностике не менялась.
|
||||
|
||||
Предлагаемый вариант исправления
|
||||
|
||||
Упаковка должна учитывать фактически добавляемые описания/данные и допускать
|
||||
audio-only пачку, если целый Opus в неё помещается. Резерв под видео не должен
|
||||
навсегда блокировать принятую audio-запись. Простой вариант — заполнять audio
|
||||
до его реальной ёмкости, затем видео в оставшееся место; нехватка места под видео
|
||||
означает следующую пачку, а не потерю audio. Wire и неделимость Opus сохраняются.
|
||||
Если важно гарантировать часть видео в каждой смешанной пачке, нужен явный
|
||||
переход к отдельной audio-only пачке для такой головы очереди; это более сложный вариант.
|
||||
|
||||
Проверить1044/1045 байтов, максимальное audio-only содержимое1077 для FEC1+0,
|
||||
точное заполнение capacity, очередь видео/пустую очередь и прежний default8+0.
|
||||
Отдельно различать запись, которая вообще не помещается в доступную FEC-пачку,
|
||||
и запись, не поместившуюся только в текущий остаток: диагностика должна показывать
|
||||
packet_length, остаток/полную ёмкость и причину, не маскируя всё как expiry.
|
||||
Реализация не исправлялась; дальнейший аудит остановлен по AGENTS.md:10.
|
||||
В publisher.h добавлено предупреждение, задача открыта в doc/tasks.md.
|
||||
Новый полный build/check, sanitizers и Qt/Android runtime в этом продолжении
|
||||
не запускались; прежний check195/195 из раздела5 новый дефект не покрывает.
|
||||
|
||||
Оставшиеся уточнения новых заголовков (не выполнены после остановки):
|
||||
- playback_audio: create/destroy lifetime, producer/consumer дисциплина всех getters,
|
||||
потребление через needs_input, feed0/1/-1/-2, PLC до100мс, prepare после finish,
|
||||
EOF в empty означает извлечённый PCM, а не проигранный аппаратурой звук.
|
||||
- viewer: единый callback manager, notify без reentry, асинхронный stop до освобождения ID,
|
||||
пределы demand/video<=records/audio_ahead, нулевой снимок status до открытия,
|
||||
уничтожение manager требует остановки callers и живого instance.
|
||||
- publisher: чтение настроек FEC при actor-open, snapshot смешивает локальные очереди
|
||||
текущего источника и общие ETCP-счётчики ядра, PTS/revision/KEY admission,
|
||||
ошибки/состояния и условное сохранение кольца локальным readers после FIN.
|
||||
|
||||
7. Исправление упаковки и продвижения треков — 26.10.2026
|
||||
|
||||
Принят локальный вариант без изменения wire, публичного API и неделимости Opus.
|
||||
Без ожидающего видео рассчитывается только фактическая стоимость audio: header32,
|
||||
descriptor16, payload. Условие допуска допускает равенство полной ёмкости.
|
||||
Для обычной смешанной пачки сохранён резерв descriptor32 и хотя бы байта видео.
|
||||
Если первая audio-запись помещается только отдельно, публикуется audio-only пачка.
|
||||
Локальный video_turn отдаёт второй слот того же actor видео; coalescing20ms в этом
|
||||
слоте не применяется. Между actor-вызовами состояние не добавлено; лимит2 пачки,
|
||||
mutex, порядок PTS и смещения фрагментов сохраняются. Публикуемые записи расходуются
|
||||
только после успешных encode/publish; очередь при ошибке закрывается прежним путём.
|
||||
|
||||
Opus длиннее capacity-header32-descriptor16 actor удаляет с WARN, содержащим ID,
|
||||
PTS, bytes, max и capacity. Это окончательный отказ конкретной записи, не WAIT
|
||||
до истечения TTL. Следующая запись продолжает обрабатываться. Проверка находится
|
||||
в actor: во время OPENING producer ещё не знает выбранную FEC-ёмкость.
|
||||
feed=1 означает копирование в ограниченную очередь; асинхронный отказ и TTL
|
||||
оговорены в publisher.h и stream.txt. Удаление ошибочной записи не меняет video KEY.
|
||||
DEBUG показывает успешную отдельную аудиопачку и отложенное видео, TRACE — состав
|
||||
каждой опубликованной пачки; WARN сообщает точную причину невозможности упаковки.
|
||||
|
||||
Практический масштаб
|
||||
|
||||
Воспроизведение использует валидный Opus, искусственно увеличенный opus_packet_pad.
|
||||
Обычный desktop/Android encoder настроен AUDIO128kbit/s/20ms; средний бюджет320 байт
|
||||
не является пределом VBR. Потеря обычного естественно закодированного звука не
|
||||
подтверждена. При default8+0 capacity9833 одиночный Opus<=1275 помещается.
|
||||
Ёмкость1125 относится ко всем конфигурациям с n-r=1 при штатном RS-size, включая2/1.
|
||||
|
||||
Проверки
|
||||
|
||||
Временная копия и реальный publisher/RS/uAV2:21/21 PASS, без tracked leaks;
|
||||
/tmp/stream-plan-check.log. Старый бинарник проваливает границу1045:
|
||||
/tmp/stream-plan-baseline.log. Исходники проекта при этой проверке плана не менялись.
|
||||
|
||||
Штатный tests/test_stream_publisher.c и цель tests/Makefile.am:23/23 PASS.
|
||||
Границы1044/1045/1077/1078/1275, точная пачка из двух audio-записей, раздельные
|
||||
пачки, крупная audio-очередь32 записи, unfit в начале/середине/подряд, video-only,
|
||||
mixed и продолжения видео, OPENING, Stop/FIN, n/r1/0,2/1,2/0,8/0, лимит8 Opus.
|
||||
Callback проверяет фактические PTS/длины, декодирует Opus960 frames/channel,
|
||||
проверяет каждый байт/offset видео. Все ожидаемые данные получены до TTL250ms.
|
||||
На Linux wrap uasync_set_timeout запрещает publisher_media timer после крупного
|
||||
Opus и короткого P-кадра: оба должны публиковаться в том же actor. Проверка не
|
||||
сравнивает задержки процессора с порогом5ms. Cleanup каждого ядра восстанавливает
|
||||
tracked allocation count. Другие платформы сохраняют проверки содержимого/порядка.
|
||||
|
||||
./build.sh --no-gui -j4 PASS: /tmp/stream-publisher-fix-build.log.
|
||||
./tests/test_stream_publisher PASS: /tmp/stream-publisher-fix-regression.log.
|
||||
Перед итоговой проверкой origin/master8e746195 объединён без конфликтов в ced9dc23.
|
||||
Повторная ./build.sh --no-gui -j4 PASS: /tmp/stream-publisher-fix-merged-build.log.
|
||||
Профиль8/8 PASS: stream_publisher,core,media,playback,tx_stats,android_stream,service,member;
|
||||
/tmp/stream-publisher-fix-profile-results.log и /tmp/stream-publisher-fix-test_*.log.
|
||||
Полный ./check.sh PASS:200 passed,0 failed,0 skipped; новый publisher-тест764ms.
|
||||
Proxy9/9,burst PASS,load8/8,134217728 байт; полное время175с.
|
||||
Лог /tmp/stream-publisher-fix-full-check.log. git diff --check PASS.
|
||||
Sanitizers, Qt GUI и Android на устройстве в этом исправлении не запускались.
|
||||
Отметки аудита4[V]/10[~]/4[ ] не расширяются исправлением кода.
|
||||
+6
-2
@@ -31,7 +31,8 @@ nat=debug
|
||||
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
|
||||
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
|
||||
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
|
||||
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
|
||||
IPv6 и отсутствие выбранного IPv4 дают `SKIPPED` с причиной `unsupported socket`.
|
||||
TCP, служебные `probe_only`, SOCKS UDP и `only_local` исключены из списка проб: API возвращает для них -1.
|
||||
|
||||
## Результат
|
||||
|
||||
@@ -40,6 +41,8 @@ IPv6, SOCKS UDP и `only_local` пропускаются с явным стат
|
||||
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
|
||||
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
|
||||
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
|
||||
Нулевой возврат означает наличие снимка, включая PENDING/FAILED/SKIPPED: вызывающий проверяет статус
|
||||
и возраст `measured_at_tb` относительно `STUN_NAT_TTL_TB`. -1 не меняет выходную структуру.
|
||||
|
||||
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
|
||||
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
|
||||
@@ -89,7 +92,8 @@ TTL результата 300 секунд, refresh 180 секунд; непол
|
||||
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
|
||||
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
|
||||
История портов освобождается только при окончательном destroy ядра.
|
||||
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
|
||||
`stun_nat_recheck(instance)` сверяет текущие пути и явно повторяет измерение каждого ровно один раз
|
||||
с новым generation/портом и без старого кэша, в том числе после смены маршрута без смены IP.
|
||||
Публичные операции выполняются в uasync-потоке экземпляра.
|
||||
|
||||
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
|
||||
|
||||
+866
-1001
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,92 @@
|
||||
Аудит оставшихся контрактов transport_layer — 25–26.10.2026
|
||||
|
||||
Область и итог
|
||||
Закрыты оставшиеся auto_socket, socket_monitor, socks_client, call_ring,
|
||||
reality/reality_fingerprint/reality_record/reality_io/reality_relay и stun_nat.
|
||||
Полностью прочитаны их реализации, заголовки и связанные участки владельцев/API.
|
||||
С учётом ранее завершённых ETCP/STCP/crypto/normalizer/NCD/dummynet/packet_dump/CRC
|
||||
сверены все 32 собственных заголовка и 17 документов src/transport_layer, а также doc/stun_nat.md.
|
||||
Вложенный BBR/bbr_v3.c не имеет отдельного заголовка/документа и относится к ранее сверенной
|
||||
реализации etcp_bbr. Это закрытие аудита описаний и найденных
|
||||
при нём дефектов, а не утверждение об отсутствии любых возможных ошибок транспорта.
|
||||
|
||||
socket_monitor
|
||||
- Удалён разбор FreeBSD sockaddr, безусловно писавший gw[0] при переданном NULL.
|
||||
Уведомления разбираются по родному типу if_msghdr/ifa_msghdr/if_announcemsghdr/rt_msghdr;
|
||||
проверяются длина и версия, используется только нужный индекс интерфейса.
|
||||
- При исчезновении адреса сбрасываются старые IPv4/IPv6, сохраняется порт, корректируется IPv6 scope.
|
||||
- Обход callbacks использует ID/поколение: удаление/пересоздание сокетов не оставляет висячий next.
|
||||
- Linux проверяет источник, границы netlink, EINTR/overflow; init откатывает каждый отказ.
|
||||
- Windows объединяет уведомления и отменяет pending post при остановке после завершения native producers.
|
||||
|
||||
auto_socket
|
||||
- UDP factory возвращает именно созданный endpoint; один общий reconcile заменяет дубли IPv4/IPv6 × UDP/TCP.
|
||||
- При замене сначала снимается старый слот, неудачная попытка не оставляет dangling pointer;
|
||||
частичный начальный запуск откатывается, последующие ошибки сохраняют работающие endpoints.
|
||||
- Для wildcard TCP listener отдельно хранится опубликованный IP интерфейса: неизменённый listener стабилен,
|
||||
реальная смена адреса/типа пересоздаёт его. DOWN/исчезнувшие интерфейсы удаляются после полного scan.
|
||||
- Порты SQLite проверяются по типу/диапазону, атомарный INSERT не оставляет открытую транзакцию.
|
||||
RNG проверяется, 65535 доступен. Остановка сохраняет кеш портов, удаление интерфейса очищает его.
|
||||
- Внешние/Windows scans объединяются в отменяемый post; native источники отключаются до освобождения.
|
||||
Внешние producers должны завершиться до destroy согласно контракту.
|
||||
- Android reload сохраняет одинаковые port=0 endpoints, учитывает scope/опции и обновляет ссылки client links
|
||||
до освобождения старого config. Ошибка частичного reload логируется, новый config остаётся владельцу instance.
|
||||
- Проверены длины/границы Linux/FreeBSD уведомлений, ошибки nonblocking/register и Windows API.
|
||||
|
||||
socks_client
|
||||
- CONNECT читает ровно control frame и сохраняет следующий TCP banner для владельца.
|
||||
- Start не вызывает completion inline при немедленном connect; timer/nonblocking обязательны с полным rollback.
|
||||
- Проверяются пределы строк, VER/RSV/REP, relay port/ATYP; wildcard UDP BND IP берётся из TCP peer.
|
||||
- Отправка учитывает EINTR, SIGPIPE и нулевой прогресс, проверяются переключения write monitoring.
|
||||
- UDP send возвращает payload bytes, не wire bytes; размер проверяется до сложения, NULL/0 допустимы.
|
||||
Unwrap сохраняет выходы при отказе и допускает in-place payload.
|
||||
- DNS start остаётся синхронным getaddrinfo с первым адресом; это явно отражено в контракте.
|
||||
|
||||
call_ring
|
||||
- История отдельная для каждого потока; насыщенный count сохраняет последние 1024 записи после wrap idx.
|
||||
- Указатели печатаются через PRIxPTR, включая Win64. Dump учитывает short write/EINTR и отказ файла,
|
||||
продолжает stderr без обычного logger/выделений памяти. snprintf не объявлен async-signal-safe.
|
||||
|
||||
REALITY
|
||||
- Уточнены одноразовость session, zero-init/cleanup, сохранение traffic после Finished,
|
||||
границы codec, отсутствие replay-защиты в stateless обёртках и отдельное владение owner/I/O.
|
||||
- Traffic init сбрасывает generation, KeyUpdate увеличивает его; replay entries различают пустой slot и Unix time=0.
|
||||
- Ошибки client_start полностью удаляют новое I/O; send забирает вход только при успехе.
|
||||
- Resume сохраняет штатный finish при close_notify; transient recv EINTR не закрывает поток.
|
||||
- Relay строго проверяет destination port/timeout/длину prefix, mandatory nonblocking/timer/monitor setup.
|
||||
SIGPIPE/EINTR/send=0 обрабатываются, счётчики учитывают каждый отправленный байт.
|
||||
- HUP дочитывает остаток/EOF: прежде uasync снимал регистрацию без except handler и relay ждал idle timeout.
|
||||
Регрессия отключает idle timeout и требует полного двустороннего half-close.
|
||||
- Закрытые relay остаются в owner до call_soon; отказ его выделения не теряет контекст.
|
||||
Shutdown отменяет отложенные frees и синхронно освобождает оба списка до уничтожения owner/UASYNC.
|
||||
|
||||
STUN
|
||||
- Recheck одним scan перезапускает старые и создаёт новые пути по одному разу; раньше новый путь
|
||||
немедленно открывал второй тестовый fd, расходуя первый свежий порт без измерения.
|
||||
- Уточнены исключение TCP/probe_only/SOCKS/only_local из API, SKIPPED IPv6/неизвестного IPv4,
|
||||
статусы результата, неизменность выхода при -1 и обязанность caller проверять TTL.
|
||||
- Контракты и RFC-последовательность сверены с https://www.rfc-editor.org/rfc/rfc5780
|
||||
и https://www.rfc-editor.org/rfc/rfc8489; обычный MATCH не объявляется доказательством EIM.
|
||||
|
||||
Проверки
|
||||
- ./build.sh --no-gui -j4: PASS, /tmp/transport-audit-final-build.log.
|
||||
- ./check.sh: 194/194 PASS, 0 failed/0 skipped; proxy 9/9, burst 1/1, load 8/8,
|
||||
134217728 проверенных байт, 170 секунд; /tmp/transport-audit-final-check.log.
|
||||
- Динамические IPv4/IPv6 сценарии auto_socket во всех UDP/TCP режимах и 8 циклах: PASS,
|
||||
/tmp/transport-audit-auto-dynamic-final.log, в отдельном Linux netns.
|
||||
- Проверки скрипта сборки: 44/44 PASS, /tmp/transport-audit-build-script.log.
|
||||
- ASAN/UBSan/LSan: 12/12 PASS с detect_leaks=1/ halt_on_error=1:
|
||||
socket_monitor, auto_socket_faults, socks_client, call_ring, reality_record, reality_hello,
|
||||
reality_tls, reality_fallback, reality_relay_faults, stcp_reality_rotation, stun_nat, tun_route.
|
||||
Все встроенные lib/src/Opus инструментированы согласованно в /tmp/utun-packet-dump-asan-20261025.
|
||||
Сборка: /tmp/transport-audit-asan-build.log и /tmp/transport-audit-asan-tests-build.log.
|
||||
Первый прогон /tmp/transport-audit-asan-results.log: 10 PASS и 2 отказа тестовых перехватов glibc FORTIFY.
|
||||
Добавлены checked recvfrom/poll wrappers; оба повторных прогона PASS, без отчётов санитайзеров:
|
||||
/tmp/transport-audit-asan-test_socket_monitor-final.log,
|
||||
/tmp/transport-audit-asan-test_tun_route-final.log. Последние изменения только стенда проверены адресно.
|
||||
Остальные индивидуальные логи: /tmp/transport-audit-asan-test_*.log.
|
||||
- Нативные runtime/сборки Windows и FreeBSD недоступны и не заявляются.
|
||||
FreeBSD типы уведомлений сверены с официальными заголовками releng/14.3:
|
||||
https://github.com/freebsd/freebsd-src/blob/releng/14.3/sys/net/if.h
|
||||
https://github.com/freebsd/freebsd-src/blob/releng/14.3/sys/net/route.h
|
||||
Полнота [V] означает сверку описаний по исходникам; платформенные runtime-ограничения сохранены здесь явно.
|
||||
@@ -0,0 +1,104 @@
|
||||
Аудит utun_instance — 26.10.2026
|
||||
|
||||
Объём
|
||||
Полностью прочитаны utun_instance.c/.h и прежний utun_instance_doc.md; сверены
|
||||
вызовы конструкторов/start/stop/reload из CLI/встраивания и связанные механизмы
|
||||
ETCP/NCD/STCP, sockets, logger, очередей, SQLite и независимых сервисов.
|
||||
Заголовок и документ переписаны по фактическому коду. Core-разделы
|
||||
doc/service_lifecycle.md согласованы; весь поперечный документ остаётся [~].
|
||||
Это не полный аудит реализаций chat/call/radio/stream или GUI/Android.
|
||||
|
||||
Исправления
|
||||
- Activity/peer-sleep подписки удерживают снятые записи до выхода из внешней
|
||||
рассылки. Безопасны удаление себя/соседа, вложенное событие и добавление новой
|
||||
подписки. Повтор fn+arg не дублируется; более новая эффективная activity
|
||||
отменяет остаток старой рассылки. OOM обеих подписок логируется.
|
||||
- Конструкторы проверяют UASYNC/config/path до побочных действий; общий путь
|
||||
создания освобождает частичные ресурсы, сохраняя caller config при отказе.
|
||||
Проверяются crypto derivation, запись/close временного файла, создание каталогов
|
||||
и индекс сетей. Старый TCP-реестр индексируется по node_id, а не указателю link.
|
||||
- Выбранный путь общего logger копируется в постоянный буфер; освобождение config
|
||||
больше не оставляет заимствованную строку в logger. Уже открытый файловый logger
|
||||
сохраняет первый путь при создании/reload другого instance.
|
||||
- Reload готовит config/firewall/networks до изменения ресурсов. Смена владельцев
|
||||
(в том числе db_path, control, TUN/NAT/proxy), подсетей или automatic server-конфига
|
||||
пересоздаёт ядро с прежними core/UTUN/chat/running. Повторные имена server также
|
||||
выбирают полный путь, сохраняя отдельные секции parser без неоднозначного matching.
|
||||
- Manual reload сравнивает все server-параметры и IPv6 scope по конфигурации,
|
||||
сохраняя неизменённые sockets/ephemeral ports. TCP listener закрывается вместе
|
||||
с правильной TCP-моделью; UDP создаётся/удаляется своим API. Устранён лог через
|
||||
освобождённое имя. Probe и endpoints других владельцев не удаляются по отсутствию
|
||||
server-секции. Конфигурационные clients обновляются также без UTUN; неизменённые
|
||||
handles/таймеры сохраняются, копии не ссылаются на освобождённый config.
|
||||
- Destroy обходит sockets/connections от свежей головы после callbacks,
|
||||
освобождает старые TCP queue entries и оставшиеся transport-подписки.
|
||||
Повторный destroy/start/reload при разрушении отклоняется; SQLite close ошибки
|
||||
видны, BUSY завершается close_v2 до финализации заимствованных SQL-объектов.
|
||||
- Три headless bind разбираются одинаково: IPv4 literal, полный decimal port
|
||||
0..65535. Port=0 выбирает ОС; плохое значение не открывает порт по умолчанию.
|
||||
Отказ listener не фатален для чата, частичный контекст очищается.
|
||||
- Call/radio headless stop освобождает контекст/кодеки, снимает клиентов с UASYNC
|
||||
и отменяет cleanup/mix таймеры. Их отказы при старте дают rollback.
|
||||
- UBSan нашёл невыровненный PCM после 11-байтового headless заголовка и uint64_t
|
||||
hop-list в radio_recv. Оба читаются через выровненное хранилище; тест рации также
|
||||
использует memcpy вместо int16_t* к невыровненному payload. Wire не менялся.
|
||||
- Из заголовка удалено объявление несуществующего utun_instance_run; poll принадлежит
|
||||
caller. Уточнены владение, состояния, ошибки init/reload, подписки и диагностика.
|
||||
|
||||
Контракты и ограничения
|
||||
Все lifecycle операции выполняются в потоке своего UASYNC, stop/destroy/reload —
|
||||
вне callbacks останавливаемых модулей. UASYNC принадлежит caller и живёт до конца
|
||||
очистки. Core start при отказе сохраняет подготовленные ресурсы для retry/destroy;
|
||||
init при отказе позднего сервиса сохраняет ранее запущенные сервисы.
|
||||
Create уже выполняет I/O и создаёт listeners/SQLite/router/proxy/monitoring.
|
||||
Create_from_str требует временного файла /tmp и не является чистым buffer parser.
|
||||
Logger общий для процесса; его изменения и config updater не входят в транзакцию reload.
|
||||
|
||||
Partial reload не атомарен: отказ endpoint сохраняет новый config и отсутствие
|
||||
endpoint для следующей попытки. Подготовка clients при ошибке сохраняет прежние
|
||||
самостоятельные handles. Full reload возвращает NULL, если новое ядро не удалось
|
||||
создать/запустить после уничтожения старого; caller использует возвращённый указатель.
|
||||
Порядок списков/padding модели может консервативно выбрать restart. IPv6 selector
|
||||
temporary/permanent меняет адрес в модели при разрешении, поэтому повторный reload
|
||||
может консервативно пересоздать такой endpoint.
|
||||
Caller завершает borrowed statements/backups; close_v2 не разрешает использовать
|
||||
уничтоженный instance. Живые ресурсы в diagnose_leaks сами по себе не доказывают утечку.
|
||||
Отменённые heap-таймеры UASYNC освобождаются при следующем poll, а pool cache — при
|
||||
уничтожении UASYNC; тест сверяет active/counters и общий баланс после обоих шагов.
|
||||
|
||||
Проверки
|
||||
- Полная обычная сборка PASS: /tmp/utun-instance-audit-build-final.log.
|
||||
- Первый полный check.sh: 196/196 PASS, 0 failed/0 skipped; proxy9/9, burst1/1,
|
||||
load8/8, 134217728 байт, 189с; /tmp/utun-instance-audit-final-check.log.
|
||||
Он предшествовал последней правке выравнивания radio hop-list.
|
||||
- Профиль после исправлений: test_instance_contract, test_services, test_ncd_config,
|
||||
test_tun_route, test_etcp_transport, test_stcp, test_node_conn_direct,
|
||||
test_call_headless, test_radio_headless PASS; три изменённых адресных теста
|
||||
повторены в /tmp/utun-instance-audit-final-test_*.log.
|
||||
- Новый test_instance_contract проверяет nested callbacks/OOM, constructor ownership,
|
||||
правильный TCP queue index, неизменённые ephemeral endpoints, malformed config,
|
||||
отказ network queue/create endpoint с retry, UDP/TCP замену и удаление listener,
|
||||
duplicate server names, full reload с сохранением stopped/core-only состояния,
|
||||
SQLite BUSY/finalize, другой UASYNC, db_path restart, recursive mkdir и жизнь
|
||||
logger path после освобождения config. Headless: три port=0, реальные TCP-клиенты,
|
||||
stop/start с сохранением SQLite, три timer faults и malformed binds.
|
||||
- ASAN/UBSan/LSan 11/11 PASS, detect_leaks=1, halt_on_error=1:
|
||||
те же девять instance/transport/headless тестов плюс config_parser/config_updater.
|
||||
Итог: /tmp/utun-instance-audit-asan-results-complete.log; отдельные логи
|
||||
/tmp/utun-instance-audit-asan-complete-test_*.log.
|
||||
Отдельная копия /tmp/utun-config-instance-asan-20261026, все lib/src/Opus
|
||||
инструментированы согласованно. Встроенный Opus не перекрывает CFLAGS в этой
|
||||
тестовой копии; рабочая normal-сборка с sanitizer-объектами не смешивалась.
|
||||
Сборки: /tmp/utun-instance-audit-asan-build.log,
|
||||
/tmp/utun-instance-audit-asan-core-final.log и ...-asan-tests-build.log.
|
||||
Ранние наборы 9/11 и 10/11 выявили описанные alignment defects; окончательный
|
||||
набор после исправления обеих причин полностью PASS.
|
||||
- git diff --check PASS. Нативные Windows/FreeBSD сборки/runtime, GUI/Android и
|
||||
аппаратные TUN/media в этом этапе не запускались; Linux TUN lifecycle проверен mock-тестом.
|
||||
|
||||
Окончательный штатный check.sh после последней правки: 196/196 PASS,
|
||||
0 failed/0 skipped, proxy9/9, burst1/1, load8/8, 134217728 байт, 173с.
|
||||
Лог /tmp/utun-instance-audit-complete-check.log. После этого изменены только комментарии
|
||||
о повторном core_start (он сохраняет running) и итоговые документы/чек-лист.
|
||||
utun_instance.h и utun_instance_doc.md отмечены [V]; ограничения общего документа
|
||||
service_lifecycle.md и прежние записи остальных модулей сохранены.
|
||||
@@ -15,6 +15,8 @@ libuasync_a_SOURCES = \
|
||||
socket_compat.h \
|
||||
platform_compat.c \
|
||||
platform_compat.h \
|
||||
file_io.c \
|
||||
file_io.h \
|
||||
getmyip.c \
|
||||
getmyip.h \
|
||||
swm_min.c \
|
||||
|
||||
+7
-3
@@ -34,23 +34,27 @@ struct adns_result {
|
||||
struct sockaddr_in addrs[ADNS_MAX_ADDRS]; /* AF_INET, sin_port = 0 */
|
||||
};
|
||||
|
||||
/* res заимствован до возврата callback; handle запроса к этому моменту уже освобождён. */
|
||||
typedef void (*adns_done_cb)(const struct adns_result* res, void* arg);
|
||||
|
||||
struct adns_opts {
|
||||
struct sockaddr_in server; /* sin_family != 0 → использовать этот сервер (иначе системные) */
|
||||
int timeout_ms; /* таймаут на сервер, по умолчанию 2000 */
|
||||
int timeout_ms; /* Таймаут на сервер в мс; <=0 → 2000, округляется вверх до целых секунд. */
|
||||
int max_attempts; /* попыток на сервер, по умолчанию 2 */
|
||||
};
|
||||
|
||||
struct adns_query; /* opaque */
|
||||
|
||||
/* Запускает асинхронный A-запрос. Коллбэк вызывается в потоке event loop.
|
||||
Возвращает handle (NULL при немедленной ошибке: невалидные аргументы/нет памяти). */
|
||||
* Возвращает handle, автоматически освобождаемый до callback либо при отмене.
|
||||
* NULL: неверные аргументы, нехватка памяти, отсутствие DNS-серверов, ошибка создания
|
||||
* резолвера, постановки запроса или регистрации отложенного запуска; callback не вызывается. */
|
||||
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
|
||||
const struct adns_opts* opts,
|
||||
adns_done_cb cb, void* arg);
|
||||
|
||||
/* Отмена ожидающего запроса: коллбэк НЕ вызывается. Только из потока loop. */
|
||||
/* Отмена ожидающего запроса: освобождает handle, коллбэк НЕ вызывается. Только из потока loop.
|
||||
* После завершения/отмены handle недействителен; NULL допустим. */
|
||||
void adns_cancel(struct adns_query* q);
|
||||
|
||||
/* Системные DNS-серверы (IPv4). Возвращает количество; out[].sin_port = htons(53).
|
||||
|
||||
+36
-28
@@ -272,6 +272,10 @@ void debug_set_masks(debug_category_t category_idx, debug_level_t level) {
|
||||
debug_set_category_level(category_idx, level);
|
||||
}
|
||||
|
||||
void debug_enable_timestamp(int enable) {
|
||||
g_debug_config.timestamp_enabled = !!enable;
|
||||
}
|
||||
|
||||
void debug_enable_function_name(int enable) {
|
||||
g_debug_config.function_name_enabled = enable;
|
||||
}
|
||||
@@ -323,15 +327,24 @@ void debug_disable_file_output(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Reopen log file (append mode) for SIGHUP. Complete operation with full error logging. */
|
||||
/* Переоткрыть лог в append-режиме; при ошибке открытия сохранить прежний поток. */
|
||||
int debug_reopen_log(void) {
|
||||
if (!g_debug_config.output_file) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "No log path for reopen");
|
||||
return -1;
|
||||
}
|
||||
const char* path = g_debug_config.output_file;
|
||||
debug_disable_file_output();
|
||||
debug_enable_file_output(path, 0); // append
|
||||
FILE* new_file = fopen(path, "a");
|
||||
if (!new_file) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
FILE* old_file = g_debug_config.file_output;
|
||||
g_debug_config.file_output = new_file;
|
||||
if (old_file && fclose(old_file) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Log reopened: %s", path);
|
||||
return 0;
|
||||
}
|
||||
@@ -391,13 +404,15 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
|
||||
va_start(args, format);
|
||||
|
||||
/* Add timestamp with microseconds: hh:mm:ss-xxx.yyy */
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL);
|
||||
time_t tv_sec = (time_t)tv.tv_sec;
|
||||
struct tm* tm_info = localtime(&tv_sec);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
|
||||
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
|
||||
if (g_debug_config.timestamp_enabled) {
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL);
|
||||
time_t tv_sec = (time_t)tv.tv_sec;
|
||||
struct tm* tm_info = localtime(&tv_sec);
|
||||
char time_str[32];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
|
||||
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
|
||||
}
|
||||
|
||||
/* Add thread marker if set */
|
||||
if (g_debug_config.thread_marker > 0) {
|
||||
@@ -460,7 +475,7 @@ void debug_set_log_hook(debug_log_hook_fn hook) {
|
||||
g_log_hook = hook;
|
||||
}
|
||||
|
||||
/* Parse debug configuration from string (e.g., "ll_queue:debug,uasync:info,memory:warn,etcp:error") */
|
||||
/* Настройка отдельных категорий: "sys=debug,etcp=error"; разделитель ':' также допустим. */
|
||||
int debug_parse_config(const char* config_string) {
|
||||
if (!config_string || !*config_string) {
|
||||
return 0;
|
||||
@@ -468,12 +483,10 @@ int debug_parse_config(const char* config_string) {
|
||||
|
||||
char* str = u_strdup(config_string);
|
||||
if (!str) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to allocate debug configuration copy");
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug_category_t categories = 0;
|
||||
debug_level_t level = DEBUG_LEVEL_NONE;
|
||||
|
||||
char* token = strtok(str, ",");
|
||||
while (token) {
|
||||
/* Trim leading spaces */
|
||||
@@ -481,15 +494,16 @@ int debug_parse_config(const char* config_string) {
|
||||
token++;
|
||||
}
|
||||
|
||||
char* colon = strchr(token, ':');
|
||||
if (!colon) {
|
||||
char* separator = strpbrk(token, "=:");
|
||||
if (!separator) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Missing '=' or ':' in debug setting '%s'", token);
|
||||
u_free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*colon = '\0';
|
||||
*separator = '\0';
|
||||
char* category_name = token;
|
||||
char* level_name = colon + 1;
|
||||
char* level_name = separator + 1;
|
||||
|
||||
/* Trim leading spaces in level_name */
|
||||
while (isspace(*level_name)) {
|
||||
@@ -497,22 +511,16 @@ int debug_parse_config(const char* config_string) {
|
||||
}
|
||||
|
||||
debug_category_t cat = get_category_by_name(category_name);
|
||||
if (cat == DEBUG_CATEGORY_NONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown debug category '%s'", category_name);
|
||||
if (cat <= DEBUG_CATEGORY_NONE || cat >= DEBUG_CATEGORY_COUNT) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown or unsupported debug category '%s' (expected an individual category)", category_name);
|
||||
u_free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
debug_level_t lev = debug_level_from_name(level_name);
|
||||
|
||||
/* Set per-category level */
|
||||
int idx = 0;
|
||||
while (!(cat & (1ULL << idx)) && idx < DEBUG_CATEGORY_COUNT) {
|
||||
idx++;
|
||||
}
|
||||
if (idx < DEBUG_CATEGORY_COUNT) {
|
||||
g_debug_config.category_levels[idx] = lev;
|
||||
}
|
||||
debug_set_category_level(cat, lev);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Applied debug setting: %s=%s (category=%d, level=%d)", category_name, level_name, cat, lev);
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
+35
-17
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Runtime debug configuration system
|
||||
* Provides flexible control over debug output without recompilation
|
||||
* Управление диагностикой: debug_config_init → уровни категорий и настройки вывода → DEBUG_*.
|
||||
* NONE наследует общий уровень, DISABLED отключает категорию. Имена настроек — category=level.
|
||||
* Открытый файл имеет приоритет над консолью; hook получает сообщения независимо от выбора выхода.
|
||||
* Уровни: disabled, none, error, warn, info, debug, trace; отдельный уровень категории заменяет общий, не max().
|
||||
* Пример: debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_parse_config("etcp=trace,config=info").
|
||||
* DEBUG_* фильтруют до форматирования; прямой debug_output фильтр не проверяет.
|
||||
* Строка с префиксами ограничена буфером 4096 байт и обрезается; каждый вывод fflush.
|
||||
* Модуль и смена глобальных настроек/файлов не синхронизированы; параллельный доступ согласует владелец.
|
||||
*/
|
||||
|
||||
#ifndef DEBUG_CONFIG_H
|
||||
@@ -86,9 +92,9 @@ typedef struct {
|
||||
int timestamp_enabled; // Include timestamps in output
|
||||
int function_name_enabled; // Include function names
|
||||
int file_line_enabled; // Include file:line info
|
||||
const char* output_file; // NULL = stdout, otherwise file path
|
||||
const char* output_file; // Заимствованный путь для reopen; строка должна жить, пока поле её хранит
|
||||
|
||||
// Dual output support
|
||||
// Выход: файл, иначе консоль при console_enabled != 0
|
||||
FILE* file_output; // File handle for log file (NULL if not open)
|
||||
int console_enabled; // Enable console output
|
||||
int thread_marker; // Thread identification (0=none, e.g. 2 for uasync)
|
||||
@@ -97,7 +103,8 @@ typedef struct {
|
||||
/* Global debug configuration */
|
||||
extern debug_config_t g_debug_config;
|
||||
|
||||
/* Initialize debug system with default settings */
|
||||
/* До использования: ERROR, категории NONE, консоль stdout, timestamp/function/file:line включены.
|
||||
* Повторный init сбрасывает поля, но не закрывает уже открытый файл; перед ним закрыть файл отдельно. */
|
||||
void debug_config_init(void);
|
||||
|
||||
/* Set debug level */
|
||||
@@ -116,16 +123,23 @@ void debug_set_categories(debug_category_t categories);
|
||||
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
|
||||
void debug_set_masks(debug_category_t categories, debug_level_t level);
|
||||
|
||||
/* Configure output options */
|
||||
/* Configure output options */
|
||||
/* Ненулевое enable включает временную метку, 0 отключает. */
|
||||
void debug_enable_timestamp(int enable);
|
||||
void debug_enable_function_name(int enable);
|
||||
void debug_enable_file_line(int enable);
|
||||
void debug_set_output_file(const char* file_path);
|
||||
/* Открыть ненулевой file_path в append-режиме, только если файл ещё не открыт; путь заимствован. */
|
||||
void debug_set_output_file(const char* file_path);
|
||||
|
||||
/* Dual output configuration */
|
||||
/* Заменить файл: truncate != 0 → обрезать, иначе append. NULL ничего не меняет.
|
||||
* Путь заимствован; успешное открытие также выключает console_enabled. */
|
||||
void debug_enable_file_output(const char* file_path, int truncate);
|
||||
/* Закрыть файл; прежнее значение console_enabled не восстанавливается. */
|
||||
void debug_disable_file_output(void);
|
||||
/* Переоткрыть текущий путь в append-режиме: 0 — успех, -1 — ошибка с диагностикой.
|
||||
* При ошибке открытия сохраняет прежний поток; console_enabled не меняет. */
|
||||
int debug_reopen_log(void);
|
||||
/* Вывод в консоль разрешён только при отсутствии открытого файла. */
|
||||
void debug_enable_console(int enable);
|
||||
|
||||
|
||||
@@ -137,7 +151,7 @@ typedef struct {
|
||||
ip_str_t ip_to_str(const void *addr, int family);// big endian addr
|
||||
ip_str_t sockaddr_storage_to_str(const struct sockaddr_storage *addr);// автоопределение v4 или v6
|
||||
|
||||
// hex dump в лог
|
||||
// Hex dump: первые min(len, 128) байт; при len > 128 добавляет "...". Применяется фильтр уровня/категории.
|
||||
void log_dump(int level, int category, const char* prefix, const uint8_t* data, size_t len);
|
||||
|
||||
/* Check if debug output should be shown for given level and category */
|
||||
@@ -146,13 +160,13 @@ int debug_should_output(debug_level_t level, debug_category_t category_idx);
|
||||
/* Get current debug level */
|
||||
debug_level_t debug_get_level(void);
|
||||
|
||||
/* Get category name by index */
|
||||
/* Заимствованное имя категории; для неизвестного индекса возвращает "UNKNOWN". */
|
||||
const char* debug_get_category_name(debug_category_t category_idx);
|
||||
|
||||
/* Get category by name string */
|
||||
/* Поиск без учёта регистра: индекс, ALL для "all", NONE при NULL/неизвестном имени. */
|
||||
debug_category_t get_category_by_name(const char* name);
|
||||
|
||||
/* Get level by name string with error reporting */
|
||||
/* Поиск без учёта регистра: NULL → NONE; неизвестное имя логируется и даёт INFO (fallback). */
|
||||
debug_level_t debug_level_from_name(const char* name);
|
||||
|
||||
/* Set category level using text names (main API) */
|
||||
@@ -164,16 +178,20 @@ int debug_apply_category_config(const char* category_name, const char* level_nam
|
||||
/* Apply global debug level from text name */
|
||||
void debug_apply_global_level(const char* level_name);
|
||||
|
||||
/* Format and output debug message (internal use by macros) */
|
||||
/* Форматирует сообщение printf-формата с необязательными метками; фильтрацию выполняет вызывающий.
|
||||
* Время календарное, локальное, hh:mm:ss-миллисекунды.микросекунды; положительный thread_marker выводится отдельно. */
|
||||
void debug_output(debug_level_t level, debug_category_t category_idx,
|
||||
const char* function, const char* file, int line,
|
||||
const char* format, ...);
|
||||
|
||||
/* Log hook — if set, called with every debug message (for JNI/bridge integration) */
|
||||
/* Hook вызывается синхронно для форматированного сообщения до записи в выход; NULL снимает hook.
|
||||
* category/message заимствованы на время вызова, message указывает на стековый буфер; для хранения скопировать. */
|
||||
typedef void (*debug_log_hook_fn)(int level, const char* category, const char* message);
|
||||
void debug_set_log_hook(debug_log_hook_fn hook);
|
||||
|
||||
/* Parse debug configuration from string (e.g. "etcp=trace,config=info") */
|
||||
/* Настройка отдельных категорий: "etcp=trace,config=info" (также ':' вместо '=').
|
||||
* NULL/пустая строка ничего не меняет. Возвращает 0 при успехе, -1 при ошибке;
|
||||
* настройки до ошибочного элемента остаются применёнными. */
|
||||
int debug_parse_config(const char* config_string);
|
||||
|
||||
/* Convenience macros for debug output */
|
||||
@@ -202,8 +220,8 @@ int debug_parse_config(const char* config_string);
|
||||
debug_output(DEBUG_LEVEL_TRACE, category, __FUNCTION__, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
|
||||
} } while(0)
|
||||
|
||||
/* Backward compatibility - default to ERROR level if not specified */
|
||||
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_ALL, fmt, ##__VA_ARGS__)
|
||||
/* Сообщение без явной категории: ERROR в категории GENERAL, с обычной фильтрацией. */
|
||||
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, fmt, ##__VA_ARGS__)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
# debug_config — Runtime-система отладочного логирования
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Модуль обеспечивает гибкое управление отладочным выводом без перекомпиляции. Позволяет:
|
||||
- Задавать **глобальный уровень** логирования (error/warn/info/debug/trace).
|
||||
- **Покатегорийно** включать/выключать или переопределять уровень (26 категорий: etcp, crypto, tun, routing, bgp, bbr, …).
|
||||
- Вести **двойной вывод**: в консоль (stdout) и/или в файл.
|
||||
- Настраивать формат сообщений (временные метки с микросекундами, имя функции, файл:строка).
|
||||
- Парсить конфигурационную строку вида `"etcp=trace,config=info"` из конфиг-файла.
|
||||
|
||||
Логика вывода: итоговый уровень = **max(глобальный уровень, уровень категории)**. Сообщение печатается, если его уровень ≤ итогового.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### 2.1. Инициализация
|
||||
|
||||
```c
|
||||
#include "debug_config.h"
|
||||
|
||||
debug_config_init(); // уровень по умолчанию: ERROR, консоль включена
|
||||
debug_set_level(DEBUG_LEVEL_INFO); // поднять глобальный уровень до INFO
|
||||
debug_enable_file_output("/tmp/utun.log", 1); // писать в файл (truncate=1)
|
||||
debug_apply_category_config("etcp", "trace"); // для ETCP-категории — всё
|
||||
```
|
||||
|
||||
### 2.2. Макросы логирования
|
||||
|
||||
```c
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Decrypt failed: len=%d", len);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Retransmit timeout, seq=%u", seq);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Route added: %s -> %s", dst, gw);
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_TUN, "Packet recv: %zu bytes", n);
|
||||
```
|
||||
|
||||
Макросы делают проверку `debug_should_output()` **перед** вызовом `debug_output()`, поэтому дорогой `vsnprintf` не вызывается, если вывод не нужен.
|
||||
|
||||
### 2.3. Конфигурационная строка
|
||||
|
||||
Формат: `"категория=уровень,категория=уровень,..."`. Парсится вызовом `debug_parse_config()`. Пример из конфиг-файла:
|
||||
|
||||
```ini
|
||||
debug = etcp=trace,config=info,crypto=error
|
||||
```
|
||||
|
||||
Категории: `none, uasync, ll_queue, connection, etcp, crypto, memory, timing, config, tun, routing, timers, normalizer, bgp, socket, control, dump, traffic, debug, general, nat, keepalive, etcp_route, bbr, etcp_dump, connectivity, all`.
|
||||
|
||||
Уровни: `disabled, none, error, warn, info, debug, trace`.
|
||||
|
||||
**Важно:** старый формат `ll_queue:debug` (двоеточие) тоже работает через `debug_parse_config`, но в конфиге рекомендуется `=`.
|
||||
|
||||
### 2.4. Двойной вывод (консоль + файл)
|
||||
|
||||
- По умолчанию вывод в **stdout**.
|
||||
- `debug_enable_file_output(path, truncate)` — перенаправляет вывод в файл, консоль отключается.
|
||||
- `debug_disable_file_output()` — закрывает файл.
|
||||
- `debug_reopen_log()` — переоткрывает лог-файл (используется по SIGHUP для ротации).
|
||||
- `debug_enable_console(1/0)` — ручное управление консольным выводом.
|
||||
|
||||
### 2.5. Вспомогательные утилиты
|
||||
|
||||
```c
|
||||
log_dump(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DUMP, "payload", data, len); // hex-дамп (до 128 байт)
|
||||
|
||||
ip_str_t s = sockaddr_storage_to_str(&addr); // "192.168.1.1:8080" или "[::1]:443"
|
||||
ip_str_t s = ip_to_str(&addr, AF_INET); // "192.168.1.1" (без порта)
|
||||
```
|
||||
|
||||
### 2.6. Нюансы
|
||||
|
||||
- **Потокобезопасность:** модуль не потокобезопасен. Несколько потоков, одновременно вызывающих макросы, могут перемешать вывод (но не упадут). Файловый вывод использует `fflush()` после каждой строки.
|
||||
- **Буфер:** размер строки лога ограничен 4096 байт (`BUFFER_SIZE`), длинные сообщения обрезаются.
|
||||
- **`debug_set_output_file`** против `debug_enable_file_output`: первая вызывается один раз (кто первый — того и тапки), вторая позволяет переоткрыть файл.
|
||||
- **Флаг `-f` (foreground):** в foreground-режиме вывод идёт в stdout, в daemon-режиме — в файл.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Назначение |
|
||||
|---|---|
|
||||
| `debug_config_init()` | Инициализация: глобальный уровень ERROR, консоль включена, метки времени/функций/файлов — да |
|
||||
| `debug_set_level(level)` | Задать глобальный уровень (ERROR..TRACE) |
|
||||
| `debug_set_category_level(cat, level)` | Задать уровень конкретной категории (0 = использовать глобальный) |
|
||||
| `debug_set_category_level_by_name("etcp", "trace")` | То же, но именами строк |
|
||||
| `debug_apply_category_config("etcp", "trace")` | Установить уровень категории с логированием результата |
|
||||
| `debug_apply_global_level("info")` | Установить глобальный уровень по имени строки |
|
||||
| `debug_parse_config("etcp=trace,config=info")` | Разобрать конфигурационную строку |
|
||||
| `debug_should_output(level, cat)` | Проверить, нужно ли выводить сообщение данного уровня/категории |
|
||||
| `debug_output(level, cat, func, file, line, fmt, ...)` | Форматировать и вывести сообщение (вызывается макросами) |
|
||||
| `debug_enable_file_output(path, truncate)` | Включить вывод в файл (консоль отключается) |
|
||||
| `debug_disable_file_output()` | Закрыть файл вывода |
|
||||
| `debug_reopen_log()` | Переоткрыть лог-файл (для SIGHUP/ротации) |
|
||||
| `debug_enable_console(1/0)` | Включить/выключить консольный вывод |
|
||||
| `debug_get_level()` | Получить текущий глобальный уровень |
|
||||
| `get_category_by_name("etcp")` | Получить ID категории по имени строки |
|
||||
| `debug_level_from_name("trace")` | Получить уровень по имени строки |
|
||||
| `debug_get_category_name(cat)` | Получить имя категории по ID |
|
||||
| `log_dump(level, cat, prefix, data, len)` | Hex-дамп данных (первые 128 байт) в лог |
|
||||
| `ip_to_str(addr, family)` | IP-адрес → строка (без порта). Статический буфер |
|
||||
| `sockaddr_storage_to_str(addr)` | sockaddr_storage → "ip:port" (IPv6 в скобках). Статический буфер |
|
||||
| `DEBUG_ERROR/WARN/INFO/DEBUG/TRACE(cat, fmt, ...)` | Макросы логирования с проверкой `debug_should_output()` |
|
||||
| `DEBUG_OUTPUT(fmt, ...)` | Устаревший макрос совместимости (ERROR, категория ALL) |
|
||||
|
||||
**Глобальная переменная:** `g_debug_config` (тип `debug_config_t`) — хранит всё состояние системы логирования.
|
||||
|
||||
**Структура `debug_config_t`:**
|
||||
- `level` — глобальный уровень (по умолчанию ERROR)
|
||||
- `category_levels[26]` — уровни по категориям (NONE = использовать глобальный, DISABLED = никогда не выводить)
|
||||
- `timestamp_enabled` / `function_name_enabled` / `file_line_enabled` — формат вывода
|
||||
- `file_output` — FILE* файла (NULL если вывод в консоль)
|
||||
- `console_enabled` — вывод в stdout
|
||||
- `thread_marker` — числовой маркер потока (0 = нет)
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
#include "file_io.h"
|
||||
#include "platform_compat.h"
|
||||
#include "debug_config.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
static SRWLOCK temp_mutex = SRWLOCK_INIT;
|
||||
#define TEMP_LOCK() AcquireSRWLockExclusive(&temp_mutex)
|
||||
#define TEMP_UNLOCK() ReleaseSRWLockExclusive(&temp_mutex)
|
||||
#else
|
||||
#include <pthread.h>
|
||||
static pthread_mutex_t temp_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
#define TEMP_LOCK() pthread_mutex_lock(&temp_mutex)
|
||||
#define TEMP_UNLOCK() pthread_mutex_unlock(&temp_mutex)
|
||||
#endif
|
||||
|
||||
/* Реестр заимствует стабильный адрес владельца, не выделяет память и общий для workers/instances. */
|
||||
static struct file_io_temp *owned_temps;
|
||||
|
||||
static void file_io_temp_untrack(struct file_io_temp *output) {
|
||||
struct file_io_temp **p = &owned_temps;
|
||||
while (*p && *p != output) p = &(*p)->next_owned;
|
||||
if (*p) *p = output->next_owned;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary owner missing from registry path=%s", output->temporary);
|
||||
output->next_owned = NULL;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) {
|
||||
if (path && *path && strlen(path) < FILE_IO_PATH_MAX
|
||||
&& MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, FILE_IO_PATH_MAX)) return 0;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid UTF-8/oversized path winerr=%lu", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
int file_io_inspect(const char *path, int follow, struct file_io_info *info) {
|
||||
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX || !info) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect invalid path/output"); return -1;
|
||||
}
|
||||
struct file_io_info result = {0};
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return -1;
|
||||
DWORD attributes = GetFileAttributesW(wide);
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES) {
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s winerr=%lu", path, error); return -1;
|
||||
}
|
||||
if (!follow && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) result.type = FILE_IO_LINK;
|
||||
else {
|
||||
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
|
||||
BY_HANDLE_FILE_INFORMATION data;
|
||||
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &data);
|
||||
if (valid) {
|
||||
result.type = GetFileType(handle) != FILE_TYPE_DISK ? FILE_IO_OTHER
|
||||
: (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_IO_DIRECTORY : FILE_IO_REGULAR;
|
||||
uint64_t size = ((uint64_t)data.nFileSizeHigh << 32) | data.nFileSizeLow;
|
||||
valid = size <= INT64_MAX;
|
||||
result.size = (int64_t)size;
|
||||
result.identity[0] = data.dwVolumeSerialNumber;
|
||||
result.identity[1] = ((uint64_t)data.nFileIndexHigh << 32) | data.nFileIndexLow;
|
||||
}
|
||||
DWORD error = GetLastError();
|
||||
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
|
||||
if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file handle inspect failed path=%s winerr=%lu", path, error); return -1; }
|
||||
}
|
||||
#else
|
||||
struct stat data;
|
||||
if ((follow ? stat(path, &data) : lstat(path, &data))) {
|
||||
if (errno == ENOENT) return 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s errno=%d", path, errno); return -1;
|
||||
}
|
||||
result.type = S_ISREG(data.st_mode) ? FILE_IO_REGULAR : S_ISDIR(data.st_mode) ? FILE_IO_DIRECTORY
|
||||
: S_ISLNK(data.st_mode) ? FILE_IO_LINK : FILE_IO_OTHER;
|
||||
if (data.st_size < 0 || (uint64_t)data.st_size > INT64_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file size out of range path=%s", path); return -1;
|
||||
}
|
||||
result.size = (int64_t)data.st_size;
|
||||
result.identity[0] = data.st_dev; result.identity[1] = data.st_ino;
|
||||
#endif
|
||||
*info = result;
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *file_io_open_read(const char *path) {
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file source is not regular/readable path=%s", path ? path : "(null)"); return NULL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return NULL;
|
||||
FILE *file = _wfopen(wide, L"rb");
|
||||
#else
|
||||
FILE *file = fopen(path, "rb");
|
||||
#endif
|
||||
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file open read failed path=%s errno=%d", path, errno);
|
||||
return file;
|
||||
}
|
||||
|
||||
FILE *file_io_open_update(const char *path) {
|
||||
struct file_io_info info;
|
||||
if (file_io_inspect(path, 0, &info) || info.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file update requires regular file path=%s", path ? path : "(null)"); return NULL;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return NULL;
|
||||
FILE *file = _wfopen(wide, L"r+b");
|
||||
#else
|
||||
FILE *file = fopen(path, "r+b");
|
||||
#endif
|
||||
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file update open failed path=%s errno=%d", path, errno);
|
||||
return file;
|
||||
}
|
||||
|
||||
int file_io_paths(const char *source, const char *destination) {
|
||||
struct file_io_info input, output;
|
||||
if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid source path=%s", source ? source : "(null)"); return -1;
|
||||
}
|
||||
int status = file_io_inspect(destination, 0, &output);
|
||||
if (status == 1) return 0;
|
||||
if (status < 0) return -1;
|
||||
if (output.type != FILE_IO_REGULAR) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file destination is not regular path=%s type=%d", destination, output.type); return -1;
|
||||
}
|
||||
return !memcmp(input.identity, output.identity, sizeof(input.identity));
|
||||
}
|
||||
|
||||
/* Синхронизировать именно открытый файл; повторного открытия имени нет. */
|
||||
static int file_io_sync_descriptor(FILE *file, const char *path) {
|
||||
#ifdef _WIN32
|
||||
int result = _commit(_fileno(file));
|
||||
#else
|
||||
int result = fsync(fileno(file));
|
||||
#endif
|
||||
if (result) {
|
||||
int error = errno ? errno : EIO;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync failed path=%s errno=%d", path, error);
|
||||
errno = error;
|
||||
}
|
||||
return result ? -1 : 0;
|
||||
}
|
||||
|
||||
int file_io_sync_file(const char *path) {
|
||||
FILE *file = file_io_open_update(path);
|
||||
if (!file) return -1;
|
||||
int result = file_io_sync_descriptor(file, path);
|
||||
if (fclose(file)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync close failed path=%s errno=%d", path, errno); result = -1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int file_io_sync_directory(const char *path) {
|
||||
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync invalid path"); return -1;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
char parent[FILE_IO_PATH_MAX];
|
||||
memcpy(parent, path, strlen(path) + 1);
|
||||
char *slash = strrchr(parent, '/');
|
||||
if (!slash) memcpy(parent, ".", 2);
|
||||
else if (slash == parent) slash[1] = 0;
|
||||
else *slash = 0;
|
||||
int fd = open(parent, O_RDONLY | O_DIRECTORY);
|
||||
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory open failed path=%s errno=%d", parent, errno); return -1; }
|
||||
int error = fsync(fd) ? (errno ? errno : EIO) : 0;
|
||||
if (close(fd) && !error) error = errno ? errno : EIO;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync/close failed path=%s errno=%d", parent, error); return -1; }
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Только rename; владелец temp снимает регистрацию до длительного directory sync. */
|
||||
static int file_io_rename(const char *source, const char *destination, unsigned flags) {
|
||||
#ifdef _WIN32
|
||||
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(source, from) || file_io_wide(destination, to)) return -1;
|
||||
DWORD options = MOVEFILE_REPLACE_EXISTING | ((flags & FILE_IO_DURABLE) ? MOVEFILE_WRITE_THROUGH : 0);
|
||||
if (!MoveFileExW(from, to, options)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s winerr=%lu", source, destination, GetLastError());
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
(void)flags;
|
||||
if (rename(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s errno=%d", source, destination, errno); return -1;
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
int file_io_publish(const char *source, const char *destination) {
|
||||
if (file_io_paths(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid source/destination"); return -1;
|
||||
}
|
||||
if (file_io_rename(source, destination, FILE_IO_DURABLE)) return -1;
|
||||
if (file_io_sync_directory(destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file installed but directory durability unconfirmed destination=%s", destination); return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file durably installed destination=%s", destination);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags) {
|
||||
if (!output || output->file || output->temporary[0] || !destination || !*destination
|
||||
|| (flags & ~FILE_IO_DURABLE) || strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
|
||||
}
|
||||
output->flags = flags;
|
||||
for (unsigned attempt = 0; attempt < 8; attempt++) {
|
||||
uint8_t random[16]; char suffix[33];
|
||||
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
|
||||
for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]);
|
||||
char temporary[FILE_IO_PATH_MAX];
|
||||
snprintf(temporary, sizeof(temporary), "%s.tmp.%s", destination, suffix);
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(temporary, wide)) return -1;
|
||||
TEMP_LOCK();
|
||||
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
|
||||
#else
|
||||
TEMP_LOCK();
|
||||
int fd = open(temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
|
||||
#endif
|
||||
if (fd < 0) {
|
||||
int error = errno; TEMP_UNLOCK();
|
||||
if (error == EEXIST) continue;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, error); return -1;
|
||||
}
|
||||
memcpy(output->temporary, temporary, strlen(temporary) + 1);
|
||||
output->next_owned = owned_temps; owned_temps = output;
|
||||
TEMP_UNLOCK(); /* Файл виден очистке только вместе с зарегистрированным владельцем. */
|
||||
#ifdef _WIN32
|
||||
output->file = _fdopen(fd, "wb+");
|
||||
if (!output->file) _close(fd);
|
||||
#else
|
||||
output->file = fdopen(fd, "wb+");
|
||||
if (!output->file) close(fd);
|
||||
#endif
|
||||
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s durable=%u", output->temporary, flags & FILE_IO_DURABLE);
|
||||
return 0;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
|
||||
}
|
||||
|
||||
int file_io_temp_finish(struct file_io_temp *output) {
|
||||
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
|
||||
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
|
||||
if (!error && (output->flags & FILE_IO_DURABLE) && file_io_sync_descriptor(output->file, output->temporary))
|
||||
error = errno ? errno : EIO;
|
||||
if (fclose(output->file) && !error) error = errno ? errno : EIO;
|
||||
output->file = NULL;
|
||||
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/sync/close failed path=%s errno=%d", output->temporary, error); return -1; }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary finished path=%s durable=%u", output->temporary, output->flags & FILE_IO_DURABLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void file_io_temp_discard(struct file_io_temp *output) {
|
||||
if (!output) return;
|
||||
if (output->file) {
|
||||
if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup close failed errno=%d", errno);
|
||||
output->file = NULL;
|
||||
}
|
||||
if (!output->temporary[0]) return;
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
int valid = file_io_wide(output->temporary, wide) == 0;
|
||||
TEMP_LOCK();
|
||||
int error = valid ? _wremove(wide) : -1;
|
||||
#else
|
||||
TEMP_LOCK();
|
||||
int error = remove(output->temporary);
|
||||
#endif
|
||||
if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno);
|
||||
file_io_temp_untrack(output);
|
||||
output->temporary[0] = 0;
|
||||
TEMP_UNLOCK();
|
||||
}
|
||||
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) {
|
||||
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
|
||||
}
|
||||
TEMP_LOCK();
|
||||
if (file_io_rename(output->temporary, destination, output->flags)) { TEMP_UNLOCK(); return -1; }
|
||||
file_io_temp_untrack(output);
|
||||
output->temporary[0] = 0;
|
||||
TEMP_UNLOCK();
|
||||
if ((output->flags & FILE_IO_DURABLE) && file_io_sync_directory(destination)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary installed but directory durability unconfirmed destination=%s", destination); return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s durable=%u", destination, output->flags & FILE_IO_DURABLE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int file_io_remove_orphan(const char *path) {
|
||||
if (!path || !*path) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file orphan removal without path"); return -1; }
|
||||
#ifdef _WIN32
|
||||
wchar_t wide[FILE_IO_PATH_MAX];
|
||||
if (file_io_wide(path, wide)) return -1;
|
||||
#endif
|
||||
TEMP_LOCK();
|
||||
for (struct file_io_temp *owner = owned_temps; owner; owner = owner->next_owned) {
|
||||
if (!strcmp(owner->temporary, path)) {
|
||||
TEMP_UNLOCK();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file orphan cleanup keeps active temporary path=%s", path);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int result = _wremove(wide);
|
||||
#else
|
||||
int result = remove(path);
|
||||
#endif
|
||||
int error = errno; TEMP_UNLOCK();
|
||||
if (!result) return 1;
|
||||
if (error == ENOENT) return 0;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "file orphan removal failed path=%s errno=%d", path, error);
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* file_io — синхронные файловые операции: UTF-8 пути, идентичность и публикация через соседний temp.
|
||||
* inspect различает найденный/отсутствующий файл и ошибку; paths сравнивает идентичность файлов.
|
||||
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
|
||||
* Активный temp автоматически защищён от remove_orphan во всех потоках до publish/discard, включая время после finish.
|
||||
* Пока temp активен, структуру нельзя копировать/перемещать; с одним temp работает один владелец.
|
||||
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
|
||||
* FILE_IO_DURABLE сохраняет данные при finish и имя при publish. POSIX temp имеет права 0600.
|
||||
* После успешного rename temp отсоединён даже при ошибке синхронизации каталога: отката публикации нет. */
|
||||
#ifndef FILE_IO_H
|
||||
#define FILE_IO_H
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define FILE_IO_PATH_MAX 4096
|
||||
#define FILE_IO_DURABLE 1u
|
||||
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
|
||||
struct file_io_info {
|
||||
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
|
||||
int64_t size; // размер в байтах, без сужения до int
|
||||
uint64_t identity[3]; // идентификатор файла внутри файловой системы, сравнивать весь массив
|
||||
};
|
||||
struct file_io_temp {
|
||||
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
|
||||
FILE *file; // открытый wb+; NULL после finish
|
||||
struct file_io_temp *next_owned; // внутренний реестр живых temp; изменяет только file_io под блокировкой
|
||||
unsigned flags; // режим, заданный open: 0 либо FILE_IO_DURABLE на весь срок операции
|
||||
};
|
||||
|
||||
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
|
||||
int file_io_inspect(const char *path, int follow, struct file_io_info *info);
|
||||
#ifdef _WIN32
|
||||
// Преобразовать непустой UTF-8 путь в wide[FILE_IO_PATH_MAX]; 0/-1.
|
||||
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]);
|
||||
#endif
|
||||
// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему.
|
||||
FILE *file_io_open_read(const char *path);
|
||||
// Открыть существующий regular-файл для чтения/записи без усечения; конечные ссылки запрещены. UTF-8, NULL при ошибке.
|
||||
FILE *file_io_open_update(const char *path);
|
||||
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
|
||||
int file_io_paths(const char *source, const char *destination);
|
||||
// Сохранить существующий закрытый regular-файл без конечной ссылки; UTF-8, 0/-1.
|
||||
int file_io_sync_file(const char *path);
|
||||
// Сохранить каталог, содержащий path; без разделителя используется '.', для '/file' — '/'. UTF-8, 0/-1.
|
||||
int file_io_sync_directory(const char *path);
|
||||
// Переместить отдельный готовый файл с durable заменой destination; caller владеет source. 0/-1.
|
||||
// Ошибка после rename не откатывает замену; сохраняется каталог назначения. Файлы/каталоги не подменяются извне.
|
||||
int file_io_publish(const char *source, const char *destination);
|
||||
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
|
||||
// flags=0 — flush/close и atomic rename; FILE_IO_DURABLE — также sync файла и каталога назначения.
|
||||
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags);
|
||||
// Проверить flush/[sync]/close, обнулить file при любом результате; temp остаётся собственностью output.
|
||||
int file_io_temp_finish(struct file_io_temp *output);
|
||||
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
|
||||
void file_io_temp_discard(struct file_io_temp *output);
|
||||
// Повторно проверить paths и заменить destination закрытым соседним temp; одинаковые файлы отклоняются. 0/-1.
|
||||
// При ошибке до rename temporary сохраняется; после rename он пуст, в том числе при отказе directory sync.
|
||||
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
|
||||
/* Удалить уже признанный сиротой файл, атомарно проверив владение file_io_temp во всех потоках.
|
||||
* 1 — удалён, 0 — активный temp или уже отсутствует, -1 — ошибка (логируется).
|
||||
* path должен иметь то же нормализованное написание, что и путь при temp_open; рабочий каталог не меняется. */
|
||||
int file_io_remove_orphan(const char *path);
|
||||
#endif
|
||||
+6
-4
@@ -3,6 +3,8 @@
|
||||
*
|
||||
* Определяет локальный IP, который ОС выберет для отправки
|
||||
* на заданный удаленный адрес через заданный локальный адрес.
|
||||
* Передать local/remote одного семейства AF_INET или AF_INET6 и буфер result.
|
||||
* Создаёт и закрывает временный UDP-сокет; предназначен для выбора маршрута, не для обработки каждого пакета.
|
||||
*/
|
||||
|
||||
#ifndef GETMYIP_H
|
||||
@@ -26,13 +28,13 @@ extern "C" {
|
||||
* Определяет локальный IP-адрес, который ОС выберет для отправки
|
||||
* на заданный удаленный адрес через заданный локальный адрес.
|
||||
*
|
||||
* Создает временный UDP-сокет, выполняет bind на local_addr,
|
||||
* Создает временный UDP-сокет, выполняет bind на копию local_addr с портом 0,
|
||||
* connect к remote_addr и получает выбранный локальный IP через getsockname.
|
||||
*
|
||||
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения)
|
||||
* @param remote_addr Удаленный адрес (для определения маршрута)
|
||||
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения); входной порт игнорируется
|
||||
* @param remote_addr Удаленный адрес того же семейства (для определения маршрута)
|
||||
* @param result Заполняемый локальный IP (порт = 0)
|
||||
* @return 0 при успехе, -1 при ошибке
|
||||
* @return 0 при успехе, -1 при NULL, неподдерживаемом/разном семействе или ошибке socket/bind/connect/getsockname
|
||||
*/
|
||||
int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
const struct sockaddr_storage *remote_addr,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
# Get My IP (getmyip.h)
|
||||
|
||||
## 1. Назначение
|
||||
Определение локального IP-адреса, который ОС выберет для исходящего UDP-соединения на заданный удалённый хост. Используется в `etcp_connections.c` при установке соединения: зная remote-адрес, модуль узнаёт, какой source IP ядро назначит пакетам (нужно для проверки NAT и привязки сокета к правильному интерфейсу).
|
||||
|
||||
## 2. Как пользоваться
|
||||
Передать локальный адрес-подсказку (или `0.0.0.0` для автоопределения) и удалённый адрес. Функция создаст временный UDP-сокет, сделает bind+connect, считает выбранный IP через `getsockname` и вернёт его в `result` (порт = 0, только IP).
|
||||
|
||||
```c
|
||||
struct sockaddr_storage local_hint; // AF_INET6, sin6_addr = in6addr_any
|
||||
struct sockaddr_storage remote; // AF_INET6, адрес сервера
|
||||
struct sockaddr_storage chosen_ip;
|
||||
|
||||
// ... заполнить local_hint и remote ...
|
||||
|
||||
if (get_outgoing_local_ip(&local_hint, &remote, &chosen_ip) == 0) {
|
||||
// chosen_ip.ss_family == AF_INET6, sin6_addr содержит выбранный source IP
|
||||
// chosen_ip.sin6_port == 0
|
||||
}
|
||||
```
|
||||
|
||||
**Нюансы:**
|
||||
- Функция временно создаёт и закрывает UDP-сокет — нельзя вызывать в hot path
|
||||
- `local_addr` и `remote_addr` должны быть одного семейства (оба AF_INET или оба AF_INET6)
|
||||
- Порт в `local_addr` заменяется на 0 (чтобы не конфликтовать с основными сокетами)
|
||||
- Порт в `result` всегда 0 — только IP
|
||||
|
||||
## 3. API
|
||||
|
||||
### Функция
|
||||
- **`get_outgoing_local_ip(local_addr, remote_addr, result)`** — создаёт временный UDP-сокет, привязывает к `local_addr` (с портом 0), делает `connect` к `remote_addr` для вычисления маршрута ядром, получает выбранный source IP через `getsockname`. Возвращает 0 при успехе, -1 при ошибке (NULL-аргументы, несовпадение семейств, ошибки сокета).
|
||||
|
||||
### Зависимости
|
||||
- `socket_compat.h` — кроссплатформенные обёртки: `socket_create_udp`, `SOCKET_INVALID`, `socket_close_wrapper`, `socket_get_error`, `socket_strerror`
|
||||
@@ -5,6 +5,8 @@
|
||||
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
|
||||
* Callback получает полные key/value, действительные только до его возврата.
|
||||
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
|
||||
* Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые;
|
||||
* хвост после закрывающей } игнорируется даже без остановки callback.
|
||||
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
|
||||
*/
|
||||
|
||||
|
||||
+26
-14
@@ -129,15 +129,7 @@ struct ll_entry* ll_alloc_lldgram(uint16_t len) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
void queue_free(struct ll_queue* q) {
|
||||
if (!q) return;
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "queue_free: freeing queue %p, head=%p, tail=%p, count=%d",
|
||||
q, q->head, q->tail, q->count);
|
||||
|
||||
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
|
||||
// Это упрощает архитектуру и предотвращает double-u_free
|
||||
|
||||
static void queue_cancel_notifications(struct ll_queue* q) {
|
||||
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
|
||||
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
|
||||
struct queue_waiter* w = q->waiter_head;
|
||||
@@ -150,11 +142,6 @@ void queue_free(struct ll_queue* q) {
|
||||
q->waiter_head = NULL;
|
||||
q->waiter_tail = NULL;
|
||||
|
||||
// Освободить хеш-таблицу
|
||||
if (q->hash_table) {
|
||||
u_free(q->hash_table);
|
||||
}
|
||||
|
||||
// Отменить отложенное возобновление
|
||||
if (q->resume_timeout_id) {
|
||||
uasync_call_soon_cancel(q->ua, q->resume_timeout_id);
|
||||
@@ -170,6 +157,31 @@ void queue_free(struct ll_queue* q) {
|
||||
q->on_get_arg = NULL;
|
||||
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
|
||||
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
|
||||
q->callback_suspended = 1;
|
||||
}
|
||||
|
||||
void queue_clear(struct ll_queue* q) {
|
||||
if (!q) return;
|
||||
#ifdef QUEUE_THREAD_CHECK
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "[%s] discard queue: packets=%d bytes=%zu", q->name, q->count, q->total_bytes);
|
||||
queue_cancel_notifications(q);
|
||||
struct ll_entry* entry = q->head;
|
||||
q->head = q->tail = NULL; q->count = 0; q->total_bytes = 0;
|
||||
if (q->hash_table) memset(q->hash_table, 0, q->hash_size * sizeof(*q->hash_table));
|
||||
while (entry) {
|
||||
struct ll_entry* next = entry->next;
|
||||
entry->owner = NULL; entry->next = entry->prev = entry->hash_next = NULL;
|
||||
queue_dgram_free(entry); queue_entry_free(entry); entry = next;
|
||||
}
|
||||
}
|
||||
|
||||
void queue_free(struct ll_queue* q) {
|
||||
if (!q) return;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "queue_free: freeing queue %p, head=%p, count=%d", (void*)q, (void*)q->head, q->count);
|
||||
queue_cancel_notifications(q);
|
||||
if (q->hash_table) u_free(q->hash_table);
|
||||
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
|
||||
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
|
||||
|
||||
|
||||
+97
-49
@@ -41,8 +41,8 @@ extern "C" {
|
||||
* @note Важные правила использования автозабора (queue_set_callback):
|
||||
* 1. Коллбэк получает управление только если в очереди есть элементы И !callback_suspended
|
||||
* 2. Внутри коллбэка ОБЯЗАТЕЛЬНО: queue_data_get() → обработать элемент → queue_resume_callback()
|
||||
* 3. Без вызова queue_resume_callback очередь навсегда застрянет
|
||||
* 4. Не извлекайте элементы вручную вне коллбэка — это нарушит логику
|
||||
* 3. Без queue_resume_callback автозабор остаётся приостановленным
|
||||
* 4. Ручное извлечение допустимо; для продолжения автозабора после него тоже нужен resume
|
||||
* 5. queue_data_get() автоматически suspend'ит коллбэки для предотвращения рекурсии
|
||||
*
|
||||
* @note Память:
|
||||
@@ -61,7 +61,7 @@ struct queue_waiter_handle;
|
||||
* @brief Элемент очереди (переменного размера).
|
||||
*
|
||||
* Память выделяется одним блоком: [struct ll_entry + data[size]].
|
||||
* Поле dgram — отдельный блок (malloc / пул / внешняя память).
|
||||
* Поле dgram — отдельный блок (u_malloc / пул / внешняя память со своим dgram_free_fn).
|
||||
*/
|
||||
struct ll_entry {
|
||||
char* name;
|
||||
@@ -75,9 +75,9 @@ struct ll_entry {
|
||||
uint8_t* dgram; // Указатель на данные пакета
|
||||
void (*dgram_free_fn)(uint8_t* data); // Кастомная функция освобождения dgram
|
||||
struct memory_pool* dgram_pool; // Пул для dgram (если используется)
|
||||
uint32_t index_hash; // Хеш по первым 4 байтам индекса
|
||||
uint32_t index_hash; // FNV-1a (32 бита) по всем index_size байтам data[] от index_offset
|
||||
struct ll_entry* hash_next; // Следующий в хеш-цепочке
|
||||
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = malloc)
|
||||
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = u_malloc)
|
||||
uint8_t data[0]; // Гибкий массив пользовательских данных (размер = size)
|
||||
};
|
||||
|
||||
@@ -181,7 +181,7 @@ struct ll_queue {
|
||||
#endif
|
||||
};
|
||||
|
||||
/* ==================== Создание / уничтожение ==================== */
|
||||
// ==================== Создание / уничтожение ====================
|
||||
|
||||
/**
|
||||
* @brief Создаёт новую пустую очередь.
|
||||
@@ -189,8 +189,8 @@ struct ll_queue {
|
||||
* @param hash_size размер хеш-таблицы (0 = поиск по id отключён)
|
||||
* @param index_offset смещение ключа индекса в data[] каждого entry (0 если hash_size=0)
|
||||
* @param index_size размер ключа индекса (0 если hash_size=0)
|
||||
* @param name имя очереди для отладки
|
||||
* @return указатель на очередь или NULL при ошибке выделения памяти
|
||||
* @param name заимствованное имя для отладки, должно жить до queue_free
|
||||
* @return указатель на очередь или NULL при NULL ua / ошибке выделения памяти
|
||||
*/
|
||||
struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_offset, uint16_t index_size, char* name);
|
||||
|
||||
@@ -200,42 +200,55 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o
|
||||
*
|
||||
* @warning НЕ ОСВОБОЖДАЕТ элементы в очереди!
|
||||
* Их нужно извлечь через queue_data_get() и освободить через queue_entry_free().
|
||||
* Перед дренажем снять callback и отменить ожидания владельцев, затем для каждого entry освободить dgram и entry.
|
||||
* queue_free отменяет свои отложенные уведомления и waiters; user_arg не освобождает. NULL допустим.
|
||||
*/
|
||||
void queue_free(struct ll_queue* q);
|
||||
|
||||
/* ==================== Конфигурация ==================== */
|
||||
/* Аварийный сброс: освободить все entry/dgram, отменить waiters и отложенные уведомления без их вызова.
|
||||
* Не выделяет память (кроме действий пользовательского dgram_free_fn). Очередь/индекс/обычный callback/пороги сохраняются.
|
||||
* empty/on_get обработчики снимаются, callback приостановлен; владельцы заново ставят ожидания после восстановления.
|
||||
* Вызывать в потоке ua; NULL допустим. Для продолжения автозабора нужен queue_resume_callback(). */
|
||||
void queue_clear(struct ll_queue* q);
|
||||
|
||||
// ==================== Конфигурация ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает максимальное количество элементов в очереди.
|
||||
* @param q очередь
|
||||
* @param lim лимит (-1 = без ограничения)
|
||||
* @param lim лимит (любое отрицательное значение = без ограничения)
|
||||
*
|
||||
* При превышении лимита новые элементы автоматически освобождаются.
|
||||
*/
|
||||
void queue_set_size_limit(struct ll_queue* q, int lim);
|
||||
|
||||
/* ==================== Автозабор элементов ==================== */
|
||||
// ==================== Автозабор элементов ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает коллбэк для автоматического извлечения элементов.
|
||||
* @param q очередь
|
||||
* @param cbk_fn функция-коллбэк
|
||||
* @param cbk_fn функция-коллбэк (NULL отключает автозабор)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Коллбэк вызывается автоматически при наличии элементов и разрешённом состоянии.
|
||||
* Только заменяет функцию и аргумент: не запускает обработку, не снимает callback_suspended
|
||||
* и не отменяет уже запланированный resume. Отложенный resume использует новые функцию и аргумент.
|
||||
* Для запуска автозабора из уже заполненной очереди вызовите queue_resume_callback().
|
||||
*/
|
||||
void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Возобновляет работу коллбэка после обработки элемента в следующем event loop.
|
||||
* @brief Разрешает автозабор и планирует отложенный вызов коллбэка.
|
||||
* @param q очередь
|
||||
*
|
||||
* @warning ОБЯЗАТЕЛЬНО вызывать после завершения обработки элемента в коллбэке.
|
||||
* Иначе автозабор элементов навсегда остановится.
|
||||
* Сразу снимает callback_suspended. Если есть элементы и callback, планирует uasync_call_soon,
|
||||
* если resume ещё не запланирован. При исполнении повторно проверяет наличие элементов,
|
||||
* callback и отсутствие приостановки. Последующий put в пустую очередь может вызвать callback
|
||||
* синхронно, если callback_defer == 0.
|
||||
* @note Вызывать после завершения обработки извлечённого элемента для продолжения автозабора.
|
||||
*/
|
||||
void queue_resume_callback(struct ll_queue* q);
|
||||
|
||||
/* ==================== Пороговое ожидание (backpressure) ==================== */
|
||||
// ==================== Пороговое ожидание (backpressure) ====================
|
||||
|
||||
/**
|
||||
* @brief Устанавливает общий порог освобождения очереди.
|
||||
@@ -244,11 +257,15 @@ void queue_resume_callback(struct ll_queue* q);
|
||||
* @param max_bytes максимальный объём данных (0 = не проверять)
|
||||
*
|
||||
* Порог — общий на всю очередь. Все waiters в связном списке используют этот порог.
|
||||
* При достижении порога вызывается следующий waiter из списка (FIFO / round-robin).
|
||||
* После успешного get/remove при выполнении порога вызывается один следующий waiter из FIFO-списка.
|
||||
* Сам setter старые ожидания не проверяет. Байт-порог использует int_len, зафиксированный при put.
|
||||
* По умолчанию: max_packets=0, max_bytes=0 (пустая очередь).
|
||||
*/
|
||||
void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
|
||||
|
||||
// enable=0 — синхронный waiter (по умолчанию), !=0 — через uasync_call_soon.
|
||||
// Deferred-уведомление не резервирует место и повторно порог не проверяет: очередь к исполнению может заполниться.
|
||||
// Повторную регистрацию producer из его callback при свободной очереди откладывать, чтобы избежать рекурсии.
|
||||
void queue_set_waiter_defer(struct ll_queue* q, int enable);
|
||||
|
||||
/**
|
||||
@@ -268,28 +285,38 @@ void queue_set_callback_defer(struct ll_queue* q, int enable);
|
||||
* @param cbk_fn коллбэк (NULL для отмены)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Коллбэк вызывается асинхронно (через uasync_call_soon) когда count становится 0.
|
||||
* После вызова сбрасывается — для повторного использования нужно вызвать снова.
|
||||
* Заменяет регистрацию и отменяет её прежний отложенный вызов. При count == 0 сразу планирует
|
||||
* uasync_call_soon; иначе вызов планируется при опустошении через queue_data_get/queue_remove_data.
|
||||
* При исполнении снова проверяет count: если очередь заполнилась, регистрация сохраняется
|
||||
* до следующего опустошения. Функция и аргумент сбрасываются ДО вызова callback,
|
||||
* поэтому внутри него можно зарегистрировать новое уведомление.
|
||||
*/
|
||||
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Устанавливает коллбэк вызываемый после каждого queue_data_get (deferred через call_soon).
|
||||
* @brief Устанавливает коллбэк после каждого успешного queue_data_get (deferred через call_soon).
|
||||
* @param q очередь
|
||||
* @param cbk_fn коллбэк (NULL для отмены)
|
||||
* @param arg пользовательский аргумент
|
||||
*
|
||||
* Уведомления подсчитываются, не сливаются в одно; remove их не создаёт.
|
||||
* NULL отменяет отложенный вызов и обнуляет накопленные уведомления. Замена использует новые функцию/arg.
|
||||
*/
|
||||
void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
|
||||
/**
|
||||
* @brief Регистрирует ожидание освобождения очереди до общего порога.
|
||||
* @param q очередь
|
||||
* @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}`
|
||||
* @param h handle, полностью обнулённый перед первым использованием: `struct queue_waiter_handle h = {0}`
|
||||
* @param callback функция, вызываемая при достижении порога
|
||||
* @param arg аргумент коллбэка
|
||||
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
|
||||
*
|
||||
* Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется.
|
||||
* При корректных аргументах повторная регистрация в той же очереди отменяет прежнее ожидание.
|
||||
* Если h->internal != NULL, перед переносом в другую очередь обязательно вызовите
|
||||
* queue_waiter_cancel(old_q, h): очередь старого ожидания в handle не хранится.
|
||||
* Уже отложенный callback (h->call_soon_id != NULL) отменяется по сохранённому h->defer_q,
|
||||
* поэтому в этом состоянии handle можно передать другой очереди без отдельной отмены.
|
||||
* При queue_free отменяются также уже запланированные уведомления.
|
||||
*/
|
||||
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
@@ -297,44 +324,61 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
|
||||
/**
|
||||
* @brief Отменяет ожидание (удаляет из списка, если зарегистрирован).
|
||||
* @param q очередь
|
||||
* @param q очередь исходного ожидания (для h->internal); не NULL
|
||||
* @param h указатель на handle
|
||||
*
|
||||
* Безопасно вызывать в любом состоянии: если waiter не зарегистрирован — ничего не делает.
|
||||
* Отложенный callback отменяется через сохранённую h->defer_q. Если ожидания и отложенного
|
||||
* callback нет — ничего не делает. При h->internal != NULL передавать другую очередь нельзя.
|
||||
*/
|
||||
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
|
||||
|
||||
/* ==================== Работа с данными ==================== */
|
||||
// ==================== Работа с данными ====================
|
||||
|
||||
/**
|
||||
* Общие правила владения для всех четырёх queue_data_put*:
|
||||
* - При q == NULL или entry == NULL возвращается -1 без освобождения памяти.
|
||||
* - Элемент должен быть извлечён из прежней очереди: owner, prev и next должны быть NULL.
|
||||
* Повторная вставка связанного элемента приводит к abort().
|
||||
* - При остальных отказах (лимит, неверный вид put или границы индекса) освобождаются
|
||||
* сначала dgram через queue_dgram_free(), затем entry через queue_entry_free().
|
||||
* - При успехе владение передаётся очереди/потребителю. Put может синхронно вызвать callback,
|
||||
* который обработает и освободит entry и даже очередь до возврата из put.
|
||||
* Использовать эти указатели после put можно только при гарантированном времени жизни.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в конец очереди (FIFO).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
* @param q очередь без хеш-таблицы (hash_size == 0)
|
||||
* @param entry элемент; правила передачи владения см. выше
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
|
||||
*/
|
||||
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
* @param q очередь без хеш-таблицы (hash_size == 0)
|
||||
* @param entry элемент; правила передачи владения см. выше
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
|
||||
*/
|
||||
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в конец очереди с хеш-индексом (использует index_offset/index_size из очереди).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — ошибка
|
||||
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
|
||||
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
|
||||
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
|
||||
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
|
||||
*/
|
||||
int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/**
|
||||
* @brief Добавляет элемент в начало очереди с хеш-индексом (использует index_offset/index_size из очереди).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @return 0 — успех, -1 — ошибка
|
||||
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
|
||||
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
|
||||
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
|
||||
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
|
||||
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
|
||||
*/
|
||||
int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
@@ -356,35 +400,35 @@ struct ll_entry* queue_data_get(struct ll_queue* q);
|
||||
*/
|
||||
int queue_entry_count(struct ll_queue* q);
|
||||
|
||||
/* ==================== Управление памятью элементов ==================== */
|
||||
// ==================== Управление памятью элементов ====================
|
||||
|
||||
// проверить правильность очереди (корректность цепочки, счетчики элементов и байтов)
|
||||
int queue_check_consistency(struct ll_queue* q);
|
||||
|
||||
/**
|
||||
* @brief Выделяет элемент с отдельным буфером dgram (malloc).
|
||||
* @brief Выделяет элемент с отдельным буфером dgram через u_malloc (entry->len=0, memlen=len).
|
||||
* @param len требуемый размер dgram
|
||||
* @return элемент или NULL при ошибке
|
||||
*/
|
||||
struct ll_entry* ll_alloc_lldgram(uint16_t len);
|
||||
|
||||
/**
|
||||
* @brief Создаёт элемент с пользовательским буфером data[] (malloc одним блоком).
|
||||
* @param data_size размер data[]
|
||||
* @brief Создаёт обнулённый элемент с пользовательским буфером data[] (u_malloc одним блоком).
|
||||
* @param data_size размер data[], должен помещаться в uint16_t
|
||||
* @return элемент или NULL при ошибке
|
||||
*/
|
||||
struct ll_entry* queue_entry_new(size_t data_size);
|
||||
|
||||
/**
|
||||
* @brief Создаёт элемент из пула памяти.
|
||||
* @param pool пул
|
||||
* @return элемент или NULL, если пул пуст
|
||||
* @param pool пул с object_size >= sizeof(struct ll_entry); разность размеров должна помещаться в uint16_t
|
||||
* @return элемент или NULL при ошибке выделения; пустой кеш пула пополняется автоматически
|
||||
*/
|
||||
struct ll_entry* queue_entry_new_from_pool(struct memory_pool* pool);
|
||||
|
||||
/**
|
||||
* @brief Освобождает структуру элемента (возвращает в пул или free).
|
||||
* @param entry элемент
|
||||
* @brief Освобождает структуру элемента (возвращает в пул или u_free).
|
||||
* @param entry извлечённый из очереди элемент; NULL допустим
|
||||
*
|
||||
* @note НЕ освобождает dgram! Используйте queue_dgram_free() отдельно.
|
||||
*/
|
||||
@@ -393,10 +437,14 @@ void queue_entry_free(struct ll_entry* entry);
|
||||
/**
|
||||
* @brief Освобождает буфер dgram элемента.
|
||||
* @param entry элемент
|
||||
*
|
||||
* При dgram != NULL вызывает dgram_free_fn, если она задана; иначе возвращает буфер в dgram_pool,
|
||||
* если задан пул; иначе вызывает u_free. Затем сбрасывает dgram и len. Сам entry не освобождает.
|
||||
* Внешнему буферу нужен подходящий dgram_free_fn.
|
||||
*/
|
||||
void queue_dgram_free(struct ll_entry* entry);
|
||||
|
||||
/* ==================== Поиск и удаление ==================== */
|
||||
// ==================== Поиск и удаление ====================
|
||||
|
||||
/**
|
||||
* @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании).
|
||||
@@ -426,12 +474,12 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q,
|
||||
*/
|
||||
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
/* ==================== Утилиты ==================== */
|
||||
// ==================== Утилиты ====================
|
||||
|
||||
/**
|
||||
* @brief Возвращает суммарный объём данных в очереди.
|
||||
* @param q очередь
|
||||
* @return количество байт
|
||||
* @return сумма int_len (снимки len при put); изменение len связанного entry счётчик не пересчитывает
|
||||
*/
|
||||
static inline size_t queue_total_bytes(struct ll_queue* q) {
|
||||
return q->total_bytes;
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
# ll_queue — Двусвязная FIFO-очередь с автозабором и backpressure
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Двусвязная очередь элементов (FIFO) с автоматическим вызовом callback при появлении данных, пороговым ожиданием освобождения (backpressure), хеш-индексом для поиска и поддержкой пулов памяти. Используется как основной механизм передачи данных между компонентами внутри одного потока uasyc: TUN ↔ ETCP ↔ routing ↔ normalizer и т.д.
|
||||
|
||||
Работает **строго в одном потоке** (один uasyc). Для многопоточного доступа не предназначена.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### 2.1. Создание очереди
|
||||
|
||||
```c
|
||||
// Без хеша (простая очередь)
|
||||
struct ll_queue* q = queue_new(ua, 0, 0, 0, "my_queue");
|
||||
|
||||
// С хешем — для быстрого поиска по ключу внутри data[]
|
||||
// hash_size=256, ключ лежит в data[] по смещению 0, размер ключа 4 байта
|
||||
struct ll_queue* q = queue_new(ua, 256, 0, 4, "my_hash_queue");
|
||||
```
|
||||
|
||||
### 2.2. Создание элемента
|
||||
|
||||
Три способа, по убыванию производительности:
|
||||
|
||||
```c
|
||||
// 1. Из memory pool (самый быстрый)
|
||||
struct ll_entry* e = queue_entry_new_from_pool(instance->pkt_pool);
|
||||
|
||||
// 2. Через malloc (data[] фиксированного размера одним блоком)
|
||||
struct ll_entry* e = queue_entry_new(sizeof(struct my_data));
|
||||
|
||||
// 3. Элемент + отдельный буфер dgram (для переменного размера данных)
|
||||
struct ll_entry* e = ll_alloc_lldgram(dgram_len);
|
||||
memcpy(e->dgram, src_data, dgram_len);
|
||||
e->len = dgram_len;
|
||||
```
|
||||
|
||||
### 2.3. Запись в очередь (backpressure)
|
||||
|
||||
**Главное правило: не забиваем очередь!** Добавляем следующий элемент только когда очередь опустела до заданного порога. Для этого используется Пороговое ожидание:
|
||||
|
||||
```c
|
||||
// 1. Настроить порог (например: ждать пока count<=0, т.е. очередь совсем пуста)
|
||||
queue_set_threshold(q, 0, 0);
|
||||
|
||||
// 2. В структуре-производителе завести handle:
|
||||
struct queue_waiter_handle my_waiter = {0};
|
||||
|
||||
// 3. При готовности отправить — зарегистрировать ожидание:
|
||||
static void my_send_cb(struct ll_queue* q, void* arg) {
|
||||
struct my_producer* p = (struct my_producer*)arg;
|
||||
struct ll_entry* e = ... // создать элемент
|
||||
e->len = data_len;
|
||||
queue_data_put_with_index(q, e);
|
||||
// Снова зарегистрироваться на следующую порцию
|
||||
queue_waiter_wait(q, &p->waiter, my_send_cb, p);
|
||||
}
|
||||
|
||||
// Первичная регистрация:
|
||||
queue_waiter_wait(q, &my_waiter, my_send_cb, producer);
|
||||
|
||||
// 4. При деинициализации — отменить ожидание:
|
||||
queue_waiter_cancel(q, &my_waiter);
|
||||
```
|
||||
|
||||
### 2.4. Чтение из очереди (callback)
|
||||
|
||||
```c
|
||||
// Установить callback — будет вызываться при появлении элементов
|
||||
queue_set_callback(q, my_callback, my_data);
|
||||
|
||||
static void my_callback(struct ll_queue* q, void* arg) {
|
||||
// 1. ИЗВЛЕЧЬ элемент
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
|
||||
// 2. ОБРАБОТАТЬ
|
||||
if (e->dgram && e->len > 0) {
|
||||
do_something(e->dgram, e->len);
|
||||
queue_dgram_free(e); // сначала освободить dgram
|
||||
}
|
||||
queue_entry_free(e); // потом освободить сам entry
|
||||
|
||||
// 3. ОБЯЗАТЕЛЬНО возобновить callback
|
||||
queue_resume_callback(q);
|
||||
}
|
||||
```
|
||||
|
||||
**Критически важно:** `queue_data_get()` автоматически приостанавливает callback-и (`callback_suspended = 1`). Без вызова `queue_resume_callback()` очередь **навсегда застрянет** — callback больше никогда не вызовется.
|
||||
|
||||
Во **всех ветках** (включая ранние return по ошибке) должен быть `queue_resume_callback(q)`:
|
||||
|
||||
```c
|
||||
// ПРАВИЛЬНО:
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
if (!e->dgram) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
|
||||
|
||||
// НЕПРАВИЛЬНО — очередь зависнет:
|
||||
if (!e) return;
|
||||
```
|
||||
|
||||
### 2.5. Поиск по индексу
|
||||
|
||||
```c
|
||||
// Создать очередь с хешем (ключ 4 байта по смещению 0):
|
||||
struct ll_queue* q = queue_new(ua, 256, 0, 4, "indexed_q");
|
||||
|
||||
// Добавлять элементы через _with_index (хеш вычисляется автоматически):
|
||||
queue_data_put_with_index(q, entry);
|
||||
|
||||
// Искать:
|
||||
uint32_t key = 42;
|
||||
struct ll_entry* found = queue_find_data_by_index(q, &key);
|
||||
|
||||
// Если несколько элементов с одним ключом — продолжать поиск:
|
||||
struct ll_entry* next = queue_find_next_by_index(q, &key, found);
|
||||
```
|
||||
|
||||
### 2.6. Освобождение ресурсов
|
||||
|
||||
```c
|
||||
// 1. Снять callback
|
||||
queue_set_callback(q, NULL, NULL);
|
||||
|
||||
// 2. Отменить всех waiter-ов
|
||||
queue_waiter_cancel(q, &my_waiter);
|
||||
|
||||
// 3. Извлечь и освободить ВСЕ оставшиеся элементы
|
||||
struct ll_entry* e;
|
||||
while ((e = queue_data_get(q)) != NULL) {
|
||||
queue_dgram_free(e);
|
||||
queue_entry_free(e);
|
||||
}
|
||||
// После извлечения всех элементов resume не нужен — callback уже снят.
|
||||
|
||||
// 4. Освободить очередь
|
||||
queue_free(q);
|
||||
```
|
||||
|
||||
**Важно:** `queue_free()` **не освобождает элементы**. Их нужно извлечь через `queue_data_get()` и освободить вручную.
|
||||
|
||||
### 2.7. Приоритетная вставка (LIFO)
|
||||
|
||||
```c
|
||||
// Вставить в начало очереди (высокий приоритет):
|
||||
queue_data_put_first(q, entry);
|
||||
queue_data_put_first_with_index(q, entry);
|
||||
```
|
||||
|
||||
### 2.8. Удаление из середины
|
||||
|
||||
```c
|
||||
queue_remove_data(q, entry); // удаляет из очереди, НЕ освобождает память
|
||||
queue_dgram_free(entry); // освободить dgram отдельно
|
||||
queue_entry_free(entry); // освободить entry отдельно
|
||||
```
|
||||
|
||||
После `queue_remove_data()` элемент больше не принадлежит очереди — вызывающий сам отвечает за его освобождение.
|
||||
|
||||
### 2.9. Callback при опустошении
|
||||
|
||||
```c
|
||||
static void on_empty(struct ll_queue* q, void* arg) {
|
||||
// Очередь стала пустой (count==0)
|
||||
// Вызывается однократно, после вызова сбрасывается
|
||||
}
|
||||
|
||||
queue_set_empty_callback(q, on_empty, my_data);
|
||||
```
|
||||
|
||||
## 3. API
|
||||
|
||||
### Создание / уничтожение
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_new(ua, hash_size, index_offset, index_size, name)` | Создать очередь. `hash_size=0` — без хеша. `ua` обязателен (для uasync_call_soon). |
|
||||
| `queue_free(q)` | Освободить очередь (НЕ элементы — их надо извлечь и освободить отдельно). |
|
||||
|
||||
### Создание / освобождение элементов
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_entry_new(data_size)` | Выделить entry с data[] через malloc. |
|
||||
| `queue_entry_new_from_pool(pool)` | Выделить entry из memory pool. |
|
||||
| `ll_alloc_lldgram(len)` | Выделить entry + отдельный буфер dgram (malloc). |
|
||||
| `queue_entry_free(entry)` | Освободить entry (возврат в pool или free). НЕ освобождает dgram! |
|
||||
| `queue_dgram_free(entry)` | Освободить dgram (через dgram_free_fn, pool или free). |
|
||||
|
||||
### Чтение (callback)
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
|
||||
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
|
||||
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
|
||||
| `queue_set_callback_defer(q, enable)` | `enable=1` — первый запуск callback из `put` откладывается через uasync_call_soon (очистка стека). По умолчанию `0` (синхронно). Повторные запуски всегда deferred. |
|
||||
|
||||
### Запись
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_data_put(q, entry)` | Добавить в конец (FIFO). Для очередей БЕЗ хеша. |
|
||||
| `queue_data_put_with_index(q, entry)` | Добавить в конец с хеш-индексом. |
|
||||
| `queue_data_put_first(q, entry)` | Добавить в начало (LIFO, высокий приоритет). |
|
||||
| `queue_data_put_first_with_index(q, entry)` | Добавить в начало с хеш-индексом. |
|
||||
|
||||
### Backpressure
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_set_threshold(q, max_packets, max_bytes)` | Задать общий порог. Когда `count <= max_packets` И `total_bytes <= max_bytes`, waiter-ы пробуждаются (FIFO). |
|
||||
| `queue_waiter_wait(q, h, callback, arg)` | Зарегистрировать ожидание. Возвращает 1 (уже выполнено), 0 (в очереди), -1 (ошибка). |
|
||||
| `queue_waiter_cancel(q, h)` | Отменить ожидание. Безопасно вызывать в любом состоянии. |
|
||||
| `queue_set_waiter_defer(q, enable)` | Отложенный вызов waiter-ов (через uasync_call_soon). |
|
||||
| `queue_set_empty_callback(q, cbk_fn, arg)` | Одноразовый callback при count==0. |
|
||||
| `queue_set_on_get(q, cbk_fn, arg)` | Callback после каждого queue_data_get (deferred). |
|
||||
|
||||
### Поиск / удаление
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_find_data_by_index(q, index_key)` | Найти по ключу (требуется hash_size>0). Размер ключа = q->index_size. |
|
||||
| `queue_find_next_by_index(q, index_key, prev_entry)` | Следующий элемент с тем же ключом. |
|
||||
| `queue_remove_data(q, entry)` | Удалить из очереди (память НЕ освобождает). |
|
||||
|
||||
### Утилиты
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `queue_entry_count(q)` | Текущее количество элементов. |
|
||||
| `queue_total_bytes(q)` | Суммарный объём данных (сумма len). |
|
||||
| `queue_set_size_limit(q, lim)` | Ограничение на количество элементов (при превышении новые удаляются). |
|
||||
| `queue_check_consistency(q)` | Проверка целостности (счётчики, циклы, prev/next). Только для отладки. |
|
||||
@@ -13,6 +13,7 @@
|
||||
#define CANARY 0xDEADBEEF
|
||||
#define PADDING_FILL 0xAA
|
||||
#define METADATA_SIZE 128
|
||||
#define ALLOCATION_OVERHEAD (12 + (size_t)2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE)
|
||||
#define POINTER_SIZE sizeof(void*)
|
||||
#define LOCATION_MAX (METADATA_SIZE - POINTER_SIZE * 2)
|
||||
#define PREV_OFFSET LOCATION_MAX
|
||||
@@ -89,7 +90,7 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
uint8_t* prefix_start = base + METADATA_SIZE;
|
||||
uint32_t* pre_canary = (uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE);
|
||||
uint32_t size = *(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4);
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
|
||||
uint8_t* user = (uint8_t*)ptr;
|
||||
bool error = false;
|
||||
for (size_t i = 0; i < BOUNDARY_CHECK_SIZE; i++) {
|
||||
@@ -117,7 +118,7 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
}
|
||||
if (error) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Allocated at: %s", location, text, alloc_loc);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %u bytes", location, text, size, total);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %zu bytes", location, text, size, total);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Full memory dump at base %p:", location, text, base);
|
||||
hex_dump(base, total);
|
||||
exit(EXIT_FAILURE);
|
||||
@@ -125,13 +126,21 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
}
|
||||
|
||||
void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
size_t total = size;
|
||||
if (total > SIZE_MAX - ALLOCATION_OVERHEAD) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC OVERFLOW: %u user bytes + %zu overhead bytes at %s", size, ALLOCATION_OVERHEAD, location);
|
||||
return NULL;
|
||||
}
|
||||
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
|
||||
size, (double)size / (1024.0 * 1024.0), location);
|
||||
}
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
total += ALLOCATION_OVERHEAD;
|
||||
uint8_t* base = (uint8_t*)malloc(total);
|
||||
if (!base && size > 0) return NULL;
|
||||
if (!base) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC FAILED: %u user bytes, %zu total bytes at %s", size, total, location);
|
||||
return NULL;
|
||||
}
|
||||
memset(base, PADDING_FILL, total);
|
||||
// Set metadata
|
||||
size_t loc_len = strlen(location);
|
||||
@@ -209,7 +218,7 @@ void u_free_impl(void* ptr, const char* location) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Base: %p", (void*)base);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Allocated at: %s", alloc_loc);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "User size: %u bytes", size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %u bytes", size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %zu bytes", (size_t)size + ALLOCATION_OVERHEAD);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Active allocs: %zu", active_count);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "--- Metadata dump at %p (%u bytes) ---", (void*)base, (unsigned)(METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8));
|
||||
hex_dump(base, METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8);
|
||||
@@ -231,7 +240,7 @@ void u_free_impl(void* ptr, const char* location) {
|
||||
allocated_count--;
|
||||
frees_count++;
|
||||
UNLOCK();
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
|
||||
memset(base, 0, total);
|
||||
free(base);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
|
||||
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
|
||||
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
|
||||
* realloc при ошибке сохраняет старый блок, при size=0 освобождает его.
|
||||
* realloc живого блока при ошибке сохраняет его, при size=0 освобождает; NULL обрабатывается как malloc.
|
||||
* Размеры аргументов _impl — uint32_t; до вызова проверить, что исходные size_t помещаются в этот диапазон.
|
||||
* Список аллокаций защищён мьютексом; доступ к одному блоку и чтение счётчика согласует вызывающий.
|
||||
* Защитные поля проверяются для живых блоков; обращение к уже освобождённому/чужому указателю недопустимо.
|
||||
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
|
||||
*/
|
||||
#ifndef MEM_H
|
||||
@@ -15,18 +18,30 @@ extern "C" {
|
||||
#define XSTR(s) STR(s)
|
||||
#define STR(s) #s
|
||||
#define LOCATION __FILE__ ":" XSTR(__LINE__)
|
||||
// Проверяет canary/padding живого блока; text/location — строки диагностики. NULL ptr допустим.
|
||||
// Обнаруженное повреждение логируется в SYS и завершает процесс через exit(EXIT_FAILURE).
|
||||
void u_check(void* ptr, const char* text, const char* location);
|
||||
#define u_malloc(size) u_malloc_impl(size, LOCATION)
|
||||
#define u_calloc(nmemb, size) u_calloc_impl(nmemb, size, LOCATION)
|
||||
#define u_realloc(ptr, size) u_realloc_impl(ptr, size, LOCATION)
|
||||
#define u_free(ptr) u_free_impl(ptr, LOCATION)
|
||||
#define u_strdup(s) u_strdup_impl(s, LOCATION)
|
||||
// Выделяет size байт с метаданными; NULL при OOM/переполнении полного размера. size=0 тоже создаёт учтённый блок.
|
||||
// location — обязательная строка места вызова, копируется в метаданные; payload не обнуляется.
|
||||
void* u_malloc_impl(uint32_t size, const char* location);
|
||||
// Выделяет и обнуляет nmemb*size байт; NULL при произведении > UINT32_MAX или ошибке malloc.
|
||||
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location);
|
||||
// NULL ptr → malloc(size), включая size=0. Для живого ptr: size=0 → free и NULL;
|
||||
// иначе новый блок с min(старый размер, size) байтами данных. При OOM возвращает NULL, старый блок сохранён.
|
||||
// Результат сохранять временно: tmp = u_realloc(ptr, size); if (tmp) ptr = tmp; (при size > 0).
|
||||
void* u_realloc_impl(void* ptr, uint32_t size, const char* location);
|
||||
// Проверяет и освобождает живой блок из этих обёрток; NULL допустим. Обычный free к нему неприменим.
|
||||
void u_free_impl(void* ptr, const char* location);
|
||||
// Копирует ASCIIZ-строку (strlen(s)+1 должно помещаться в uint32_t); NULL при NULL s/OOM. Освобождать u_free.
|
||||
char* u_strdup_impl(const char* s, const char* location);
|
||||
// После остановки изменяющих список потоков: статистика и дампы живых блоков; сами блоки не освобождает.
|
||||
void u_report_unfreed_blocks(void);
|
||||
// Число живых блоков, чтение без блокировки; параллельные аллокации/освобождения нужно исключить извне.
|
||||
size_t u_get_allocated_count(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# mem (Memory Wrappers с отслеживанием утечек)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Обёртки над стандартными `malloc`/`calloc`/`realloc`/`free`/`strdup` с детекцией:
|
||||
- **Buffer overflow/underflow** — каждый блок окружён канарейками (`0xDEADBEEF`) и padding-областями, которые проверяются при `u_free` и `u_realloc`. При повреждении — подробный дамп блока и аварийное завершение.
|
||||
- **Double free** — двусвязный список всех живых аллокаций; повторное освобождение детектируется по отсутствию блока в списке.
|
||||
- **Утечки памяти** — `u_report_unfreed_blocks()` при завершении выводит все незакрытые блоки с местом аллокации (`файл:строка`), размером и hex-дампом данных.
|
||||
|
||||
Функции потокобезопасны (мьютекс/критическая секция).
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "mem.h"
|
||||
|
||||
// Вместо malloc/calloc/realloc/free/strdup используй соответствующие макросы:
|
||||
void* buf = u_malloc(data_size);
|
||||
char* str = u_strdup("hello");
|
||||
buf = u_realloc(buf, new_size);
|
||||
u_free(buf);
|
||||
u_free(str);
|
||||
|
||||
// В конце main (перед return) — отчёт о незакрытых блоках:
|
||||
u_report_unfreed_blocks();
|
||||
|
||||
// Текущее число живых аллокаций (для диагностики):
|
||||
size_t count = u_get_allocated_count();
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Макросы (`u_malloc`, `u_calloc`, `u_free`, и т.д.) автоматически подставляют `__FILE__:__LINE__` через `LOCATION` — это нужно для диагностики утечек и повреждений. Напрямую вызывай `_impl`-функции только если нужно явно передать location.
|
||||
- `u_realloc(NULL, size)` эквивалентен `u_malloc(size)`, `u_realloc(ptr, 0)` эквивалентен `u_free(ptr)`.
|
||||
- Потокобезопасность гарантирована, но аллокации с метаданными заметно дороже обычного `malloc` — используй `memory_pool` для hot-path объектов.
|
||||
- При обнаружении повреждения (overflow/underflow/double-free) программа завершается через `exit(EXIT_FAILURE)` после вывода отладочной информации.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Описание |
|
||||
|---|---|
|
||||
| `u_malloc(size)` | Аналог `malloc` с boundary check и трекингом |
|
||||
| `u_calloc(nmemb, size)` | Аналог `calloc` (выделяет + обнуляет) |
|
||||
| `u_realloc(ptr, size)` | Аналог `realloc`, с проверкой целостности старого блока |
|
||||
| `u_free(ptr)` | Освобождение с проверкой canary, double-free и удалением из списка |
|
||||
| `u_strdup(s)` | Аналог `strdup` через u_malloc |
|
||||
| `u_report_unfreed_blocks()` | Выводит все незакрытые блоки с дампом; вызывается при shutdown |
|
||||
| `u_get_allocated_count()` | Возвращает количество живых аллокаций |
|
||||
| `u_check(ptr, text, location)` | Ручная проверка целостности блока (canary + padding) |
|
||||
+13
-5
@@ -4,6 +4,10 @@
|
||||
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
|
||||
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
|
||||
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
|
||||
/* Хвост блока хранит canary, места alloc/free и метку занятости. Повреждение или повторный возврат
|
||||
* кешированного блока логируется в SYS и навсегда останавливает текущий поток циклом диагностики.
|
||||
* Пример для очередей: init(sizeof(struct ll_entry) + inline_size, name), затем queue_entry_new_from_pool.
|
||||
* Объекты, уже переданные в u_free при переполнении кеша, больше нельзя проверять или возвращать. */
|
||||
#ifndef MEMORY_POOL_H
|
||||
#define MEMORY_POOL_H
|
||||
|
||||
@@ -25,21 +29,25 @@ struct memory_pool {
|
||||
void* free_head; // Голова linked list свободных блоков (next хранится в начале каждого блока)
|
||||
int free_count; // Количество свободных блоков
|
||||
size_t object_size; // Размер объектов в пуле (пользовательские данные)
|
||||
size_t allocations; // Общее количество аллокаций (включая новые malloc)
|
||||
size_t allocations; // Попытки выделить новый блок, включая неудачные; выдачи из кеша не учитываются
|
||||
size_t reuse_count; // Количество повторных использований из пула
|
||||
uint8_t alloc_tag_counter; // монотонный счётчик для детекции double-free (1 байт)
|
||||
uint8_t alloc_tag_counter; // Не используется; init устанавливает 1. Double-free проверяется по метке в хвосте блока
|
||||
const char* name; // Имя пула для диагностики (напр. "data_pool", "pkt_pool")
|
||||
};
|
||||
|
||||
// сам пул:
|
||||
// если используем с элементами ll_queue то не забываем что к object_size надо прибавить sizeof(struct ll_entry)
|
||||
// Создаёт пустой пул: object_size >= sizeof(void*), name живёт до destroy; NULL при OOM.
|
||||
// Для ll_queue размер включает sizeof(struct ll_entry) и пользовательский data[].
|
||||
struct memory_pool* memory_pool_init(size_t object_size, const char* name);
|
||||
// Освобождает кеш и пул, но не выданные объекты; NULL допустим. Все владельцы должны предварительно вернуть блоки.
|
||||
void memory_pool_destroy(struct memory_pool* pool);
|
||||
|
||||
// элементы пула:
|
||||
// Выдаёт обнулённый payload из кеша или нового блока; NULL при NULL pool/OOM. location — место вызова в хвосте.
|
||||
void* memory_pool_alloc_impl(struct memory_pool* pool, const char* location);
|
||||
// Возвращает свой живой obj в кеш или u_free, если кеш заполнен; NULL pool/obj ничего не делает.
|
||||
void memory_pool_free_impl(struct memory_pool* pool, void* obj, const char* location);
|
||||
// Копирует счётчики попыток новых аллокаций и выдач из кеша; любой выход может быть NULL, NULL pool ничего не меняет.
|
||||
void memory_pool_get_stats(struct memory_pool* pool, size_t* allocations, size_t* reuse_count);
|
||||
// Общее число кешированных блоков всех пулов; глобальный счётчик не синхронизирован.
|
||||
size_t memory_pool_get_total_free_blocks(void);
|
||||
|
||||
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# memory_pool (Object Pool Allocator)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Быстрый аллокатор объектов фиксированного размера. Вместо постоянных `malloc`/`free` хранит до 64 освобождённых объектов в linked list для повторного использования. Применяется для hot-path объектов, которые часто создаются и уничтожаются: пакеты, фрагменты, ACK-пакеты, структуры маршрутизации, TCP-сегменты lwIP и т.д.
|
||||
|
||||
Встроенная защита:
|
||||
- **Buffer overflow** — канарейка `0xDEADBEAF` после пользовательских данных проверяется при освобождении. При повреждении — бесконечный цикл (hang) для отладки.
|
||||
- **Double free** — однобайтный счётчик в метаданных; повторное освобождение детектируется и вызывает hang.
|
||||
- Вся диагностика пишется через `DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, ...)`.
|
||||
|
||||
Не потокобезопасен — каждый пул используется из одного потока (обычно в рамках одного u_async event loop).
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "memory_pool.h"
|
||||
|
||||
// 1. Создать пул (обычно в init-функции инстанса):
|
||||
struct memory_pool* pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM), "pkt_pool");
|
||||
|
||||
// 2. Выделить объект (если есть свободный — вернёт из пула, иначе u_calloc):
|
||||
struct ETCP_DGRAM* dgram = memory_pool_alloc(pkt_pool);
|
||||
|
||||
// 3. Освободить объект (возвращается в пул, если там меньше 64 блоков):
|
||||
memory_pool_free(pkt_pool, dgram);
|
||||
|
||||
// 4. Получить статистику:
|
||||
size_t allocs, reuse;
|
||||
memory_pool_get_stats(pkt_pool, &allocs, &reuse);
|
||||
// reuse много → эффективно; reuse мало → пул слишком мал для нагрузки
|
||||
|
||||
// 5. Проверить, в пуле ли объект (0 — не в пуле, 1 — в пуле):
|
||||
if (memory_pool_is_freed(pkt_pool, obj)) { /* уже освобождён */ }
|
||||
|
||||
// 6. Уничтожить пул (при shutdown):
|
||||
memory_pool_destroy(pkt_pool);
|
||||
// реально освобождает все кэшированные блоки через u_free() и сам пул.
|
||||
```
|
||||
|
||||
**Важно:**
|
||||
- Если объекты пула используются как элементы `ll_queue`, в `object_size` нужно закладывать `sizeof(struct ll_entry)`. Например: `memory_pool_init(sizeof(struct ll_entry) + sizeof(struct dummynet_pkt), "pkt_pool")`.
|
||||
- `memory_pool_alloc` обнуляет объект перед возвратом (защита от утечки старых данных).
|
||||
- При заполнении пула (64 свободных блока) лишние `memory_pool_free` реально вызывают `u_free` — блок не кэшируется.
|
||||
- `memory_pool_destroy` проходит по всем свободным блокам и вызывает `u_free` для каждого, затем освобождает сам `struct memory_pool`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция/макрос | Описание |
|
||||
|---|---|
|
||||
| `memory_pool_init(object_size, name)` | Создаёт пул для объектов заданного размера. `name` — для диагностики. |
|
||||
| `memory_pool_alloc(pool)` | Выделяет объект: из кэша (если есть) или через `u_calloc`. Обнуляет перед возвратом. |
|
||||
| `memory_pool_free(pool, obj)` | Возвращает объект в пул (до 64) или вызывает `u_free`. Проверяет canary и double-free. |
|
||||
| `memory_pool_destroy(pool)` | Уничтожает пул: освобождает все кэшированные блоки и сам пул. |
|
||||
| `memory_pool_get_stats(pool, &allocs, &reuse)` | Статистика: общее число аллокаций и число повторных использований из кэша. |
|
||||
| `memory_pool_get_total_free_blocks()` | Глобальный счётчик свободных блоков во всех пулах (для диагностики). |
|
||||
| `memory_pool_is_freed(pool, obj)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). |
|
||||
+20
-12
@@ -2,6 +2,12 @@
|
||||
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
|
||||
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
|
||||
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
|
||||
* Подключать этот заголовок вместо прямых POSIX includes для общего кода.
|
||||
* Windows: htobe16/32/64 и be16/32/64toh через byteswap, strcasecmp/strncasecmp через _stricmp/_strnicmp,
|
||||
* memmem через inline-поиск (пустой образец возвращает начало), poll через WSAPoll, mkdir через _mkdir.
|
||||
* utun_gettimeofday — календарное Unix-время (не монотонное); Windows игнорирует tz.
|
||||
* Windows pipe — _pipe(4096, _O_BINARY). fcntl: F_GETFL возвращает 0, F_SETFL с O_NONBLOCK включает
|
||||
* неблокирующий режим; остальные флаги игнорируются, обратное переключение этим адаптером не выполняется.
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_COMPAT_H
|
||||
@@ -178,34 +184,36 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Generate cryptographically secure random bytes
|
||||
// Returns 0 on success, -1 on error
|
||||
// Заполняет buffer криптографическими случайными байтами: POSIX /dev/urandom, Windows BCryptGenRandom.
|
||||
// buffer не NULL, len > 0 (Windows: помещается в ULONG). 0 — всё заполнено, -1 — ошибка/неполное чтение.
|
||||
int random_bytes(uint8_t *buffer, size_t len);
|
||||
|
||||
// Get IPv4 address of interface by its index (ifindex)
|
||||
// Returns IPv4 address in network byte order, 0 on error
|
||||
// POSIX: getifaddrs; Windows: IP Helper. Адрес в network byte order, 0 при ошибке/отсутствии адреса.
|
||||
uint32_t get_interface_ip_by_index(uint32_t netif_index);
|
||||
|
||||
// Get IPv6 address of interface by its index (ifindex)
|
||||
// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6
|
||||
// POSIX фильтрует по IFA_F_TEMPORARY в ifa_flags, но getifaddrs не гарантирует адресные флаги;
|
||||
// для точного выбора temporary/stable на Linux использовать get_interface_ipv6_addr_nl().
|
||||
// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::.
|
||||
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found
|
||||
int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out);
|
||||
|
||||
// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags)
|
||||
// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary
|
||||
// prefer_stable=0: prefer temporary over stable
|
||||
// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper.
|
||||
// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип.
|
||||
// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index().
|
||||
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error
|
||||
int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out);
|
||||
|
||||
// Determine the interface index for the default route (outgoing traffic)
|
||||
// Creates a temp UDP socket, connects to a well-known address to force
|
||||
// kernel routing, then matches the chosen source IP to getifaddrs()
|
||||
// POSIX: UDP connect к 8.8.8.8:53 или [2001:4860:4860::8888]:53, сопоставление локального IP с getifaddrs().
|
||||
// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу,
|
||||
// который может отличаться от default route при более специфичных маршрутах.
|
||||
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
|
||||
uint32_t get_default_route_netif_index(int family);
|
||||
|
||||
// Классифицирует IPv4-адрес (network byte order):
|
||||
// 1 = глобальный/публичный (маршрутизируемый в интернете),
|
||||
// 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
|
||||
// Классифицирует IPv4 (network byte order) по встроенному списку: 0 для 0/8, 10/8, 127/8, 169.254/16,
|
||||
// 172.16/12, 192.168/16, 100.64/10, 224/4 и 240/4; 1 для остальных. Это не проверка достижимости адреса.
|
||||
int ip_is_public(uint32_t addr_be);
|
||||
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
# platform_compat — Cross-platform compatibility layer
|
||||
|
||||
## 1. Назначение
|
||||
Унифицирует различия между POSIX (Linux/FreeBSD) и Windows (MSYS2 UCRT64) на уровне системных вызовов и типов. Предоставляет единый API для байтового порядка, строковых функций, энтропии, работы с сетевыми интерфейсами и времени. Полностью заголовочный (inline) для макросов и тривиальных обёрток, `.c` только для нетривиальных реализаций.
|
||||
|
||||
## 2. Как пользоваться
|
||||
Подключить `"platform_compat.h"` — всё остальное разрешается автоматически в зависимости от `_WIN32`.
|
||||
|
||||
### Байтовый порядок
|
||||
```c
|
||||
uint16_t v16 = htobe16(x); // host → big-endian
|
||||
uint32_t v32 = be32toh(x); // big-endian → host
|
||||
uint64_t v64 = be64toh(x); // big-endian → host
|
||||
// На Linux использует <endian.h>, на Windows — _byteswap_*
|
||||
```
|
||||
|
||||
### Криптостойкий random
|
||||
```c
|
||||
uint8_t salt[8];
|
||||
if (random_bytes(salt, sizeof(salt)) != 0) { /* ошибка */ }
|
||||
// Linux: /dev/urandom, Windows: BCryptGenRandom
|
||||
```
|
||||
|
||||
### Сетевые интерфейсы
|
||||
```c
|
||||
// Определить интерфейс маршрута по умолчанию
|
||||
uint32_t ifidx = get_default_route_netif_index(AF_INET); // или AF_INET6
|
||||
|
||||
// Получить IPv4 адрес интерфейса по индексу
|
||||
uint32_t ipv4 = get_interface_ip_by_index(ifidx); // network byte order
|
||||
|
||||
// Получить IPv6 адрес (постоянный или временный)
|
||||
uint8_t ipv6[16];
|
||||
if (get_interface_ipv6_by_index(ifidx, 1, ipv6) == 0) { // temporary=1
|
||||
// ipv6 содержит 16 байт адреса
|
||||
}
|
||||
```
|
||||
**Windows**: `get_interface_ip_by_index`, `get_interface_ipv6_by_index`, `get_default_route_netif_index` — заглушки (возвращают 0/-1).
|
||||
|
||||
### Время
|
||||
```c
|
||||
struct timeval tv;
|
||||
utun_gettimeofday(&tv, NULL); // макрос: на Linux → gettimeofday, на Windows → inline-реализация
|
||||
```
|
||||
|
||||
### Прочее
|
||||
- `strcasecmp`/`strncasecmp` — на Windows `_stricmp`/`_strnicmp`
|
||||
- `memmem` — на Windows inline-реализация `compat_memmem`
|
||||
- `pipe` — на Windows `_pipe` с `_O_BINARY`
|
||||
- `poll` — на Windows `WSAPoll`, флаги `POLLIN`/`POLLOUT` и т.д. определены если отсутствуют
|
||||
- `ssize_t` — определён если отсутствует
|
||||
- `fcntl` — на Windows упрощённая реализация только для `F_SETFL`/`O_NONBLOCK`
|
||||
- `utun_mkdir` — кроссплатформенный `mkdir`
|
||||
|
||||
### Ограничения
|
||||
- Функции сетевых интерфейсов работают только на POSIX, на Windows — заглушки
|
||||
- `get_default_route_netif_index` создаёт временный UDP-сокет к `8.8.8.8` (IPv4) или `2001:4860:4860::8888` (IPv6) для определения интерфейса
|
||||
- Только IPv4/IPv6; `family` только `AF_INET` или `AF_INET6`
|
||||
|
||||
## 3. API
|
||||
|
||||
### Байтовый порядок
|
||||
| Макрос | Назначение |
|
||||
|--------|------------|
|
||||
| `htobe16(x)` / `be16toh(x)` | 16-bit host ↔ big-endian |
|
||||
| `htobe32(x)` / `be32toh(x)` | 32-bit host ↔ big-endian |
|
||||
| `htobe64(x)` / `be64toh(x)` | 64-bit host ↔ big-endian |
|
||||
|
||||
### Основные функции
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `random_bytes(buffer, len)` | Криптостойкие случайные байты. 0 — успех, -1 — ошибка |
|
||||
| `get_interface_ip_by_index(ifindex)` | IPv4 адрес интерфейса в network byte order, 0 при ошибке |
|
||||
| `get_interface_ipv6_by_index(ifindex, temporary, out)` | IPv6 адрес (temporary=1 — временный privacy-адрес), 0 — успех |
|
||||
| `get_default_route_netif_index(family)` | ifindex дефолтного маршрута через connect() к внешнему IP |
|
||||
|
||||
### Макросы-заменители (условные)
|
||||
| Макрос | Назначение |
|
||||
|--------|------------|
|
||||
| `utun_gettimeofday(tv, tz)` | gettimeofday (inline на Windows) |
|
||||
| `utun_mkdir(path, mode)` | mkdir |
|
||||
+23
-7
@@ -24,12 +24,16 @@
|
||||
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
|
||||
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
|
||||
* • serialize_decode принимает буфер БЕЗ заголовка.
|
||||
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
|
||||
* • serialize_free освобождает структуру, динамические поля схемы и узлы списков; обнуляет указатель на структуру.
|
||||
* Указатели внутри элементов массивов и узлов списков рекурсивно не освобождаются.
|
||||
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
|
||||
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
|
||||
* • Переменные counts ограничены 65535; elem_size=1 добавляет NUL и для byte-массивов.
|
||||
* • При encode counts больше 65535 молча усекаются: сохраняются первые 65535 элементов массива/списка или байтов строки.
|
||||
* • elem_size=1 добавляет NUL и для byte-массивов.
|
||||
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
|
||||
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
|
||||
* • Схема доверенная: offsets, типы, размеры и выравнивание полей/next проверяет её автор, не decode.
|
||||
* • Независимые операции не используют общее изменяемое состояние; доступ к одной структуре согласует владелец.
|
||||
*
|
||||
* @example
|
||||
* // 1. Описание структуры
|
||||
@@ -103,7 +107,7 @@ struct SerializeField {
|
||||
uint16_t offset; ///< смещение поля в структуре
|
||||
uint8_t elem_size; ///< размер элемента (для fixed = размер данных, для array/linked = размер одного элемента)
|
||||
uint8_t type; ///< SERIALIZE_TYPE_*
|
||||
uint16_t len_offset; ///< смещение поля-счётчика (игнорируется для type 0,1,2)
|
||||
uint16_t len_offset; ///< type 3–5: смещение счётчика в структуре; type 6: смещение next в узле; type 0–2: игнорируется
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -114,25 +118,37 @@ struct SerializeSchema {
|
||||
uint16_t struct_size; ///< размер основной структуры
|
||||
uint16_t max_size; ///< максимальный размер сериализованных данных (0 = без ограничения)
|
||||
uint16_t header_len; ///< длина заголовка
|
||||
const struct SerializeField* fields;
|
||||
const struct SerializeField* fields; ///< field_count описателей в порядке записи; схема заимствуется на время вызова
|
||||
};
|
||||
|
||||
#define SERIALIZE_ERR_OK 0 ///< успех
|
||||
#define SERIALIZE_ERR_BUF -1 ///< ошибка выделения памяти
|
||||
#define SERIALIZE_ERR_BUF -1 ///< нехватка памяти; decode также: обрезанные данные или неизвестный тип поля
|
||||
#define SERIALIZE_ERR_NULL -2 ///< NULL-указатель
|
||||
#define SERIALIZE_ERR_SIZE -3 ///< превышен max_size
|
||||
#define SERIALIZE_ERR_NOTSUP -4 ///< неподдерживаемый тип
|
||||
#define SERIALIZE_ERR_SIZE -3 ///< encode: превышен max_size
|
||||
#define SERIALIZE_ERR_NOTSUP -4 ///< encode: неподдерживаемый тип поля
|
||||
|
||||
/* Кодирует structure по schema; при header_len > 0 header указывает минимум на header_len байт.
|
||||
* При успехе *out_buf — новый блок u_malloc (освободить u_free), *out_len — фактически записанная длина с header.
|
||||
* Входная структура и её вложенные данные остаются у владельца. На ошибке выходные значения не меняются.
|
||||
* Возврат: OK, NULL (обязательные аргументы), BUF (OOM), SIZE (max_size), NOTSUP (неизвестный тип).
|
||||
* ASCIIZ требует elem_size=1; порядок fields задаёт формат, сортировка по offset не требуется. */
|
||||
int serialize_encode(const void* structure,
|
||||
const struct SerializeSchema* schema,
|
||||
const uint8_t* header,
|
||||
uint8_t** out_buf,
|
||||
size_t* out_len);
|
||||
|
||||
/* Декодирует in_len байт БЕЗ header: передавать buf + header_len, len - header_len.
|
||||
* При успехе *structure — новая структура с выделенными полями; очищать serialize_free с той же схемой.
|
||||
* При ошибке частично созданные объекты освобождаются, *structure = NULL. Старый *structure не освобождает.
|
||||
* max_size не ограничивает decode; хвост после последнего поля игнорируется.
|
||||
* Возврат: OK, NULL (аргументы), BUF (OOM, обрезанные данные или неизвестный тип). */
|
||||
int serialize_decode(const uint8_t* in_buf, size_t in_len,
|
||||
const struct SerializeSchema* schema,
|
||||
void** structure);
|
||||
|
||||
// Освобождает выделенную через u_malloc структуру, поля схемы и узлы списков, обнуляет *structure; NULL допустим.
|
||||
// Вложенные указатели внутри элементов массивов/узлов не обходит; к структуре на стеке применять нельзя.
|
||||
void serialize_free(const struct SerializeSchema* schema, void** structure);
|
||||
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Binary Serialization Library
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Библиотека бинарной сериализации C-структур с динамическими полями (строки, массивы, singly-linked списки) в компактный бинарный буфер и обратно. Предназначена для кодирования/декодирования сообщений сетевого протокола (контрольный сервер, синхронизация БД, обмен конфигурацией).
|
||||
|
||||
Что сериализуется:
|
||||
- Фиксированные поля (целые, флаги, структуры) — копируются «как есть».
|
||||
- ASCIIZ-строки (`char*`) — длина определяется через `strlen`, сохраняется с терминальным нулём.
|
||||
- Динамические массивы (`uint8_t*`) — счётчик элементов в поле `UINT8`/`UINT16`/`UINT32` или ровно 1 элемент (`ARRAY_FIXED`).
|
||||
- Singly-linked списки — `next`-указатели не сериализуются, сохраняются только `count+data`.
|
||||
|
||||
Формат: header (`header_len` байт, передаётся отдельно) + данные полей. Длина переменных полей кодируется 2 байтами (`uint16_t`, big-endian).
|
||||
|
||||
Потокобезопасность: нет, однопоточное использование.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
```c
|
||||
#include "../lib/serialize.h"
|
||||
|
||||
// --- 1. Определяем структуру ---
|
||||
typedef struct Node {
|
||||
uint32_t id;
|
||||
struct Node *next;
|
||||
} Node;
|
||||
|
||||
typedef struct {
|
||||
uint8_t version;
|
||||
uint8_t name_len;
|
||||
char *name; // ASCIIZ (elem_size=1)
|
||||
uint16_t addrs_cnt;
|
||||
uint8_t *addrs; // массив байт (elem_size=1)
|
||||
Node *list; // linked list
|
||||
} Message;
|
||||
|
||||
// --- 2. Описываем схему ---
|
||||
static const struct SerializeField msg_fields[] = {
|
||||
{offsetof(Message, version), sizeof(uint8_t), SERIALIZE_TYPE_FIXED, 0},
|
||||
{offsetof(Message, name), 1, SERIALIZE_TYPE_ARRAY_U8, offsetof(Message, name_len)},
|
||||
{offsetof(Message, addrs), 1, SERIALIZE_TYPE_ARRAY_U16, offsetof(Message, addrs_cnt)},
|
||||
{offsetof(Message, list), sizeof(Node), SERIALIZE_TYPE_LINKED, offsetof(Node, next)}
|
||||
};
|
||||
|
||||
static const struct SerializeSchema msg_schema = {
|
||||
.field_count = 4,
|
||||
.struct_size = sizeof(Message),
|
||||
.max_size = 4096, // 0 = без лимита
|
||||
.header_len = 4, // байты заголовка (напр. версия протокола)
|
||||
.fields = msg_fields
|
||||
};
|
||||
|
||||
// --- 3. Сериализация ---
|
||||
Message msg = { .version = 1, .name = "test", .name_len = 4, .list = NULL };
|
||||
msg.addrs_cnt = 2;
|
||||
msg.addrs = u_malloc(2); msg.addrs[0] = 0xAA; msg.addrs[1] = 0xBB;
|
||||
|
||||
uint8_t header[] = {0x01, 0x00, 0x00, 0x00}; // 4-байтный заголовок
|
||||
uint8_t *buf = NULL;
|
||||
size_t len;
|
||||
if (serialize_encode(&msg, &msg_schema, header, &buf, &len) != SERIALIZE_ERR_OK)
|
||||
goto fail;
|
||||
|
||||
// buf содержит: header (4) + version (1) + [len=2]name(4) + '\0' + [len=2]addrs(2)
|
||||
|
||||
// --- 4. Десериализация (buf без заголовка!) ---
|
||||
Message *restored = NULL;
|
||||
uint8_t *data_ptr = buf + msg_schema.header_len; // пропускаем header
|
||||
if (serialize_decode(data_ptr, len - msg_schema.header_len, &msg_schema, (void**)&restored) != SERIALIZE_ERR_OK)
|
||||
goto fail;
|
||||
|
||||
// restored->name == "test", restored->name_len == 4
|
||||
// restored->addrs[0] == 0xAA, restored->addrs_cnt == 2
|
||||
// restored->list == NULL (список пуст — не падает)
|
||||
|
||||
// --- 5. Очистка ---
|
||||
serialize_free(&msg_schema, (void**)&restored); // освобождает всё: name, addrs, list, структуру
|
||||
u_free(buf);
|
||||
```
|
||||
|
||||
**Ключевые нюансы:**
|
||||
- Поля в `schema.fields` должны идти в порядке возрастания `offset` в структуре.
|
||||
- `serialize_decode` принимает буфер **без заголовка** — передаётся `buf + schema.header_len`.
|
||||
- `serialize_free` освобождает структуру и все вложенные динамические поля (строки, массивы, списки).
|
||||
- При ошибке `serialize_decode` **автоматически освобождает** всю частично выделенную память → `*structure = NULL`.
|
||||
- `max_size > 0` — жёсткий лимит, при превышении `SERIALIZE_ERR_SIZE`.
|
||||
- Длина переменных полей всегда 2 байта (максимум 65535 элементов).
|
||||
- ASCIIZ-строки (`elem_size=1`) сохраняются с терминальным `\0`; пустая строка → 1 байт (`\0`).
|
||||
- Для linked list поле `len_offset` указывает смещение `next`-указателя внутри узла.
|
||||
- Передавать буфер в `serialize_decode` **без заголовка** — он не знает про `header_len`.
|
||||
|
||||
## 3. API
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `serialize_encode(structure, schema, header, &buf, &len)` | Кодирует структуру. `header` копируется в начало `buf`. |
|
||||
| `serialize_decode(in_buf, in_len, schema, &structure)` | Декодирует буфер **без заголовка**. При ошибке всё освобождает. |
|
||||
| `serialize_free(schema, &structure)` | Рекурсивно освобождает структуру и динамические поля, затем обнуляет указатель. |
|
||||
|
||||
**Типы полей (`SERIALIZE_TYPE_*`):**
|
||||
|
||||
| Тип | `data` | `elem_size` | `len_offset` |
|
||||
|-----|--------|-------------|--------------|
|
||||
| `FIXED` (0) | Встроенные данные | Размер поля | Не исп. |
|
||||
| `ASCIIZ` (1) | `char*` | 1 | Не исп. (длина = `strlen`) |
|
||||
| `ARRAY_FIXED` (2) | `uint8_t*` (ровно 1 элемент) | Размер элемента | Не исп. |
|
||||
| `ARRAY_U8` (3) | `uint8_t*` | Размер элемента | Смещение `uint8_t`-счётчика |
|
||||
| `ARRAY_U16` (4) | `uint8_t*` | Размер элемента | Смещение `uint16_t`-счётчика |
|
||||
| `ARRAY_U32` (5) | `uint8_t*` | Размер элемента | Смещение `uint32_t`-счётчика |
|
||||
| `LINKED` (6) | `void*` (голова списка) | Размер узла | Смещение `next` внутри узла |
|
||||
|
||||
**Коды возврата:**
|
||||
- `SERIALIZE_ERR_OK (0)` — успех.
|
||||
- `SERIALIZE_ERR_BUF (-1)` — ошибка выделения памяти.
|
||||
- `SERIALIZE_ERR_NULL (-2)` — NULL-указатель во входных параметрах.
|
||||
- `SERIALIZE_ERR_SIZE (-3)` — превышен `max_size`.
|
||||
- `SERIALIZE_ERR_NOTSUP (-4)` — неподдерживаемый тип поля.
|
||||
+3
-2
@@ -82,7 +82,8 @@ static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
|
||||
}
|
||||
|
||||
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
|
||||
* data может указывать на внешний буфер (вшитая модель) — тогда не освобождается. */
|
||||
* owned_buf передаётся детектору только при успехе; при ошибке остаётся у вызывающего.
|
||||
* Для вшитой модели owned_buf == NULL, её байты не освобождаются. */
|
||||
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
|
||||
const uint8_t* owned_buf, const char* label) {
|
||||
silero_vad_t* vad = NULL;
|
||||
@@ -107,7 +108,6 @@ static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t le
|
||||
return NULL;
|
||||
}
|
||||
vad->ort = ort;
|
||||
vad->model_data = (uint8_t*)owned_buf;
|
||||
vad->model_len = len;
|
||||
|
||||
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
|
||||
@@ -127,6 +127,7 @@ static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t le
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
|
||||
|
||||
ort->ReleaseSessionOptions(opts);
|
||||
vad->model_data = (uint8_t*)owned_buf;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
|
||||
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "socket_compat.h"
|
||||
#include "debug_config.h"
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
// ==================== Windows Implementation ====================
|
||||
@@ -211,6 +212,27 @@ int socket_set_mark(socket_t sock, int mark) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Ошибки send остаются у вызывающего; закрытый пир не завершает процесс сигналом. */
|
||||
ssize_t socket_send(socket_t sock, const void* buf, size_t len) {
|
||||
#ifdef _WIN32
|
||||
if (len > INT_MAX) { WSASetLastError(WSAEMSGSIZE); return SOCKET_ERROR_CODE; }
|
||||
return send(sock, (const char*)buf, (int)len, 0);
|
||||
#elif defined(MSG_NOSIGNAL)
|
||||
return send(sock, buf, len, MSG_NOSIGNAL);
|
||||
#elif defined(SO_NOSIGPIPE)
|
||||
int enabled = 1;
|
||||
if (setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof(enabled)) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "cannot disable SIGPIPE for socket=%d: %s", (int)sock, strerror(errno));
|
||||
return SOCKET_ERROR_CODE;
|
||||
}
|
||||
return send(sock, buf, len, 0);
|
||||
#else
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "safe TCP send is unsupported on this platform");
|
||||
errno = ENOTSUP;
|
||||
return SOCKET_ERROR_CODE;
|
||||
#endif
|
||||
}
|
||||
|
||||
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
|
||||
const struct sockaddr* dest, socklen_t dest_len) {
|
||||
#ifdef _WIN32
|
||||
|
||||
+21
-6
@@ -2,6 +2,8 @@
|
||||
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
|
||||
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
|
||||
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
|
||||
* Использование: platform_init → create_udp → set_nonblocking/options → sendto/recvfrom → close → cleanup.
|
||||
* До регистрации в uasync вызывающий должен включить nonblocking. Winsock init/cleanup согласовать между потоками.
|
||||
*/
|
||||
|
||||
#ifndef SOCKET_COMPAT_H
|
||||
@@ -45,13 +47,16 @@ extern "C" {
|
||||
#define ERR_INTR EINTR
|
||||
#endif
|
||||
|
||||
// Platform initialization/cleanup
|
||||
// POSIX: 0 без системной инициализации. Windows: WSAStartup при первом init, счётчик вложенности; 0 / -1.
|
||||
int socket_platform_init(void);
|
||||
// Парный cleanup: Windows уменьшает счётчик и вызывает WSACleanup на нуле; POSIX ничего не освобождает.
|
||||
void socket_platform_cleanup(void);
|
||||
|
||||
// Socket operations
|
||||
// UDP-сокет для family (обычно AF_INET/AF_INET6); SOCKET_INVALID при ошибке. Владение передаётся вызывающему.
|
||||
socket_t socket_create_udp(int family);
|
||||
// Включает nonblocking: 0 — успех, -1 — ошибка.
|
||||
int socket_set_nonblocking(socket_t sock);
|
||||
/* Закрыть сокет средствами ОС; на POSIX sock=0 оставляет дескриптор открытым и возвращает 0. */
|
||||
int socket_close_wrapper(socket_t sock);
|
||||
static inline int socket_get_error(void) {
|
||||
#ifdef _WIN32
|
||||
@@ -61,22 +66,32 @@ static inline int socket_get_error(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Socket options
|
||||
// Пробует установить оба буфера SO_SNDBUF/SO_RCVBUF: 0 при успехе обоих, -1 при любой ошибке; отката нет.
|
||||
int socket_set_buffers(socket_t sock, int sndbuf, int rcvbuf);
|
||||
// Устанавливает SO_REUSEADDR в значение reuse; 0 / -1.
|
||||
int socket_set_reuseaddr(socket_t sock, int reuse);
|
||||
// SO_BINDTODEVICE для строки ifname (не NULL); 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_bind_to_device(socket_t sock, const char* ifname);
|
||||
// SO_MARK в значение mark; 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
|
||||
int socket_set_mark(socket_t sock, int mark);
|
||||
|
||||
// I/O operations
|
||||
/* Один TCP send без SIGPIPE и изменения обработчиков процесса. Буфер заимствован;
|
||||
* возвращает число байт либо SOCKET_ERROR_CODE, включая EAGAIN. Ошибку логирует владелец.
|
||||
* MSG_NOSIGNAL либо SO_NOSIGPIPE; без поддержки возвращает ENOTSUP. Windows: len <= INT_MAX. */
|
||||
ssize_t socket_send(socket_t sock, const void* buf, size_t len);
|
||||
|
||||
// Один sendto/recvfrom с flags=0: число байт или SOCKET_ERROR_CODE; errno/WSA-код читать через socket_get_error.
|
||||
// буферами владеет вызывающий; для Windows len должен помещаться в int. dest_len — размер адреса назначения;
|
||||
// src_len при приёме — входная ёмкость/выходная длина адреса (может быть NULL по правилам recvfrom ОС).
|
||||
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
|
||||
const struct sockaddr* dest, socklen_t dest_len);
|
||||
ssize_t socket_recvfrom(socket_t sock, void* buf, size_t len,
|
||||
struct sockaddr* src, socklen_t* src_len);
|
||||
|
||||
// Utility
|
||||
// Заимствованный текст ошибки: Windows — один статический буфер (перезаписывается следующим вызовом), POSIX — strerror.
|
||||
const char* socket_strerror(int err);
|
||||
|
||||
// взять порт из sockaddr_storage
|
||||
// IPv4/IPv6-порт в host byte order; 0 при NULL или другом семействе.
|
||||
uint16_t ss_get_port(const struct sockaddr_storage *addr);
|
||||
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
|
||||
uint16_t socket_get_bound_port(socket_t sock);
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# socket_compat — Cross-platform socket abstraction
|
||||
|
||||
## 1. Назначение
|
||||
Унифицирует работу с UDP-сокетами между POSIX и Windows (MSYS2 UCRT64). Скрывает различия в типах дескрипторов (`int` vs `SOCKET`), инициализации подсистемы (WSAStartup/WSACleanup), кодах ошибок и сигнатурах системных вызовов.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### Инициализация (однократно при старте)
|
||||
```c
|
||||
if (socket_platform_init() != 0) { /* фатальная ошибка */ }
|
||||
// ... работа с сокетами ...
|
||||
socket_platform_cleanup(); // при завершении
|
||||
```
|
||||
На Windows WSAStartup/WSACleanup с refcount — можно вызывать init/cleanup вложенно.
|
||||
|
||||
### Создание и настройка сокета
|
||||
```c
|
||||
socket_t sock = socket_create_udp(AF_INET); // или AF_INET6
|
||||
if (sock == SOCKET_INVALID) { /* ошибка */ }
|
||||
|
||||
socket_set_nonblocking(sock); // обязательно для u_async
|
||||
socket_set_reuseaddr(sock, 1);
|
||||
socket_set_buffers(sock, 256*1024, 256*1024); // SO_SNDBUF/SO_RCVBUF
|
||||
socket_bind_to_device(sock, "eth0"); // только Linux, SO_BINDTODEVICE
|
||||
socket_set_mark(sock, 42); // только Linux, SO_MARK
|
||||
```
|
||||
|
||||
### Отправка и приём
|
||||
```c
|
||||
ssize_t sent = socket_sendto(sock, buf, len, (struct sockaddr*)&addr, addr_len);
|
||||
ssize_t recv = socket_recvfrom(sock, buf, sizeof(buf), (struct sockaddr*)&src, &src_len);
|
||||
|
||||
uint16_t port = ss_get_port(&src); // порт из sockaddr_storage
|
||||
```
|
||||
|
||||
### Обработка ошибок
|
||||
```c
|
||||
if (sent == SOCKET_ERROR_CODE) {
|
||||
int err = socket_get_error();
|
||||
if (err == ERR_WOULDBLOCK) { /* нормально для неблокирующего */ }
|
||||
// на POSIX ERR_WOULDBLOCK == EWOULDBLOCK (может совпадать с EAGAIN)
|
||||
DEBUG_ERROR(..., "%s", socket_strerror(err));
|
||||
}
|
||||
```
|
||||
|
||||
### Закрытие
|
||||
```c
|
||||
socket_close_wrapper(sock); // на POSIX: close() с защитой от fd 0
|
||||
```
|
||||
|
||||
### Ключевые нюансы
|
||||
- **Всегда вызывать `socket_platform_init()` перед работой** — иначе на Windows сокеты не будут работать
|
||||
- **Всегда `socket_set_nonblocking()`** — u_async требует неблокирующих сокетов
|
||||
- `socket_t` — на POSIX `int`, на Windows `SOCKET` (unsigned)
|
||||
- `SOCKET_INVALID` — константа для невалидного сокета (`-1` на POSIX, `INVALID_SOCKET` на Windows)
|
||||
- `SOCKET_ERROR_CODE` — код ошибки сокетных вызовов (`-1` на POSIX, `SOCKET_ERROR` на Windows)
|
||||
- `socket_strerror()` на Windows использует статический буфер — не thread-safe для параллельного использования
|
||||
- `socket_bind_to_device` и `socket_set_mark` — только Linux, на остальных платформах возвращают -1 с DEBUG-сообщением
|
||||
- `socket_close_wrapper` на POSIX не закроет fd 0 (защита)
|
||||
|
||||
## 3. API
|
||||
|
||||
### Типы и константы
|
||||
| Имя | Назначение |
|
||||
|-----|------------|
|
||||
| `socket_t` | Кроссплатформенный тип дескриптора сокета (`int` / `SOCKET`) |
|
||||
| `SOCKET_INVALID` | Невалидный сокет (`-1` / `INVALID_SOCKET`) |
|
||||
| `SOCKET_ERROR_CODE` | Код ошибки сокетного вызова (`-1` / `SOCKET_ERROR`) |
|
||||
| `ERR_WOULDBLOCK` / `ERR_AGAIN` / `ERR_INTR` | Кроссплатформенные коды ошибок |
|
||||
|
||||
### Инициализация
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_platform_init()` | Инициализация сокетной подсистемы (WSAStartup на Windows). 0 — успех |
|
||||
| `socket_platform_cleanup()` | Деинициализация (WSACleanup на Windows), refcount |
|
||||
|
||||
### Создание и настройка
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_create_udp(family)` | Создать UDP-сокет (AF_INET или AF_INET6) |
|
||||
| `socket_set_nonblocking(sock)` | Перевести в неблокирующий режим |
|
||||
| `socket_set_buffers(sock, snd, rcv)` | Установить SO_SNDBUF и SO_RCVBUF |
|
||||
| `socket_set_reuseaddr(sock, reuse)` | Установить SO_REUSEADDR |
|
||||
| `socket_bind_to_device(sock, ifname)` | Привязать к интерфейсу (Linux SO_BINDTODEVICE) |
|
||||
| `socket_set_mark(sock, mark)` | Установить SO_MARK (Linux) |
|
||||
|
||||
### I/O
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_sendto(sock, buf, len, dest, dest_len)` | Отправить UDP-датаграмму |
|
||||
| `socket_recvfrom(sock, buf, len, src, src_len)` | Принять UDP-датаграмму |
|
||||
|
||||
### Утилиты
|
||||
| Функция | Назначение |
|
||||
|---------|------------|
|
||||
| `socket_get_error()` | Текущий код ошибки (inline: `WSAGetLastError()` / `errno`) |
|
||||
| `socket_strerror(err)` | Текстовое описание ошибки (Windows: статический буфер, не thread-safe) |
|
||||
| `socket_close_wrapper(sock)` | Закрыть сокет (POSIX: защита от закрытия fd 0) |
|
||||
| `ss_get_port(addr)` | Извлечь порт в host byte order из `sockaddr_storage` |
|
||||
+1
-1
@@ -37,7 +37,7 @@ char *strbuf_detach(struct strbuf *sb);
|
||||
/* reset len=0, keep allocation */
|
||||
void strbuf_reset(struct strbuf *sb);
|
||||
|
||||
/* printf-like append. auto-grows. returns written bytes or -1 on OOM */
|
||||
/* printf-добавление с ростом буфера: число байтов без NUL или < 0 при ошибке форматирования/роста. */
|
||||
int strbuf_addf(struct strbuf *sb, const char *fmt, ...)
|
||||
__attribute__((format(printf, 2, 3)));
|
||||
|
||||
|
||||
+8
-3
@@ -1,5 +1,6 @@
|
||||
/* swm_min — минимум последних window_size целых значений (окно по числу добавлений, не по времени).
|
||||
* create → add/get_min → destroy; память окна выделяется при create, внутренняя синхронизация отсутствует. */
|
||||
* create → add/get_min → destroy; память O(window_size) выделяется при create, внутренней синхронизации нет.
|
||||
* Монотонный deque: add амортизированно O(1), get_min O(1); значения int, включая отрицательные. */
|
||||
#ifndef SLIDING_WINDOW_MIN_H
|
||||
#define SLIDING_WINDOW_MIN_H
|
||||
|
||||
@@ -14,24 +15,28 @@ typedef struct SlidingWindowMin SlidingWindowMin;
|
||||
/**
|
||||
* Создаёт новую структуру скользящего минимума.
|
||||
* @param window_size размер окна (должен быть > 0)
|
||||
* @return указатель на структуру или NULL при ошибке выделения памяти
|
||||
* @return указатель на структуру или NULL при window_size <= 0 / ошибке выделения памяти
|
||||
*/
|
||||
SlidingWindowMin* swm_create(int window_size);
|
||||
|
||||
/**
|
||||
* Уничтожает структуру и освобождает всю динамически выделенную память.
|
||||
* @param swm окно; NULL допустим
|
||||
*/
|
||||
void swm_destroy(SlidingWindowMin* swm);
|
||||
|
||||
/**
|
||||
* Добавляет новое значение в окно.
|
||||
* @param swm окно; при NULL ничего не делает
|
||||
* @param val новое целое значение
|
||||
* После вызова swm_get_min() сразу вернёт минимум последних min(window_size, добавленных) значений.
|
||||
*/
|
||||
void swm_add(SlidingWindowMin* swm, int val);
|
||||
|
||||
/**
|
||||
* Возвращает текущий минимум в окне.
|
||||
* Если окно пустое — возвращает INT_MAX.
|
||||
* @param swm окно
|
||||
* @return минимум; если окно пустое или swm == NULL — INT_MAX
|
||||
*/
|
||||
int swm_get_min(const SlidingWindowMin* swm);
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Sliding Window Minimum (swm_min.h)
|
||||
|
||||
## 1. Назначение
|
||||
Структура данных для отслеживания минимума в скользящем окне фиксированного размера. Добавление значения и запрос минимума — O(1) амортизированное время. Память — O(window_size). Предназначена для отслеживания минимального RTT в ETCP/BBR (в кодовой базе зарезервирована, ожидает интеграции).
|
||||
|
||||
## 2. Как пользоваться
|
||||
Создать окно заданного размера, добавлять значения по мере поступления, запрашивать текущий минимум.
|
||||
|
||||
```c
|
||||
SlidingWindowMin *swm = swm_create(10); // окно из 10 последних значений
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
swm_add(swm, get_new_rtt());
|
||||
int min_rtt = swm_get_min(swm); // минимум последних ≤10 значений
|
||||
}
|
||||
|
||||
swm_destroy(swm);
|
||||
```
|
||||
|
||||
**Нюансы:**
|
||||
- `swm_get_min` на пустом окне возвращает `INT_MAX`
|
||||
- `window_size` должен быть > 0, иначе `swm_create` вернёт NULL
|
||||
- Значения — `int`, отрицательные допустимы
|
||||
|
||||
## 3. API
|
||||
|
||||
### Структура
|
||||
- **`SlidingWindowMin`** — непрозрачная структура (opaque pointer). Содержит кольцевой буфер значений, deque индексов, позицию и размер окна.
|
||||
|
||||
### Функции
|
||||
- **`swm_create(window_size)`** — выделяет SlidingWindowMin, инициализирует пустой deque и кольцевой буфер размера `window_size`. Возвращает NULL при ошибке выделения памяти.
|
||||
- **`swm_destroy(swm)`** — освобождает все ресурсы структуры.
|
||||
- **`swm_add(swm, val)`** — добавляет значение в окно. Автоматически вытесняет индексы, вышедшие за границу окна, и удаляет из deque значения, которые больше текущего (поддерживает монотонный минимум).
|
||||
- **`swm_get_min(swm)`** — возвращает минимум текущего окна за O(1). Для пустого окна — `INT_MAX`.
|
||||
|
||||
### Алгоритм
|
||||
Использует монотонный deque: в голове deque всегда индекс минимального элемента текущего окна. При добавлении из хвоста удаляются элементы ≥ нового — они никогда не станут минимумом, пока новый элемент в окне.
|
||||
|
||||
Тест: `tests/test_swm_min.c`
|
||||
+130
-63
@@ -5,11 +5,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
#include <limits.h>
|
||||
#ifndef MSG_NOSIGNAL
|
||||
#define MSG_NOSIGNAL 0
|
||||
#endif
|
||||
@@ -28,6 +24,38 @@ static void tcp_conn_handle_error(struct tcp_conn* tc, int err);
|
||||
static void connect_timeout_cb(void* arg);
|
||||
static const uint8_t tcp_fin_sentinel;
|
||||
|
||||
// Сохраняем fd при HUP, но не оставляем level-triggered событие в poll без работы.
|
||||
// Очередь восстанавливает регистрацию с новым generation; write_buf обслуживается независимо от чтения.
|
||||
static int sync_socket_monitoring(struct tcp_conn* tc) {
|
||||
if (tc->destroyed || tc->error || tc->closed || tc->sock == SOCKET_INVALID) return -1;
|
||||
int read = !tc->read_paused && !tc->read_stopped && !tc->fin_remote;
|
||||
int write = !tc->connected || tc->write_buf != NULL;
|
||||
if (tc->hup_received && !read && !write) {
|
||||
if (tc->socket_id) {
|
||||
uasync_remove_socket_t(tc->ua, tc->sock); tc->socket_id = NULL;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: park HUP fd=%d rq=%d paused=%u stopped=%u fin=%u",
|
||||
(int)tc->sock, tc->read_queue->count, tc->read_paused, tc->read_stopped, tc->fin_remote);
|
||||
}
|
||||
tc->write_monitor = 0;
|
||||
return 0;
|
||||
}
|
||||
if (!tc->socket_id) {
|
||||
tc->socket_id = uasync_add_socket_t(tc->ua, tc->sock, read_cb, write_cb, error_cb, "tcp_io", tc);
|
||||
if (!tc->socket_id) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot restore socket registration fd=%d", (int)tc->sock);
|
||||
tcp_conn_handle_error(tc, ENOMEM); return -1;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: restore HUP fd=%d read=%d write=%d", (int)tc->sock, read, write);
|
||||
}
|
||||
if (uasync_set_socket_read(tc->ua, tc->socket_id, read) != ERR_OK ||
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, write) != ERR_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: monitoring update failed fd=%d read=%d write=%d", (int)tc->sock, read, write);
|
||||
tcp_conn_handle_error(tc, EIO); return -1;
|
||||
}
|
||||
tc->write_monitor = write;
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct tcp_conn* tcp_conn_create(
|
||||
struct UASYNC* ua, socket_t sock,
|
||||
size_t entry_data_size, size_t write_chunk_size,
|
||||
@@ -119,10 +147,15 @@ void tcp_conn_destroy(struct tcp_conn* tc) {
|
||||
if (!tc) return;
|
||||
if (tc->destroyed) return;
|
||||
tc->destroyed = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_local=%d closed=%d",
|
||||
(int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_local, tc->closed);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_pending=%d fin_local=%d closed=%d",
|
||||
(int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_pending, tc->fin_local, tc->closed);
|
||||
|
||||
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
|
||||
if (tc->connect_timer) {
|
||||
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: destroy could not cancel connect timer fd=%d timer=%p",
|
||||
(int)tc->sock, tc->connect_timer);
|
||||
tc->connect_timer = NULL;
|
||||
}
|
||||
|
||||
if (tc->socket_id) {
|
||||
uasync_remove_socket_t(tc->ua, tc->sock);
|
||||
@@ -153,6 +186,12 @@ void tcp_conn_destroy(struct tcp_conn* tc) {
|
||||
static void tcp_conn_handle_error(struct tcp_conn* tc, int err)
|
||||
{
|
||||
tc->error = 1;
|
||||
if (tc->connect_timer) {
|
||||
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: error cleanup could not cancel connect timer fd=%d timer=%p err=%d",
|
||||
(int)tc->sock, tc->connect_timer, err);
|
||||
tc->connect_timer = NULL;
|
||||
}
|
||||
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
queue_set_empty_callback(tc->read_queue, NULL, NULL);
|
||||
@@ -194,7 +233,7 @@ static void tcp_conn_handle_error(struct tcp_conn* tc, int err)
|
||||
static void read_cb(socket_t sock, void* arg) {
|
||||
(void)sock;
|
||||
struct tcp_conn* tc = (struct tcp_conn*)arg;
|
||||
if (!tc || tc->sock == SOCKET_INVALID || !tc->read_queue) return;
|
||||
if (!tc || tc->sock == SOCKET_INVALID || !tc->read_queue || tc->read_paused || tc->read_stopped || tc->fin_remote) return;
|
||||
|
||||
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
|
||||
uint8_t* buf = memory_pool_alloc(tc->data_pool);
|
||||
@@ -202,8 +241,7 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: read_cb alloc failed fd=%d", (int)tc->sock);
|
||||
if (e) queue_entry_free(e);
|
||||
if (buf) memory_pool_free(tc->data_pool, buf);
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
tc->read_paused = 1;
|
||||
tcp_conn_handle_error(tc, ENOMEM);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,9 +253,12 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
queue_data_put(rq, e);
|
||||
// tc мог быть уничтожен через callback chain — read_queue обнулён
|
||||
if (rq && tc->read_queue && tc->read_queue->count >= tc->read_high_water && !tc->read_paused) {
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
tc->read_paused = 1;
|
||||
queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc);
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
if (queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot wait for read queue fd=%d", (int)tc->sock);
|
||||
tcp_conn_handle_error(tc, ENOMEM);
|
||||
}
|
||||
}
|
||||
} else if (n == 0) {
|
||||
memory_pool_free(tc->data_pool, buf);
|
||||
@@ -225,8 +266,8 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
if (tc->fin_remote) return;
|
||||
tc->fin_remote = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock);
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
if (tc->read_queue->count == 0) {
|
||||
if (tc->on_fin) tc->on_fin(tc, tc->arg);
|
||||
} else {
|
||||
@@ -244,9 +285,16 @@ static void read_cb(socket_t sock, void* arg) {
|
||||
static void resume_read_cb(struct ll_queue* q, void* arg) {
|
||||
(void)q;
|
||||
struct tcp_conn* tc = (struct tcp_conn*)arg;
|
||||
if (!tc || tc->sock == SOCKET_INVALID) return;
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 1);
|
||||
if (!tc || tc->sock == SOCKET_INVALID || tc->destroyed || tc->read_stopped || tc->fin_remote) return;
|
||||
if (tc->read_queue->count > tc->read_low_water) {
|
||||
if (queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot renew read waiter fd=%d", (int)tc->sock);
|
||||
tcp_conn_handle_error(tc, ENOMEM);
|
||||
}
|
||||
return;
|
||||
}
|
||||
tc->read_paused = 0;
|
||||
sync_socket_monitoring(tc);
|
||||
}
|
||||
|
||||
static void fin_deferred_cb(struct ll_queue* q, void* arg) {
|
||||
@@ -306,8 +354,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) {
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 0);
|
||||
tc->write_monitor = 0;
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT OFF fd=%d (queue empty)", (int)tc->sock);
|
||||
queue_resume_callback(q);
|
||||
notify_flushed(tc);
|
||||
@@ -325,7 +372,14 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
} else if (e->dgram == &tcp_fin_sentinel) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock);
|
||||
queue_entry_free(e); queue_resume_callback(q);
|
||||
shutdown(tc->sock, SHUT_WR);
|
||||
tc->fin_pending = 0;
|
||||
if (shutdown(tc->sock, SHUT_WR) != 0) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: shutdown write failed fd=%d err=%d (%s)",
|
||||
(int)tc->sock, err, socket_strerror(err));
|
||||
tcp_conn_handle_error(tc, err);
|
||||
return;
|
||||
}
|
||||
tc->fin_local = 1;
|
||||
if (tc->on_fin_sent) tc->on_fin_sent(tc, tc->arg);
|
||||
} else {
|
||||
@@ -356,8 +410,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
tc->write_offset = 0;
|
||||
memory_pool_free(tc->data_pool, e->dgram);
|
||||
queue_entry_free(e);
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 1);
|
||||
tc->write_monitor = 1;
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT ON fd=%d (partial send, wbuf=%zu bytes)", (int)tc->sock, tc->write_len);
|
||||
} else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
|
||||
tc->write_buf = memory_pool_alloc(tc->data_pool);
|
||||
@@ -373,8 +426,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
|
||||
tc->write_offset = 0;
|
||||
memory_pool_free(tc->data_pool, e->dgram);
|
||||
queue_entry_free(e);
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 1);
|
||||
tc->write_monitor = 1;
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT ON fd=%d (EAGAIN, wbuf=%zu bytes)", (int)tc->sock, tc->write_len);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: send error fd=%d errno=%d", (int)tc->sock, errno);
|
||||
@@ -394,7 +446,12 @@ static void write_cb(socket_t sock, void* arg) {
|
||||
socklen_t len = sizeof(err);
|
||||
if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&err, &len) == 0 && err == 0) {
|
||||
tc->connected = 1;
|
||||
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
|
||||
if (tc->connect_timer) {
|
||||
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connected socket could not cancel connect timer fd=%d timer=%p",
|
||||
(int)tc->sock, tc->connect_timer);
|
||||
tc->connect_timer = NULL;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect ok fd=%d", (int)tc->sock);
|
||||
if (tc->on_connected) tc->on_connected(tc, tc->arg);
|
||||
if (tc->destroyed) return;
|
||||
@@ -408,10 +465,7 @@ static void write_cb(socket_t sock, void* arg) {
|
||||
if (flush_write_buf(tc)) return;
|
||||
if (!tc->write_buf) {
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: write_cb flushed fd=%d wmon=%d (resume WQ cb)", (int)tc->sock, tc->write_monitor);
|
||||
if (tc->write_queue->waiter_defer) {
|
||||
uasync_set_socket_write(tc->ua, tc->socket_id, 0);
|
||||
tc->write_monitor = 0;
|
||||
}
|
||||
if (sync_socket_monitoring(tc) < 0) return;
|
||||
queue_resume_callback(tc->write_queue);
|
||||
notify_flushed(tc);
|
||||
}
|
||||
@@ -427,33 +481,13 @@ static void error_cb(socket_t sock, void* arg) {
|
||||
if (!tc || tc->sock == SOCKET_INVALID) return;
|
||||
int so_err = 0; socklen_t so_len = sizeof(so_err);
|
||||
if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&so_err, &so_len) == 0 && so_err == 0) {
|
||||
// Peer полностью закрыл свой сокет (EPOLLHUP) после того, как FIN уже прочитан (fin_remote=1).
|
||||
// EPOLLHUP — level-triggered и «always reported»: если оставить сокет в epoll, каждая итерация
|
||||
// epoll_wait мгновенно вернёт HUP и error_cb зациклится на 100% CPU. Закрываем сокет.
|
||||
if (tc->fin_remote && tc->fin_local) {
|
||||
// Обе TCP-половины закрыты, но владелец ещё может передавать ранее прочитанные данные.
|
||||
// Убираем level-triggered HUP из poll; CLOSE остаётся решением владельца.
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: both FINs fd=%d — detach poll, await owner drain", (int)tc->sock);
|
||||
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
|
||||
tc->socket_id = NULL; tc->write_monitor = 0;
|
||||
return;
|
||||
}
|
||||
if (tc->fin_remote) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d — peer fully closed after FIN, closing",
|
||||
(int)tc->sock);
|
||||
tcp_conn_push_close(tc);
|
||||
return;
|
||||
}
|
||||
// Грациозное закрытие peer (FIN, EPOLLHUP) при приостановленном чтении: FIN пришёл не как
|
||||
// EPOLLIN (чтение отключено backpressure-ом), а как HUP. Возобновляем чтение, чтобы
|
||||
// read_cb дослил остаток и прочитал FIN (recv()==0 → fin_remote → отложенный on_fin).
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d fin_r=%d fin_l=%d read_paused=%d rq=%d — resume read",
|
||||
(int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused,
|
||||
tc->read_queue ? tc->read_queue->count : 0);
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 1);
|
||||
tc->read_paused = 0;
|
||||
read_cb(tc->sock, tc);
|
||||
if (!tc->hup_received)
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: HUP fd=%d fin_r=%u fin_l=%u paused=%u stopped=%u rq=%d",
|
||||
(int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused, tc->read_stopped, tc->read_queue->count);
|
||||
tc->hup_received = 1;
|
||||
if (tc->write_buf && flush_write_buf(tc)) return;
|
||||
if (!tc->fin_remote && !tc->read_paused && !tc->read_stopped) read_cb(tc->sock, tc);
|
||||
if (!tc->destroyed && !tc->error && !tc->closed) sync_socket_monitoring(tc);
|
||||
return;
|
||||
}
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: async error fd=%d connected=%d err=%d so_err=%d fin_r=%d fin_l=%d closed=%d wmon=%d",
|
||||
@@ -467,11 +501,13 @@ static void error_cb(socket_t sock, void* arg) {
|
||||
|
||||
int tcp_conn_push_fin(struct tcp_conn* tc) {
|
||||
if (!tc || tc->sock == SOCKET_INVALID) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin invalid tc"); return -1; }
|
||||
if (tc->fin_pending) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already pending fd=%d", (int)tc->sock); return -1; }
|
||||
if (tc->fin_local) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already fin_local fd=%d", (int)tc->sock); return -1; }
|
||||
if (tc->closed) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already closed fd=%d", (int)tc->sock); return -1; }
|
||||
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
|
||||
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin entry_pool exhausted fd=%d", (int)tc->sock); return -1; }
|
||||
e->dgram = (uint8_t*)&tcp_fin_sentinel; e->len = 0;
|
||||
tc->fin_pending = 1;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin fd=%d wq=%d", (int)tc->sock, tc->write_queue->count + 1);
|
||||
queue_data_put(tc->write_queue, e);
|
||||
return 0;
|
||||
@@ -495,9 +531,9 @@ void tcp_conn_set_flushed(struct tcp_conn* tc, void (*on_flushed)(struct tcp_con
|
||||
|
||||
void tcp_conn_pause_read(struct tcp_conn* tc) {
|
||||
if (!tc || tc->sock == SOCKET_INVALID) return;
|
||||
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
|
||||
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
|
||||
tc->read_paused = 1;
|
||||
tc->read_stopped = 1; tc->read_paused = 1;
|
||||
sync_socket_monitoring(tc);
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
@@ -514,11 +550,42 @@ static void connect_timeout_cb(void* arg) {
|
||||
}
|
||||
}
|
||||
|
||||
void tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms) {
|
||||
if (!tc) return;
|
||||
if (timeout_ms < 0) timeout_ms = 0;
|
||||
tc->connect_timeout_ms = timeout_ms;
|
||||
if (!tc->connected && timeout_ms > 0) {
|
||||
tc->connect_timer = uasync_set_timeout(tc->ua, timeout_ms * 10, tc, connect_timeout_cb, "tcp_conn_ct");
|
||||
/* Замена атомарна относительно event loop: сначала резервируем новый таймер, затем отменяем прежний. */
|
||||
int tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms) {
|
||||
if (!tc) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: set connect timeout: tc=NULL timeout=%d", timeout_ms);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (tc->destroyed || tc->error || tc->closed || tc->sock == SOCKET_INVALID) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: set connect timeout on inactive tc fd=%d destroyed=%d error=%d closed=%d",
|
||||
(int)tc->sock, tc->destroyed, tc->error, tc->closed);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (timeout_ms > INT_MAX / 10) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connect timeout=%d exceeds limit=%d ms fd=%d",
|
||||
timeout_ms, INT_MAX / 10, (int)tc->sock);
|
||||
return -EINVAL;
|
||||
}
|
||||
if (timeout_ms < 0) timeout_ms = 0;
|
||||
void* timer = NULL;
|
||||
if (!tc->connected && timeout_ms > 0) {
|
||||
timer = uasync_set_timeout(tc->ua, timeout_ms * 10, tc, connect_timeout_cb, "tcp_conn_ct");
|
||||
if (!timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot create connect timer fd=%d timeout=%d ms; old timeout=%d retained",
|
||||
(int)tc->sock, timeout_ms, tc->connect_timeout_ms);
|
||||
return -ENOMEM;
|
||||
}
|
||||
}
|
||||
void* old_timer = tc->connect_timer;
|
||||
if (old_timer && uasync_cancel_timeout(tc->ua, old_timer) != ERR_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot cancel connect timer fd=%d timer=%p", (int)tc->sock, old_timer);
|
||||
if (timer && uasync_cancel_timeout(tc->ua, timer) != ERR_OK)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot cancel replacement connect timer fd=%d timer=%p", (int)tc->sock, timer);
|
||||
return -EIO;
|
||||
}
|
||||
tc->connect_timer = timer;
|
||||
tc->connect_timeout_ms = timeout_ms;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect timeout updated fd=%d connected=%d timeout=%d ms old_timer=%p timer=%p",
|
||||
(int)tc->sock, tc->connected, timeout_ms, old_timer, timer);
|
||||
return 0;
|
||||
}
|
||||
+51
-19
@@ -9,7 +9,8 @@
|
||||
// FIN (recv==0) → on_fin (сразу или через empty_callback после дренажа очереди).
|
||||
//
|
||||
// Запись: внешний код кладёт entry в write_queue через queue_data_put().
|
||||
// write_queue_fetch_cb (deferred) → send(), при EAGAIN → write_buf + EPOLLOUT.
|
||||
// Первый write_queue_fetch_cb может быть синхронным; продолжение — через uasync_call_soon.
|
||||
// send(), при EAGAIN → write_buf + EPOLLOUT.
|
||||
// Всё отправлено → on_flushed.
|
||||
//
|
||||
// FIN/Close: tcp_conn_push_fin/close ставят сентинел в write_queue.
|
||||
@@ -18,6 +19,17 @@
|
||||
//
|
||||
// Ошибка: любая ошибка сокета → tcp_conn_handle_error → close + null коллбэков → on_error.
|
||||
// Реализация on_error обязана вызвать tcp_conn_destroy. После возврата tc недействителен.
|
||||
//
|
||||
// Все операции и callbacks — в потоке ua. После create установить consumer на read_queue:
|
||||
// queue_set_callback(tc->read_queue, on_read, arg).
|
||||
// Consumer извлекает entry, обрабатывает e->len байт, возвращает dgram в tc->data_pool,
|
||||
// освобождает entry и вызывает queue_resume_callback(q) для продолжения чтения.
|
||||
// Для записи выделить entry из tc->entry_pool и dgram из tc->data_pool, заполнить len и dgram_pool;
|
||||
// затем queue_data_put(tc->write_queue, e). Длина должна помещаться в буфер и uint16_t.
|
||||
// После принятого FIN новые данные не добавлять; закрывать сокет через push_close/destroy.
|
||||
// HUP не снимает backpressure/явную pause_read: регистрация без доступной работы временно удаляется,
|
||||
// low_water восстанавливает её с новым поколением uasync. FD/очереди сохраняются до решения владельца.
|
||||
// Ошибка восстановления/ожидания очереди вызывает on_error, как остальные ошибки TCP.
|
||||
|
||||
#ifndef TCP_IO_H
|
||||
#define TCP_IO_H
|
||||
@@ -40,15 +52,18 @@ struct tcp_conn {
|
||||
struct ll_queue* read_queue; // сокет → данные (блоки до entry_data_size)
|
||||
struct ll_queue* write_queue; // данные → сокет (блоки до write_chunk_size)
|
||||
|
||||
int read_high_water;
|
||||
int read_low_water;
|
||||
int rcvbuf_size;
|
||||
uint8_t read_paused;
|
||||
int read_high_water; // Число входных entry для приостановки чтения
|
||||
int read_low_water; // Порог очереди для возобновления чтения
|
||||
int rcvbuf_size; // Запрошенный SO_RCVBUF; <=0 — не настраивать буферы
|
||||
uint8_t read_paused; // Чтение приостановлено; low_water снимает только автоматическую паузу
|
||||
uint8_t read_stopped; // Владелец запретил чтение; low_water/HUP не снимает запрет
|
||||
uint8_t hup_received; // Получен HUP с SO_ERROR=0; idle registration снимается до готовности очереди
|
||||
|
||||
uint8_t write_monitor; // 1 = EPOLLOUT активен
|
||||
uint8_t connected;
|
||||
uint8_t error;
|
||||
uint8_t fin_remote; // FIN получен от удалённой стороны (recv == 0)
|
||||
uint8_t fin_pending; // FIN поставлен в write_queue, shutdown ещё не выполнен
|
||||
uint8_t fin_local; // FIN отправлен удалённой стороне (shutdown SHUT_WR)
|
||||
uint8_t closed; // сокет полностью закрыт (close)
|
||||
uint8_t destroyed; // 1 = tcp_conn_destroy вызван, tc ожидает отложенного free
|
||||
@@ -71,14 +86,21 @@ struct tcp_conn {
|
||||
|
||||
// Коллбэки
|
||||
void (*on_connected)(struct tcp_conn* tc, void* arg); // исходящий connect завершён
|
||||
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен от удалённой стороны (fin_remote=1)
|
||||
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен; вызов сразу при пустой read_queue, иначе после её дренажа
|
||||
void (*on_fin_sent)(struct tcp_conn* tc, void* arg); // FIN отправлен удалённой стороне (fin_local=1)
|
||||
void (*on_error)(struct tcp_conn* tc, int err, void* arg); // фатальная ошибка (вызывается после close сокета; обязан вызвать tcp_conn_destroy)
|
||||
void (*on_flushed)(struct tcp_conn* tc, void* arg); // все данные записи отправлены (write_queue + write_buf пусты)
|
||||
void (*on_closed)(struct tcp_conn* tc, void* arg); // сокет закрыт через очередь (после close)
|
||||
void* arg;
|
||||
void* arg; // Заимствованный контекст всех callbacks, модуль его не освобождает
|
||||
};
|
||||
|
||||
// Регистрирует подключённый/подключающийся неблокирующий sock в ua; сам connect не вызывает.
|
||||
// entry_data_size — ёмкость recv-блока, write_chunk_size — размер буфера записи; оба 1..UINT16_MAX,
|
||||
// max этих размеров должен вмещать void* (требование пула). Пороги: 0 <= low < high, проверяет вызывающий.
|
||||
// rcvbuf_size > 0 пробует SO_SNDBUF=0/SO_RCVBUF=rcvbuf_size; ошибка опции логируется, create продолжается.
|
||||
// Создаёт две очереди и два пула; on_fin/on_error и arg сохраняются без передачи владения arg.
|
||||
// Результат — tc или NULL при неверных ua/sock, OOM/ошибке регистрации; при NULL sock остаётся вызывающему.
|
||||
// При успехе сокетом владеет tc. Пулы и выданные из них блоки живут до отложенного освобождения tc.
|
||||
struct tcp_conn* tcp_conn_create(
|
||||
struct UASYNC* ua,
|
||||
socket_t sock,
|
||||
@@ -91,37 +113,47 @@ struct tcp_conn* tcp_conn_create(
|
||||
void (*on_error)(struct tcp_conn* tc, int err, void* arg),
|
||||
void* arg);
|
||||
|
||||
// Дренирует очереди, закрывает сокет, уничтожает пулы, освобождает tc.
|
||||
// Сразу отменяет callbacks/таймер, дренирует очереди и закрывает сокет.
|
||||
// Пулы и tc освобождаются через uasync_call_soon в том же uasync-потоке.
|
||||
// NULL безопасен; повторный destroy допустим только до отложенного free.
|
||||
// Владелец после destroy больше не использует tc; event loop должен выполнить отложенную очистку.
|
||||
// Ранее извлечённые entry/dgram вернуть до освобождения пулов; перед destroy ua дренировать call_soon вне callbacks.
|
||||
void tcp_conn_destroy(struct tcp_conn* tc);
|
||||
|
||||
// Запись: queue_data_put(tc->write_queue, entry) напрямую.
|
||||
// Автозабор (deferred) сам шлёт когда сокет готов.
|
||||
// Порог: 32 записи, при заполнении queue_waiter_wait.
|
||||
// Первый автозабор может быть синхронным; queue_resume_callback откладывает продолжение.
|
||||
// Порог backpressure: count <= 32, не жёсткий лимит ёмкости; производитель ждёт через queue_waiter_wait.
|
||||
// Обычные буферы записи должны принадлежать data_pool: отправка возвращает их непосредственно в этот пул.
|
||||
|
||||
// Поставить сигналы в очередь отправки. Все данные в очереди перед сигналом
|
||||
// будут отправлены до его обработки. Кодирование: dgram=NULL → close, len=0+dgram!=NULL → FIN.
|
||||
// будут переданы ОС до его обработки. Сигналы имеют len=0: dgram=NULL → CLOSE,
|
||||
// dgram=&tcp_fin_sentinel → FIN (внутренний указатель, сигналы ставить только через API).
|
||||
// tcp_conn_push_fin(tc) — FIN: после отправки предшествующих данных вызывает
|
||||
// shutdown(SHUT_WR) и on_fin_sent.
|
||||
// Повторный вызов игнорируется (fin_sent уже установлен).
|
||||
// Повторный вызов возвращает -1 при fin_pending или fin_local.
|
||||
// tcp_conn_push_close(tc) — CLOSE: после отправки предшествующих данных закрывает
|
||||
// сокет (close), вызывает on_closed.
|
||||
// После close сокет удалён из uasync, tc->closed=1.
|
||||
// Повторный вызов игнорируется.
|
||||
// После фактического закрытия повторный вызов возвращает -1.
|
||||
// Оба API: 0 = сигнал принят; -1 = ошибка. Callback может выполниться до возврата из push.
|
||||
int tcp_conn_push_fin(struct tcp_conn* tc);
|
||||
int tcp_conn_push_close(struct tcp_conn* tc);
|
||||
|
||||
// Одноразовый коллбэк: вызывается когда write_queue + write_buf полностью опустели.
|
||||
// После вызова сбрасывается. Установить повторно можно в любой момент.
|
||||
// Сбрасывается перед вызовом; callback может установить новое уведомление.
|
||||
// Setter только заменяет функцию (NULL снимает); для уже пустой очереди сам вызов не планирует.
|
||||
void tcp_conn_set_flushed(struct tcp_conn* tc, void (*on_flushed)(struct tcp_conn* tc, void* arg));
|
||||
|
||||
// Принудительная остановка чтения: убирает EPOLLIN и отменяет read_waiter.
|
||||
void tcp_conn_pause_read(struct tcp_conn* tc);
|
||||
|
||||
// Таймаут установления соединения (мс, 0 = без таймаута). Если соединение ещё не
|
||||
// установлено (connected=0), взводит таймер; по истечении вызывает
|
||||
// tcp_conn_handle_error(ETIMEDOUT) → on_error. Таймер гасится при успешном connect
|
||||
// и при tcp_conn_destroy.
|
||||
void tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms);
|
||||
// Заменить connect-таймаут: мс от текущего вызова, <=0 отменяет таймер.
|
||||
// Только для живого tc в его uasync-потоке. Для connected=1 таймер не создаётся.
|
||||
// Допустимый положительный диапазон: <= INT_MAX/10 (uasync принимает 0.1 мс).
|
||||
// Возврат: 0 = применено; -EINVAL = аргументы/состояние, -ENOMEM = создание таймера,
|
||||
// -EIO = нарушение состояния отменяемого таймера. При ошибке прежняя настройка сохраняется.
|
||||
// Функция не вызывает callbacks. Истечение → on_error(ETIMEDOUT); отмена при connect/error/destroy.
|
||||
int tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
# tcp_io — Управление TCP-соединением на uasync + ll_queue
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
Асинхронный TCP-коннектор, построенный на event loop `uasync` и lock-free очередях `ll_queue`.
|
||||
Одна структура `struct tcp_conn` = одно TCP-соединение.
|
||||
Используется в SOCKS/HTTP-прокси, STCP, TCP-прокси и тестах.
|
||||
|
||||
**Ключевые свойства:**
|
||||
- Чтение, запись, ошибки и connect — через `uasync` (epoll/kqueue/poll).
|
||||
- Два независимых `ll_queue`: `read_queue` (входящие данные) и `write_queue` (исходящие).
|
||||
- Два memory pool: `entry_pool` (struct ll_entry) и `data_pool` (буферы), всё выделение на горячем пути через пулы.
|
||||
- Backpressure на чтение: high_water/low_water + `queue_waiter_wait`, EPOLLIN вкл/выкл по уровню заполнения очереди.
|
||||
- Backpressure на запись: порог 32 записи в write_queue + `queue_waiter_wait`, EPOLLOUT вкл/выкл при частичной отправке.
|
||||
- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — оба как сентинелы в write_queue, все предшествующие данные гарантированно отправлены.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
### Создание соединения
|
||||
|
||||
```c
|
||||
struct tcp_conn* tc = tcp_conn_create(
|
||||
ua, sock,
|
||||
1500, // entry_data_size — размер буфера для одного recv
|
||||
8192, // write_chunk_size — размер буфера для одной send-операции
|
||||
32, // read_high_water — порог приостановки EPOLLIN
|
||||
8, // read_low_water — порог возобновления EPOLLIN
|
||||
0, // rcvbuf_size — размер буфера сокета (0 = не менять)
|
||||
on_fin_cb, on_error_cb, arg);
|
||||
```
|
||||
|
||||
После создания нужно установить callback на read_queue и включить отложенную обработку:
|
||||
|
||||
```c
|
||||
queue_set_callback(tc->read_queue, on_read_cb, my_conn);
|
||||
queue_set_waiter_defer(tc->read_queue, 1);
|
||||
```
|
||||
|
||||
### Чтение данных (в on_read_cb)
|
||||
|
||||
```c
|
||||
static void on_read_cb(struct ll_queue* q, void* arg) {
|
||||
struct my_conn* c = (struct my_conn*)arg;
|
||||
struct ll_entry* e = queue_data_get(q);
|
||||
if (!e) { queue_resume_callback(q); return; }
|
||||
// обработать e->dgram (e->len байт)
|
||||
queue_entry_free(e);
|
||||
queue_resume_callback(q); // обязательно!
|
||||
}
|
||||
```
|
||||
|
||||
### Запись данных
|
||||
|
||||
Пользователь напрямую кладёт entry в `write_queue`, отправка авто (deferred callback сам шлёт когда сокет готов):
|
||||
|
||||
```c
|
||||
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
|
||||
uint8_t* buf = memory_pool_alloc(tc->data_pool);
|
||||
memcpy(buf, data, len);
|
||||
e->dgram = buf; e->len = (uint16_t)len;
|
||||
queue_data_put(tc->write_queue, e);
|
||||
```
|
||||
|
||||
При заполнении (порог 32) нужно ждать через `queue_waiter_wait`.
|
||||
|
||||
### Graceful shutdown
|
||||
|
||||
```c
|
||||
tcp_conn_push_fin(tc); // shutdown(SHUT_WR) после отправки всех данных → on_fin_sent
|
||||
tcp_conn_push_close(tc); // close сокета после отправки всех данных → on_closed
|
||||
```
|
||||
|
||||
Можно вызывать из callback-ов (on_fin, on_read_cb и т.д.).
|
||||
Повторные вызовы игнорируются (fin_local=1/closed=1 — возвращают -1).
|
||||
|
||||
### Уничтожение
|
||||
|
||||
```c
|
||||
void tcp_conn_destroy(struct tcp_conn* tc);
|
||||
```
|
||||
|
||||
Идемпотентен (`destroyed` флаг). Дренирует обе очереди, закрывает сокет, освобождает пулы через отложенный `uasync_call_soon`.
|
||||
|
||||
### Обработка ошибок
|
||||
|
||||
`on_error(tc, err, arg)` вызывается после закрытия сокета. **Реализация on_error обязана вызвать tcp_conn_destroy.** После возврата из on_error `tc` недействителен.
|
||||
|
||||
```c
|
||||
static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
|
||||
struct my_conn* c = (struct my_conn*)arg;
|
||||
// почистить свои ресурсы
|
||||
tcp_conn_destroy(tc); // tc после этого использовать нельзя
|
||||
socket_close_wrapper(other_sock);
|
||||
u_free(c);
|
||||
}
|
||||
```
|
||||
|
||||
### on_flushed (одноразовый)
|
||||
|
||||
```c
|
||||
tcp_conn_set_flushed(tc, my_flushed_cb);
|
||||
// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют, затем сбросится
|
||||
```
|
||||
|
||||
### Приостановка чтения
|
||||
|
||||
```c
|
||||
tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_waiter
|
||||
```
|
||||
|
||||
### Ограничения и многопоточность
|
||||
|
||||
- **Однопоточный.** Всё работает в одном `uasync` event loop. Нельзя вызывать из другого потока.
|
||||
- **FIN-сентинел в очереди.** После `push_fin` данные продолжат отправляться, сам FIN выполнится только когда все предшествующие данные отправлены.
|
||||
- **Нельзя вызывать close сокета напрямую.** Только через `tcp_conn_push_close` — иначе нарушится порядок обработки очереди.
|
||||
- **Можно вызывать push_fin/push_close из callback-ов** — защита от повторного вызова встроена (fin_local/closed флаги).
|
||||
|
||||
## 3. API
|
||||
|
||||
### Структуры
|
||||
|
||||
| Поле | Назначение |
|
||||
|------|-----------|
|
||||
| `struct tcp_conn` | Всё состояние TCP-соединения: сокет, очереди, пулы, коллбэки, флаги |
|
||||
|
||||
### Жизненный цикл
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_conn_create(ua, sock, entry_data_size, write_chunk_size, read_high_water, read_low_water, rcvbuf_size, on_fin, on_error, arg)` | Создать соединение: аллоцирует tc, два memory pool, два ll_queue, регистрирует сокет в uasync с EPOLLIN+EPOLLOUT. Вовращает NULL при ошибке |
|
||||
| `tcp_conn_destroy(tc)` | Дренирует read_queue и write_queue, закрывает сокет, освобождает пулы через `uasync_call_soon`. Идемпотентен |
|
||||
|
||||
### Коллбэки (поля struct tcp_conn)
|
||||
|
||||
| Поле | Когда вызывается |
|
||||
|------|-----------------|
|
||||
| `on_fin(tc, arg)` | FIN получен от удалённой стороны (recv==0): сразу если read_queue пуста, иначе через `queue_set_empty_callback` после дренажа |
|
||||
| `on_fin_sent(tc, arg)` | FIN отправлен (`shutdown(SHUT_WR)`) — после отправки всех данных перед FIN-сентинелом |
|
||||
| `on_error(tc, err, arg)` | Фатальная ошибка сокета. Вызывается после close сокета. **Обязан вызвать tcp_conn_destroy.** После возврата tc недействителен |
|
||||
| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — после вызова сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` |
|
||||
| `on_closed(tc, arg)` | Сокет закрыт через CLOSE-сентинел (`tcp_conn_push_close`) после отправки всех предшествующих данных |
|
||||
|
||||
### Graceful shutdown
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_conn_push_fin(tc)` | Помещает FIN-сентинел (`dgram=&tcp_fin_sentinel, len=0`) в write_queue. После отправки предшествующих данных: shutdown(SHUT_WR), fin_local=1, вызывает on_fin_sent. Повторный вызов → -1 |
|
||||
| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. Повторный вызов → -1 |
|
||||
|
||||
### Прочее
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `tcp_conn_set_flushed(tc, cb)` | Установить одноразовый коллбэк на полное опустошение write_queue |
|
||||
| `tcp_conn_pause_read(tc)` | Принудительно выключить EPOLLIN и отменить ожидание низкого порога |
|
||||
+7
-2
@@ -3,7 +3,12 @@
|
||||
* cancel помечает запись; её data передаётся free_callback при последующей очистке или destroy.
|
||||
* Без free_callback data не освобождается. Успешный pop передаёт data вызывающему.
|
||||
* index_ptr, если задан, должен жить до удаления записи; heap обновляет индекс при перестановках.
|
||||
* Все операции одного heap выполняются в одном потоке, внутренней синхронизации нет. */
|
||||
* Все операции одного heap выполняются в одном потоке, внутренней синхронизации нет.
|
||||
* Внешние индексы 0-based; после удаления их значение больше не определяет запись и не сбрасывается автоматически.
|
||||
* push/cancel_at — O(log n), cancel с поиском — O(n). Отмена ставит deleted=1, expiration=0 и поднимает запись.
|
||||
* peek/pop очищают k отменённых корней за O(k log n), вызывая free_callback; успешный pop его не вызывает.
|
||||
* Равные expiration не гарантируют FIFO. Размер включает ещё не очищенные отменённые записи.
|
||||
* Пример: create → set_free_callback → push(expiration, data, &живой_индекс) → peek/pop или cancel_at → destroy. */
|
||||
|
||||
#ifndef TIMEOUT_HEAP_H
|
||||
#define TIMEOUT_HEAP_H
|
||||
@@ -16,7 +21,7 @@ extern "C" {
|
||||
#include <stdint.h> // For uint64_t
|
||||
#include <stddef.h> // For size_t
|
||||
|
||||
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync использует timebase: 0.1 мс).
|
||||
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync хранит здесь миллисекунды).
|
||||
|
||||
typedef struct {
|
||||
TimeoutTime expiration; // Sort key (smaller = earlier)
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# timeout_heap — Min-heap для управления таймерами
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
timeout_heap — легковесная реализация min-heap (двоичная куча с минимумом в корне) для хранения таймеров в порядке возрастания времени срабатывания. Используется модулем `u_async` как внутренняя структура `UASYNC::timeout_heap`.
|
||||
|
||||
Ключевая особенность — **ленивое удаление (lazy deletion)**: при отмене таймера элемент не удаляется из кучи, а помечается флагом `deleted = 1`, его `expiration` обнуляется, и он всплывает в корень (bubble-up). Реальное удаление происходит при `peek`/`pop` — корень с `deleted == 1` выкидывается, а данные освобождаются через пользовательский `free_callback`. Это даёт O(log n) на отмену без полного удаления из середины кучи.
|
||||
|
||||
## 2. Как пользоваться
|
||||
|
||||
Модуль используется только внутри `u_async.c`, пользователь напрямую с ним не работает. Приводится для понимания внутреннего устройства.
|
||||
|
||||
### Создание и уничтожение
|
||||
|
||||
```c
|
||||
TimeoutHeap* h = timeout_heap_create(16); // начальная ёмкость 16
|
||||
timeout_heap_set_free_callback(h, ua, timeout_node_free_callback);
|
||||
// ... использование ...
|
||||
timeout_heap_destroy(h); // уничтожает все элементы через free_callback
|
||||
```
|
||||
|
||||
### Вставка и извлечение
|
||||
|
||||
```c
|
||||
size_t index;
|
||||
timeout_heap_push(h, expiration_ms, my_data, &index);
|
||||
// index обновляется при перемещениях элемента в куче
|
||||
|
||||
TimeoutEntry entry;
|
||||
if (timeout_heap_peek(h, &entry) == 0) {
|
||||
// entry.expiration — ближайшее время срабатывания
|
||||
// entry.data — пользовательские данные
|
||||
}
|
||||
|
||||
if (timeout_heap_pop(h, &entry) == 0) {
|
||||
// элемент удалён из кучи, данные нужно освободить вручную или через free_callback
|
||||
}
|
||||
```
|
||||
|
||||
### Отмена
|
||||
|
||||
```c
|
||||
// O(n) — линейный поиск по expiration+data:
|
||||
timeout_heap_cancel(h, expiration, data);
|
||||
|
||||
// O(log n) — удаление по индексу (для внешнего отслеживания позиции):
|
||||
timeout_heap_cancel_at(h, index, data);
|
||||
```
|
||||
|
||||
### Ленивое удаление: как это работает
|
||||
|
||||
1. `cancel` / `cancel_at` ставит `deleted = 1`, `expiration = 0`, вызывает `bubble_up` — элемент всплывает в корень.
|
||||
2. При следующем `peek`/`pop`: если корень `deleted`, он удаляется из кучи (`remove_root`), данные освобождаются через `free_callback`. Повторяется, пока корень не окажется активным.
|
||||
3. O(log n) как на cancel, так и на последующее удаление.
|
||||
|
||||
## 3. API
|
||||
|
||||
### Жизненный цикл
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_create(capacity)` | Создать кучу с начальной ёмкостью. Расширяется автоматически при заполнении. |
|
||||
| `timeout_heap_destroy(h)` | Уничтожить кучу. Все данные (включая помеченные deleted) освобождаются через `free_callback` или `free()`. |
|
||||
| `timeout_heap_set_free_callback(h, user_data, cb)` | Установить callback для освобождения данных при удалении deleted-узлов. Если NULL — данные не освобождаются. |
|
||||
|
||||
### Основные операции
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_push(h, exp, data, &index)` | Вставить элемент с временем срабатывания `exp`. `index_ptr` (опционально) отслеживает позицию элемента в куче — обновляется при всех перемещениях. O(log n). |
|
||||
| `timeout_heap_peek(h, &entry)` | Посмотреть ближайший **неудалённый** элемент без удаления. Попутно вычищает deleted-корни через `free_callback`. O(log n) в худшем случае. |
|
||||
| `timeout_heap_pop(h, &entry)` | Извлечь ближайший неудалённый элемент. Аналогично чистит deleted. O(log n). |
|
||||
|
||||
### Отмена
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_cancel(h, exp, data)` | Отменить по совпадению `expiration + data`. Линейный поиск — O(n). Для массового использования предпочитать `cancel_at`. |
|
||||
| `timeout_heap_cancel_at(h, index, data)` | Отменить по индексу (с проверкой data). O(log n) — ленивое удаление с bubble-up. |
|
||||
|
||||
### Статистика
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `timeout_heap_get_size(h)` | Текущий размер кучи (включая deleted-элементы). |
|
||||
| `timeout_heap_get_freed_count(h)` | Количество узлов, освобождённых через `free_callback` (не реализовано в текущей версии — возвращает 0). |
|
||||
|
||||
### Структуры
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
TimeoutTime expiration; // время срабатывания (ключ сортировки)
|
||||
void *data; // пользовательские данные
|
||||
size_t *index_ptr; // указатель на внешнюю переменную с индексом (обновляется при перемещениях)
|
||||
int deleted; // 0 = активен, 1 = помечен на удаление
|
||||
} TimeoutEntry;
|
||||
|
||||
struct TimeoutHeap {
|
||||
TimeoutEntry *heap; // динамический массив
|
||||
size_t size; // текущее количество элементов
|
||||
size_t capacity; // выделенная ёмкость
|
||||
void* user_data; // аргумент для free_callback
|
||||
void (*free_callback)(void*, void*); // освобождение данных deleted-узлов
|
||||
};
|
||||
```
|
||||
|
||||
Индексация в коде: внешний API использует 0-based индексы в массиве `heap[]`. Внутренние операции (`bubble_up`, `heapify_down`) оперируют 1-based индексами через макросы `PARENT(i)`, `LEFT_CHILD(i)`, `RIGHT_CHILD(i)` для упрощения арифметики кучи.
|
||||
+1
-1
@@ -1867,7 +1867,7 @@ err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task) {
|
||||
pthread_mutex_unlock(&ua->posted_lock);
|
||||
#endif
|
||||
if (found) u_free(task);
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion is not pending");
|
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion already taken or absent");
|
||||
return found ? ERR_OK : ERR_FAIL;
|
||||
}
|
||||
|
||||
|
||||
+75
-36
@@ -1,7 +1,8 @@
|
||||
/* Цикл событий одного потока: сокеты (epoll/poll/select), таймеры и FIFO call_soon.
|
||||
* create → регистрация callbacks → poll/mainloop → destroy вне callbacks после остановки владельцев.
|
||||
* Таймауты задаются в timebase 0.1мс; arg принадлежит вызывающему, освобождает его callback/владелец.
|
||||
* API сокетов/таймеров используется в потоке loop. Из другого потока — post/post_reserved и wakeup;
|
||||
* Таймауты задаются в timebase 0.1мс, положительный абсолютный срок округляется вверх до миллисекунды.
|
||||
* arg принадлежит вызывающему, освобождает его callback/владелец; отмена и destroy arg не освобождают.
|
||||
* API сокетов/таймеров используется в потоке loop. Из другого потока — post/post_reserved, wakeup и stop;
|
||||
* публикация не продлевает время жизни ua/arg, их освобождение нужно согласовать с остановкой producers. */
|
||||
|
||||
#ifndef UASYNC_H
|
||||
@@ -43,12 +44,12 @@ struct timeout_node; // Forward declaration
|
||||
* До первого замера valid=0; window_us растёт до секунды. При задержке замера
|
||||
* вклад граничного интервала в среднее оценивается пропорционально времени. */
|
||||
struct uasync_load {
|
||||
double current_percent;
|
||||
double average_percent;
|
||||
uint64_t sample_time_us;
|
||||
uint64_t interval_us;
|
||||
uint64_t window_us;
|
||||
int valid;
|
||||
double current_percent; // CPU потока за последний фактический интервал, 0..100%
|
||||
double average_percent; // Среднее за накопленное окно до секунды, 0..100%
|
||||
uint64_t sample_time_us; // Время снимка в шкале get_time_us
|
||||
uint64_t interval_us; // Длительность последнего интервала
|
||||
uint64_t window_us; // Длительность учтённого окна среднего
|
||||
int valid; // 1 = снимок готов; 0 = ещё нет замера или ошибка часов
|
||||
};
|
||||
|
||||
struct uasync_load_interval {
|
||||
@@ -73,17 +74,17 @@ struct UASYNC {
|
||||
size_t timer_free_count;
|
||||
size_t socket_alloc_count;
|
||||
size_t socket_free_count;
|
||||
// Wakeup pipe for interrupting poll
|
||||
int wakeup_pipe[2]; // [0] read, [1] write
|
||||
// Wakeup: eventfd на Linux, pipe на остальных POSIX, UDP-сокеты на Windows
|
||||
int wakeup_pipe[2]; // [0] чтение, [1] запись; для eventfd значения совпадают
|
||||
int wakeup_initialized;
|
||||
// Cached pollfd array for optimized poll operations (fallback for non-Linux)
|
||||
// Кеш pollfd для POSIX fallback poll, включая Linux без epoll
|
||||
struct pollfd* poll_fds; // Pre-allocated pollfd array
|
||||
int poll_fds_capacity; // Current capacity of poll_fds
|
||||
int poll_fds_count; // Number of valid entries in poll_fds
|
||||
int poll_fds_dirty; // 1 if poll_fds needs rebuild
|
||||
// Epoll support (Linux only)
|
||||
int epoll_fd; // epoll file descriptor (-1 if not using epoll)
|
||||
int use_epoll; // 1 if using epoll, 0 if using poll
|
||||
int use_epoll; // 1 = epoll; 0 = poll на POSIX или select на Windows
|
||||
struct posted_task* posted_tasks_head;
|
||||
struct posted_task* posted_tasks_tail;
|
||||
|
||||
@@ -92,8 +93,8 @@ struct UASYNC {
|
||||
#else
|
||||
pthread_mutex_t posted_lock;
|
||||
#endif
|
||||
volatile sig_atomic_t stop;
|
||||
volatile sig_atomic_t running; // 0=shutdown mode (API rejected), 1=full init done
|
||||
volatile sig_atomic_t stop; // Флаг выхода из mainloop; stop устанавливает, автоматически не сбрасывается
|
||||
volatile sig_atomic_t running; // Статус, выставляемый владельцем через mark_*; API его не проверяет
|
||||
uint64_t last_poll_exit_us;
|
||||
struct uasync_load load;
|
||||
struct uasync_load_interval load_history[12]; // 10 периодов + граничные интервалы при задержках
|
||||
@@ -106,82 +107,120 @@ struct UASYNC {
|
||||
typedef struct UASYNC uasync_t;
|
||||
typedef struct UASYNC UASYNC_t;
|
||||
|
||||
// Атомарно читает статус владельца (NULL → 0); состояние stop и работу poll не определяет.
|
||||
static inline int uasync_is_running(struct UASYNC* ua) {
|
||||
if (!ua) return 0;
|
||||
return __atomic_load_n(&ua->running, __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
// Instance API - основной API для работы с uasync
|
||||
// Создаёт готовый к регистрации и poll экземпляр с running=0, stop=0; NULL при ошибке инициализации.
|
||||
struct UASYNC* uasync_create(void);
|
||||
// Уничтожение только вне callbacks, после возврата из poll/mainloop.
|
||||
// Уничтожение вне callbacks после возврата из poll/mainloop и завершения всех producers; NULL допустим.
|
||||
// close_fds != 0 закрывает оставшиеся зарегистрированные fd; внутренние wakeup/epoll закрываются всегда.
|
||||
// Ожидающие таймеры и задачи удаляются без вызова callbacks; user_arg остаётся владельцу.
|
||||
void uasync_destroy(struct UASYNC* ua, int close_fds);
|
||||
// Атомарно выставляют running=1/0. Не меняют stop, не запускают/останавливают цикл и не запрещают API.
|
||||
void uasync_mark_running(struct UASYNC* ua);
|
||||
void uasync_mark_stopped(struct UASYNC* ua);
|
||||
|
||||
// текущее время (timebase 0.1ms)
|
||||
// Время в единицах 0.1мс: Linux CLOCK_BOOTTIME (включает suspend), прочие POSIX CLOCK_MONOTONIC, Windows QPC.
|
||||
uint64_t get_time_tb(void);
|
||||
// текущее время в микросекундах (для burst-измерений)
|
||||
// Время в микросекундах в той же шкале; календарные часы ОС на неё не влияют.
|
||||
uint64_t get_time_us(void);
|
||||
|
||||
// Timeouts, timebase = 0.1 mS
|
||||
// Как работать с таймаутами если таймаут может быть использован несколько раз:
|
||||
// 1. заводим дескриптор таймаута и обнуляем
|
||||
// 2. перед активацией таймаута проверяем дескриптор (t_id) на null
|
||||
// 3. при cancel или срабатывании дескриптор обнуляем
|
||||
// 3. после успешного cancel или в начале callback дескриптор обнуляем
|
||||
// это обеспечит отсутствие утечек и задвоений таймаутов
|
||||
// Однократный таймер: timeout_tb >= 0, callback не NULL, user_arg передаётся без копирования.
|
||||
// При timeout_tb > 0 срок = ceil((now_us + timeout_tb*100)/1000) мс; при 0 готов к обработке сразу.
|
||||
// Callback вызывается при обработке heap, а не синхронно из set_timeout; задержка исполнения возможна.
|
||||
// name сохраняется по указателю без ограничения в 15 символов; строка должна жить до освобождения узла.
|
||||
// Возвращает handle или NULL при неверных аргументах/ошибке выделения ресурсов.
|
||||
void* uasync_set_timeout(struct UASYNC* ua, int timeout_tb, void* user_arg, timeout_callback_t callback, const char* name);
|
||||
// Только для ещё живого таймера этого ua: ERR_OK при отмене, ERR_FAIL при неверных аргументах/отсутствии в heap.
|
||||
// Освобождение узла отложено до очистки heap/destroy. После освобождения старый handle использовать нельзя.
|
||||
err_t uasync_cancel_timeout(struct UASYNC* ua, void* t_id);
|
||||
|
||||
// Immediate execution in next mainloop (FIFO order)
|
||||
// Добавляет callback(user_arg) в FIFO; handle или NULL при неверных аргументах/ошибке выделения.
|
||||
// Один проход берёт исходную порцию; poll обрабатывает очередь в начале и после ожидания, перед heap.
|
||||
// Поэтому callback может выполниться в текущем poll. Синхронно из call_soon он не вызывается.
|
||||
void* uasync_call_soon(struct UASYNC* ua, void* user_arg, timeout_callback_t callback);
|
||||
// Только для ещё живого узла call_soon этого ua: зануляет callback, освобождение — при обработке/destroy.
|
||||
// ERR_OK при отмене (включая повторную до обработки узла), ERR_FAIL при NULL/чужом ua.
|
||||
// После успешной отмены обнулить handle; после исполнения/освобождения обращаться к нему нельзя.
|
||||
err_t uasync_call_soon_cancel(struct UASYNC* ua, void* t_id);
|
||||
|
||||
// Sockets - for regular file descriptors (pipe, file)
|
||||
// Регистрирует fd с callbacks чтения/записи/исключения (каждый может быть NULL) и user_arg без копирования.
|
||||
// name — заимствованная строка для логов, должна жить до удаления регистрации; NULL допустим.
|
||||
// Возвращает handle или NULL при неверном fd, дублирующей регистрации или ошибке ресурсов/backend.
|
||||
// Handle кодирует индекс без поколения, устойчив к расширению массива, но недействителен после удаления.
|
||||
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_arg);
|
||||
// Sockets - for socket_t (cross-platform sockets)
|
||||
// То же для socket_t; callbacks получают значение sock. Возврат и владение как у uasync_add_socket.
|
||||
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_arg);
|
||||
// Удаляет регистрацию по актуальному handle, не закрывает fd и не освобождает user_arg; ERR_OK / ERR_FAIL.
|
||||
// Слот и значение handle могут быть переиспользованы новой регистрацией; старый handle нужно обнулить.
|
||||
err_t uasync_remove_socket(struct UASYNC* ua, void* s_id);
|
||||
// Удаляет регистрацию типа SOCK по значению sock; fd остаётся открыт. ERR_OK / ERR_FAIL.
|
||||
err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock);
|
||||
|
||||
// Динамическое управление мониторингом чтения/записи для сокета
|
||||
// Меняет мониторинг чтения/записи по актуальному handle: enable != 0 включает, 0 отключает.
|
||||
// Callback должен быть задан при регистрации. ERR_OK при успехе, ERR_FAIL при неверном handle/ошибке backend.
|
||||
err_t uasync_set_socket_read(struct UASYNC* ua, void* s_id, int enable);
|
||||
err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable);
|
||||
|
||||
// Single iteration of event loop with timeout (timebase units)
|
||||
// Один проход в потоке владельца: call_soon, ожидание/I/O/posted tasks, call_soon и готовые таймеры.
|
||||
// timeout_tb < 0 — без пользовательского предела ожидания; 0 — без блокирующего ожидания;
|
||||
// > 0 — предел системного ожидания в 0.1мс с округлением вверх до миллисекунд, сокращаемый таймерами.
|
||||
// После stop ожидание неблокирующее. Время callbacks не ограничено timeout_tb; ошибка ожидания может прервать проход.
|
||||
void uasync_poll(struct UASYNC* ua, int timeout_tb);
|
||||
// Синхронно опустошает только call_soon, включая новые порции из callbacks; постоянный самоперезапуск мешает возврату.
|
||||
void uasync_drain_immediate(struct UASYNC* ua);
|
||||
|
||||
// Цикл до uasync_stop (возвращается после остановки).
|
||||
// Повторяет poll(ua, -1), пока stop == 0; уже установленный stop не сбрасывает и сразу возвращается.
|
||||
void uasync_mainloop(struct UASYNC* ua);
|
||||
// Атомарно устанавливает stop=1 и пытается разбудить ожидание; допустим из другого потока при живом ua.
|
||||
// Не прерывает текущий poll/callback и оставшуюся обработку событий. running не изменяет.
|
||||
void uasync_stop(struct UASYNC* ua);
|
||||
|
||||
// Debug statistics
|
||||
// Копирует накопительные счётчики выделения/освобождения; любой выходной указатель может быть NULL.
|
||||
void uasync_get_stats(struct UASYNC* ua, size_t* timer_alloc, size_t* timer_free, size_t* socket_alloc, size_t* socket_free);
|
||||
|
||||
// Актуализирует снимок по сроку 100 мс. Только в потоке uasync; наружу передавать копию через события.
|
||||
// ERR_OK при копировании (в том числе с valid=0), ERR_FAIL при NULL ua/load.
|
||||
err_t uasync_get_load(struct UASYNC* ua, struct uasync_load* load);
|
||||
|
||||
// Handle — кодированный индекс; сохраняется при расширении массива сокетов.
|
||||
// Находит регистрацию по fd: 0 и актуальный handle, либо -1. socket_id обязателен.
|
||||
// При допустимых аргументах и отсутствии регистрации пишет NULL; при неверных аргументах выход не меняет.
|
||||
int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id);
|
||||
|
||||
// Print all resources (timers, sockets) for debugging
|
||||
// Выводит счётчики, записи heap таймеров (включая отменённые) и активные сокеты с заданным prefix.
|
||||
void uasync_print_resources(struct UASYNC* ua, const char* prefix);
|
||||
|
||||
// Wakeup mechanism for interrupting poll
|
||||
// Пробуждение: POSIX пишет uint64_t(1) в eventfd/pipe, Windows посылает байт в UDP-сокет; 0 / -1.
|
||||
// Допустим из другого потока; ua и внутренний endpoint должны оставаться живы.
|
||||
int uasync_wakeup(struct UASYNC* ua);
|
||||
int uasync_get_wakeup_fd(struct UASYNC* ua); // returns write fd for wakeup pipe (for signal handlers)
|
||||
// Заимствованный endpoint записи или -1 при NULL/неинициализированном wakeup. Не закрывать самостоятельно.
|
||||
// Linux: eventfd (тот же fd для чтения/записи; писать uint64_t), прочие POSIX: pipe, Windows: UDP-сокет.
|
||||
int uasync_get_wakeup_fd(struct UASYNC* ua);
|
||||
|
||||
// сообщить async (из другого thread) чтобы он вызвал callback с аргументом
|
||||
// Потокобезопасно публикует callback(user_arg) для потока, обслуживающего ua. ua/callback должны быть не NULL.
|
||||
// При OOM задача не публикуется, ошибка только логируется; статуса возврата нет. user_arg не копируется.
|
||||
// При destroy ожидающая задача удаляется без callback; user_arg во всех случаях остаётся владельцу.
|
||||
void uasync_post(struct UASYNC* ua, uasync_post_callback_t callback, void* user_arg);
|
||||
|
||||
/* Передать заранее выделенный u_malloc task без аллокаций. uasync освобождает
|
||||
* task после callback. Производитель больше не обращается к task после передачи.
|
||||
* Отмена — в потоке владельца вне callbacks, после завершения производителя;
|
||||
* ERR_OK означает, что task удалён из очереди и освобождён. */
|
||||
/* Потокобезопасно передаёт заранее выделенный через u_malloc task без новых аллокаций; ua/task не NULL.
|
||||
* До передачи задать callback и arg. Производитель после передачи к task не обращается.
|
||||
* uasync освобождает task после callback, успешной отмены или при destroy без callback; arg не освобождает.
|
||||
* Публикация не проверяет running/stop. Ошибка wakeup не удаляет уже поставленную задачу и не возвращается. */
|
||||
void uasync_post_reserved(struct UASYNC* ua, struct posted_task* task);
|
||||
// Отмена в потоке владельца вне callbacks после завершения производителя; ua/task не NULL.
|
||||
// ERR_OK — task найден в очереди и освобождён; ERR_FAIL — не найден (в том числе уже взят на исполнение).
|
||||
err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task);
|
||||
|
||||
// синхронизация памяти (для доступности из других потоков)
|
||||
// Lock/unlock posted_lock у живого ua; не ждёт исполнения задач. post уже публикует данные под этим мьютексом.
|
||||
void uasync_memsync(struct UASYNC* ua);
|
||||
|
||||
|
||||
|
||||
+82
-40
@@ -5,9 +5,9 @@
|
||||
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр `struct UASYNC` и запускает `uasync_mainloop()` / `uasync_poll()`. Модуль объединяет:
|
||||
|
||||
- **Сокеты** — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
|
||||
- **Таймеры** — однократные таймеры с точностью 0.1 мс, реализованные на базе `timeout_heap` (min-heap) и `memory_pool`.
|
||||
- **Немедленное выполнение** — FIFO-очередь `uasync_call_soon` для отложенного запуска callback в ближайшей итерации цикла.
|
||||
- **Межпоточную связь** — `uasync_post` позволяет другому потоку безопасно запланировать callback в главном потоке (wakeup через pipe/UDP-сокет).
|
||||
- **Таймеры** — однократные таймеры на базе `timeout_heap` (min-heap) и `memory_pool`. Аргумент задаётся в единицах 0.1 мс; положительный абсолютный срок округляется вверх до миллисекунды.
|
||||
- **Отложенное выполнение** — FIFO-очередь `uasync_call_soon`. Callback может выполниться в текущем `uasync_poll`, при следующем проходе обработки этой очереди.
|
||||
- **Межпоточную связь** — `uasync_post` / `uasync_post_reserved` публикуют callback для потока, обслуживающего экземпляр (wakeup через eventfd/pipe/UDP-сокет).
|
||||
|
||||
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
|
||||
|
||||
@@ -15,22 +15,24 @@ u_async — центральный планировщик проекта uTun.
|
||||
|
||||
### Правила
|
||||
|
||||
- **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков.
|
||||
- **Один обслуживающий поток на экземпляр.** Независимые экземпляры могут работать в разных потоках. Сокеты, таймеры и обработка очередей конкретного экземпляра принадлежат его потоку. Из другого потока разрешены `uasync_post`, `uasync_post_reserved`, `uasync_wakeup` и `uasync_stop` при согласованном времени жизни экземпляра.
|
||||
- **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`.
|
||||
- **Закрытие из callback.** Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте `uasync_stop`; `uasync_destroy` выполняйте после возврата из `uasync_poll` / `uasync_mainloop`, вне callbacks.
|
||||
- **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`).
|
||||
- **Владение контекстом.** `user_arg` не копируется и автоматически не освобождается. При отмене или уничтожении ожидающей задачи её callback не вызывается; контекст освобождает владелец. Перед `destroy` завершите producers, которые могут обращаться к экземпляру или публиковать задачи.
|
||||
- **Статусы `running` и `stop` независимы.** После `create` оба равны 0, API уже доступен. `mark_running` / `mark_stopped` только выставляют статус владельца; цикл и регистрацию задач не меняют. `stop` устанавливает отдельный флаг выхода из `mainloop`, который автоматически не сбрасывается.
|
||||
|
||||
### Типовой сценарий
|
||||
|
||||
```c
|
||||
// 1. Создать uasync
|
||||
struct UASYNC* ua = uasync_create();
|
||||
if (!ua) { /* ошибка */ }
|
||||
if (!ua) return;
|
||||
|
||||
// 2. Зарегистрировать сокет
|
||||
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, my_ctx);
|
||||
// или для socket_t:
|
||||
void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, my_ctx);
|
||||
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, "my_fd", my_ctx);
|
||||
// Вместо предыдущей строки для socket_t:
|
||||
// void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, "my_socket", my_ctx);
|
||||
|
||||
// 3. Зарегистрировать таймер (timebase = 0.1 мс, т.е. timeout_tb=100 = 10 мс)
|
||||
void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
|
||||
@@ -39,7 +41,7 @@ void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
|
||||
uasync_cancel_timeout(ua, t_id);
|
||||
t_id = NULL;
|
||||
|
||||
// 5. Отложенное выполнение в следующей итерации
|
||||
// 5. Отложенное выполнение при обработке FIFO call_soon
|
||||
void* soon_id = uasync_call_soon(ua, my_ctx, deferred_cb);
|
||||
|
||||
// 6. Динамически включить/отключить мониторинг записи на сокете
|
||||
@@ -49,11 +51,11 @@ uasync_set_socket_write(ua, sock_id, 0); // отключить
|
||||
// 7. Запустить главный цикл (блокирующий, выход через uasync_stop)
|
||||
uasync_mainloop(ua);
|
||||
|
||||
// 8. Или один шаг с таймаутом (timebase, -1 = бесконечно)
|
||||
uasync_poll(ua, 500); // ждать до 50 мс или первого события
|
||||
// 8. Вместо mainloop можно вызывать отдельные шаги:
|
||||
// uasync_poll(ua, 500); // системное ожидание до 50 мс, время callbacks этим не ограничено
|
||||
|
||||
// 9. Завершение
|
||||
uasync_destroy(ua, 1); // close_fds=1 — закрыть все сокеты
|
||||
uasync_destroy(ua, 1); // закрыть оставшиеся зарегистрированные fd и внутренние ресурсы
|
||||
```
|
||||
|
||||
### Таймеры: правильный паттерн
|
||||
@@ -85,7 +87,7 @@ if (reconnect_timer) {
|
||||
// Из другого потока:
|
||||
void notify_main(void* arg) {
|
||||
UTUN_INSTANCE* u = arg;
|
||||
// работаем в главном потоке — можно вызывать любые функции uasync, трогать сокеты и т.д.
|
||||
// Работаем в потоке, обслуживающем u->ua: здесь можно регистрировать таймеры и сокеты.
|
||||
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
|
||||
}
|
||||
|
||||
@@ -94,10 +96,21 @@ void thread_func(UTUN_INSTANCE* u) {
|
||||
uasync_post(u->ua, notify_main, u);
|
||||
}
|
||||
|
||||
// Для гарантии видимости памяти из другого потока перед uasync_post:
|
||||
uasync_memsync(u->ua);
|
||||
// Отдельный uasync_memsync перед post не требуется: публикация и получение задачи используют posted_lock.
|
||||
```
|
||||
|
||||
Публикация не продлевает время жизни `ua` или `arg` и не проверяет `running` / `stop`.
|
||||
Обычный `uasync_post` выделяет `posted_task`; при OOM пишет ошибку в лог и не ставит задачу, статуса возврата нет.
|
||||
Для завершения worker без выделения памяти заранее выделите `posted_task` через `u_malloc`, заполните `callback` и `arg`
|
||||
и один раз передайте в `uasync_post_reserved`. После передачи производитель к задаче не обращается.
|
||||
Задачу освобождает uasync после исполнения, успешной отмены или при уничтожении без вызова callback; `arg` не освобождается.
|
||||
Ошибка wakeup оставляет уже опубликованную задачу в очереди; обе функции публикации не возвращают эту ошибку.
|
||||
|
||||
Отмена через `uasync_cancel_post` выполняется в потоке владельца вне callbacks после завершения производителя.
|
||||
`ERR_OK` означает, что задача найдена в ожидающей очереди и освобождена; `ERR_FAIL` — не найдена.
|
||||
Порция, уже взятая на исполнение, в этой очереди не находится. `uasync_memsync` лишь захватывает и отпускает
|
||||
`posted_lock`; он не ждёт исполнения задач и не заменяет согласование времени жизни контекста.
|
||||
|
||||
### Получение времени
|
||||
|
||||
```c
|
||||
@@ -111,52 +124,75 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_create()` | Создать экземпляр: аллоцирует структуру, `socket_array`, `timeout_heap`, `memory_pool`, epoll/poll, wakeup pipe/сокет. |
|
||||
| `uasync_destroy(ua, close_fds)` | Уничтожить экземпляр. При `close_fds=1` закрывает все зарегистрированные fd. Перед уничтожением выводит диагностику ресурсов и проверяет на утечки (abort при несовпадении аллокаций). |
|
||||
| `uasync_stop(ua)` | Установить флаг `stop = 1` — на следующей итерации `uasync_mainloop` выйдет. |
|
||||
| `uasync_mainloop(ua)` | Бесконечный цикл `while(!stop) uasync_poll(ua, -1)`. |
|
||||
| `uasync_poll(ua, timeout_tb)` | Одна итерация: обработать сокеты + таймеры. `timeout_tb` = максимальное ожидание в timebase; `-1` = ждать следующего таймера (или бесконечно, если нет таймеров). |
|
||||
| `uasync_create()` | Создать готовый к регистрации и опросу экземпляр с `running=0`, `stop=0`. Возвращает указатель или `NULL` при ошибке инициализации. |
|
||||
| `uasync_destroy(ua, close_fds)` | Уничтожить вне callbacks после возврата из poll/mainloop и завершения producers. При `close_fds != 0` закрывает оставшиеся зарегистрированные fd; внутренние ресурсы закрывает всегда. Ожидающие задачи и таймеры удаляет без callbacks и без освобождения `arg`. Проверяет баланс внутренних аллокаций (abort при несовпадении). `NULL` допустим. |
|
||||
| `uasync_mark_running(ua)` / `uasync_mark_stopped(ua)` | Атомарно выставить `running=1/0`. Не меняют `stop`, не управляют циклом и не ограничивают API. |
|
||||
| `uasync_is_running(ua)` | Атомарно прочитать статус владельца; для `NULL` возвращает 0. |
|
||||
| `uasync_stop(ua)` | Атомарно выставить `stop=1` и попытаться разбудить ожидание. Допустим из другого потока при живом экземпляре. Текущий poll, оставшуюся обработку событий и callbacks не прерывает; `running` не меняет. |
|
||||
| `uasync_mainloop(ua)` | Цикл `while(!stop) uasync_poll(ua, -1)`. При уже установленном `stop` сразу возвращается, флаг не сбрасывает. |
|
||||
| `uasync_poll(ua, timeout_tb)` | Один проход обработки событий. Отрицательное значение снимает пользовательский предел системного ожидания; 0 запрещает блокирующее ожидание; положительное задаёт предел в 0.1 мс с округлением вверх до миллисекунд, сокращаемый таймерами. Время callbacks этим пределом не ограничено. После stop ожидание неблокирующее. |
|
||||
|
||||
### Таймеры (timebase = 0.1 мс)
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Запланировать однократный таймер. Возвращает дескриптор `void*` для отмены. `name` — до 15 символов, используется в логах. |
|
||||
| `uasync_cancel_timeout(ua, t_id)` | Отменить таймер по дескриптору. После отмены дескриптор нужно обнулить — повторный cancel даст ошибку. |
|
||||
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Однократный таймер: `timeout_tb >= 0`, callback обязателен. Возвращает handle или `NULL` при неверных аргументах/ошибке ресурсов. `arg` и строка `name` сохраняются по указателю; ограничения имени в 15 символов нет. |
|
||||
| `uasync_cancel_timeout(ua, t_id)` | Отменить ещё живой таймер этого экземпляра: `ERR_OK` / `ERR_FAIL`. При успехе узел помечается отменённым; освобождение отложено до очистки heap или destroy. После успеха обнулить handle. |
|
||||
|
||||
Для положительного `timeout_tb` абсолютный срок в миллисекундах равен
|
||||
`ceil((now_us + timeout_tb * 100) / 1000)`, где `now_us` взят при регистрации.
|
||||
При нулевом интервале срок равен `floor(now_us / 1000)` и таймер готов к ближайшей обработке heap.
|
||||
Callback синхронно из `set_timeout` не вызывается; его исполнение может задержаться из-за ожидания или других callbacks.
|
||||
Строка `name` должна оставаться доступной до освобождения узла, включая отложенную очистку после отмены.
|
||||
В начале callback сохранённый handle нужно обнулить. После исполнения, освобождения или destroy старый указатель
|
||||
использовать нельзя: безопасная ошибка при повторной отмене не гарантируется.
|
||||
|
||||
### Немедленное выполнение (FIFO)
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_call_soon(ua, arg, cb)` | Запланировать callback на ближайшую итерацию цикла. Используется, когда нужно отложить выполнение на «сразу после текущих событий». Узел выделяется из того же `timeout_pool`. |
|
||||
| `uasync_call_soon_cancel(ua, t_id)` | Отменить — просто зануляет callback, узел будет освобождён при обработке очереди (O(1)). |
|
||||
| `uasync_call_soon(ua, arg, cb)` | Добавить callback в FIFO для отложенного исполнения. Возвращает handle или `NULL` при неверных аргументах/ошибке ресурсов; узел выделяется из `timeout_pool`. |
|
||||
| `uasync_call_soon_cancel(ua, t_id)` | Занулить callback ещё живого узла этого экземпляра за O(1); узел освобождается при обработке очереди/destroy. `ERR_OK` при отмене, `ERR_FAIL` при `NULL`/чужом экземпляре. После успеха обнулить handle. |
|
||||
| `uasync_drain_immediate(ua)` | Синхронно обработать все порции call_soon до опустошения, включая новые callbacks. Не обрабатывает posted tasks, сокеты или heap таймеров. Постоянный самоперезапуск callback может не дать функции вернуться. |
|
||||
|
||||
Один проход обработки FIFO берёт исходную порцию. Обычный успешный `poll` обрабатывает FIFO в начале,
|
||||
затем после ожидания и обработки I/O/posted tasks, перед проверкой heap таймеров.
|
||||
Поэтому callback из обработчика сокета или из первой порции call_soon может выполниться в том же `poll`.
|
||||
Ошибка ожидания может прервать проход до второй обработки FIFO. Гарантии «следующая итерация poll/mainloop» нет.
|
||||
При повторной отмене call_soon до обработки узла возможен `ERR_OK`; после освобождения узла старый handle недействителен.
|
||||
|
||||
### Сокеты
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, arg)` | Добавить fd (pipe, file). `r_cb/w_cb/e_cb` могут быть NULL. Возвращает дескриптор `void*`. |
|
||||
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, arg)` | Добавить `socket_t` (кросс-платформенный сокет). |
|
||||
| `uasync_remove_socket(ua, s_id)` | Удалить сокет по дескриптору. Помечает слот неактивным, не освобождает индексную ячейку (защита от stale epoll-событий через gen). |
|
||||
| `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. |
|
||||
| `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
|
||||
| `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. |
|
||||
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти handle сокета по fd (кодированный индекс, устойчивый к расширению массива). |
|
||||
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, name, arg)` | Зарегистрировать fd. Callbacks могут быть `NULL`; `name` и `arg` заимствуются. Возвращает handle либо `NULL` при неверном fd, дублирующей регистрации или ошибке ресурсов/backend. |
|
||||
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, name, arg)` | То же для `socket_t`; callbacks получают значение сокета. |
|
||||
| `uasync_remove_socket(ua, s_id)` | Удалить регистрацию по актуальному handle, не закрывая fd и не освобождая `arg`. Слот становится доступным для повторного использования. `ERR_OK` / `ERR_FAIL`. |
|
||||
| `uasync_remove_socket_t(ua, sock)` | Удалить регистрацию типа SOCK по значению `socket_t`, не закрывая сокет. `ERR_OK` / `ERR_FAIL`. |
|
||||
| `uasync_set_socket_read(ua, s_id, enable)` | По актуальному handle включить (`enable != 0`) / отключить (`0`) мониторинг чтения для заданного callback. `ERR_OK` при успехе, `ERR_FAIL` при неверном handle/ошибке backend. |
|
||||
| `uasync_set_socket_write(ua, s_id, enable)` | То же для мониторинга записи. |
|
||||
| `uasync_lookup_socket(ua, fd, &s_id)` | 0 и актуальный handle, либо -1. При допустимых аргументах и отсутствии регистрации записывает `NULL`; при неверных аргументах выход не меняет. |
|
||||
|
||||
Handle кодирует индекс без поколения и сохраняется при расширении массива. После удаления его нужно обнулить:
|
||||
новая регистрация может получить то же числовое значение. Поколения защищают внутреннюю обработку событий,
|
||||
но не делают безопасными вызовы API со старым handle. Строка `name` должна жить до удаления регистрации.
|
||||
|
||||
### Межпоточное взаимодействие
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Запланировать callback в главном потоке. Выделяет `posted_task`, добавляет в связный список под мьютексом, будит главный поток через `uasync_wakeup`. |
|
||||
| `uasync_memsync(ua)` | **Потокобезопасно.** Барьер памяти (lock/unlock `posted_lock`) для гарантии видимости данных, записанных из другого потока перед `uasync_post`. |
|
||||
| `uasync_wakeup(ua)` | Разбудить `poll`/`epoll_wait` записью байта в wakeup pipe (POSIX) или send в UDP-сокет (Windows). Можно вызывать из обработчика сигналов. |
|
||||
| `uasync_get_wakeup_fd(ua)` | Получить write-fd wakeup pipe для использования в `signalfd` или кастомных механизмах. |
|
||||
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Выделить и опубликовать задачу для потока экземпляра, попытаться разбудить его. При OOM задача не публикуется, ошибка логируется; статуса возврата нет. |
|
||||
| `uasync_post_reserved(ua, task)` | **Потокобезопасно.** Передать заранее выделенный через `u_malloc` task с заполненными callback/arg без новых аллокаций. `ua` и `task` обязательны. Владение task переходит uasync. |
|
||||
| `uasync_cancel_post(ua, task)` | В потоке владельца вне callbacks после завершения производителя: `ERR_OK` при удалении и освобождении ожидающего task, `ERR_FAIL`, если он не найден. `ua` и `task` обязательны. |
|
||||
| `uasync_memsync(ua)` | Lock/unlock `posted_lock` живого экземпляра. Не ждёт исполнения задач; публикация через post уже использует этот мьютекс. |
|
||||
| `uasync_wakeup(ua)` | 0 при успешной записи, -1 при ошибке/неинициализированном wakeup. POSIX: запись `uint64_t(1)` в eventfd/pipe; Windows: send одного байта в UDP-сокет. Экземпляр должен оставаться живым. |
|
||||
| `uasync_get_wakeup_fd(ua)` | Заимствованный endpoint записи, либо -1 при `NULL`/неинициализированном wakeup. Linux: eventfd, тот же fd для чтения и записи (писать `uint64_t`); прочие POSIX: pipe; Windows: UDP-сокет. Самостоятельно не закрывать. |
|
||||
|
||||
### Время
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| `get_time_tb()` | Монотонное время в timebase (0.1 мс). `clock_gettime(CLOCK_MONOTONIC)` на POSIX, `QueryPerformanceCounter` на Windows. |
|
||||
| `get_time_tb()` | Время в единицах 0.1 мс: Linux `CLOCK_BOOTTIME` (включает suspend), остальные POSIX `CLOCK_MONOTONIC`, Windows `QueryPerformanceCounter`. Изменение календарных часов на шкалу не влияет. |
|
||||
| `get_time_us()` | Монотонное время в микросекундах (для burst-измерений производительности). |
|
||||
|
||||
### Диагностика
|
||||
@@ -189,9 +225,13 @@ CPU измеряется с первого `uasync_poll` или `uasync_get_load
|
||||
|
||||
GUI показывает округлённое секундное среднее 0–100% поверх полосы с градиентом зелёный → жёлтый → красный:
|
||||
desktop — рядом с заголовком каналов, Android — рядом с версией в списке групп.
|
||||
Запрос `chat_core_collect_cpu_load_trampoline` возвращает короткое событие `CHAT_EVT_CPU_LOAD` (`valid:1, average_percent:1`).
|
||||
GUI опрашивает ядро каждые 333 мс, не более одного незавершённого запроса. Android опрашивает только пока экран списка групп RESUMED;
|
||||
при паузе или уходе с экрана coroutine отменяется, фоновых запросов и таймеров ядра для индикатора нет.
|
||||
Запрос `chat_core_collect_cpu_load_trampoline` выполняется в потоке ядра и возвращает событие `CHAT_EVT_CPU_LOAD`:
|
||||
два байта `[valid][average_percent]`, где среднее округлено до целого процента. При `valid=0` GUI показывает 0%.
|
||||
Desktop-таймер проверяет готовность ядра каждые 333 мс; Android отправляет запросы с паузой 333 мс,
|
||||
только пока экран списка групп RESUMED. В обоих случаях допускается не более одного незавершённого запроса.
|
||||
При паузе или уходе с Android-экрана coroutine опроса отменяется и новые запросы не отправляются.
|
||||
Уже опубликованный запрос не отменяется: ответ снимает pending, но при скрытом экране валидный ответ не меняет индикатор.
|
||||
Для индикатора не создаются таймеры ядра.
|
||||
|
||||
### Внутренние структуры
|
||||
|
||||
@@ -200,6 +240,8 @@ GUI опрашивает ядро каждые 333 мс, не более одн
|
||||
- **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение.
|
||||
- **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`.
|
||||
|
||||
Таймеры используют монотонное время: коррекция календарных часов не меняет срок срабатывания. Положительные интервалы округляются вверх до миллисекунд; нулевой интервал готов немедленно. Каждый проход обрабатывает исходную порцию `call_soon`, затем проверяет таймеры и сокеты; новые callbacks остаются на следующий проход. `uasync_drain_immediate` явно обрабатывает все порции до опустошения очереди.
|
||||
Таймеры используют шкалу `get_time_us`; правила округления абсолютного срока и порядок обработки
|
||||
call_soon описаны в разделах API выше. Сокеты и posted tasks обрабатываются после ожидания перед heap таймеров;
|
||||
ошибка ожидания может завершить проход раньше. Установка `stop` не прерывает эти обработчики.
|
||||
|
||||
Для fallback `poll` массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает `NULL` из `uasync_create` / `uasync_add_socket*`; существующие регистрации сохраняются. Перестроение массива в `uasync_poll` не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода.
|
||||
+3
-2
@@ -9,13 +9,14 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* data — первый пиксель, size — доступные байты от него; stride — шаг строки/пикселя. */
|
||||
/* data — первый пиксель, size — доступные байты от него.
|
||||
* row_stride — шаг строки 1..16777216 байт, pixel_stride — шаг пикселя 1..4096 байт. */
|
||||
struct video_plane {
|
||||
uint8_t *data;
|
||||
size_t size, row_stride, pixel_stride;
|
||||
};
|
||||
|
||||
/* src_width/height — размеры источника; rotation — 0/90/180/270 градусов.
|
||||
/* src_width/height — размеры источника, каждый 1..4096; rotation — 0/90/180/270 градусов.
|
||||
* При 90/270 размеры назначения переставлены. Возвращает 0 либо -1 с диагностикой;
|
||||
* все границы проверяются до первой записи. */
|
||||
int video_plane_rotate(const struct video_plane *src, const struct video_plane *dst,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "net_emulator.h"
|
||||
#include "u_async.h"
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -79,7 +80,7 @@ static int parse_ip_port(const char *str, struct sockaddr_in *addr) {
|
||||
static int parse_delay_range(const char *str, delay_range_t *range) {
|
||||
if (!str || !range) return -1;
|
||||
|
||||
char *dash = strchr(str, '-');
|
||||
const char *dash = strchr(str, '-');
|
||||
if (!dash) {
|
||||
// Single value
|
||||
range->min_ms = atoi(str);
|
||||
@@ -103,10 +104,10 @@ static int parse_delay_range(const char *str, delay_range_t *range) {
|
||||
static int parse_rule(const char *line, rule_t *rule) {
|
||||
if (!line || !rule) return -1;
|
||||
|
||||
char *prob_start = strstr(line, "prob=");
|
||||
const char *prob_start = strstr(line, "prob=");
|
||||
if (!prob_start) return -1;
|
||||
|
||||
char *comma = strchr(prob_start, ',');
|
||||
const char *comma = strchr(prob_start, ',');
|
||||
if (!comma) return -1;
|
||||
|
||||
// Parse probability
|
||||
@@ -117,7 +118,7 @@ static int parse_rule(const char *line, rule_t *rule) {
|
||||
if (rule->probability_percent < 0 || rule->probability_percent > 100) return -1;
|
||||
|
||||
// Parse delay
|
||||
char *delay_start = strstr(comma, "delay=");
|
||||
const char *delay_start = strstr(comma, "delay=");
|
||||
if (!delay_start) return -1;
|
||||
|
||||
char delay_str[64];
|
||||
@@ -179,6 +180,7 @@ int parse_emulator_config(const char *filename, net_emulator_t *emulator) {
|
||||
|
||||
current_endpoint = &emulator->endpoints[emulator->endpoint_count];
|
||||
memset(current_endpoint, 0, sizeof(endpoint_t));
|
||||
current_endpoint->sockfd = -1;
|
||||
|
||||
// Parse listen address
|
||||
if (parse_ip_port(endpoint_str, ¤t_endpoint->listen_addr) < 0) {
|
||||
@@ -417,7 +419,7 @@ int net_emulator_init(net_emulator_t *emulator) {
|
||||
if (create_endpoint_socket(ep) < 0) {
|
||||
fprintf(stderr, "Failed to create socket for endpoint %s:%u\n",
|
||||
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
|
||||
uasync_destroy(emulator->ua);
|
||||
uasync_destroy(emulator->ua, 0);
|
||||
emulator->ua = NULL;
|
||||
return -1;
|
||||
}
|
||||
@@ -425,7 +427,11 @@ int net_emulator_init(net_emulator_t *emulator) {
|
||||
ep->emulator = emulator;
|
||||
|
||||
// Add socket to u_async
|
||||
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep);
|
||||
if (!uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "net_emulator_init: socket registration failed fd=%d endpoint=%d", ep->sockfd, i);
|
||||
net_emulator_cleanup(emulator);
|
||||
return -1;
|
||||
}
|
||||
printf("[INIT] Listening on %s:%u\n",
|
||||
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
|
||||
}
|
||||
@@ -439,7 +445,7 @@ void net_emulator_run(net_emulator_t *emulator) {
|
||||
if (!emulator || !emulator->running || !emulator->ua) return;
|
||||
|
||||
printf("[EMULATOR] Starting main loop. Press Ctrl+C to stop.\n");
|
||||
uasync_mainloop(emulator->ua); // This never returns
|
||||
uasync_mainloop(emulator->ua); // Возвращается после uasync_stop.
|
||||
}
|
||||
|
||||
// Clean up emulator resources
|
||||
@@ -455,7 +461,7 @@ void net_emulator_cleanup(net_emulator_t *emulator) {
|
||||
}
|
||||
|
||||
if (emulator->ua) {
|
||||
uasync_destroy(emulator->ua);
|
||||
uasync_destroy(emulator->ua, 0);
|
||||
emulator->ua = NULL;
|
||||
}
|
||||
|
||||
@@ -466,6 +472,7 @@ void net_emulator_cleanup(net_emulator_t *emulator) {
|
||||
int main(int argc, char *argv[]) {
|
||||
setbuf(stdout, NULL);
|
||||
setbuf(stderr, NULL);
|
||||
debug_config_init();
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Usage: %s <config_file>\n", argv[0]);
|
||||
@@ -500,4 +507,4 @@ int main(int argc, char *argv[]) {
|
||||
// Cleanup (never reached)
|
||||
net_emulator_cleanup(&g_emulator);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+30
-22
@@ -1,4 +1,8 @@
|
||||
// net_emulator.h - Network emulator for testing utun with delay, loss, reordering
|
||||
/* IPv4 UDP-эмулятор одного потока uasync: правила случайной задержки и остаточная вероятность потерь.
|
||||
* Обнулить состояние → parse_emulator_config → init → run → cleanup после возврата из run.
|
||||
* Конфиг: [IPv4:port], строки prob=N,delay=min-max (мс); до 100 endpoint, до 10 правил на endpoint.
|
||||
* Для каждого пакета выбирается одно правило по накопленной вероятности; остаток до 100% означает drop.
|
||||
* Разные задержки могут менять порядок пакетов; отдельного механизма перестановки по reorder_probability нет. */
|
||||
#ifndef NET_EMULATOR_H
|
||||
#define NET_EMULATOR_H
|
||||
|
||||
@@ -22,8 +26,8 @@ typedef struct net_emulator_s net_emulator_t;
|
||||
|
||||
// Delay range (milliseconds)
|
||||
typedef struct {
|
||||
int min_ms;
|
||||
int max_ms;
|
||||
int min_ms; // Минимальная задержка >=0
|
||||
int max_ms; // Верхняя граница включительно; max >= min, значение <= INT_MAX/10 для timebase
|
||||
} delay_range_t;
|
||||
|
||||
// Rule: probability and delay
|
||||
@@ -35,16 +39,15 @@ typedef struct {
|
||||
// Network endpoint configuration
|
||||
typedef struct {
|
||||
struct sockaddr_in listen_addr; // Address to listen on
|
||||
struct sockaddr_in target_addr; // Address to forward to (port - 10000)
|
||||
struct sockaddr_in target_addr; // Тот же IP; порт listen-10000 при listen>=10000, иначе исходный порт
|
||||
rule_t rules[MAX_RULES_PER_ENDPOINT];
|
||||
int rule_count;
|
||||
int total_probability; // Sum of probabilities (should be <= 100)
|
||||
int total_probability; // Сумма 0..100%; вероятность потери — 100-total_probability
|
||||
|
||||
// For reordering simulation
|
||||
int reorder_probability; // Probability to reorder with next packet (0-100)
|
||||
int reorder_probability; // Не используется и не читается из конфигурации
|
||||
|
||||
// Socket descriptor
|
||||
int sockfd;
|
||||
int sockfd; // Созданный endpoint-сокет; -1 до создания/после cleanup
|
||||
net_emulator_t* emulator; // Parent emulator instance
|
||||
} endpoint_t;
|
||||
|
||||
@@ -52,34 +55,39 @@ typedef struct {
|
||||
struct net_emulator_s {
|
||||
endpoint_t endpoints[MAX_ENDPOINTS];
|
||||
int endpoint_count;
|
||||
int running;
|
||||
uasync_t* ua; // uasync instance
|
||||
int running; // init завершён; сам флаг не останавливает mainloop, нужен uasync_stop(ua)
|
||||
uasync_t* ua; // Принадлежит эмулятору, создаётся init и уничтожается cleanup
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Parse configuration file
|
||||
* @param filename Configuration file path
|
||||
* @param emulator Emulator state to fill
|
||||
* @return 0 on success, -1 on error
|
||||
* @brief Читает endpoint и правила; перед первым вызовом обнулить emulator, живой экземпляр не перенастраивать.
|
||||
* @param filename путь к конфигу; строки с ';' и пустые пропускаются
|
||||
* @param emulator состояние, в которое добавляются endpoint; прежние записи не очищаются
|
||||
* @return 0 после разбора; -1 при NULL, ошибке открытия, неверном endpoint, превышении лимитов/суммы вероятностей
|
||||
* Ошибочное правило логируется и пропускается без -1; прочие нераспознанные строки игнорируются.
|
||||
* При -1 уже разобранная часть сохраняется. Диапазон задержек должен помещаться в INT_MAX/10 мс.
|
||||
*/
|
||||
int parse_emulator_config(const char *filename, net_emulator_t *emulator);
|
||||
|
||||
/**
|
||||
* @brief Initialize network emulator
|
||||
* @param emulator Emulator state
|
||||
* @return 0 on success, -1 on error
|
||||
* @brief Создаёт собственный uasync и неблокирующие UDP-сокеты настроенных endpoint.
|
||||
* @param emulator состояние после parse, без живого ua; endpoint/rule counts должны помещаться в массивы
|
||||
* @return 0 — сокеты созданы и зарегистрированы, -1 — NULL/ошибка ресурсов; после ошибки вызвать cleanup
|
||||
* При ручном заполнении endpoint до init установить sockfd=-1. Ошибка init может оставить созданные сокеты до cleanup.
|
||||
*/
|
||||
int net_emulator_init(net_emulator_t *emulator);
|
||||
|
||||
/**
|
||||
* @brief Run network emulator main loop
|
||||
* @param emulator Emulator state
|
||||
* @brief Запускает блокирующий mainloop, возвращается после uasync_stop(emulator->ua).
|
||||
* @param emulator успешно инициализированное состояние; NULL/нет ua/running=0 ничего не делает
|
||||
* Остановка и cleanup выполняются при согласованном времени жизни состояния, cleanup — вне callbacks.
|
||||
*/
|
||||
void net_emulator_run(net_emulator_t *emulator);
|
||||
|
||||
/**
|
||||
* @brief Clean up emulator resources
|
||||
* @param emulator Emulator state
|
||||
* @brief Закрывает созданные endpoint-сокеты и уничтожает собственный ua, сбрасывает running.
|
||||
* @param emulator состояние после init (в том числе неудачного); NULL и повторный cleanup допустимы
|
||||
* Перед cleanup завершить отложенные отправки: уничтожение таймеров uasync само не освобождает их packet-аргументы.
|
||||
*/
|
||||
void net_emulator_cleanup(net_emulator_t *emulator);
|
||||
|
||||
@@ -87,4 +95,4 @@ void net_emulator_cleanup(net_emulator_t *emulator);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NET_EMULATOR_H */
|
||||
#endif /* NET_EMULATOR_H */
|
||||
+9
-3
@@ -2,7 +2,7 @@ bin_PROGRAMS = utun
|
||||
noinst_LIBRARIES = libutun.a
|
||||
|
||||
EXTRA_DIST = lwip_tcp/COPYING dnsmasq/COPYING dnsmasq/COPYING-v3 routing_layer/address_profile.h routing_layer/route_ping_doc.md \
|
||||
stream/stream_playback_audio.h stream/stream_playback_audio.cpp
|
||||
stream/stream_playback_audio.h stream/stream_playback_audio.cpp config_ini.h
|
||||
|
||||
stream_core_files = \
|
||||
stream/stream.h \
|
||||
@@ -79,6 +79,8 @@ utun_CORE_SOURCES = \
|
||||
transport_layer/stun_nat.h \
|
||||
transport_layer/etcp_keepalive.c \
|
||||
transport_layer/etcp_bbr.c \
|
||||
transport_layer/etcp_tx.c \
|
||||
transport_layer/etcp_tx.h \
|
||||
transport_layer/etcp_loadbalancer.c \
|
||||
transport_layer/etcp_debug.c \
|
||||
transport_layer/etcp_dump.c \
|
||||
@@ -99,7 +101,6 @@ utun_CORE_SOURCES = \
|
||||
transport_layer/pkt_normalizer.c \
|
||||
transport_layer/packet_dump.c \
|
||||
transport_layer/etcp_api.c \
|
||||
transport_layer/etcp_connect.c \
|
||||
transport_layer/node_conn_direct.c \
|
||||
transport_layer/socket_monitor.c \
|
||||
transport_layer/auto_socket.c \
|
||||
@@ -184,6 +185,8 @@ utun_CORE_SOURCES = \
|
||||
chat/chat_sync.c \
|
||||
chat/chat_admin.c \
|
||||
chat/chat_join.c \
|
||||
chat/chat_join_port.c \
|
||||
chat/chat_join_port.h \
|
||||
chat/member_sync.c \
|
||||
chat/merkle_sync.c \
|
||||
chat/merkle_tree.c \
|
||||
@@ -266,6 +269,8 @@ libutun_a_SOURCES = \
|
||||
transport_layer/stun_nat.h \
|
||||
transport_layer/etcp_keepalive.c \
|
||||
transport_layer/etcp_bbr.c \
|
||||
transport_layer/etcp_tx.c \
|
||||
transport_layer/etcp_tx.h \
|
||||
transport_layer/etcp_loadbalancer.c \
|
||||
transport_layer/etcp_debug.c \
|
||||
transport_layer/etcp_dump.c \
|
||||
@@ -286,7 +291,6 @@ libutun_a_SOURCES = \
|
||||
transport_layer/pkt_normalizer.c \
|
||||
transport_layer/packet_dump.c \
|
||||
transport_layer/etcp_api.c \
|
||||
transport_layer/etcp_connect.c \
|
||||
transport_layer/node_conn_direct.c \
|
||||
transport_layer/socket_monitor.c \
|
||||
transport_layer/auto_socket.c \
|
||||
@@ -371,6 +375,8 @@ libutun_a_SOURCES = \
|
||||
chat/chat_sync.c \
|
||||
chat/chat_admin.c \
|
||||
chat/chat_join.c \
|
||||
chat/chat_join_port.c \
|
||||
chat/chat_join_port.h \
|
||||
chat/member_sync.c \
|
||||
chat/merkle_sync.c \
|
||||
chat/invite_link.c \
|
||||
|
||||
+16
-13
@@ -46,10 +46,13 @@ int broadcast_init(struct TOPO_GROUP* group) {
|
||||
ctx->messages = queue_new(group->instance->ua, 64, BMSG_UUID_OFFSET, BROADCAST_UUID_SIZE, "broadcast_msg");
|
||||
if (!ctx->messages) { u_free(ctx); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: queue_new failed"); return -1; }
|
||||
|
||||
group->broadcast = ctx;
|
||||
|
||||
ctx->cleanup_timer = uasync_set_timeout(group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer");
|
||||
if (!ctx->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id);
|
||||
if (!ctx->cleanup_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id);
|
||||
queue_free(ctx->messages); u_free(ctx);
|
||||
return -1;
|
||||
}
|
||||
group->broadcast = ctx;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_init ok grp=%016llx", (unsigned long long)group->group_id);
|
||||
return 0;
|
||||
@@ -158,6 +161,7 @@ static void broadcast_fire_cbks(struct broadcast_ctx* ctx, const uint8_t* uuid,
|
||||
static void broadcast_cleanup_timer_cb(void* arg) {
|
||||
struct broadcast_ctx* ctx = (struct broadcast_ctx*)arg;
|
||||
if (!ctx || !ctx->group) return;
|
||||
ctx->cleanup_timer = NULL;
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
if (standby_get_sleep_tb() > 0) {
|
||||
ctx->cleanup_wait = standby_wait(ctx, broadcast_cleanup_timer_cb);
|
||||
@@ -168,20 +172,19 @@ static void broadcast_cleanup_timer_cb(void* arg) {
|
||||
uint64_t now_tb = get_time_tb();
|
||||
|
||||
int removed = 0;
|
||||
while (1) {
|
||||
struct ll_entry* e = queue_data_get(ctx->messages);
|
||||
if (!e) break;
|
||||
while (ctx->messages->head) {
|
||||
struct ll_entry* e = ctx->messages->head;
|
||||
struct broadcast_msg* msg = (struct broadcast_msg*)e->data;
|
||||
if (now_tb - msg->local_time_tb > BROADCAST_TTL_TB) {
|
||||
queue_entry_free(e); removed++;
|
||||
} else {
|
||||
queue_data_put_first(ctx->messages, e);
|
||||
break;
|
||||
}
|
||||
if (now_tb - msg->local_time_tb <= BROADCAST_TTL_TB) break;
|
||||
queue_entry_free(queue_data_get(ctx->messages));
|
||||
removed++;
|
||||
}
|
||||
if (removed > 0) DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: removed %d stale msgs grp=%016llx", removed, (unsigned long long)ctx->group->group_id);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: grp=%016llx removed=%d cached=%d",
|
||||
(unsigned long long)ctx->group->group_id, removed, queue_entry_count(ctx->messages));
|
||||
|
||||
ctx->cleanup_timer = uasync_set_timeout(ctx->group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer");
|
||||
if (!ctx->cleanup_timer)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_cleanup: timer rearm failed grp=%016llx", (unsigned long long)ctx->group->group_id);
|
||||
}
|
||||
|
||||
void broadcast_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn, const uint8_t* payload, size_t payload_len) {
|
||||
|
||||
+18
-5
@@ -2,7 +2,9 @@
|
||||
* Контекст и кеш увиденных UUID принадлежат группе; TTL — срок хранения UUID, не счётчик сетевых хопов.
|
||||
* init_instance регистрирует общий ETCP-диспетчер, init(group) создаёт состояние отдельной группы.
|
||||
* Все вызовы/callbacks — uasync-поток; recv получает заимствованные uuid/data только на время callback.
|
||||
* send копирует данные и возвращает 0 / -1; 0 не подтверждает доставку каждому участнику. */
|
||||
* send копирует данные и возвращает 0 / -1; 0 не подтверждает доставку каждому участнику.
|
||||
* Получение нового UUID вызывает подписчиков; локальный send их не вызывает. Кеш общий для send/recv группы.
|
||||
* Внутри recv-callback нельзя менять список подписчиков или уничтожать группу/контекст broadcast. */
|
||||
#ifndef BROADCAST_H
|
||||
#define BROADCAST_H
|
||||
|
||||
@@ -19,25 +21,36 @@ struct UTUN_INSTANCE;
|
||||
#define BROADCAST_MAX_DATA 1600
|
||||
#define BROADCAST_UUID_SIZE 16
|
||||
#define BROADCAST_CLEANUP_INTERVAL_TB 600000 // 1 минута
|
||||
#define BROADCAST_TTL_TB 1200000 // 2 минуты
|
||||
#define BROADCAST_TTL_TB 1200000 // 2 минуты; удаление при возрасте > TTL на периодической очистке
|
||||
|
||||
// uuid — 16 байт, data — data_len байт; заимствованы только на время вызова. arg — контекст из add_cbk.
|
||||
typedef void (*broadcast_recv_fn)(const uint8_t* uuid, const uint8_t* data, uint16_t data_len, void* arg);
|
||||
|
||||
struct broadcast_cbk_entry {
|
||||
broadcast_recv_fn fn;
|
||||
void* arg;
|
||||
struct broadcast_cbk_entry* next;
|
||||
broadcast_recv_fn fn; // Функция подписчика
|
||||
void* arg; // Заимствованный контекст, модуль его не освобождает
|
||||
struct broadcast_cbk_entry* next; // Следующая регистрация (новые добавляются в начало)
|
||||
};
|
||||
|
||||
struct broadcast_ctx;
|
||||
|
||||
// Создаёт кеш и таймер живой group с instance/ua; 0 — успех, -1 — NULL group, повторный init или ошибка ресурсов.
|
||||
int broadcast_init(struct TOPO_GROUP* group);
|
||||
// Отменяет таймер/standby-ожидание, освобождает кеш и регистрации, но не arg подписчиков; NULL/нет контекста допустимы.
|
||||
void broadcast_destroy(struct TOPO_GROUP* group);
|
||||
// data не NULL даже при data_len=0; data_len <= BROADCAST_MAX_DATA, группа должна быть инициализирована.
|
||||
// 0 — данные сохранены в кеше и рассылка предпринята; -1 — аргументы/отсутствие контекста/OOM записи кеша.
|
||||
// Ошибки выделения/отправки отдельным соседям не меняют 0; собственные recv-callbacks не вызываются.
|
||||
int broadcast_send(struct TOPO_GROUP* group, const uint8_t* data, uint16_t data_len);
|
||||
// Добавляет fn(arg) в начало; дубликаты разрешены и вызываются отдельно. При NULL/нет контекста/OOM не добавляет.
|
||||
// arg должен жить до удаления регистрации или destroy; список не менять из recv-callback.
|
||||
void broadcast_add_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg);
|
||||
// Удаляет первое совпадение fn+arg (последнее добавленное); отсутствие совпадения ничего не меняет.
|
||||
void broadcast_remove_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg);
|
||||
|
||||
// Регистрирует диспетчер ETCP_ID_BROADCAST для instance; возвращает результат etcp_bind или -1 при NULL.
|
||||
int broadcast_init_instance(struct UTUN_INSTANCE* instance);
|
||||
// Снимает общий диспетчер, но не уничтожает контексты отдельных групп; NULL допустим.
|
||||
void broadcast_destroy_instance(struct UTUN_INSTANCE* instance);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+10
-7
@@ -4,7 +4,7 @@
|
||||
// медиа (CALL_MEDIA) → HANGUP/ENDED. Звонок из CHAT-группы или DM — модуль
|
||||
// оперирует (group_id, peer_node_id), маршрутизация через etcp_router.
|
||||
//
|
||||
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи.
|
||||
// Сигналинг, аудио и видео требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT между конечными узлами.
|
||||
// Реестр сессий: ll_queue с hash-индексом по call_id (8 байт).
|
||||
//
|
||||
// Thread-модель: всё в uasync-потоке (single-threaded). Освобождение сессии
|
||||
@@ -815,9 +815,9 @@ static void call_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) {
|
||||
const uint8_t* body = d + ROUTER_SVC_PAYLOAD_OFF;
|
||||
size_t blen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
|
||||
|
||||
if (subcmd != CALL_SUBCMD_MEDIA &&
|
||||
(d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unauthenticated call signaling command=%u", subcmd);
|
||||
if ((d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unprotected call packet command=%u peer=%016llx flags=%02x",
|
||||
subcmd, (unsigned long long)from, d[ROUTER_SVC_FLAGS_OFF]);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return;
|
||||
}
|
||||
|
||||
@@ -916,12 +916,15 @@ int call_init(struct UTUN_INSTANCE* inst) {
|
||||
}
|
||||
|
||||
ctx->initialized = 1;
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_CALL_AUDIO, call_etcp_recv) || call_video_transport_bind(inst)) {
|
||||
if (etcp_router_require_crypto(inst, ETCP_RT_ID_CALL, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|
||||
|| etcp_router_bind(inst, ETCP_RT_ID_CALL_AUDIO, call_etcp_recv)
|
||||
|| etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_AUDIO, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|
||||
|| call_video_transport_bind(inst)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call media channel bind failed");
|
||||
call_destroy(inst); return -1;
|
||||
}
|
||||
call_publish_phone_status(inst);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x)", CALL_ID, ETCP_RT_ID_CALL);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x audio/video E2E=AES-128-CCM+Ed25519 required)", CALL_ID, ETCP_RT_ID_CALL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1012,7 +1015,7 @@ static void call_tx_flush(struct call_session* s) {
|
||||
uint32_t wait_ms = (uint32_t)(get_time_tb() / 10) - packet.ts_ms;
|
||||
++s->tx_wait_frames; s->tx_wait_total_ms += wait_ms;
|
||||
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
|
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, 0) == 0) {
|
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
|
||||
s->c_tx_frames++;
|
||||
if (packet.capture_pts_us != INT64_MIN) call_video_audio_sent(s, packet.capture_pts_us + 20000);
|
||||
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id);
|
||||
|
||||
+30
-15
@@ -1,11 +1,14 @@
|
||||
// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance
|
||||
// call — управление P2P аудио/видеозвонком одного узла.
|
||||
// Начать вызов через start, ответить через accept/decline, завершить через hangup.
|
||||
// Состояния и результаты приходят событиями CHAT_EVT_CALL_* из chat_event.h.
|
||||
// Аудио подключается через call_audio.h, видеотрек — через call_video_transport.h или call_video_port.h.
|
||||
//
|
||||
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
|
||||
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
|
||||
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
|
||||
// Сигналинг, аудио и видео идут через etcp_router с обязательными ROUTE_CRYPTO_SIGN|ENCRYPT; соединение
|
||||
// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT).
|
||||
//
|
||||
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
|
||||
// Все функции, кроме явно отмеченных, — в uasync-потоке. GUI/headless вызывают через trampoline
|
||||
// (gui_bridge_post_uasync_fn / uasync_post).
|
||||
#ifndef CALL_H
|
||||
#define CALL_H
|
||||
@@ -20,26 +23,32 @@ struct UTUN_INSTANCE;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── жизненный цикл ── */
|
||||
// ── жизненный цикл ──
|
||||
|
||||
int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */
|
||||
|
||||
void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */
|
||||
|
||||
/* ── сигналинг (uasync-поток) ── */
|
||||
// ── сигналинг (uasync-поток) ──
|
||||
|
||||
/* Caller: ch_id — группа либо "dm:<conv_id>" (BGP-группа выбирается при запуске).
|
||||
* out_call_id — случайный uint64, генерит вызывающий. */
|
||||
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
|
||||
|
||||
/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */
|
||||
// Callee: ответить/отклонить. hangup — любая сторона в любой момент.
|
||||
void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
|
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
|
||||
void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
/* Проверка занятости обычным звонком; только uasync, включая входящий/исходящий. */
|
||||
|
||||
// Проверка занятости обычным звонком; только uasync, включая входящий/исходящий.
|
||||
int call_busy(struct UTUN_INSTANCE* inst);
|
||||
/* Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync. */
|
||||
|
||||
// Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync.
|
||||
void call_continue(struct UTUN_INSTANCE* inst, uint64_t call_id);
|
||||
/* Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости. */
|
||||
|
||||
// Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости.
|
||||
enum call_policy {
|
||||
CALL_POLICY_PUBLISH_PHONE = 1, CALL_POLICY_WARN_PHONE = 2, CALL_POLICY_REJECT_PHONE = 4,
|
||||
CALL_POLICY_BLOCK_CALLS = 8, CALL_POLICY_BLOCK_RADIO = 16
|
||||
@@ -47,14 +56,15 @@ enum call_policy {
|
||||
/* Живое состояние ОС: 0=неизвестно, 1=свободен, 2=занят; policy — маска call_policy.
|
||||
* Только uasync. Изменение статуса публикуется в BGP, если включён PUBLISH_PHONE. */
|
||||
void call_set_phone_state(struct UTUN_INSTANCE* inst, int state, unsigned policy);
|
||||
|
||||
/* Флаги BGP PHONE_KNOWN/BUSY с учётом звонка приложения и разрешения публикации.
|
||||
* Desktop без звонка свободен; у mobile состояние ОС может быть неизвестно. Только uasync. */
|
||||
uint8_t call_phone_status(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Read-only онлайн-проверка (GUI может звать из любого потока). */
|
||||
// Read-only онлайн-проверка (GUI может звать из любого потока).
|
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
|
||||
|
||||
/* ── медиа (релей Opus-кадров, uasync-поток) ── */
|
||||
// ── медиа (релей Opus-кадров, uasync-поток) ──
|
||||
|
||||
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
|
||||
* Только когда звонок ACTIVE. seq/ts выставляются автоматически. */
|
||||
@@ -62,9 +72,10 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t*
|
||||
const struct call_capture_time* timing);
|
||||
|
||||
/* Коллбэк приёма медиа: вызывается в uasync-потоке на каждый Opus-кадр пира.
|
||||
* Используется headless-аудио-сокетом (и в фазе 3 — декодером GUI). */
|
||||
* Используется headless-аудиосокетом; opus заимствован до возврата callback. */
|
||||
typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
const uint8_t* opus, int len, void* arg);
|
||||
|
||||
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg);
|
||||
|
||||
/* ── аудио-движок (опционально, GUI-сборки) ──
|
||||
@@ -73,7 +84,7 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
|
||||
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
|
||||
|
||||
struct call_audio_ops {
|
||||
/* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */
|
||||
// uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер).
|
||||
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
|
||||
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
|
||||
@@ -83,7 +94,7 @@ struct call_audio_ops {
|
||||
};
|
||||
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops);
|
||||
|
||||
/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */
|
||||
// Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline.
|
||||
struct call_media_arg {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint64_t call_id;
|
||||
@@ -93,7 +104,7 @@ struct call_media_arg {
|
||||
};
|
||||
void call_send_media_trampoline(void* arg);
|
||||
|
||||
/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */
|
||||
// ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ──
|
||||
|
||||
struct call_start_arg {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -108,9 +119,13 @@ struct call_id_arg {
|
||||
};
|
||||
|
||||
void call_start_trampoline (void* arg);
|
||||
|
||||
void call_accept_trampoline (void* arg);
|
||||
|
||||
void call_decline_trampoline(void* arg);
|
||||
|
||||
void call_hangup_trampoline (void* arg);
|
||||
|
||||
void call_continue_trampoline(void* arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -198,8 +198,9 @@ static void ca_parse_frames(struct call_audio_client* cli) {
|
||||
CH_ID, flen, CA_PCM_FRAME_BYTES);
|
||||
} else {
|
||||
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
|
||||
int16_t pcm[CA_FRAME_SAMPLES]; memcpy(pcm, data, sizeof(pcm)); // wire header 11 байт не выровнен для int16_t
|
||||
uint8_t opus[256];
|
||||
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
|
||||
int olen = enc ? opus_codec_encode(enc, pcm, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
|
||||
if (olen <= 0) {
|
||||
ca->c_enc_dropped++;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
|
||||
@@ -346,7 +347,10 @@ int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
|
||||
|
||||
call_set_media_recv_cb(inst, ca_media_recv, ca);
|
||||
ca->cleanup_timer = uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
|
||||
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
|
||||
if (!ca->cleanup_timer) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
|
||||
call_headless_destroy(inst); return -1;
|
||||
}
|
||||
|
||||
ca->running = 1;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%u", CH_ID, bind_ip, bound_port);
|
||||
@@ -361,7 +365,7 @@ uint16_t call_headless_get_port(struct UTUN_INSTANCE* inst) {
|
||||
|
||||
void call_headless_destroy(struct UTUN_INSTANCE* inst) {
|
||||
struct call_audio* ca = ca_of(inst);
|
||||
if (!ca || !ca->running) return;
|
||||
if (!ca) return;
|
||||
ca->running = 0;
|
||||
if (ca->cleanup_timer) { uasync_cancel_timeout(ca->ua, ca->cleanup_timer); ca->cleanup_timer = NULL; }
|
||||
call_set_media_recv_cb(inst, NULL, NULL);
|
||||
@@ -379,4 +383,5 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
|
||||
ca->clients = NULL; ca->client_count = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
|
||||
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
|
||||
inst->call_audio = NULL; u_free(ca);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36)
|
||||
// call_proto.h — wire-формат сигналинга (ETCP_RT_ID_CALL 0x36) и аудио (ETCP_RT_ID_CALL_AUDIO 0x3F).
|
||||
// CALL_MEDIA несёт только CALL_TRACK_AUDIO; видео использует call_video.h и отдельные каналы
|
||||
// ETCP_RT_ID_CALL_VIDEO_CONTROL 0x3D / ETCP_RT_ID_CALL_VIDEO 0x3E (call_video_transport.h).
|
||||
//
|
||||
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
|
||||
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи
|
||||
// (уже внутри шифрованного канала etcp_router).
|
||||
// Сигналинг и медиа требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT: шифрование тела
|
||||
// и подпись заголовка/шифртекста между конечными узлами, независимо от маршрута.
|
||||
#ifndef CALL_PROTO_H
|
||||
#define CALL_PROTO_H
|
||||
|
||||
|
||||
+51
-13
@@ -1,8 +1,8 @@
|
||||
/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API
|
||||
* потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst.
|
||||
* Один TX mailbox (до восьми кадров, каждый <=2MiB) + восемь RX кадров <=4MiB;
|
||||
* владение pull.data передаётся через u_free. TX не ждёт накопления пачки.
|
||||
* Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */
|
||||
/* call_video_port — обмен готовыми видеокадрами между платформенными workers и ядром звонка.
|
||||
* Attach подключает ядро; для активного call_id: open → send/pull/status → close. Перед отправкой настроить encoder через format.
|
||||
* Attach/detach — uasync; остальные API потокобезопасны. До detach остановить пользователей порта.
|
||||
* Send копирует кадры; pull передаёт буфер вызывающему, освобождение — u_free(). Команды исполняются в uasync.
|
||||
* В процессе один порт: TX до восьми кадров по 2 МиБ, RX до восьми кадров и 4 МиБ суммарно. */
|
||||
#ifndef CALL_VIDEO_PORT_H
|
||||
#define CALL_VIDEO_PORT_H
|
||||
#include "call_video.h"
|
||||
@@ -11,40 +11,78 @@ struct UTUN_INSTANCE;
|
||||
extern "C" {
|
||||
#endif
|
||||
struct call_video_port_status {
|
||||
struct call_video_caps peer;
|
||||
uint64_t key_serial, rate_generation, rate_request, configured_generation;
|
||||
uint32_t rate_bps;
|
||||
uint32_t minimum_bps, maximum_bps;
|
||||
int have_peer, error;
|
||||
struct call_video_caps peer; // принятые возможности кодеков пира
|
||||
uint64_t key_serial, rate_generation, rate_request, configured_generation; // счётчик запросов KEY, ID команды битрейта и настроенное поколение
|
||||
uint32_t rate_bps; // запрошенный кодеру битрейт, бит/с; результат передать через applied
|
||||
uint32_t minimum_bps, maximum_bps; // действующий диапазон регулятора, бит/с
|
||||
int have_peer, error; // возможности пира получены / настройка завершилась ошибкой
|
||||
int can_encode; // Новые raw входы допустимы; готовый TX всё равно сохраняется и повторяется.
|
||||
int tx_idle; // Mailbox свободен; новый формат не обгонит кадры прежнего поколения в mailbox.
|
||||
uint64_t decoded_generation;
|
||||
int decoded_accepted;
|
||||
uint64_t decoded_generation; // поколение, для которого удалённый decoder сообщил результат
|
||||
int decoded_accepted; // 1 — удалённый decoder принял поколение, 0 — отказ
|
||||
unsigned rx_frames; // Полные encoded кадры порта; pending decoder/GUI учитывает RX worker.
|
||||
};
|
||||
// ── Привязка к ядру ──
|
||||
|
||||
// Подключить единственное ядро к порту; 0/-1.
|
||||
int call_video_port_attach(struct UTUN_INSTANCE *inst);
|
||||
|
||||
// Отменить ожидающий post и освободить очереди; выполнять до уничтожения ядра.
|
||||
void call_video_port_detach(struct UTUN_INSTANCE *inst);
|
||||
|
||||
// ── Открытие и закрытие видеотрека ──
|
||||
|
||||
// Скопировать локальные capabilities и запланировать настройку видеотрека; 0/-1, не подтверждение готовности пира.
|
||||
int call_video_port_open(uint64_t id, const struct call_video_caps *caps);
|
||||
|
||||
// Закрыть текущий id и очистить его очереди; чужой id игнорируется.
|
||||
void call_video_port_close(uint64_t id);
|
||||
|
||||
// ── Состояние и уведомления ──
|
||||
|
||||
// Скопировать текущее состояние; 0 — снимок получен, -1 — нет активного id/ядра или out.
|
||||
int call_video_port_status(uint64_t id, struct call_video_port_status *out);
|
||||
|
||||
/* Неблокирующий накопительный счётчик полных принятых RX кадров текущего звонка.
|
||||
* Pull не уменьшает его. Для общего запаса вычитаются показанные/отклонённые кадры.
|
||||
* 0 для закрытого/другого звонка; новый open после остановки readers начинает с нуля. */
|
||||
uint64_t call_video_port_received(uint64_t id);
|
||||
|
||||
/* Уведомление RX без обращения к port API из callback; вызывается под mutex порта.
|
||||
* 1 — установлен, 0 — звонок ещё не открыт/уже закрыт. NULL снимает callback до освобождения arg. */
|
||||
int call_video_port_set_wake(uint64_t id, void (*wake)(void *), void *arg);
|
||||
|
||||
// ── Настройка кодера и исходящие кадры ──
|
||||
|
||||
// Передать фактический формат и битрейт открытого encoder; формат копируется. 0 — команда принята, -1 — отказ.
|
||||
int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps);
|
||||
|
||||
// Подтвердить команду status.rate_generation/rate_request: bps — применённый битрейт, 0 — отказ encoder. 0/-1.
|
||||
int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps);
|
||||
|
||||
#define CALL_VIDEO_PORT_TX_FRAMES 8u
|
||||
#define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME)
|
||||
/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят,
|
||||
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */
|
||||
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES.
|
||||
* Входные кадры всегда остаются у вызывающего. При 0 повторить ту же пачку после освобождения порта. */
|
||||
int call_video_port_send(const struct call_video_frame *frames, unsigned count);
|
||||
|
||||
// ── Входящие кадры ──
|
||||
|
||||
// Забрать старейший полный RX-кадр: 1 — получен, out->data освободить через u_free; 0 — кадра нет, out не меняется.
|
||||
int call_video_port_pull(uint64_t id, struct call_video_frame *out);
|
||||
|
||||
// ── Управление качеством и восстановлением ──
|
||||
|
||||
// Запланировать запрос нового KEY удалённому encoder после потери опорных кадров.
|
||||
void call_video_port_request_key(uint64_t id);
|
||||
|
||||
// Задать диапазон битрейта в бит/с: 300000..4000000, кратно 1000; 0 — команда принята, -1 — отказ.
|
||||
int call_video_port_range(uint64_t id, uint32_t minimum_bps, uint32_t maximum_bps);
|
||||
|
||||
// Сообщить результат запуска decoder поколения generation: accepted=0/1; 0 — команда принята, -1 — отказ.
|
||||
int call_video_port_decoded(uint64_t id, uint64_t generation, int accepted);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -262,7 +262,9 @@ static void flush(struct call_video_transport *v) {
|
||||
f->data = head->data + f->offset;
|
||||
size_t length;
|
||||
if (call_video_fragment_encode(f, wire, sizeof(wire), &length)
|
||||
|| etcp_router_conn_send(conn, wire, length, 0)) { discard_tx(v, "router_send"); return; }
|
||||
|| etcp_router_conn_send(conn, wire, length, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) {
|
||||
discard_tx(v, "router_send"); return;
|
||||
}
|
||||
f->offset += f->length;
|
||||
if (f->offset == f->size) {
|
||||
if (f->pts_us % 1000000 < 34000)
|
||||
@@ -483,6 +485,13 @@ static void receive(struct ETCP_CONN *conn, struct ll_entry *entry) {
|
||||
}
|
||||
const uint8_t *data = entry->dgram + ROUTER_SVC_PAYLOAD_OFF;
|
||||
size_t length = entry->len - ROUTER_SVC_PAYLOAD_OFF;
|
||||
if (!length) goto done; // Локальное уведомление роутера о закрытии, не медиапакет.
|
||||
if ((entry->dgram[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED))
|
||||
!= (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unprotected call video packet svc=%u flags=%02x bytes=%zu",
|
||||
entry->dgram[0], entry->dgram[ROUTER_SVC_FLAGS_OFF], length);
|
||||
goto done;
|
||||
}
|
||||
uint64_t group, peer, id = 0;
|
||||
memcpy(&group, entry->dgram + ROUTER_SVC_GROUP_OFF, 8); memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
|
||||
int control = entry->dgram[0] == ETCP_RT_ID_CALL_VIDEO_CONTROL;
|
||||
@@ -558,6 +567,10 @@ int call_video_transport_bind(struct UTUN_INSTANCE *inst) {
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_CALL_VIDEO, receive)) {
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL); return -1;
|
||||
}
|
||||
if (etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|
||||
|| etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_VIDEO, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) {
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO); etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL); return -1;
|
||||
}
|
||||
etcp_router_set_receive_ready(inst, ETCP_RT_ID_CALL_VIDEO, ready); return 0;
|
||||
}
|
||||
void call_video_transport_unbind(struct UTUN_INSTANCE *inst) {
|
||||
|
||||
+66
-18
@@ -23,11 +23,13 @@ enum { AK_REQUEST = 1, AK_REQUEST_ACK, AK_GRANT, AK_DENY, AK_RESULT_ACK };
|
||||
|
||||
struct ak_file {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel[32], source[1024], cipher[1024], plain[1024], temporary[1040], type[32];
|
||||
char channel[32], source[1024], cipher[1024], plain[1024], type[32];
|
||||
struct file_io_temp output; // Единственный собственный temp; стабильный адрес до completion.
|
||||
uint8_t id[16], key[32], message[32], hash[32], reply[32];
|
||||
struct attachment_info info;
|
||||
uint64_t author, request;
|
||||
int encrypt, error;
|
||||
int plain_prepared, published; // Только реально подготовленный snapshot и выполненный rename дают право очистки.
|
||||
};
|
||||
|
||||
/* Все ошибки SQLite содержат конкретную операцию. */
|
||||
@@ -94,10 +96,8 @@ invalid:
|
||||
|
||||
/* Разделённые деревья исключают раздачу plaintext и удаление его обходом orphan media. */
|
||||
static int ak_paths(struct ak_file* job) {
|
||||
const char* slash = strrchr(CC(job->inst)->db_path, '/');
|
||||
char base[512], hex[33]; ak_hex(job->id, hex);
|
||||
if (slash) snprintf(base, sizeof(base), "%.*s", (int)(slash - CC(job->inst)->db_path), CC(job->inst)->db_path);
|
||||
else snprintf(base, sizeof(base), ".");
|
||||
if (media_index_get_base(CC(job->inst)->db, base)) return -1;
|
||||
int a = snprintf(job->cipher, sizeof(job->cipher), "%s/media/%s/%s.uenc", base, job->channel, hex);
|
||||
int b = snprintf(job->plain, sizeof(job->plain), "%s/decrypted/%s/%s.file", base, job->channel, hex);
|
||||
if (a < 0 || b < 0 || (size_t)a >= sizeof(job->cipher) || (size_t)b >= sizeof(job->plain)) {
|
||||
@@ -121,12 +121,18 @@ static int ak_parent(const char* path) {
|
||||
static void ak_file_work(void* arg) {
|
||||
struct ak_file* job = arg; job->error = -1;
|
||||
if (ak_parent(job->plain) || ak_parent(job->cipher)) return;
|
||||
snprintf(job->temporary, sizeof(job->temporary), "%s.part", job->encrypt ? job->cipher : job->plain);
|
||||
if (job->encrypt) {
|
||||
if (ma_copy_file(job->source, job->plain) || encrypted_file_sync(job->plain) || encrypted_file_sync_directory(job->plain) ||
|
||||
encrypted_file_write(job->plain, job->temporary, job->key, job->author, job->id, &job->info, job->type)) return;
|
||||
} else if (encrypted_file_read(job->cipher, job->temporary, job->key, job->author, job->id, job->hash, &job->info, job->type)) return;
|
||||
if (encrypted_file_publish(job->temporary, job->encrypt ? job->cipher : job->plain)) return;
|
||||
if (ma_copy_file(job->source, job->plain)) return;
|
||||
job->plain_prepared = 1;
|
||||
if (file_io_sync_file(job->plain) || file_io_sync_directory(job->plain) ||
|
||||
encrypted_file_write(job->plain, job->cipher, &job->output, job->key, job->author, job->id, &job->info, job->type)) return;
|
||||
} else if (encrypted_file_read(job->cipher, job->plain, &job->output, job->key, job->author, job->id,
|
||||
job->hash, &job->info, job->type)) return;
|
||||
int error = file_io_temp_publish(&job->output, job->encrypt ? job->plain : job->cipher, job->encrypt ? job->cipher : job->plain);
|
||||
job->published = !job->output.temporary[0]; // Rename уже выполнен и при последующем отказе directory sync.
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: output publication encrypt=%d installed=%d error=%d",
|
||||
job->encrypt, job->published, error);
|
||||
if (error) return;
|
||||
job->error = 0;
|
||||
}
|
||||
|
||||
@@ -174,11 +180,12 @@ static void ak_file_done(void* arg, int error) {
|
||||
if (job->encrypt) chat_event_submitted(inst, job->request, 0);
|
||||
}
|
||||
if (job->encrypt && remove(job->source) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: prepared source cleanup failed");
|
||||
if ((!success || !valid) && job->temporary[0] && remove(job->temporary) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary cleanup failed");
|
||||
file_io_temp_discard(&job->output);
|
||||
if (!valid) {
|
||||
if (remove(job->plain) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
|
||||
if (job->encrypt && remove(job->cipher) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
|
||||
if (((job->encrypt && job->plain_prepared) || (!job->encrypt && job->published)) && remove(job->plain) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
|
||||
if (job->encrypt && job->published && remove(job->cipher) && errno != ENOENT)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
|
||||
}
|
||||
if (valid) {
|
||||
if (success && !job->encrypt) chat_media_cache_enforce(inst);
|
||||
@@ -191,7 +198,7 @@ static void ak_file_done(void* arg, int error) {
|
||||
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
|
||||
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request) {
|
||||
if (!inst || !CC(inst) || CC(inst)->attachment_closing || !inst->media_async || !ak_channel(channel) ||
|
||||
!source || strlen(source) >= 1024 || !type || strlen(type) >= 32 || attachment_validate(info)) {
|
||||
!source || strlen(source) >= 1024 || encrypted_file_validate_type(type) || attachment_validate(info)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid send arguments/state"); return -1;
|
||||
}
|
||||
struct ak_file* job = u_calloc(1, sizeof(*job));
|
||||
@@ -544,9 +551,52 @@ static void ak_tick(void* arg) {
|
||||
if (!CC(inst)->attachment_timer) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: timer allocation failed");
|
||||
}
|
||||
|
||||
struct ak_temp_cleanup { const char* root; const char* extension; unsigned removed; };
|
||||
|
||||
/* Только <channel>/<UUID>.<uenc|file>.tmp.<UUID>; чужие имена и активные owners сохраняются. */
|
||||
static void ak_remove_temp(void* arg, const char* path, int64_t size) {
|
||||
(void)size;
|
||||
struct ak_temp_cleanup* cleanup = arg;
|
||||
size_t prefix = strlen(cleanup->root);
|
||||
if (strncmp(path, cleanup->root, prefix) || path[prefix] != '/') return;
|
||||
const char* relative = path + prefix + 1;
|
||||
const char* slash = strchr(relative, '/');
|
||||
if (!slash || slash - relative >= 20) return;
|
||||
char channel[20]; memcpy(channel, relative, (size_t)(slash - relative)); channel[slash - relative] = 0;
|
||||
if (!ak_channel(channel)) return;
|
||||
const char* name = slash + 1;
|
||||
const char* hex = "0123456789abcdef";
|
||||
if (strlen(name) != 74 || strspn(name, hex) != 32 || memcmp(name + 32, cleanup->extension, 5) ||
|
||||
memcmp(name + 37, ".tmp.", 5) || strspn(name + 42, hex) != 32) return;
|
||||
int removed = file_io_remove_orphan(path);
|
||||
if (removed > 0) cleanup->removed++;
|
||||
else if (removed < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery failed path=%s", path);
|
||||
}
|
||||
|
||||
/* И при crash до публикации, и при прерванной расшифровке temp не является готовым вложением. */
|
||||
static void ak_recover_temps(struct UTUN_INSTANCE* inst) {
|
||||
char base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(CC(inst)->db, base)) return;
|
||||
const char* trees[] = {"media", "decrypted"};
|
||||
for (unsigned i = 0; i < 2; i++) {
|
||||
char root[MEDIA_BASE_MAX + 16]; snprintf(root, sizeof(root), "%s/%s", base, trees[i]);
|
||||
struct file_io_info info;
|
||||
int status = file_io_inspect(root, 0, &info);
|
||||
if (status == 1) continue;
|
||||
if (status < 0) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot inspect temporary tree path=%s", root); continue; }
|
||||
struct ak_temp_cleanup cleanup = {.root = root, .extension = i ? ".file" : ".uenc"};
|
||||
if (ma_dir_walk(root, ak_remove_temp, &cleanup))
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery incomplete path=%s removed=%u", root, cleanup.removed);
|
||||
else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery path=%s removed=%u", root, cleanup.removed);
|
||||
}
|
||||
}
|
||||
|
||||
/* После crash/отмены убрать только собственные подготовки, не имеющие сообщения.
|
||||
* Вызывается до запуска workers либо после их полного завершения. */
|
||||
static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
|
||||
ak_recover_temps(inst);
|
||||
char base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(CC(inst)->db, base)) return;
|
||||
char** channels = NULL; int count = 0, columns = 0;
|
||||
if (sqlite3_get_table(CC(inst)->db, "SELECT channel_id FROM channels", &channels, &count, &columns, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: recovery channel scan failed: %s", sqlite3_errmsg(CC(inst)->db)); return;
|
||||
@@ -566,16 +616,14 @@ static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
|
||||
for (int row = 0; row < rows; row++) {
|
||||
for (int field = 0; field < fields; field++) {
|
||||
const char* path = paths[(row + 1) * fields + field];
|
||||
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path);
|
||||
if ((remove(path) && errno != ENOENT) || (remove(temporary) && errno != ENOENT)) {
|
||||
if (remove(path) && errno != ENOENT) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: unfinished file removal failed path=%s: %s", path, strerror(errno)); failed = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_free_table(paths);
|
||||
if (rows && !failed) {
|
||||
const char* slash = strrchr(CC(inst)->db_path, '/');
|
||||
unsigned prefix = slash ? (unsigned)(slash - CC(inst)->db_path) : 1;
|
||||
unsigned prefix = (unsigned)strlen(base);
|
||||
char cleanup[768];
|
||||
snprintf(cleanup, sizeof(cleanup), "DELETE FROM media_files WHERE chat_id='%s' AND location IN"
|
||||
"(SELECT substr(a.cipher_path,%u) FROM attachment_keys a WHERE %s)", channel, prefix + 2, condition);
|
||||
|
||||
+44
-12
@@ -1,8 +1,8 @@
|
||||
/* Зашифрованные групповые вложения и ручная выдача ключей.
|
||||
* Все API кроме identity — uasync-поток. Ключи, запросы и outbox локальны.
|
||||
* init после media_async; quiesce до его destroy, destroy после workers.
|
||||
* Отдельный router-сервис требует E2E-шифрование и подпись каждого пакета.
|
||||
* Очереди переживают restart; для доставки нужен живой маршрут обеих сторон. */
|
||||
/* chat_attachment — вложения канала с шифрованием и ручной выдачей ключа автором.
|
||||
* send готовит контейнер и публикует сообщение; downloaded/want запускают получение ключа и расшифровку.
|
||||
* Автор отвечает на запрос через decide. Ключи и очередь доставки сохраняются в локальной БД.
|
||||
* Все вызовы, кроме identity, — uasync. init после media_async; quiesce до остановки workers, destroy после них.
|
||||
* Обмен ключами подписан и зашифрован; для доставки нужен живой маршрут обеих сторон. */
|
||||
#ifndef CHAT_ATTACHMENT_H
|
||||
#define CHAT_ATTACHMENT_H
|
||||
#include "../media_async/attachment.h"
|
||||
@@ -11,25 +11,52 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
// ── Жизненный цикл модуля ──
|
||||
|
||||
// Подключить таблицы, router-сервис и обработку сохранённых задач; 0/-1.
|
||||
int chat_attachment_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// Запретить новые подготовки и остановить таймер; запущенные workers завершаются отдельно.
|
||||
void chat_attachment_quiesce(struct UTUN_INSTANCE* inst);
|
||||
|
||||
// После завершения workers убрать незавершённые подготовки и отключить router-сервис.
|
||||
void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
|
||||
/* source — собственный подготовленный файл приложения, передаётся во владение. */
|
||||
|
||||
// ── Отправка и учёт вложений ──
|
||||
|
||||
/* Асинхронно отправить подготовленный файл; channel — десятичный ID, reply — 32 байта ID ответа либо нули.
|
||||
* Аргументы копируются. 0 — задача принята, source передан задаче и будет удалён даже при ошибке подготовки.
|
||||
* -1 — отказ запуска, source остаётся у вызывающего. Ненулевой request возвращается в CHAT_EVT_MSG_SUBMITTED. */
|
||||
/* type проверяется encrypted_file_validate_type до выделения задачи, генерации UUID/ключа и записи в БД. */
|
||||
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
|
||||
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request);
|
||||
/* UUID из подписанного имени контейнера; принимает канонический base64-prefix тела медиа. */
|
||||
|
||||
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1. Подпись проверяет вызывающий.
|
||||
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]);
|
||||
/* Связать заранее сохранённый авторский ключ с подписанным сообщением до публикации. */
|
||||
|
||||
// Связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1. Массивы копируются в БД.
|
||||
int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16],
|
||||
const uint8_t message[32], const uint8_t hash[32]);
|
||||
/* Учесть проверенное db_sync сообщение; обычные сообщения игнорируются. */
|
||||
|
||||
// Учесть проверенное db_sync сообщение и сохранить метаданные контейнера; 0/-1. Обычные сообщения игнорируются.
|
||||
int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32],
|
||||
uint64_t author, const char* json, size_t length);
|
||||
/* После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. */
|
||||
|
||||
// ── Получение файла и выдача доступа ──
|
||||
|
||||
// После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать.
|
||||
void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
|
||||
/* Ручная загрузка на storage-узле также означает запрос пользовательского доступа. */
|
||||
|
||||
// Ручная загрузка на storage-узле также означает запрос пользовательского доступа.
|
||||
void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
|
||||
|
||||
/* Сохранить решение автора по ожидающему запросу peer: approve!=0 — выдать ключ, 0 — отказать.
|
||||
* 0 — решение и исходящий ответ записаны в БД, -1 — ошибка/нет прав или ожидающего запроса; доставка идёт позже. */
|
||||
int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve);
|
||||
|
||||
// ── Команда интерфейса ──
|
||||
|
||||
struct chat_attachment_decision {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel[32];
|
||||
@@ -37,9 +64,14 @@ struct chat_attachment_decision {
|
||||
uint64_t peer;
|
||||
int approve;
|
||||
};
|
||||
// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; результат — CHAT_EVT_ATTACHMENT_KEYS_CHANGED.
|
||||
void chat_attachment_decision_trampoline(void* arg);
|
||||
/* Канал удаляется локально: отменить очереди, стереть ключи и частные копии. */
|
||||
|
||||
// ── Локальное удаление канала ──
|
||||
|
||||
// Канал удаляется локально: отменить очереди, стереть ключи и частные копии.
|
||||
void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+33
-38
@@ -60,30 +60,33 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
|
||||
|
||||
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
|
||||
|
||||
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
|
||||
if (chat_feedback_ensure_channel(inst, ch_id) < 0) return;
|
||||
if (si_find(inst, ch_id)) return;
|
||||
if (topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return;
|
||||
|
||||
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
|
||||
|
||||
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
|
||||
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !inst->topo_groups) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel preparation without active chat/topology"); return -1;
|
||||
}
|
||||
if (chat_feedback_ensure_channel(inst, ch_id) < 0 || topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return -1;
|
||||
uint64_t gid = strtoull(ch_id, NULL, 10);
|
||||
if (gid != 0 && inst->topo_groups && !topo_groups_find(inst->topo_groups, gid)) {
|
||||
struct TOPO_GROUP* g = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
|
||||
if (g) member_sync_subscribe_group(inst, g);
|
||||
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
|
||||
int created_group = !group;
|
||||
if (!group) group = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
|
||||
if (!group || member_sync_subscribe_group(inst, group) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel group preparation failed ch=%s", ch_id);
|
||||
if (group && created_group) topo_groups_remove_group(inst->topo_groups, gid);
|
||||
return -1;
|
||||
}
|
||||
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, tbl_msg, gid);
|
||||
if (si) {
|
||||
si_register(inst, si, ch_id);
|
||||
db_sync_set_insert_cb(si, on_msg_inserted, inst);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
|
||||
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: db_sync_instance_add failed for ch=%s",
|
||||
CC_ID, ch_id);
|
||||
struct DB_SYNC_INSTANCE* si = si_find(inst, ch_id);
|
||||
int created_sync = !si;
|
||||
char table[80]; msg_table_name(ch_id, table, sizeof(table));
|
||||
if (!si) si = db_sync_instance_add(inst, table, gid);
|
||||
if (!si) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel sync allocation failed ch=%s", ch_id);
|
||||
if (created_group) topo_groups_remove_group(inst->topo_groups, gid);
|
||||
return -1;
|
||||
}
|
||||
if (created_sync) { si_register(inst, si, ch_id); db_sync_set_insert_cb(si, on_msg_inserted, inst); }
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "channel prepared ch=%s new_group=%d new_sync=%d", ch_id, created_group, created_sync);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ─── создание канала ─── */
|
||||
@@ -319,20 +322,6 @@ void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) {
|
||||
|
||||
/* ─── локальное удаление канала ─── */
|
||||
|
||||
static const char* cc_ui_state_get(struct chat_core_ctx* cc, const char* key) {
|
||||
static char val[512];
|
||||
val[0] = '\0';
|
||||
sqlite3_stmt* st = NULL;
|
||||
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char* v = (const char*)sqlite3_column_text(st, 0);
|
||||
if (v) snprintf(val, sizeof(val), "%s", v);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
@@ -341,6 +330,7 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
return;
|
||||
}
|
||||
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — empty ch_id", CC_ID); return; }
|
||||
if (chat_media_defer_channel_delete(inst, ch_id)) return;
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel BEGIN ch=%s", CC_ID, ch_id);
|
||||
uint64_t gid = strtoull(ch_id, NULL, 10);
|
||||
@@ -368,10 +358,14 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
chat_sync_on_channel_deleted(inst, ch_id);
|
||||
|
||||
/* 6. удалить медиафайлы канала на диске + строки media_files */
|
||||
media_index_delete_channel(cc->db, ch_id, cc_ui_state_get(cc, "media_base"));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
int media_error = media_index_get_base(cc->db, media_base) || media_index_delete_channel(cc->db, ch_id, media_base);
|
||||
if (media_error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "channel=%s removed with incomplete media cleanup; retry required", ch_id);
|
||||
|
||||
/* 7. удалить таблицы и метаданные канала */
|
||||
topo_node_sqlite_channel_delete(cc->db, ch_id);
|
||||
if (topo_node_sqlite_channel_delete(cc->db, ch_id)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel metadata deletion failed ch=%s", ch_id); return;
|
||||
}
|
||||
|
||||
/* 8. уведомить GUI */
|
||||
{
|
||||
@@ -380,7 +374,8 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
|
||||
chat_event_post(inst, CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
|
||||
}
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx", CC_ID, ch_id, (unsigned long long)gid);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx media_cleanup=%s",
|
||||
CC_ID, ch_id, (unsigned long long)gid, media_error ? "incomplete" : "complete");
|
||||
}
|
||||
|
||||
void chat_core_delete_channel_trampoline(void* arg) {
|
||||
|
||||
+19
-9
@@ -107,6 +107,11 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
||||
if (!cc->shared_db) sqlite3_close(cc->db);
|
||||
inst->chat_core = NULL; u_free(cc); return -1;
|
||||
}
|
||||
if (strlen(db_path) >= sizeof(cc->db_path) || media_index_init(cc->db)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media schema/path initialization failed");
|
||||
if (!cc->shared_db) sqlite3_close(cc->db);
|
||||
inst->chat_core = NULL; u_free(cc); return -1;
|
||||
}
|
||||
cc->inst = inst;
|
||||
cc->my_node_id = inst->node_id;
|
||||
|
||||
@@ -141,9 +146,6 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
||||
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */
|
||||
chat_member_init(inst);
|
||||
|
||||
/* create media_files table */
|
||||
media_index_init(cc->db);
|
||||
|
||||
db_exec(cc,
|
||||
"CREATE TABLE IF NOT EXISTS local_identity ("
|
||||
" id INTEGER PRIMARY KEY CHECK (id = 1),"
|
||||
@@ -172,13 +174,20 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
|
||||
);
|
||||
/* store media_base for block_req handler */
|
||||
{
|
||||
char mbase[512];
|
||||
char mbase[MEDIA_BASE_MAX];
|
||||
const char* ls = strrchr(db_path, '/');
|
||||
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", (int)(ls - db_path), db_path);
|
||||
else snprintf(mbase, sizeof(mbase), "%s", db_path);
|
||||
char sql[1024]; snprintf(sql, sizeof(sql),
|
||||
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase);
|
||||
sqlite3_exec(cc->db, sql, NULL, NULL, NULL);
|
||||
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", ls == db_path ? 1 : (int)(ls - db_path), db_path);
|
||||
else snprintf(mbase, sizeof(mbase), ".");
|
||||
sqlite3_stmt* stored = NULL;
|
||||
int rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key='media_base'", -1, &stored, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(stored);
|
||||
int base_error = rc == SQLITE_ROW ? media_index_get_base(cc->db, mbase)
|
||||
: rc == SQLITE_DONE ? media_index_set_base(cc->db, mbase) : -1;
|
||||
int finish = stored ? sqlite3_finalize(stored) : SQLITE_OK;
|
||||
if (base_error || finish != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media_base initialization failed rc=%d: %s", rc, sqlite3_errmsg(cc->db));
|
||||
chat_core_destroy(inst); return -1;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase);
|
||||
}
|
||||
|
||||
@@ -245,6 +254,7 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!cc || !cc->initialized) return;
|
||||
cc->initialized = 0;
|
||||
chat_member_destroy(inst);
|
||||
chat_attachment_destroy(inst);
|
||||
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */
|
||||
|
||||
|
||||
+85
-50
@@ -2,6 +2,8 @@
|
||||
* chat_core — API каналов, сообщений, профиля и настроек одного узла.
|
||||
* chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра.
|
||||
* Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event.
|
||||
* Порядок: utun_core_start → chat_service_start → команды чата → chat_service_stop.
|
||||
* Сообщения отправляются через submit_message или submit_media_message; результат GUI — CHAT_EVT_MSG_SUBMITTED.
|
||||
*
|
||||
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
|
||||
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
|
||||
@@ -19,10 +21,11 @@
|
||||
struct sqlite3;
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
/* ── Жизненный цикл ── */
|
||||
// ── Жизненный цикл ──
|
||||
|
||||
/* Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. */
|
||||
// Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1.
|
||||
int chat_service_start(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата.
|
||||
* Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */
|
||||
void chat_service_stop(struct UTUN_INSTANCE* inst);
|
||||
@@ -30,7 +33,8 @@ void chat_service_stop(struct UTUN_INSTANCE* inst);
|
||||
/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1.
|
||||
* Для запуска всего чат-сервиса использовать chat_service_start(). */
|
||||
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
|
||||
/* Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. */
|
||||
|
||||
// Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра.
|
||||
void chat_core_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
|
||||
@@ -44,14 +48,16 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
|
||||
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
|
||||
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */
|
||||
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
|
||||
|
||||
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Заимствованная БД текущего chat_core; NULL до init/после destroy. */
|
||||
// Заимствованная БД текущего chat_core; NULL до init/после destroy.
|
||||
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
|
||||
/* 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. */
|
||||
|
||||
// 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети.
|
||||
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* ── Отправка сообщения (GUI → uasync) ── */
|
||||
// ── Отправка сообщения (GUI → uasync) ──
|
||||
|
||||
struct chat_msg_submit {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -72,31 +78,38 @@ struct chat_msg_submit {
|
||||
uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */
|
||||
};
|
||||
|
||||
/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1. */
|
||||
// Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1.
|
||||
int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
|
||||
|
||||
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
|
||||
/* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0.
|
||||
* req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки.
|
||||
* media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */
|
||||
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
|
||||
/* Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора. */
|
||||
|
||||
// Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора.
|
||||
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id);
|
||||
|
||||
/* ── DB-операции (оставлены для интроспекции) ── */
|
||||
// ── DB-операции (оставлены для интроспекции) ──
|
||||
|
||||
/* Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. */
|
||||
// Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса.
|
||||
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
/* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта.
|
||||
* 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */
|
||||
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
|
||||
|
||||
/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1.
|
||||
* Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */
|
||||
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
|
||||
/* Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. */
|
||||
|
||||
// Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1.
|
||||
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65.
|
||||
* out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */
|
||||
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* ── Создание канала (GUI → uasync) ── */
|
||||
// ── Создание канала (GUI → uasync) ──
|
||||
|
||||
struct chat_channel_create {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -112,16 +125,21 @@ struct chat_channel_create {
|
||||
|
||||
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
|
||||
|
||||
/* Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт. */
|
||||
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
/* Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. */
|
||||
/* Подготовить таблицы, группу и синхронизацию; 0/-1. Ошибка удаляет новые runtime-ресурсы,
|
||||
* проверенные данные БД сохраняются. Членства на других узлах не даёт. */
|
||||
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
// Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего.
|
||||
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
|
||||
|
||||
void chat_core_create_channel_trampoline(void* arg);
|
||||
/* Сгенерировать ключи и создать канал, добавив себя владельцем. */
|
||||
|
||||
// Сгенерировать ключи и создать канал, добавив себя владельцем.
|
||||
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
|
||||
|
||||
void chat_core_create_channel_auto_trampoline(void* arg);
|
||||
|
||||
/* ── Локальное удаление канала (GUI → uasync) ── */
|
||||
// ── Локальное удаление канала (GUI → uasync) ──
|
||||
|
||||
struct chat_delete_channel_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -130,31 +148,39 @@ struct chat_delete_channel_req {
|
||||
|
||||
/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы
|
||||
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
|
||||
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
|
||||
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют.
|
||||
* Исходящие video/register задачи сначала завершаются; удаление продолжит последний completion.
|
||||
* Новые исходящие медиа этого канала в это время отклоняются. Событие CHANNEL_DELETED приходит после удаления метаданных;
|
||||
* ошибка файловой очистки логируется отдельно, записи неочищенных файлов сохраняются для повтора. */
|
||||
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_delete_channel_trampoline(void* arg);
|
||||
|
||||
/* ── Утилиты ── */
|
||||
// ── Утилиты ──
|
||||
|
||||
/* Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. */
|
||||
// Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет.
|
||||
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
|
||||
/* Сохранить имя узла и обновить собственные member-записи каналов. */
|
||||
|
||||
// Сохранить имя узла и обновить собственные member-записи каналов.
|
||||
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
|
||||
|
||||
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
|
||||
void chat_core_update_my_name_trampoline(void* arg);
|
||||
/* Обновить адреса topo_node и собственные member-записи каналов. */
|
||||
|
||||
// Обновить адреса topo_node и собственные member-записи каналов.
|
||||
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync.
|
||||
* Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */
|
||||
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
|
||||
// Сохранение key-value в ui_state (вызывается из uasync-потока)
|
||||
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
|
||||
|
||||
/* Per-channel «play sound on new message» (ui_state: ch_<id>_sound_on_message, default 1=on).
|
||||
* set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */
|
||||
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
|
||||
|
||||
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
|
||||
@@ -163,18 +189,20 @@ void chat_core_set_sound_on_message_trampoline(void* arg);
|
||||
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
|
||||
void chat_core_save_ui_state_trampoline(void* arg);
|
||||
|
||||
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
|
||||
// Установка chat-настройки (name=value) через chat_setting API (GUI → uasync)
|
||||
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
|
||||
|
||||
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
|
||||
void chat_core_set_setting_trampoline(void* arg);
|
||||
|
||||
/* Запуск подключения к узлам выбранного канала (GUI → uasync) */
|
||||
// Запуск подключения к узлам выбранного канала (GUI → uasync)
|
||||
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
void chat_core_connect_channel_trampoline(void* arg);
|
||||
|
||||
/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */
|
||||
// Однократная попытка подключения к конкретному узлу канала (GUI → uasync)
|
||||
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
|
||||
|
||||
void chat_core_connect_node_trampoline(void* arg);
|
||||
|
||||
struct chat_connect_node_req {
|
||||
@@ -183,10 +211,12 @@ struct chat_connect_node_req {
|
||||
uint64_t node_id;
|
||||
};
|
||||
|
||||
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
|
||||
/* Передать в uasync блок chat_msg_submit, выделенный u_calloc/u_malloc; trampoline освобождает его.
|
||||
* data отдельно не освобождается: удобно разместить содержимое в хвосте того же блока.
|
||||
* Непустой media_src выбирает подготовку вложения, иначе — обычную отправку. Результат — по request_id. */
|
||||
void chat_core_submit_trampoline(void* arg);
|
||||
|
||||
/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */
|
||||
// Трамплин для ручного запуска загрузки медиа (JNI → uasync)
|
||||
struct attachment_dl_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel_id[64];
|
||||
@@ -194,14 +224,15 @@ struct attachment_dl_req {
|
||||
};
|
||||
void chat_core_attachment_download_trampoline(void* arg);
|
||||
|
||||
/* Обновление local_attrs для записи (вызывается из uasync-потока) */
|
||||
// Обновление local_attrs для записи (вызывается из uasync-потока)
|
||||
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
|
||||
const uint8_t* author_sig,
|
||||
const char* local_attrs_json);
|
||||
|
||||
/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */
|
||||
// Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs)
|
||||
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
|
||||
uint64_t author_node_id);
|
||||
|
||||
void chat_core_mark_voice_played_trampoline(void* arg);
|
||||
|
||||
struct voice_played_req {
|
||||
@@ -211,26 +242,25 @@ struct voice_played_req {
|
||||
uint64_t author_node_id;
|
||||
};
|
||||
|
||||
/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */
|
||||
// Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst)
|
||||
void chat_core_collect_status_trampoline(void* arg);
|
||||
|
||||
/* Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. */
|
||||
// Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD.
|
||||
void chat_core_collect_cpu_load_trampoline(void* arg);
|
||||
|
||||
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */
|
||||
// Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst
|
||||
void chat_core_collect_conn_list_trampoline(void* arg);
|
||||
|
||||
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
|
||||
|
||||
/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */
|
||||
// Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg)
|
||||
void chat_core_collect_conn_metrics_trampoline(void* arg);
|
||||
|
||||
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */
|
||||
void chat_core_collect_member_detail_trampoline(void* arg);
|
||||
/* Диагностика карточки участника: запрос/codec находятся в chat_member.h. */
|
||||
|
||||
/* ── Headless CLI API ── */
|
||||
// ── Headless CLI API ──
|
||||
|
||||
/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
|
||||
// Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}]
|
||||
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}].
|
||||
@@ -242,20 +272,24 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i
|
||||
* connected означает наличие транспорта в реестре, а не его UP или готовность группы. */
|
||||
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
|
||||
|
||||
/* Имя узла из таблицы nodes */
|
||||
// Имя узла из таблицы nodes
|
||||
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
|
||||
|
||||
/* Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку. */
|
||||
// Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку.
|
||||
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file);
|
||||
|
||||
int chat_core_name_userinfo(const char* name, char* out, size_t capacity);
|
||||
|
||||
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
|
||||
uint8_t signature[64], uint64_t* timestamp);
|
||||
|
||||
/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ.
|
||||
* Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle.
|
||||
* Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */
|
||||
struct chat_profile_option { const char* key; const char* value; };
|
||||
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
|
||||
const struct chat_profile_option* options, size_t count);
|
||||
|
||||
struct chat_avatar_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
uint64_t id, group, server;
|
||||
@@ -263,33 +297,33 @@ struct chat_avatar_req {
|
||||
};
|
||||
void chat_core_set_my_avatar_trampoline(void* arg);
|
||||
|
||||
/* ── Редактирование adm_tags (владелец канала) ── */
|
||||
// ── Редактирование adm_tags (владелец канала) ──
|
||||
|
||||
#define CHAT_MEMBER_TAGS_MAX_KEYS 16
|
||||
#define CHAT_MEMBER_TAGS_KEY_SZ 64
|
||||
|
||||
struct chat_member_tags;
|
||||
|
||||
/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
|
||||
// Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет.
|
||||
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
|
||||
|
||||
/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
|
||||
// Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit.
|
||||
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
|
||||
|
||||
/* Получить значение ключа. Возвращает NULL если ключ не найден. */
|
||||
// Получить значение ключа. Возвращает NULL если ключ не найден.
|
||||
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
|
||||
|
||||
/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */
|
||||
// Заменить все теги данными из JSON-строки (плоский, строковые значения).
|
||||
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
|
||||
|
||||
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
|
||||
* записать через member_sync_put() → синхронизируется всем. */
|
||||
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
|
||||
|
||||
/* Освободить без сохранения. */
|
||||
// Освободить без сохранения.
|
||||
void chat_member_tags_free(struct chat_member_tags* t);
|
||||
|
||||
/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
|
||||
// Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память).
|
||||
struct chat_member_tags_req {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char ch_id[64];
|
||||
@@ -298,10 +332,11 @@ struct chat_member_tags_req {
|
||||
};
|
||||
void chat_member_tags_commit_trampoline(void* arg);
|
||||
|
||||
/* ── Передача прав админа (владелец → другому узлу) ── */
|
||||
// ── Передача прав админа (владелец → другому узлу) ──
|
||||
|
||||
/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
|
||||
// Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router).
|
||||
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
|
||||
|
||||
void chat_core_transfer_admin_trampoline(void* arg);
|
||||
|
||||
struct chat_transfer_admin_req {
|
||||
|
||||
@@ -79,6 +79,7 @@ struct ms_props_cbk; /* member_sync.c */
|
||||
struct media_index_result;
|
||||
struct ms_apply_cbk; /* member_sync.c */
|
||||
struct group_avatar;
|
||||
struct media_submit_ctx;
|
||||
|
||||
struct chat_core_ctx {
|
||||
struct UTUN_INSTANCE* inst;
|
||||
@@ -92,6 +93,7 @@ struct chat_core_ctx {
|
||||
struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */
|
||||
struct group_avatar* group_avatar;
|
||||
uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */
|
||||
struct media_submit_ctx* media_submissions; /* исходящие задачи до последнего callback; удаление канала ждёт их */
|
||||
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
|
||||
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
|
||||
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
|
||||
@@ -125,6 +127,8 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32],
|
||||
int chat_msg_parse_media_body(const char* body, struct media_index_result* result);
|
||||
/* Учесть также расшифрованные производные файлы и применить общую квоту кеша. */
|
||||
void chat_media_cache_enforce(struct UTUN_INSTANCE* inst);
|
||||
/* Пометить текущие исходящие задачи канала; 1 — удаление продолжит последний completion, 0 — задач нет. */
|
||||
int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel);
|
||||
|
||||
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
|
||||
[20]="CONN_LIST", [21]="CONN_METRICS",
|
||||
[22]="MEMBER_DETAIL", [23]="MEMBER_LIST",
|
||||
[24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED",
|
||||
[26]="BGP_INFO", [27]="CONNECTING_NODES",
|
||||
[27]="CONNECTING_NODES",
|
||||
[28]="ADMIN_KEY_RECEIVED", [29]="INVITE_LINK_READY",
|
||||
[30]="INVITE_CANDIDATES", [31]="CHANNEL_DELETED",
|
||||
[32]="DM_MSG_RECEIVED", [33]="DM_CONV_UPDATED",
|
||||
|
||||
+18
-7
@@ -1,8 +1,10 @@
|
||||
/*
|
||||
* chat_event.h — система нотификаций о событиях чата
|
||||
* chat_event — доставка событий ядра чата интерфейсу или headless-клиенту.
|
||||
*
|
||||
* Производители (chat_core, chat_sync, ...) вызывают chat_event_post().
|
||||
* Потребители (chatgui, headless, будущий HTTP API) регистрируют один обработчик.
|
||||
* На экземпляр регистрируется один обработчик через chat_event_set_handler().
|
||||
* post вызывает его сразу, в том же uasync-потоке; очереди событий здесь нет.
|
||||
* data заимствован только до возврата обработчика. Для передачи в другой поток нужна копия.
|
||||
*
|
||||
* Без обработчика — данные логируются через DEBUG_DEBUG + log_dump.
|
||||
* Все вызовы — из uasync-потока.
|
||||
@@ -20,9 +22,9 @@ struct UTUN_INSTANCE;
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости) */
|
||||
// Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости)
|
||||
#define CHAT_EVT_MSG_RECEIVED 1 /* [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */
|
||||
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8] */
|
||||
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8][request_id:8], native */
|
||||
#define CHAT_EVT_NEW_PEER 3 /* [node_id:8] */
|
||||
#define CHAT_EVT_CHANNEL_UPDATED 4 /* [ch_id_len:1][ch_id:var] */
|
||||
#define CHAT_EVT_MEMBERS_CHANGED 5 /* [ch_id_len:1][ch_id:var] */
|
||||
@@ -42,11 +44,10 @@ extern "C" {
|
||||
#define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */
|
||||
#define CHAT_EVT_CONN_LIST 20 /* [count:2][entry:46B]* — список ETCP-соединений */
|
||||
#define CHAT_EVT_CONN_METRICS 21 /* текст — полный дамп метрик выбранного соединения */
|
||||
#define CHAT_EVT_MEMBER_DETAIL 22 /* [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live-снапшот для панели деталей участника */
|
||||
#define CHAT_EVT_MEMBER_DETAIL 22 /* Адресованный ответ диагностики; wire описан в chat_member.h. */
|
||||
#define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */
|
||||
#define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */
|
||||
#define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
|
||||
#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */
|
||||
#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
|
||||
#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */
|
||||
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
|
||||
@@ -90,12 +91,22 @@ extern "C" {
|
||||
#define CHAT_EVT_RADIO_CALL 65 /* [id:8LE][group:8LE][peer:8LE][state:1][reason:1][incoming:1], radio_call.h */
|
||||
#define CHAT_EVT_CALL_PHONE_BUSY 66 /* [id:8LE][reserved:1]; вызывающий выбирает продолжить либо завершить */
|
||||
|
||||
// ── Подключение получателя событий ──
|
||||
|
||||
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
|
||||
|
||||
// Заменить обработчик; NULL отключает доставку. Предыдущий обработчик не вызывается.
|
||||
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler);
|
||||
|
||||
// ── Отправка событий из ядра ──
|
||||
|
||||
// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*.
|
||||
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
|
||||
|
||||
// Сообщить этап подготовки вложения: state=0 начало, 1 готово, 2 ошибка; target — ID канала или DM.
|
||||
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state);
|
||||
/* request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки. */
|
||||
|
||||
// request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки.
|
||||
void chat_event_submitted(struct UTUN_INSTANCE* inst, uint64_t request_id, int success);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -509,7 +509,7 @@ static void hc_invite_ready(void* arg, int result, const char* link) {
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d",
|
||||
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
|
||||
char resp[1280];
|
||||
char resp[INVITE_LINK_MAX + 256];
|
||||
snprintf(resp, sizeof(resp),
|
||||
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
|
||||
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
|
||||
@@ -571,7 +571,7 @@ static void hc_handle_invite_nodes(struct headless_client* cli, int id, const ch
|
||||
|
||||
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) {
|
||||
struct headless_control* hc = cli->hc;
|
||||
char link[1024];
|
||||
char link[INVITE_LINK_MAX];
|
||||
if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; }
|
||||
struct InviteData d; char err[256];
|
||||
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; }
|
||||
@@ -580,17 +580,8 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
|
||||
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d",
|
||||
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
|
||||
|
||||
uint8_t addrs_buf[2048];
|
||||
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
|
||||
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; }
|
||||
|
||||
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId);
|
||||
chat_core_ensure_channel_ready(hc->inst, chstr); }
|
||||
|
||||
chat_core_set_my_node_id(hc->inst, hc->inst->node_id);
|
||||
|
||||
chat_sync_connect_from_invite(hc->inst, d.channelId, d.nodeId, d.pubkey,
|
||||
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, d.join_key);
|
||||
int result = chat_sync_join_link(hc->inst, 0, link, strlen(link));
|
||||
if (result) { send_response(cli, id, NULL, "join could not start (see log)"); return; }
|
||||
|
||||
char resp[512]; snprintf(resp, sizeof(resp),
|
||||
"{\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d,\"connecting\":true}",
|
||||
@@ -647,8 +638,16 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char*
|
||||
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d",
|
||||
ch, (unsigned long long)target_node_id, ip, port, proto);
|
||||
|
||||
chat_sync_invite_to_channel_with_addrs(hc->inst, ch, target_node_id,
|
||||
pubkey_bin, addrs_data, 1, addrs_len);
|
||||
struct InviteData target = { .channelId = strtoull(ch, NULL, 10), .join_key = 1, .addrCount = 1 };
|
||||
memcpy(target.pubkey, pubkey_bin, 32); target.addrs[0].family = addrs_data[1];
|
||||
target.addrs[0].proto = (uint8_t)proto; target.addrs[0].port = (uint16_t)port;
|
||||
memcpy(target.addrs[0].address, addrs_data + 4, target.addrs[0].family == 4 ? 4 : 16);
|
||||
char target_link[INVITE_LINK_MAX];
|
||||
if (sc_derive_node_id_from_pubkey(pubkey_bin) != target_node_id ||
|
||||
invite_link_encode(&target, NULL, target_link, sizeof(target_link)) < 0 ||
|
||||
chat_sync_offer_link(hc->inst, 0, target.channelId, target_link, strlen(target_link))) {
|
||||
send_response(cli, id, NULL, "invitation could not start"); return;
|
||||
}
|
||||
|
||||
char resp[256]; snprintf(resp, sizeof(resp),
|
||||
"{\"channel_id\":\"%s\",\"node_id\":\"0x%016llx\",\"inviting\":true}",
|
||||
|
||||
@@ -63,8 +63,10 @@
|
||||
* - Recovery восстанавливает сессии существующих участников и не использует
|
||||
* invite-ключ как замену локальной таблицы участников.
|
||||
*
|
||||
* J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
|
||||
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
|
||||
* J <-> C: ETCP_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
|
||||
* описан в chat_sync.h. Для INFO_REQ/RESP, REQUEST, READY и ERROR payload начинается
|
||||
* с attempt_id:8BE; C сохраняет его в waiter и возвращает в ответах. Это ID попытки,
|
||||
* не join_key и не request_id интерфейса. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
|
||||
* и шифрованием; subcommands и payload описаны ниже.
|
||||
*
|
||||
* Модуль отвечает только за маршрутизируемую (etcp_router) часть:
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "chat_join_port.h"
|
||||
#include "chat_sync.h"
|
||||
#include "invite_link.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
|
||||
struct join_command {
|
||||
struct posted_task post; /* первый член: uasync освобождает всю команду */
|
||||
struct join_command* next;
|
||||
struct chat_join_port* port;
|
||||
uint64_t request, channel, peer, offer;
|
||||
size_t len;
|
||||
int completed;
|
||||
char link[];
|
||||
};
|
||||
struct chat_join_port {
|
||||
pthread_mutex_t lock;
|
||||
unsigned refs;
|
||||
uint64_t active_request;
|
||||
struct UTUN_INSTANCE* inst; /* только callback в uasync обращается к instance */
|
||||
struct UASYNC* ua; /* жив до close; producer публикует под lock */
|
||||
struct join_command* commands;
|
||||
};
|
||||
|
||||
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_join_port* p = u_calloc(1, sizeof(*p));
|
||||
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port allocation failed"); return NULL; }
|
||||
if (pthread_mutex_init(&p->lock, NULL)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port mutex initialization failed"); u_free(p); return NULL;
|
||||
}
|
||||
p->refs = 1; p->inst = inst; p->ua = inst->ua; return p;
|
||||
}
|
||||
void chat_join_port_retain(struct chat_join_port* p) {
|
||||
if (!p) return;
|
||||
pthread_mutex_lock(&p->lock); p->refs++; pthread_mutex_unlock(&p->lock);
|
||||
}
|
||||
void chat_join_port_release(struct chat_join_port* p) {
|
||||
if (!p) return;
|
||||
pthread_mutex_lock(&p->lock); unsigned refs = --p->refs; pthread_mutex_unlock(&p->lock);
|
||||
if (!refs) { pthread_mutex_destroy(&p->lock); u_free(p); }
|
||||
}
|
||||
static void execute(void* arg) {
|
||||
struct join_command* command = arg;
|
||||
struct chat_join_port* p = command->port;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
struct join_command** link = &p->commands;
|
||||
while (*link && *link != command) link = &(*link)->next;
|
||||
if (*link) *link = command->next;
|
||||
struct UTUN_INSTANCE* inst = p->inst;
|
||||
if (!command->completed && inst && !p->active_request) p->active_request = command->request;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
if (!command->completed && inst) {
|
||||
int result = command->offer ? chat_sync_offer_link(inst, command->request, command->offer, command->link, command->len)
|
||||
: chat_sync_join_link(inst, command->request, command->link, command->len);
|
||||
if (result) {
|
||||
chat_join_port_complete(p, command->request);
|
||||
chat_sync_join_result(inst, command->request, command->channel, command->peer, result);
|
||||
}
|
||||
}
|
||||
chat_join_port_release(p); /* память command освободит uasync */
|
||||
}
|
||||
static int submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len, uint64_t offer) {
|
||||
if (!p || !request || !link || !len || len >= INVITE_LINK_MAX) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command invalid or port unavailable"); return CHAT_CONNECT_INVALID;
|
||||
}
|
||||
struct InviteData invite;
|
||||
if (invite_link_decode(link, len, &invite, NULL, 0)) return CHAT_CONNECT_INVALID;
|
||||
struct join_command* command = u_calloc(1, sizeof(*command) + len);
|
||||
if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join command allocation failed"); return CHAT_CONNECT_ERROR; }
|
||||
command->post.callback = execute; command->post.arg = command; command->port = p;
|
||||
command->offer = offer; command->request = request; command->channel = offer ? offer : invite.channelId; command->peer = invite.nodeId;
|
||||
command->len = len; memcpy(command->link, link, len);
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (!p->inst) {
|
||||
pthread_mutex_unlock(&p->lock); u_free(command);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command rejected: port closed"); return CHAT_CONNECT_CANCELLED;
|
||||
}
|
||||
if (p->active_request == request) {
|
||||
pthread_mutex_unlock(&p->lock); u_free(command);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join request already active"); return CHAT_CONNECT_BUSY;
|
||||
}
|
||||
for (struct join_command* c = p->commands; c; c = c->next) if (c->request == request) {
|
||||
pthread_mutex_unlock(&p->lock); u_free(command);
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "duplicate pending join request"); return CHAT_CONNECT_BUSY;
|
||||
}
|
||||
p->refs++; command->next = p->commands; p->commands = command;
|
||||
uasync_post_reserved(p->ua, &command->post);
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join command queued request=%llu", (unsigned long long)request);
|
||||
return 0;
|
||||
}
|
||||
void chat_join_port_close(struct chat_join_port* p) {
|
||||
if (!p) return;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
struct UTUN_INSTANCE* inst = p->inst;
|
||||
struct UASYNC* ua = p->ua;
|
||||
struct join_command* commands = p->commands;
|
||||
p->commands = NULL; p->inst = NULL; p->ua = NULL;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
while (commands) {
|
||||
struct join_command* c = commands; commands = c->next;
|
||||
c->completed = 1;
|
||||
uint64_t request = c->request, channel = c->channel, peer = c->peer;
|
||||
/* Уже извлечённый batch исполнит пустой callback и снимет свою ссылку. */
|
||||
if (uasync_cancel_post(ua, &c->post) == ERR_OK) chat_join_port_release(p);
|
||||
chat_sync_join_result(inst, request, channel, peer, CHAT_CONNECT_CANCELLED);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join port closed");
|
||||
}
|
||||
|
||||
void chat_join_port_complete(struct chat_join_port* p, uint64_t request) {
|
||||
if (!p) return;
|
||||
pthread_mutex_lock(&p->lock);
|
||||
if (p->active_request == request) p->active_request = 0;
|
||||
pthread_mutex_unlock(&p->lock);
|
||||
}
|
||||
|
||||
int chat_join_port_submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len) {
|
||||
return submit(p, request, link, len, 0);
|
||||
}
|
||||
int chat_join_port_offer(struct chat_join_port* p, uint64_t request, uint64_t channel, const char* link, size_t len) {
|
||||
if (!channel) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "offer missing channel"); return CHAT_CONNECT_INVALID; }
|
||||
return submit(p, request, link, len, channel);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/* Потокобезопасный вход команд JOIN для GUI/JNI. Контекст удерживается фронтендом
|
||||
* от bind до unbind; close в uasync отменяет команды и запрещает дальнейшие submit.
|
||||
* После close порт живёт до release последних ссылок, не содержит живого instance. */
|
||||
#ifndef CHAT_JOIN_PORT_H
|
||||
#define CHAT_JOIN_PORT_H
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
struct UTUN_INSTANCE;
|
||||
struct chat_join_port;
|
||||
/* Создать порт в uasync; одна ссылка принадлежит chat_sync, NULL при ошибке. */
|
||||
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst);
|
||||
/* Получить отдельную ссылку на порт текущего чата; только uasync, NULL до init. */
|
||||
struct chat_join_port* chat_sync_join_port(struct UTUN_INSTANCE* inst);
|
||||
/* Управление отдельной ссылкой. retain требует уже удерживаемой ссылки. */
|
||||
void chat_join_port_retain(struct chat_join_port* port);
|
||||
void chat_join_port_release(struct chat_join_port* port);
|
||||
/* uasync: снять корреляцию завершённой активной команды до уведомления. */
|
||||
void chat_join_port_complete(struct chat_join_port* port, uint64_t request);
|
||||
/* uasync: закрыть приём и завершить ожидающие команды CANCELLED; повтор допустим. */
|
||||
void chat_join_port_close(struct chat_join_port* port);
|
||||
/* Любой поток: скопировать ссылку и принять команду. 0 — будет один результат с request;
|
||||
* <0 — команда не принята, результата не будет. request != 0; link заимствован до возврата. */
|
||||
int chat_join_port_submit(struct chat_join_port* port, uint64_t request, const char* link, size_t len);
|
||||
/* Те же гарантии для приглашения адресата ссылки в собственный channel. */
|
||||
int chat_join_port_offer(struct chat_join_port* port, uint64_t request, uint64_t channel, const char* link, size_t len);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+80
-1
@@ -4,6 +4,13 @@
|
||||
#include "chat_setting.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../local_transport/protocol.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include "../routing_layer/topo_group_connect.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/sock_match.h"
|
||||
#include "../transport_layer/etcp.h"
|
||||
#include "../transport_layer/etcp_connections.h"
|
||||
#include "../transport_layer/node_conn_direct.h"
|
||||
#include "../../lib/debug_config.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include <string.h>
|
||||
@@ -76,9 +83,81 @@ static void activity(struct UTUN_INSTANCE* inst, int active, void* arg) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "failed to apply discovery activity node=%016llx", (unsigned long long)inst->node_id);
|
||||
}
|
||||
|
||||
/* Только принимающие рабочие endpoints этого IP; скрытые и SOCKS/probe-сокеты не публикуются. */
|
||||
static int route_sockets(void* arg, const struct sockaddr_storage* address, uint32_t index,
|
||||
struct local_route_socket* out, size_t capacity) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
size_t count = 0;
|
||||
for (struct ETCP_SOCKET* sock = inst->etcp_sockets; sock; sock = sock->next) {
|
||||
if (sock->type == CFG_SERVER_TYPE_PRIVATE || !sock_matches_lan(sock, address, index)) continue;
|
||||
uint16_t port = ss_get_port(&sock->local_addr);
|
||||
if (!port || !sock->generation) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN socket omitted name=%s id=%u port=%u generation=%016llx",
|
||||
sock->name, sock->sock_id, port, (unsigned long long)sock->generation); continue;
|
||||
}
|
||||
if (count == capacity) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "too many LAN sockets interface=%u capacity=%zu", index, capacity); return -1;
|
||||
}
|
||||
out[count++] = (struct local_route_socket){ .socket_id = sock->sock_id, .generation = sock->generation,
|
||||
.port = port, .protocol = sock->is_tcp ? LOCAL_ROUTE_TCP | (sock->reality_enabled ? LOCAL_ROUTE_REALITY : 0) : LOCAL_ROUTE_UDP };
|
||||
}
|
||||
return (int)count;
|
||||
}
|
||||
|
||||
/* Ключи не покидают ядро; wire magic/version/type отделяют эту подпись от других протоколов. */
|
||||
static int route_sign(void* arg, const uint8_t* data, size_t len, uint8_t signature[64]) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
if (inst->node_id != sc_derive_node_id_from_pubkey(inst->my_keys.public_key) ||
|
||||
sc_ed25519_sign(inst->my_ed25519_privkey, data, len, signature) != SC_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN endpoint signing failed node=%016llx", (unsigned long long)inst->node_id); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Отсутствие ключа не мешает открытому discovery, но не даёт адрес для NCD. */
|
||||
static int route_verify(void* arg, uint64_t peer, const uint8_t* data, size_t len, const uint8_t signature[64]) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
uint8_t key[32];
|
||||
struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, peer);
|
||||
if (node && node->timestamp) memcpy(key, node->ed25519_public_key, sizeof(key));
|
||||
else if (!inst->topo_sqlite_db || topo_node_sqlite_get_ed25519_pubkey(inst->topo_sqlite_db, peer, key)) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DISCOVERY, "LAN route ignored peer=%016llx reason=unknown signing key", (unsigned long long)peer); return 1;
|
||||
}
|
||||
if (sc_ed25519_verify(key, data, len, signature) != SC_OK) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN route signature rejected peer=%016llx", (unsigned long long)peer); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Общие адреса узла нельзя заменять, пока доступен прямой транспорт или любой BGP-путь. */
|
||||
static int route_offline(void* arg, uint64_t peer) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
struct ETCP_CONN* conn = inst->connections ? instance_find_conn(inst, peer) : NULL;
|
||||
if (conn && conn->links_up && !conn->close_requested && conn->state != 2) return 0;
|
||||
for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL;
|
||||
e; e = e->next) {
|
||||
if (topo_node_best_path(topo_node_find_by_id((struct TOPO_GROUP*)e, peer))) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Обновление помогает уже удерживаемому NCD; право создавать соединение остаётся у сервисов. */
|
||||
static void routes_changed(void* arg, uint64_t peer) {
|
||||
struct UTUN_INSTANCE* inst = arg;
|
||||
if (inst->connections && node_conn_direct_update_local(inst, peer) < 0)
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN address application failed peer=%016llx", (unsigned long long)peer);
|
||||
for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL;
|
||||
e; e = e->next) {
|
||||
struct TOPO_GROUP* group = (struct TOPO_GROUP*)e;
|
||||
if (group->group_type == TOPO_GROUP_TYPE_CHAT && !group->stopping) topo_group_connect_changed(group);
|
||||
}
|
||||
}
|
||||
|
||||
int chat_local_start(struct UTUN_INSTANCE* inst) {
|
||||
if (inst->local_transport || inst->client_type == CLIENT_TYPE_SERVER) return 0;
|
||||
struct local_transport_callbacks callbacks = { peers_changed, received, delivered, activity_changed };
|
||||
struct local_transport_callbacks callbacks = { .peers_changed = peers_changed, .received = received, .delivery = delivered,
|
||||
.activity_changed = activity_changed, .route_sockets = route_sockets, .route_sign = route_sign,
|
||||
.route_verify = route_verify, .route_offline = route_offline, .routes_changed = routes_changed };
|
||||
inst->local_transport = local_transport_create(inst->ua, inst->node_id, inst->name, &callbacks, inst);
|
||||
if (!inst->local_transport) return -1;
|
||||
if (chat_local_apply_settings(inst) < 0) { chat_local_stop(inst); return -1; }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* Адаптер локального транспорта: только доставка готовых invite-ссылок, без запуска JOIN. */
|
||||
/* Адаптер LAN discovery: готовые invite-ссылки и подписанные адреса известных узлов, без запуска JOIN.
|
||||
* Адреса подтверждаются nonce-ответом только при отсутствии прямого UP и BGP-путей во всех группах.
|
||||
* Ключи берутся из registry/SQLite и не передаются в открытых объявлениях; NODEINFO не изменяется. */
|
||||
#ifndef CHAT_LOCAL_H
|
||||
#define CHAT_LOCAL_H
|
||||
|
||||
|
||||
+115
-195
@@ -22,7 +22,7 @@
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ─── live-онлайн: узел присутствует в BGP/topo_group канала ─── */
|
||||
/* ─── live-онлайн: тот же живой путь, которым пользуется маршрутизатор ─── */
|
||||
|
||||
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
@@ -32,7 +32,7 @@ int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
|
||||
uint64_t group_id = strtoull(ch_id, NULL, 10);
|
||||
struct TOPO_GROUP* g = topo_groups_find(cc->inst->topo_groups, group_id);
|
||||
if (!g || g->group_type != TOPO_GROUP_TYPE_CHAT) return 0;
|
||||
return topo_node_find_by_id(g, node_id) != NULL;
|
||||
return topo_node_best_path(topo_node_find_by_id(g, node_id)) != NULL;
|
||||
}
|
||||
|
||||
/* Рация — живая подписка на конкретную CHAT-группу; у self нет записи в group->nodes. */
|
||||
@@ -87,13 +87,11 @@ static uint8_t parse_adm_tags_flags(const char* adm_tags) {
|
||||
/* ─── сериализация (struct → wire) ─── */
|
||||
|
||||
void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst) {
|
||||
uint64_t nid = src->node_id;
|
||||
uint16_t rtt = src->rtt;
|
||||
memcpy(dst, &nid, 8);
|
||||
mf_put64(dst, src->node_id);
|
||||
dst[8] = src->online;
|
||||
dst[9] = src->is_self;
|
||||
dst[10] = src->flags;
|
||||
memcpy(dst + 11, &rtt, 2);
|
||||
dst[11] = (uint8_t)src->rtt; dst[12] = (uint8_t)(src->rtt >> 8);
|
||||
dst[13] = src->nat_type;
|
||||
dst[14] = src->storage;
|
||||
/* Поле на проводе всегда завершается NUL и не содержит хвост памяти исходной строки. */
|
||||
@@ -102,17 +100,20 @@ void chat_core_serialize_member(const struct chat_member_display* src, uint8_t*
|
||||
mf_put64(dst + 79, src->avatar_server);
|
||||
memcpy(dst + 87, src->avatar_file, 70);
|
||||
dst[157] = src->storage_role;
|
||||
dst[158] = src->verified; dst[159] = src->node_type;
|
||||
dst[160] = src->conn_presence; dst[161] = src->conn_up;
|
||||
dst[162] = (uint8_t)src->intermediaries; dst[163] = (uint8_t)(src->intermediaries >> 8);
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: serialized node=%016llx name_bytes=%zu zero_padded=1",
|
||||
(unsigned long long)src->node_id, strlen((const char*)dst + 15));
|
||||
}
|
||||
|
||||
int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst) {
|
||||
if (!src || !dst) return -1;
|
||||
memcpy(&dst->node_id, src, 8);
|
||||
dst->node_id = mf_get64(src);
|
||||
dst->online = src[8];
|
||||
dst->is_self = src[9];
|
||||
dst->flags = src[10];
|
||||
memcpy(&dst->rtt, src + 11, 2);
|
||||
dst->rtt = (uint16_t)src[11] | ((uint16_t)src[12] << 8);
|
||||
dst->nat_type = src[13];
|
||||
dst->storage = src[14];
|
||||
memcpy(dst->name, src + 15, 64);
|
||||
@@ -120,6 +121,9 @@ int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display*
|
||||
dst->avatar_server = mf_get64(src + 79);
|
||||
memcpy(dst->avatar_file, src + 87, 70); dst->avatar_file[69] = 0;
|
||||
dst->storage_role = src[157];
|
||||
dst->verified = src[158]; dst->node_type = src[159];
|
||||
dst->conn_presence = src[160]; dst->conn_up = src[161];
|
||||
dst->intermediaries = (uint16_t)src[162] | ((uint16_t)src[163] << 8);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -144,214 +148,122 @@ static int member_storage_role(const char* tags, sqlite3_stmt* st, int source, i
|
||||
tags && json_flat_get(tags, "storage", role, sizeof(role)) == 0 && !strcmp(role, "yes");
|
||||
}
|
||||
|
||||
/* ─── запрос списка мемберов ─── */
|
||||
/* Один SELECT и одна функция заполнения для списка и одиночного обновления. */
|
||||
static void member_from_row(struct UTUN_INSTANCE* inst, const char* ch_id, sqlite3_stmt* st, struct chat_member_display* m) {
|
||||
memset(m, 0, sizeof(*m));
|
||||
m->node_id = (uint64_t)sqlite3_column_int64(st, 0);
|
||||
const char* node_name = (const char*)sqlite3_column_text(st, 1);
|
||||
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
|
||||
const char* tags = (const char*)sqlite3_column_text(st, 3);
|
||||
m->storage = (uint8_t)sqlite3_column_int(st, 4);
|
||||
m->storage_role = member_storage_role(tags, st, 7, 8, sqlite3_column_int(st, 5));
|
||||
m->is_self = m->node_id == inst->node_id;
|
||||
m->flags = parse_adm_tags_flags(tags) | member_radio_flag(inst, ch_id, m->node_id);
|
||||
if (sqlite3_column_int(st, 9)) m->flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
|
||||
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
|
||||
member_avatar(m, (const char*)sqlite3_column_text(st, 6));
|
||||
char verified[16];
|
||||
m->verified = tags && json_flat_get(tags, "verified", verified, sizeof(verified)) == 0 && !strcmp(verified, "yes");
|
||||
m->node_type = (uint8_t)sqlite3_column_int(st, 10);
|
||||
m->nat_type = (uint8_t)sqlite3_column_int(st, 11);
|
||||
m->rtt = UINT16_MAX;
|
||||
struct TOPO_GROUP* group = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ch_id, NULL, 10)) : NULL;
|
||||
struct TOPO_GROUP_NODE* node = group ? topo_node_find_by_id(group, m->node_id) : NULL;
|
||||
if (node) {
|
||||
m->conn_presence = node->conn_presence; m->conn_up = node->conn_up;
|
||||
struct TOPO_NODEPATH* path = topo_node_best_path(node);
|
||||
if (path) {
|
||||
m->rtt = topo_get_chain_rtt(node);
|
||||
m->intermediaries = path->hop_count > 0 ? path->hop_count - 1 : 0;
|
||||
m->online = 1;
|
||||
}
|
||||
}
|
||||
if (m->is_self) m->online = 1;
|
||||
if (m->flags & CHAT_MEMBER_FLAG_DELETED) m->online = 0;
|
||||
if (!m->online) m->flags &= ~(CHAT_MEMBER_FLAG_PHONE_KNOWN | CHAT_MEMBER_FLAG_PHONE_BUSY);
|
||||
const char* name = local_nick && local_nick[0] ? local_nick : node_name;
|
||||
if (name && name[0]) snprintf(m->name, sizeof(m->name), "%s", name);
|
||||
else snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF));
|
||||
}
|
||||
|
||||
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
|
||||
static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int single, uint64_t node_id,
|
||||
uint8_t** out, int* count) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!out || !count) return -1;
|
||||
*out = NULL; *count = 0;
|
||||
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — not initialized");
|
||||
return -1;
|
||||
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !cc->db || !cc->inst) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: invalid instance/channel"); return -1;
|
||||
}
|
||||
|
||||
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
|
||||
uint64_t my_id = cc->my_node_id;
|
||||
|
||||
/* 1. запрашиваем всех мемберов */
|
||||
sqlite3_stmt* st = NULL;
|
||||
char sql[512];
|
||||
char table[80]; peers_table_name(ch_id, table, sizeof(table));
|
||||
char sql[1536];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT p.node_id, COALESCE(n.name,''),"
|
||||
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
|
||||
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig,p.radio_muted"
|
||||
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
|
||||
" ORDER BY p.node_id ASC", peers_tbl);
|
||||
|
||||
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — query failed ch=%s err=%s",
|
||||
ch_id, sqlite3_errmsg(cc->db));
|
||||
return -1;
|
||||
"SELECT p.node_id,COALESCE(n.name,''),COALESCE(p.local_nick,''),COALESCE(p.adm_tags,''),"
|
||||
"p.storage,p.deleted,p.userinfo,p.source,p.adm_tags_sig,p.radio_muted,p.node_type,"
|
||||
"(SELECT CASE WHEN COUNT(*)=0 THEN 255 WHEN MAX(addr_type=1) THEN 1 WHEN MAX(addr_type=5) THEN 5"
|
||||
" WHEN MAX(addr_type=2) THEN 2 WHEN MAX(addr_type=6) THEN 6 WHEN MAX(addr_type=4) THEN 4"
|
||||
" WHEN MAX(addr_type=3) THEN 3 ELSE 0 END FROM node_addresses WHERE node_id=p.node_id)"
|
||||
" FROM \"%s\" p LEFT JOIN nodes n ON n.node_id=p.node_id %s ORDER BY p.node_id ASC",
|
||||
table, single ? "WHERE p.node_id=?" : "");
|
||||
sqlite3_stmt* st = NULL;
|
||||
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
|
||||
if (rc == SQLITE_OK && single) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: query ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
|
||||
sqlite3_finalize(st); return -1;
|
||||
}
|
||||
|
||||
struct chat_member_display* members = NULL;
|
||||
int cap = 0, cnt = 0;
|
||||
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
if (cnt >= cap) {
|
||||
cap = cap ? cap * 2 : 32;
|
||||
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
||||
if (cnt == cap) {
|
||||
cap = cap ? cap * 2 : (single ? 1 : 32);
|
||||
struct chat_member_display* tmp = u_realloc(members, (size_t)cap * sizeof(*members));
|
||||
if (!tmp) { u_free(members); sqlite3_finalize(st); return -1; }
|
||||
if (!tmp) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation ch=%s count=%d", ch_id, cnt);
|
||||
u_free(members); sqlite3_finalize(st); return -1;
|
||||
}
|
||||
members = tmp;
|
||||
}
|
||||
struct chat_member_display* m = &members[cnt];
|
||||
m->node_id = (uint64_t)sqlite3_column_int64(st, 0);
|
||||
member_avatar(m, (const char*)sqlite3_column_text(st, 6));
|
||||
m->online = (uint8_t)chat_core_member_online(inst, ch_id, m->node_id);
|
||||
const char* node_name = (const char*)sqlite3_column_text(st, 1);
|
||||
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
|
||||
const char* adm_tags = (const char*)sqlite3_column_text(st, 3);
|
||||
m->storage = (uint8_t)sqlite3_column_int(st, 4);
|
||||
m->storage_role = member_storage_role(adm_tags, st, 7, 8, sqlite3_column_int(st, 5));
|
||||
m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0);
|
||||
m->flags = parse_adm_tags_flags(adm_tags) | member_radio_flag(inst, ch_id, m->node_id);
|
||||
if (sqlite3_column_int(st, 9)) m->flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
|
||||
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
|
||||
m->rtt = 0xFFFF;
|
||||
m->nat_type = 0xFF;
|
||||
|
||||
/* display name: local_nick если задан, иначе node name, иначе short hex */
|
||||
const char* display = (local_nick && local_nick[0]) ? local_nick :
|
||||
(node_name && node_name[0]) ? node_name : NULL;
|
||||
if (display) {
|
||||
strncpy(m->name, display, sizeof(m->name) - 1);
|
||||
m->name[sizeof(m->name) - 1] = '\0';
|
||||
} else {
|
||||
snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF));
|
||||
}
|
||||
cnt++;
|
||||
member_from_row(inst, ch_id, st, &members[cnt++]);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
|
||||
if (cnt == 0) {
|
||||
*out = NULL; *count = 0;
|
||||
return 0;
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: read ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
|
||||
u_free(members); return -1;
|
||||
}
|
||||
|
||||
/* 2. RTT и NAT type из node_addresses (batch) */
|
||||
char* id_list = u_malloc((size_t)cnt * 21 + 4);
|
||||
if (!id_list) { u_free(members); return -1; }
|
||||
id_list[0] = '\0';
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (i > 0) strcat(id_list, ",");
|
||||
char nbuf[24]; snprintf(nbuf, sizeof(nbuf), "%llu", (unsigned long long)members[i].node_id);
|
||||
strcat(id_list, nbuf);
|
||||
}
|
||||
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT node_id, MIN(CASE WHEN rtt > 0 THEN rtt END),"
|
||||
" CASE WHEN MAX(addr_type=1) THEN 1"
|
||||
" WHEN MAX(addr_type=5) THEN 5"
|
||||
" WHEN MAX(addr_type=2) THEN 2"
|
||||
" WHEN MAX(addr_type=6) THEN 6"
|
||||
" WHEN MAX(addr_type=4) THEN 4"
|
||||
" WHEN MAX(addr_type=3) THEN 3"
|
||||
" ELSE 0 END"
|
||||
" FROM node_addresses WHERE node_id IN (%s) GROUP BY node_id", id_list);
|
||||
u_free(id_list);
|
||||
|
||||
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
|
||||
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
|
||||
int rtt_val = sqlite3_column_type(st, 1) == SQLITE_NULL ? 0xFFFF : sqlite3_column_int(st, 1);
|
||||
int at = sqlite3_column_int(st, 2);
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
if (members[i].node_id == nid) {
|
||||
members[i].rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val);
|
||||
members[i].nat_type = (uint8_t)at;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
/* 3. сортируем: онлайн первыми, затем по node_id (стабильно) */
|
||||
for (int i = 1; i < cnt; i++) {
|
||||
struct chat_member_display key = members[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && ((members[j].online < key.online)
|
||||
|| (members[j].online == key.online && members[j].node_id > key.node_id))) {
|
||||
members[j + 1] = members[j];
|
||||
j--;
|
||||
while (j >= 0 && (members[j].online < key.online ||
|
||||
(members[j].online == key.online && members[j].node_id > key.node_id))) {
|
||||
members[j + 1] = members[j]; j--;
|
||||
}
|
||||
members[j + 1] = key;
|
||||
}
|
||||
|
||||
/* 4. сериализуем в wire-формат */
|
||||
size_t wire_sz = (size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE;
|
||||
uint8_t* wire = u_malloc(wire_sz);
|
||||
if (!wire) { u_free(members); return -1; }
|
||||
for (int i = 0; i < cnt; i++)
|
||||
chat_core_serialize_member(&members[i], wire + i * CHAT_MEMBER_DISPLAY_SIZE);
|
||||
u_free(members);
|
||||
|
||||
*out = wire;
|
||||
*count = cnt;
|
||||
if (cnt) {
|
||||
*out = u_malloc((size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE);
|
||||
if (!*out) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: wire allocation ch=%s count=%d", ch_id, cnt);
|
||||
u_free(members); return -1;
|
||||
}
|
||||
for (int i = 0; i < cnt; i++) chat_core_serialize_member(&members[i], *out + i * CHAT_MEMBER_DISPLAY_SIZE);
|
||||
}
|
||||
u_free(members); *count = cnt;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ─── один мембер ─── */
|
||||
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
|
||||
if (!out || !count) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: null output"); return -1; }
|
||||
return member_query(inst, ch_id, 0, 0, out, count);
|
||||
}
|
||||
|
||||
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!out) return -1;
|
||||
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) return -1;
|
||||
|
||||
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
|
||||
uint64_t my_id = cc->my_node_id;
|
||||
|
||||
sqlite3_stmt* st = NULL;
|
||||
char sql[384];
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT COALESCE(n.name,''),"
|
||||
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
|
||||
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig,p.radio_muted"
|
||||
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
|
||||
" WHERE p.node_id=?", peers_tbl);
|
||||
|
||||
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
||||
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return -1; }
|
||||
|
||||
struct chat_member_display m;
|
||||
m.node_id = node_id;
|
||||
member_avatar(&m, (const char*)sqlite3_column_text(st, 5));
|
||||
m.online = (uint8_t)chat_core_member_online(inst, ch_id, node_id);
|
||||
const char* node_name = (const char*)sqlite3_column_text(st, 0);
|
||||
const char* local_nick = (const char*)sqlite3_column_text(st, 1);
|
||||
const char* adm_tags = (const char*)sqlite3_column_text(st, 2);
|
||||
m.storage = (uint8_t)sqlite3_column_int(st, 3);
|
||||
m.storage_role = member_storage_role(adm_tags, st, 6, 7, sqlite3_column_int(st, 4));
|
||||
m.is_self = (uint8_t)(node_id == my_id ? 1 : 0);
|
||||
m.flags = parse_adm_tags_flags(adm_tags) | member_radio_flag(inst, ch_id, node_id);
|
||||
if (sqlite3_column_int(st, 8)) m.flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
|
||||
if (sqlite3_column_int(st, 4)) m.flags |= CHAT_MEMBER_FLAG_DELETED;
|
||||
m.rtt = 0xFFFF;
|
||||
m.nat_type = 0xFF;
|
||||
|
||||
const char* display = (local_nick && local_nick[0]) ? local_nick :
|
||||
(node_name && node_name[0]) ? node_name : NULL;
|
||||
if (display) {
|
||||
strncpy(m.name, display, sizeof(m.name) - 1);
|
||||
m.name[sizeof(m.name) - 1] = '\0';
|
||||
} else {
|
||||
snprintf(m.name, sizeof(m.name), "0x%04X", (unsigned)(node_id & 0xFFFF));
|
||||
if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: null single output"); return -1; }
|
||||
uint8_t* wire = NULL; int count = 0;
|
||||
if (member_query(inst, ch_id, 1, node_id, &wire, &count) < 0) return -1;
|
||||
if (count != 1) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: absent ch=%s node=%016llx", ch_id, (unsigned long long)node_id);
|
||||
u_free(wire); return -1;
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
|
||||
/* RTT / NAT */
|
||||
snprintf(sql, sizeof(sql),
|
||||
"SELECT MIN(CASE WHEN rtt > 0 THEN rtt END),"
|
||||
" CASE WHEN MAX(addr_type=1) THEN 1"
|
||||
" WHEN MAX(addr_type=5) THEN 5"
|
||||
" WHEN MAX(addr_type=2) THEN 2"
|
||||
" WHEN MAX(addr_type=6) THEN 6"
|
||||
" WHEN MAX(addr_type=4) THEN 4"
|
||||
" WHEN MAX(addr_type=3) THEN 3"
|
||||
" ELSE 0 END"
|
||||
" FROM node_addresses WHERE node_id=?");
|
||||
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
int rtt_val = sqlite3_column_type(st, 0) == SQLITE_NULL ? 0xFFFF : sqlite3_column_int(st, 0);
|
||||
m.rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val);
|
||||
m.nat_type = (uint8_t)sqlite3_column_int(st, 1);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
|
||||
chat_core_serialize_member(&m, out);
|
||||
memcpy(out, wire, CHAT_MEMBER_DISPLAY_SIZE); u_free(wire);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -438,11 +350,7 @@ void chat_core_request_member_rtt_trampoline(void* arg) {
|
||||
if (!evt) { u_free(members); return; }
|
||||
|
||||
/* шлём мемберов по одному (RTT мог измениться у каждого) */
|
||||
uint16_t rtt_nodata = 0xFFFF;
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint16_t rtt_val;
|
||||
memcpy(&rtt_val, members + i * CHAT_MEMBER_DISPLAY_SIZE + 11, 2);
|
||||
if (rtt_val == rtt_nodata) continue;
|
||||
evt[0] = (uint8_t)ch_len;
|
||||
memcpy(evt + 1, ch_id, ch_len);
|
||||
evt[1 + ch_len] = 1;
|
||||
@@ -490,7 +398,19 @@ static void on_member_props_changed(uint64_t node_id, const char* adm_tags, cons
|
||||
}
|
||||
}
|
||||
|
||||
static void on_member_route_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, void* arg) {
|
||||
if (!group || !node || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0]) return;
|
||||
on_member_props_changed(node->node_id, NULL, group->channel_id, arg);
|
||||
}
|
||||
|
||||
void chat_member_init(struct UTUN_INSTANCE* inst) {
|
||||
member_sync_add_props_cbk(inst, on_member_props_changed, inst);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (BGP node events → MEMBER_UPDATED)");
|
||||
utun_add_nodeinfo_cbk(inst, on_member_route_changed, inst);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (group properties/routes -> complete MEMBER_UPDATED)");
|
||||
}
|
||||
|
||||
void chat_member_destroy(struct UTUN_INSTANCE* inst) {
|
||||
utun_remove_nodeinfo_cbk(inst, on_member_route_changed, inst);
|
||||
member_sync_remove_props_cbk(inst, on_member_props_changed, inst);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: properties/routes subscriptions removed");
|
||||
}
|
||||
+67
-6
@@ -3,21 +3,26 @@
|
||||
*
|
||||
* Используется chatgui (desktop), headless CLI, Android.
|
||||
* Live-снимки для всех потребителей формирует chat_core_get_member_list() в uasync-потоке.
|
||||
* Android дополняет ими свой JSON-список с расширенными свойствами участников.
|
||||
* Снимок содержит свойства участника и состояние маршрута именно его CHAT-группы.
|
||||
*
|
||||
* Формат на проводе (ручная сериализация, фиксированный размер 158 байт):
|
||||
* Формат на проводе (ручная сериализация, фиксированный размер 164 байта):
|
||||
* [0-7] node_id uint64_t LE
|
||||
* [8] online uint8_t
|
||||
* [9] is_self uint8_t
|
||||
* [10] flags uint8_t 0x01=supernode 0x02=admin 0x04=moder 0x08=deleted 0x10=radio 0x20=local radio mute
|
||||
* 0x40=phone status known, 0x80=busy on a call (live BGP hint)
|
||||
* [11-12] rtt uint16_t LE (ms, 0xFFFF=нет данных)
|
||||
* [11-12] rtt uint16_t LE (0,1 мс выбранного живого пути, 0xFFFF=нет данных)
|
||||
* [13] nat_type uint8_t ADDR_TYPE_*
|
||||
* [14] storage uint8_t
|
||||
* [15-78] name char[64] local_nick если задан, иначе node name. \0-padded
|
||||
* [79-86] avatar_server uint64_t LE; 0 = нет аватарки
|
||||
* [87-156] avatar_file char[70], sha256.json с NUL
|
||||
* [157] storage_role uint8_t, подписанная роль, независимо от сетевого приоритета storage
|
||||
* [158] verified uint8_t, административный тег verified=yes
|
||||
* [159] node_type uint8_t, вычисленный тип узла из peers
|
||||
* [160] conn_presence uint8_t, известные виды подключения NCONN_*
|
||||
* [161] conn_up uint8_t, работающие виды подключения NCONN_*
|
||||
* [162-163] intermediaries uint16_t LE, число посредников выбранного пути
|
||||
*/
|
||||
|
||||
#ifndef CHAT_MEMBER_H
|
||||
@@ -26,6 +31,8 @@
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
struct UTUN_INSTANCE;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -39,7 +46,7 @@ extern "C" {
|
||||
#define CHAT_MEMBER_FLAG_PHONE_KNOWN 0x40 /* опубликовано состояние телефонной занятости */
|
||||
#define CHAT_MEMBER_FLAG_PHONE_BUSY 0x80 /* занят звонком; цвет трубки в интерфейсе */
|
||||
|
||||
#define CHAT_MEMBER_DISPLAY_SIZE 158
|
||||
#define CHAT_MEMBER_DISPLAY_SIZE 164
|
||||
|
||||
/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */
|
||||
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count);
|
||||
@@ -47,7 +54,7 @@ int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uin
|
||||
/* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */
|
||||
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out);
|
||||
|
||||
/* Live-онлайн: узел присутствует в BGP/topo_group канала (self всегда онлайн).
|
||||
/* Live-онлайн: маршрутизатор располагает живым путём в группе канала (self всегда онлайн).
|
||||
Не читает nodes.online — статус не хранится в БД. */
|
||||
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
|
||||
|
||||
@@ -64,6 +71,11 @@ struct chat_member_display {
|
||||
uint64_t avatar_server;
|
||||
char avatar_file[70];
|
||||
uint8_t storage_role;
|
||||
uint8_t verified; /* административное подтверждение участника */
|
||||
uint8_t node_type; /* классификация узла, не состояние маршрута */
|
||||
uint8_t conn_presence; /* известные виды подключения NCONN_* */
|
||||
uint8_t conn_up; /* работающие виды подключения NCONN_* */
|
||||
uint16_t intermediaries; /* число посредников выбранного живого пути */
|
||||
};
|
||||
|
||||
void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst);
|
||||
@@ -83,8 +95,57 @@ int chat_core_toggle_member_radio_mute(struct UTUN_INSTANCE* inst, const char* c
|
||||
struct chat_member_radio_mute_req { struct UTUN_INSTANCE* inst; char ch_id[64]; uint64_t node_id; };
|
||||
void chat_core_toggle_member_radio_mute_trampoline(void* arg);
|
||||
|
||||
/* Инициализация: регистрирует node_props_changed callback для рассылки MEMBER_UPDATED. */
|
||||
/* Подписки свойств/маршрутов → MEMBER_UPDATED; вызываются в uasync при init/destroy чата. */
|
||||
void chat_member_init(struct UTUN_INSTANCE* inst);
|
||||
void chat_member_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Дополнительная диагностика карточки; online/RTT берутся только из chat_member_display.
|
||||
* Запрос выделяет GUI через u_calloc и передаёт в uasync; trampoline освобождает его.
|
||||
* Ответ CHAT_EVT_MEMBER_DETAIL всегда повторяет request_id/group_id/node_id, включая ошибки. */
|
||||
struct chat_member_detail_req {
|
||||
struct UTUN_INSTANCE* inst; /* ядро, в uasync которого выполняется сбор */
|
||||
uint64_t group_id; /* конкретная CHAT-группа */
|
||||
uint64_t node_id; /* выбранный участник */
|
||||
uint64_t request_id; /* ненулевой ID запроса, не повторяется при смене выбора */
|
||||
};
|
||||
|
||||
/* Wire LE: request:8, group:8, node:8, result:1, transport_flags:1, own_tcp:2,
|
||||
* hop_count:1, link_count:1, sock_count:1; далее hops:8*, links:64*, socks:48*.
|
||||
* transport_flags: bit0=conn_present, bit1=links_up, bit2=initialized; не доступность в группе.
|
||||
* hops — список выбранного пути от назначения к первому посреднику; у offline/self пустой.
|
||||
* Ошибка содержит только заголовок.
|
||||
* link: tcp:1,state:1,status:1,ready:1,server:1,local_family:1,local_addr:16,local_port:2,
|
||||
* remote_family:1,remote_addr:16,remote_port:2,socket_name:20,reality:1.
|
||||
* sock: id:4,port:2,links_up:2,links_down:2,ifname:38. Строки завершаются NUL. */
|
||||
#define CHAT_MEMBER_DETAIL_HEADER_SIZE 31
|
||||
#define CHAT_MEMBER_DETAIL_MAX_HOPS 16
|
||||
#define CHAT_MEMBER_DETAIL_MAX_LINKS 16
|
||||
#define CHAT_MEMBER_DETAIL_MAX_SOCKS 16
|
||||
#define CHAT_MEMBER_DETAIL_LINK_SIZE 64
|
||||
#define CHAT_MEMBER_DETAIL_SOCK_SIZE 48
|
||||
#define CHAT_MEMBER_DETAIL_MAX_SIZE (CHAT_MEMBER_DETAIL_HEADER_SIZE + 8 * CHAT_MEMBER_DETAIL_MAX_HOPS + \
|
||||
CHAT_MEMBER_DETAIL_LINK_SIZE * CHAT_MEMBER_DETAIL_MAX_LINKS + CHAT_MEMBER_DETAIL_SOCK_SIZE * CHAT_MEMBER_DETAIL_MAX_SOCKS)
|
||||
|
||||
enum chat_member_detail_result {
|
||||
CHAT_MEMBER_DETAIL_OK = 0,
|
||||
CHAT_MEMBER_DETAIL_UNAVAILABLE = 1, /* чат/группа остановлены */
|
||||
CHAT_MEMBER_DETAIL_NOT_MEMBER = 2, /* запись участника отсутствует */
|
||||
CHAT_MEMBER_DETAIL_INVALID = 3, /* неверные аргументы запроса */
|
||||
CHAT_MEMBER_DETAIL_ERROR = 4 /* ошибка чтения данных участника */
|
||||
};
|
||||
|
||||
/* Проверенный view заимствует массивы из входного пакета; живёт не дольше data. */
|
||||
struct chat_member_detail_view {
|
||||
uint64_t request_id, group_id, node_id; /* адресат ответа */
|
||||
uint8_t result, transport_flags; /* результат и состояние физического транспорта */
|
||||
uint16_t own_tcp_active; /* TCP-соединения self */
|
||||
uint8_t hop_count, link_count, sock_count; /* длины массивов */
|
||||
const uint8_t *hops, *links, *socks; /* неизменяемые wire-массивы */
|
||||
};
|
||||
|
||||
/* Проверить весь пакет до применения: 0=успех, -1=ошибка с логом; out при ошибке не меняется. */
|
||||
int chat_core_parse_member_detail(const uint8_t* data, size_t len, struct chat_member_detail_view* out);
|
||||
void chat_core_collect_member_detail_trampoline(void* arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
+127
-99
@@ -23,6 +23,7 @@
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/u_async.h"
|
||||
#include "../../lib/platform_compat.h"
|
||||
#include "../../lib/file_io.h"
|
||||
|
||||
#include <openssl/sha.h>
|
||||
#include <stdlib.h>
|
||||
@@ -156,6 +157,9 @@ void chat_core_submit_trampoline(void* arg) {
|
||||
/* ─── Медиа-сообщения: async регистрация в media_files + отправка ─── */
|
||||
|
||||
struct media_submit_ctx {
|
||||
struct media_submit_ctx* next;
|
||||
struct media_destination* destination; // Общая с загрузчиком резервация пути до завершения workers.
|
||||
int delete_pending;
|
||||
struct UTUN_INSTANCE* inst;
|
||||
char channel_id[64];
|
||||
char content_type[32];
|
||||
@@ -172,8 +176,34 @@ struct media_submit_ctx {
|
||||
|
||||
/* Завершение владеющей асинхронной отправки, включая все ошибки подготовки/регистрации. */
|
||||
static void media_submit_finish(struct media_submit_ctx* ctx, int success) {
|
||||
struct UTUN_INSTANCE* inst = ctx->inst;
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
char channel[64]; snprintf(channel, sizeof(channel), "%s", ctx->channel_id);
|
||||
/* Синхронный обработчик события может запросить удаление; ctx пока остаётся в списке владельца. */
|
||||
chat_event_submitted(ctx->inst, ctx->request_id, success);
|
||||
int delete_pending = ctx->delete_pending;
|
||||
struct media_submit_ctx** slot = &cc->media_submissions;
|
||||
while (*slot && *slot != ctx) slot = &(*slot)->next;
|
||||
if (*slot) *slot = ctx->next;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submission missing from owner ch=%s", channel);
|
||||
media_delivery_release(inst, ctx->destination);
|
||||
u_free(ctx);
|
||||
if (delete_pending) {
|
||||
for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next)
|
||||
if (!strcmp(pending->channel_id, channel)) return;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media submissions drained; continuing channel deletion ch=%s", channel);
|
||||
chat_core_delete_channel(inst, channel);
|
||||
}
|
||||
}
|
||||
|
||||
int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel) {
|
||||
struct chat_core_ctx* cc = CC(inst); int pending = 0;
|
||||
for (struct media_submit_ctx* ctx = cc->media_submissions; ctx; ctx = ctx->next) {
|
||||
if (strcmp(ctx->channel_id, channel)) continue;
|
||||
ctx->delete_pending = 1; pending++;
|
||||
}
|
||||
if (pending) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "channel deletion waiting for outgoing media ch=%s tasks=%d", channel, pending);
|
||||
return pending != 0;
|
||||
}
|
||||
|
||||
static void on_media_registered(void* arg, int err, const struct media_index_result* result) {
|
||||
@@ -186,6 +216,10 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
|
||||
media_submit_finish(mctx, 0);
|
||||
return;
|
||||
}
|
||||
if (mctx->delete_pending) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "prepared media retained for pending deletion ch=%s", mctx->channel_id);
|
||||
media_submit_finish(mctx, 0); return;
|
||||
}
|
||||
|
||||
struct DB_SYNC_INSTANCE* si = si_find(mctx->inst, mctx->channel_id);
|
||||
if (!si) {
|
||||
@@ -325,14 +359,8 @@ static void on_video_prepared(void* arg, int err, const struct video_info* vi) {
|
||||
CC_ID, vi->width, vi->height, (long long)vi->duration_ms);
|
||||
}
|
||||
|
||||
char media_base[1024];
|
||||
{
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash)
|
||||
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else
|
||||
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
}
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) { media_submit_finish(mctx, 0); return; }
|
||||
|
||||
media_index_register_async(
|
||||
mctx->inst->media_async, mctx->inst->ua, cc->db,
|
||||
@@ -350,6 +378,23 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
if (req) chat_event_submitted(inst, req->request_id, 0);
|
||||
return;
|
||||
}
|
||||
char media_base[MEDIA_BASE_MAX], location[MEDIA_PATH_MAX], destination[MEDIA_PATH_MAX];
|
||||
if (media_index_get_base(cc->db, media_base) || media_index_location(req->media_dest, media_base, location)
|
||||
|| media_index_path(media_base, location, destination)) {
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next) {
|
||||
if ((!strcmp(pending->channel_id, req->channel_id) && pending->delete_pending)
|
||||
|| !strcmp(pending->media_dest, destination)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media submit rejected: channel deleting/destination busy ch=%s path=%s",
|
||||
req->channel_id, req->media_dest);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
}
|
||||
if (!si_find(inst, req->channel_id)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit without channel ch=%s", req->channel_id);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
char reply_hex[65];
|
||||
if (msg_reply_hex(cc, req->channel_id, req->reply_to, reply_hex) < 0) {
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
@@ -358,7 +403,11 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
/* лимит одного медиафайла применяется и к локальным отправкам */
|
||||
uint64_t unit = inst->config->global.chatserver_storage_unit_size;
|
||||
if (unit > 0) {
|
||||
int64_t sz = (int64_t)ma_file_size(req->media_src);
|
||||
int64_t sz = ma_file_size(req->media_src);
|
||||
if (sz < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit source size unavailable path=%s", req->media_src);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
if (sz > 0 && (uint64_t)sz > unit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: media too large for local send (%lld > %llu bytes), rejected ch=%s",
|
||||
CC_ID, (long long)sz, (unsigned long long)unit, req->channel_id);
|
||||
@@ -377,28 +426,25 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
return;
|
||||
}
|
||||
|
||||
/* build media_base from db_path: dirname(db_path) e.g. /path/to/data */
|
||||
char media_base[1024];
|
||||
{
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash)
|
||||
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else
|
||||
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
}
|
||||
|
||||
/* copy fields for async callback (req is freed by trampoline immediately) */
|
||||
struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx));
|
||||
if (!mctx) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media context allocation failed", CC_ID);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
mctx->destination = media_delivery_reserve(inst, destination);
|
||||
if (!mctx->destination) { u_free(mctx); chat_event_submitted(inst, req->request_id, 0); return; }
|
||||
if (media_index_track_preparation(cc->db, req->channel_id, destination, media_base)) {
|
||||
media_delivery_release(inst, mctx->destination); u_free(mctx);
|
||||
chat_event_submitted(inst, req->request_id, 0); return;
|
||||
}
|
||||
mctx->inst = inst;
|
||||
mctx->request_id = req->request_id;
|
||||
mctx->next = cc->media_submissions; cc->media_submissions = mctx;
|
||||
memcpy(mctx->reply_hex, reply_hex, strlen(reply_hex) + 1);
|
||||
snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id);
|
||||
snprintf(mctx->content_type, sizeof(mctx->content_type), "%s", req->content_type);
|
||||
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", req->media_dest);
|
||||
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", destination);
|
||||
mctx->timestamp = req->timestamp;
|
||||
mctx->duration_ms = req->duration_ms;
|
||||
mctx->width = req->width;
|
||||
@@ -409,7 +455,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
}
|
||||
|
||||
if (req->media_video) {
|
||||
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, req->media_dest, on_video_prepared, mctx) < 0)
|
||||
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, destination, on_video_prepared, mctx) < 0)
|
||||
on_video_prepared(mctx, -1, NULL);
|
||||
return;
|
||||
}
|
||||
@@ -418,7 +464,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
|
||||
inst->media_async, inst->ua, cc->db,
|
||||
cc->my_node_id, inst->my_ed25519_privkey,
|
||||
req->channel_id,
|
||||
req->media_src, req->media_dest, req->media_copy ? 1 : 0,
|
||||
req->media_src, destination, req->media_copy ? 1 : 0,
|
||||
media_base,
|
||||
on_media_registered, mctx);
|
||||
}
|
||||
@@ -871,16 +917,10 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
char final_name[256];
|
||||
media_final_name(&result, content_type, base_filename, final_name, sizeof(final_name));
|
||||
|
||||
char dest[1024], media_base[512];
|
||||
{
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) {
|
||||
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
} else {
|
||||
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
}
|
||||
}
|
||||
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
char dest[MEDIA_PATH_MAX], media_base[MEDIA_BASE_MAX], relative[MEDIA_PATH_MAX];
|
||||
int length = snprintf(relative, sizeof(relative), "media/%s/%s", ch_id, final_name);
|
||||
if (length < 0 || length >= (int)sizeof(relative) || media_index_get_base(cc->db, media_base)
|
||||
|| media_index_path(media_base, relative, dest)) { media_index_result_free(&result); return -1; }
|
||||
|
||||
struct md_done_ctx* ctx = u_calloc(1, sizeof(*ctx));
|
||||
if (!ctx) { media_index_result_free(&result); return -1; }
|
||||
@@ -897,9 +937,13 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s ct=%s file=%s blocks=%d size=%lld dest=%s",
|
||||
CC_ID, ch_id, content_type ? content_type : "?", final_name, nb, (long long)fsize, dest);
|
||||
uint64_t gid = strtoull(ch_id, NULL, 10);
|
||||
media_download_start(inst, gid, &result, dest, media_base, author_node_id, md_download_done_cb, ctx, md_download_progress_cb, ctx);
|
||||
int started = media_download_start(inst, gid, &result, dest, media_base, author_node_id,
|
||||
md_download_done_cb, ctx, md_download_progress_cb, ctx);
|
||||
media_index_result_free(&result);
|
||||
return 0;
|
||||
if (started < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media download rejected ch=%s path=%s", ch_id, dest); u_free(ctx);
|
||||
}
|
||||
return started;
|
||||
}
|
||||
|
||||
static int md_auto_download(struct UTUN_INSTANCE* inst,
|
||||
@@ -1097,10 +1141,8 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!cc || !cc->initialized || !cc->db) return;
|
||||
|
||||
char media_base[512];
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
int total = 0;
|
||||
sqlite3_stmt* cs = NULL;
|
||||
@@ -1153,43 +1195,15 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
|
||||
char path[1536];
|
||||
snprintf(path, sizeof(path), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
|
||||
int fsz = ma_file_size(path);
|
||||
if (fsz <= 0 || (int64_t)fsz != result.file_size) {
|
||||
int64_t fsz = ma_file_size(path);
|
||||
if (fsz <= 0 || fsz != result.file_size) {
|
||||
u_free(body); media_index_result_free(&result);
|
||||
continue;
|
||||
}
|
||||
|
||||
int registered = 0;
|
||||
for (int n = 0; n < result.num_blocks; n++) {
|
||||
int exists = 0;
|
||||
sqlite3_stmt* ex = NULL;
|
||||
if (sqlite3_prepare_v2(cc->db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_blob(ex, 1, result.block_ids + n * 16, 16, SQLITE_STATIC);
|
||||
exists = (sqlite3_step(ex) == SQLITE_ROW);
|
||||
sqlite3_finalize(ex);
|
||||
}
|
||||
if (exists) continue;
|
||||
|
||||
char rel[512];
|
||||
size_t bl = strlen(media_base);
|
||||
if (strncmp(path, media_base, bl) == 0 && path[bl] == '/') snprintf(rel, sizeof(rel), "%s", path + bl + 1);
|
||||
else snprintf(rel, sizeof(rel), "%s", path);
|
||||
|
||||
int64_t chunk_size = (n == result.num_blocks - 1) ? result.file_size - (int64_t)n * result.block_size : result.block_size;
|
||||
int64_t offset = (int64_t)n * result.block_size;
|
||||
|
||||
if (media_index_register_downloaded(cc->db, result.media_id, result.block_ids + n * 16,
|
||||
result.content_hash, ch_id, rel, cc->my_node_id,
|
||||
result.file_size, chunk_size, n, offset) == 0) {
|
||||
registered++;
|
||||
}
|
||||
}
|
||||
|
||||
if (registered > 0) {
|
||||
total += registered;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: backfill registered %d blocks ch=%s file=%s",
|
||||
CC_ID, registered, ch_id, final_name);
|
||||
}
|
||||
int registered = media_index_commit(cc->db, &result, cc->my_node_id, NULL, ch_id, path, media_base);
|
||||
if (!registered) total += result.num_blocks;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media backfill registration failed ch=%s file=%s", ch_id, path);
|
||||
|
||||
u_free(body);
|
||||
media_index_result_free(&result);
|
||||
@@ -1290,7 +1304,7 @@ static void bf_collect(struct chat_core_ctx* cc, const char* media_base, const c
|
||||
|
||||
char full_path[1536];
|
||||
snprintf(full_path, sizeof(full_path), "%s/media/%s/%s", media_base, ch_id, final_name);
|
||||
int64_t actual = (int64_t)ma_file_size(full_path);
|
||||
int64_t actual = ma_file_size(full_path);
|
||||
if (actual < 0) actual = 0;
|
||||
|
||||
if (l->count == l->cap) {
|
||||
@@ -1331,10 +1345,8 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
|
||||
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
|
||||
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
|
||||
|
||||
char media_base[512];
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
struct bf_list list = {0};
|
||||
bf_collect(cc, media_base, NULL, &list);
|
||||
@@ -1401,10 +1413,8 @@ void chat_media_autodownload_backfill_channel(struct UTUN_INSTANCE* inst, const
|
||||
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
|
||||
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
|
||||
|
||||
char media_base[512];
|
||||
const char* last_slash = strrchr(cc->db_path, '/');
|
||||
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
|
||||
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
struct bf_list list = {0};
|
||||
bf_collect(cc, media_base, ch_id, &list);
|
||||
@@ -1444,12 +1454,6 @@ struct media_msg_file {
|
||||
|
||||
typedef void (*media_msg_file_cb)(const struct media_msg_file* f, void* arg);
|
||||
|
||||
static void chat_media_base(struct chat_core_ctx* cc, char* out, size_t out_sz) {
|
||||
const char* ls = strrchr(cc->db_path, '/');
|
||||
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - cc->db_path), cc->db_path);
|
||||
else snprintf(out, out_sz, "%s", cc->db_path);
|
||||
}
|
||||
|
||||
/* Итератор по всем медиасообщениям всех каналов (без фильтра по node_id/времени). */
|
||||
static void chat_media_foreach_media_message(struct chat_core_ctx* cc, const char* media_base, media_msg_file_cb cb, void* arg) {
|
||||
if (!cc->db || !cb) return;
|
||||
@@ -1588,13 +1592,14 @@ static void cache_rel_collect_cb(const struct media_msg_file* f, void* arg) {
|
||||
}
|
||||
|
||||
static void cache_file_collect_cb(const struct media_msg_file* f, void* arg) {
|
||||
int fsz = ma_file_size(f->full_path);
|
||||
if (fsz > 0) cache_file_add((struct cache_file_list*)arg, f->full_path, f->ts_ms, (int64_t)fsz);
|
||||
int64_t fsz = ma_file_size(f->full_path);
|
||||
if (fsz > 0) cache_file_add((struct cache_file_list*)arg, f->full_path, f->ts_ms, fsz);
|
||||
}
|
||||
|
||||
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
|
||||
static uint64_t chat_media_cache_scan_sum(struct chat_core_ctx* cc) {
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return cc->media_cache_bytes;
|
||||
struct cache_file_list files = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
|
||||
uint64_t sum = 0;
|
||||
@@ -1637,12 +1642,13 @@ static void orphan_walk_cb(void* arg, const char* full_path, int64_t size) {
|
||||
char* found = bsearch(&rel, ow->rels, (size_t)ow->rel_count, sizeof(char*), cache_str_cmp);
|
||||
if (found) return;
|
||||
|
||||
if (remove(full_path) == 0) {
|
||||
int removed = file_io_remove_orphan(full_path);
|
||||
if (removed > 0) {
|
||||
ow->removed++;
|
||||
ow->removed_bytes += (uint64_t)(size > 0 ? size : 0);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan removed: %s (%lld bytes)",
|
||||
CC_ID, rel, (long long)size);
|
||||
} else {
|
||||
} else if (removed < 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache orphan remove failed: %s", CC_ID, full_path);
|
||||
}
|
||||
}
|
||||
@@ -1651,12 +1657,33 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
struct chat_core_ctx* cc = CC(inst);
|
||||
if (!cc || !cc->initialized || !cc->db || !inst) return;
|
||||
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
char media_dir[600]; snprintf(media_dir, sizeof(media_dir), "%s/media", media_base);
|
||||
|
||||
struct file_io_info directory;
|
||||
int directory_status = file_io_inspect(media_dir, 0, &directory);
|
||||
if (directory_status == 1) return; // До первого вложения каталога может не быть.
|
||||
if (directory_status < 0) return;
|
||||
|
||||
struct cache_rel_list rels = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_rel_collect_cb, &rels);
|
||||
/* Ещё не опубликованные контейнеры и их временные файлы принадлежат crypto worker. */
|
||||
sqlite3_stmt* preparations = NULL;
|
||||
int prep_rc = sqlite3_prepare_v2(cc->db, "SELECT location FROM media_preparations UNION SELECT location FROM media_files",
|
||||
-1, &preparations, NULL);
|
||||
if (prep_rc == SQLITE_OK) {
|
||||
while ((prep_rc = sqlite3_step(preparations)) == SQLITE_ROW) {
|
||||
const char* location = (const char*)sqlite3_column_text(preparations, 0);
|
||||
if (location) cache_rel_add(&rels, location);
|
||||
}
|
||||
}
|
||||
int prep_finish = preparations ? sqlite3_finalize(preparations) : SQLITE_OK;
|
||||
if (prep_rc != SQLITE_DONE || prep_finish != SQLITE_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media orphan scan cannot protect indexed/preparing files rc=%d", prep_rc);
|
||||
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
|
||||
u_free(rels.items); return;
|
||||
}
|
||||
/* Конечные контейнеры принадлежат attachment_keys; активные temp защищены file_io-реестром. */
|
||||
if (cc->attachment_initialized) {
|
||||
sqlite3_stmt* active = NULL;
|
||||
if (sqlite3_prepare_v2(cc->db, "SELECT cipher_path FROM attachment_keys", -1, &active, NULL) == SQLITE_OK) {
|
||||
@@ -1666,8 +1693,6 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
size_t prefix = strlen(media_base);
|
||||
if (path && !strncmp(path, media_base, prefix) && path[prefix] == '/') {
|
||||
cache_rel_add(&rels, path + prefix + 1);
|
||||
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path + prefix + 1);
|
||||
cache_rel_add(&rels, temporary);
|
||||
}
|
||||
}
|
||||
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted cache: active scan failed rc=%d", rc);
|
||||
@@ -1680,10 +1705,12 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
|
||||
ow.media_base = media_base; ow.mb_len = strlen(media_base);
|
||||
ow.rels = rels.items; ow.rel_count = rels.count;
|
||||
ow.removed = 0; ow.removed_bytes = 0;
|
||||
ma_dir_walk(media_dir, orphan_walk_cb, &ow);
|
||||
int walk_result = ma_dir_walk(media_dir, orphan_walk_cb, &ow);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan cleanup done removed=%d bytes=%llu",
|
||||
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
|
||||
if (walk_result) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: cache orphan cleanup incomplete removed=%d bytes=%llu",
|
||||
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
|
||||
else DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan cleanup done removed=%d bytes=%llu",
|
||||
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
|
||||
|
||||
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
|
||||
u_free(rels.items);
|
||||
@@ -1702,7 +1729,8 @@ void chat_media_cache_enforce(struct UTUN_INSTANCE* inst) {
|
||||
return;
|
||||
}
|
||||
|
||||
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
|
||||
char media_base[MEDIA_BASE_MAX];
|
||||
if (media_index_get_base(cc->db, media_base)) return;
|
||||
|
||||
struct cache_file_list files = {0};
|
||||
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
|
||||
|
||||
@@ -36,7 +36,7 @@ struct chat_setting_def {
|
||||
|
||||
static const struct chat_setting_def g_setting_defs[] = {
|
||||
{"stream_subblocks", CHAT_SETTING_INT, 8, 1, 255},
|
||||
{"stream_rs_redundant", CHAT_SETTING_INT, 2, 0, 254},
|
||||
{"stream_rs_redundant", CHAT_SETTING_INT, 0, 0, 254},
|
||||
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
|
||||
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
|
||||
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
|
||||
|
||||
+101
-87
@@ -6,6 +6,8 @@
|
||||
|
||||
#include "chat_core_priv.h"
|
||||
#include "chat_event.h"
|
||||
#include "chat_member.h"
|
||||
#include "../member_files/protocol.h"
|
||||
|
||||
#include "../utun_instance.h"
|
||||
#include "../ntp_time.h"
|
||||
@@ -17,6 +19,7 @@
|
||||
#include "../transport_layer/stcp_link.h"
|
||||
#include "../transport_layer/pkt_normalizer.h"
|
||||
#include "../routing_layer/topo_node.h"
|
||||
#include "../routing_layer/topo_node_sqlite.h"
|
||||
#include "../routing_layer/topo_group.h"
|
||||
#include <string.h>
|
||||
|
||||
@@ -278,7 +281,11 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
|
||||
if (conn->input_queue) off += snprintf(buf + off, sizeof(buf) - off, "input_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_queue), queue_entry_count(conn->input_queue));
|
||||
if (conn->input_send_q) off += snprintf(buf + off, sizeof(buf) - off, "send_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_send_q), queue_entry_count(conn->input_send_q));
|
||||
if (conn->input_wait_ack) off += snprintf(buf + off, sizeof(buf) - off, "wait_ack: %uB/%dp\n", (unsigned)queue_total_bytes(conn->input_wait_ack), queue_entry_count(conn->input_wait_ack));
|
||||
if (conn->ack_q) off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->ack_q), queue_entry_count(conn->ack_q));
|
||||
unsigned ack_bytes = 0, ack_packets = 0;
|
||||
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
|
||||
ack_bytes += (unsigned)queue_total_bytes(link->ack_q); ack_packets += queue_entry_count(link->ack_q);
|
||||
}
|
||||
off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%up ", ack_bytes, ack_packets);
|
||||
if (conn->recv_q) off += snprintf(buf + off, sizeof(buf) - off, "recv_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->recv_q), queue_entry_count(conn->recv_q));
|
||||
if (conn->output_queue) off += snprintf(buf + off, sizeof(buf) - off, "out_q: %uB/%dp\n", (unsigned)queue_total_bytes(conn->output_queue), queue_entry_count(conn->output_queue));
|
||||
|
||||
@@ -357,20 +364,49 @@ void chat_core_collect_conn_metrics_trampoline(void* arg) {
|
||||
u_free(arg);
|
||||
}
|
||||
|
||||
/* ── Live-снапшот для панели деталей участника ──
|
||||
* packet: [node_id:8][flags:1][own_tcp_active:2][link_count:1](link:63B)*[sock_count:1](sock:48B)*
|
||||
* flags: bit0=conn_present bit1=conn_up(links_up) bit2=conn_initialized
|
||||
* link: [is_tcp:1][link_state:1][link_status:1][tcp_ready:1][is_server:1]
|
||||
* [local_family:1][local_addr:16][local_port:2]
|
||||
* [remote_family:1][remote_addr:16][remote_port:2]
|
||||
* [sock_name:20]
|
||||
* sock: [sock_id:4][port:2][links_up:2][links_down:2][ifname:38]
|
||||
* Собирается в worker-потоке — читает живые структуры UTUN_INSTANCE безопасно. */
|
||||
/* Диагностика выбранного участника собирается целиком в uasync; wire описан в chat_member.h. */
|
||||
#if CHAT_MEMBER_DETAIL_MAX_HOPS != MAX_HOPS
|
||||
#error "member detail hop limit must match routing"
|
||||
#endif
|
||||
|
||||
#define MEMBER_DETAIL_MAX_LINKS 16
|
||||
#define MEMBER_DETAIL_MAX_SOCKS 16
|
||||
#define MEMBER_DETAIL_LINK_SIZE 64
|
||||
#define MEMBER_DETAIL_SOCK_SIZE 48
|
||||
/* Проверяем размеры, адресата и все строки до изменения состояния GUI. */
|
||||
int chat_core_parse_member_detail(const uint8_t* data, size_t len, struct chat_member_detail_view* out) {
|
||||
if (!data || !out || len < CHAT_MEMBER_DETAIL_HEADER_SIZE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: missing header bytes=%zu", len); return -1;
|
||||
}
|
||||
struct chat_member_detail_view v = {0};
|
||||
v.request_id = mf_get64(data); v.group_id = mf_get64(data + 8); v.node_id = mf_get64(data + 16);
|
||||
v.result = data[24]; v.transport_flags = data[25];
|
||||
v.own_tcp_active = (uint16_t)data[26] | ((uint16_t)data[27] << 8);
|
||||
v.hop_count = data[28]; v.link_count = data[29]; v.sock_count = data[30];
|
||||
size_t expected = CHAT_MEMBER_DETAIL_HEADER_SIZE + (size_t)v.hop_count * 8 +
|
||||
(size_t)v.link_count * CHAT_MEMBER_DETAIL_LINK_SIZE + (size_t)v.sock_count * CHAT_MEMBER_DETAIL_SOCK_SIZE;
|
||||
if (!v.request_id || !v.group_id || !v.node_id || v.result > CHAT_MEMBER_DETAIL_ERROR || v.transport_flags > 7 ||
|
||||
v.hop_count > CHAT_MEMBER_DETAIL_MAX_HOPS || v.link_count > CHAT_MEMBER_DETAIL_MAX_LINKS ||
|
||||
v.sock_count > CHAT_MEMBER_DETAIL_MAX_SOCKS || expected != len ||
|
||||
(v.result && (v.transport_flags || v.own_tcp_active || v.hop_count || v.link_count || v.sock_count))) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: invalid packet request=%llu group=%016llx node=%016llx bytes=%zu expected=%zu",
|
||||
(unsigned long long)v.request_id, (unsigned long long)v.group_id, (unsigned long long)v.node_id, len, expected);
|
||||
return -1;
|
||||
}
|
||||
v.hops = data + CHAT_MEMBER_DETAIL_HEADER_SIZE; v.links = v.hops + v.hop_count * 8;
|
||||
v.socks = v.links + v.link_count * CHAT_MEMBER_DETAIL_LINK_SIZE;
|
||||
for (int i = 0; i < v.hop_count; i++) if (!mf_get64(v.hops + i * 8)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: zero hop index=%d", i); return -1;
|
||||
}
|
||||
for (int i = 0; i < v.link_count; i++) {
|
||||
const uint8_t* link = v.links + i * CHAT_MEMBER_DETAIL_LINK_SIZE;
|
||||
if (!memchr(link + 43, 0, 20) || link[0] > 1 || link[3] > 1 || link[4] > 1 || link[63] > 1 ||
|
||||
(link[5] && link[5] != 4 && link[5] != 6) || (link[24] && link[24] != 4 && link[24] != 6)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: invalid link index=%d", i); return -1;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < v.sock_count; i++) if (!memchr(v.socks + i * CHAT_MEMBER_DETAIL_SOCK_SIZE + 10, 0, 38)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: unterminated interface index=%d", i); return -1;
|
||||
}
|
||||
*out = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Повторяет Qt: name.lastIndexOf("_v") → name.mid(3, idx - 3) */
|
||||
static void sock_ifname(const char* name, char* out, size_t out_sz) {
|
||||
@@ -387,8 +423,35 @@ static void sock_ifname(const char* name, char* out, size_t out_sz) {
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id) {
|
||||
if (!CC(inst) || !CC(inst)->initialized || !inst) return;
|
||||
static void collect_member_detail(const struct chat_member_detail_req* req) {
|
||||
struct UTUN_INSTANCE* inst = req->inst;
|
||||
uint64_t node_id = req->node_id;
|
||||
uint8_t buf[CHAT_MEMBER_DETAIL_MAX_SIZE] = {0};
|
||||
mf_put64(buf, req->request_id); mf_put64(buf + 8, req->group_id); mf_put64(buf + 16, node_id);
|
||||
struct TOPO_GROUP* group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, req->group_id) : NULL;
|
||||
if (!req->request_id || !req->group_id || !node_id) buf[24] = CHAT_MEMBER_DETAIL_INVALID;
|
||||
else if (!inst || !CC(inst) || !CC(inst)->initialized || !group || group->group_type != TOPO_GROUP_TYPE_CHAT)
|
||||
buf[24] = CHAT_MEMBER_DETAIL_UNAVAILABLE;
|
||||
if (!buf[24]) {
|
||||
char table[80], sql[160]; peers_table_name(group->channel_id, table, sizeof(table));
|
||||
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", table);
|
||||
sqlite3_stmt* st = NULL;
|
||||
int rc = sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
if (rc == SQLITE_DONE) buf[24] = CHAT_MEMBER_DETAIL_NOT_MEMBER;
|
||||
else if (rc != SQLITE_ROW) {
|
||||
buf[24] = CHAT_MEMBER_DETAIL_ERROR;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: query request=%llu rc=%d: %s",
|
||||
(unsigned long long)req->request_id, rc, sqlite3_errmsg(CC(inst)->db));
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
if (buf[24]) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member detail: rejected request=%llu group=%016llx node=%016llx result=%u",
|
||||
(unsigned long long)req->request_id, (unsigned long long)req->group_id, (unsigned long long)node_id, buf[24]);
|
||||
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, CHAT_MEMBER_DETAIL_HEADER_SIZE); return;
|
||||
}
|
||||
|
||||
uint8_t flags = 0;
|
||||
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
|
||||
@@ -397,17 +460,6 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
if (conn->links_up) flags |= 2;
|
||||
if (conn->initialized) flags |= 4;
|
||||
}
|
||||
/* bit 3 (0x08): узел присутствует в BGP/topo_group — live-онлайн (self всегда онлайн) */
|
||||
{
|
||||
int bgp_present = (node_id == inst->node_id) ? 1 : 0;
|
||||
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
|
||||
for (struct ll_entry* ge = gl ? gl->head : NULL; !bgp_present && ge; ge = ge->next) {
|
||||
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data;
|
||||
if (group && topo_node_find_by_id(group, node_id)) bgp_present = 1;
|
||||
}
|
||||
if (bgp_present) flags |= 8;
|
||||
}
|
||||
|
||||
uint16_t own_tcp_active = 0;
|
||||
if (node_id == inst->node_id) {
|
||||
if (inst->connections) {
|
||||
@@ -428,23 +480,20 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
}
|
||||
|
||||
uint8_t link_count = 0;
|
||||
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < MEMBER_DETAIL_MAX_LINKS; lk = lk->next)
|
||||
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < CHAT_MEMBER_DETAIL_MAX_LINKS; lk = lk->next)
|
||||
link_count++;
|
||||
|
||||
uint8_t sock_count = 0;
|
||||
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count < MEMBER_DETAIL_MAX_SOCKS; s = s->next)
|
||||
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count < CHAT_MEMBER_DETAIL_MAX_SOCKS; s = s->next)
|
||||
sock_count++;
|
||||
|
||||
size_t buf_sz = 12 + (size_t)link_count * MEMBER_DETAIL_LINK_SIZE
|
||||
+ 1 + (size_t)sock_count * MEMBER_DETAIL_SOCK_SIZE;
|
||||
uint8_t* buf = (uint8_t*)u_malloc(buf_sz);
|
||||
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "collect_member_detail: buf alloc failed"); return; }
|
||||
|
||||
uint8_t* p = buf;
|
||||
memcpy(p, &node_id, 8); p += 8;
|
||||
*p++ = flags;
|
||||
memcpy(p, &own_tcp_active, 2); p += 2;
|
||||
*p++ = link_count;
|
||||
struct TOPO_GROUP_NODE* node = topo_node_find_by_id(group, node_id);
|
||||
uint8_t hop_count = 0;
|
||||
uint64_t* hops = topo_node_best_hop_list(node, &hop_count, NULL);
|
||||
buf[25] = flags; buf[26] = (uint8_t)own_tcp_active; buf[27] = (uint8_t)(own_tcp_active >> 8);
|
||||
buf[28] = hop_count; buf[29] = link_count; buf[30] = sock_count;
|
||||
uint8_t* p = buf + CHAT_MEMBER_DETAIL_HEADER_SIZE;
|
||||
for (int i = 0; i < hop_count; i++, p += 8) mf_put64(p, hops[i]);
|
||||
|
||||
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count; lk = lk->next, link_count--) {
|
||||
*p++ = lk->is_tcp ? 1 : 0;
|
||||
@@ -469,7 +518,7 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
lf = 6; memcpy(la, &((const struct sockaddr_in6*)lsa)->sin6_addr, 16); lp = ntohs(((const struct sockaddr_in6*)lsa)->sin6_port);
|
||||
}
|
||||
}
|
||||
*p++ = lf; memcpy(p, la, 16); p += 16; memcpy(p, &lp, 2); p += 2;
|
||||
*p++ = lf; memcpy(p, la, 16); p += 16; *p++ = (uint8_t)lp; *p++ = (uint8_t)(lp >> 8);
|
||||
}
|
||||
|
||||
{ uint8_t rf = 0; uint8_t ra[16] = {0}; uint16_t rp = 0;
|
||||
@@ -479,7 +528,7 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
} else if (sa->ss_family == AF_INET6) {
|
||||
rf = 6; memcpy(ra, &((const struct sockaddr_in6*)sa)->sin6_addr, 16); rp = ntohs(((const struct sockaddr_in6*)sa)->sin6_port);
|
||||
}
|
||||
*p++ = rf; memcpy(p, ra, 16); p += 16; memcpy(p, &rp, 2); p += 2;
|
||||
*p++ = rf; memcpy(p, ra, 16); p += 16; *p++ = (uint8_t)rp; *p++ = (uint8_t)(rp >> 8);
|
||||
}
|
||||
|
||||
{ char sn[20] = {0};
|
||||
@@ -489,7 +538,6 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
*p++ = lk->reality_set ? 1 : 0;
|
||||
}
|
||||
|
||||
*p++ = sock_count;
|
||||
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count; s = s->next, sock_count--) {
|
||||
uint32_t sid = s->sock_id;
|
||||
uint16_t sp = 0;
|
||||
@@ -514,56 +562,22 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
|
||||
}
|
||||
}
|
||||
char ifname[38] = {0}; sock_ifname(s->name, ifname, sizeof(ifname));
|
||||
memcpy(p, &sid, 4); p += 4;
|
||||
memcpy(p, &sp, 2); p += 2;
|
||||
memcpy(p, &lu, 2); p += 2;
|
||||
memcpy(p, &ld, 2); p += 2;
|
||||
for (int i = 0; i < 4; i++) *p++ = (uint8_t)(sid >> (8 * i));
|
||||
*p++ = (uint8_t)sp; *p++ = (uint8_t)(sp >> 8);
|
||||
*p++ = (uint8_t)lu; *p++ = (uint8_t)(lu >> 8);
|
||||
*p++ = (uint8_t)ld; *p++ = (uint8_t)(ld >> 8);
|
||||
memcpy(p, ifname, 38); p += 38;
|
||||
}
|
||||
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "member detail: request=%llu group=%016llx node=%016llx hops=%u links=%u socks=%u bytes=%zu",
|
||||
(unsigned long long)req->request_id, (unsigned long long)req->group_id, (unsigned long long)node_id,
|
||||
buf[28], buf[29], buf[30], (size_t)(p - buf));
|
||||
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
|
||||
u_free(buf);
|
||||
|
||||
/* summary text: direct RTT or BGP */
|
||||
if (conn && conn->links_up) {
|
||||
uint16_t rtt = conn->links ? conn->links->rtt_last : 0;
|
||||
if (rtt > 0) {
|
||||
char tbuf[32];
|
||||
int off = snprintf(tbuf, sizeof(tbuf), "direct, rtt:%ums", rtt / 10);
|
||||
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
|
||||
}
|
||||
} else if (conn && !conn->links_up && node_id != inst->node_id) {
|
||||
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
|
||||
for (struct ll_entry* ge = gl ? gl->head : NULL; ge; ge = ge->next) {
|
||||
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data;
|
||||
if (!group) continue;
|
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id);
|
||||
if (!nq) continue;
|
||||
uint8_t hc = 0;
|
||||
uint64_t* hops = topo_node_best_hop_list(nq, &hc, NULL);
|
||||
if (hc > 0 && hops) {
|
||||
char tbuf[512] = {0};
|
||||
uint16_t cr = topo_get_chain_rtt(nq);
|
||||
int off = snprintf(tbuf, sizeof(tbuf), "rtt:%ums hop:%u via bgp:", cr / 10, hc);
|
||||
for (uint8_t hi = 0; hi < hc && off < (int)sizeof(tbuf) - 24; hi++) {
|
||||
char hn[32] = {0};
|
||||
get_node_name(inst, hops[hi], hn, sizeof(hn));
|
||||
uint16_t hid = (uint16_t)(hops[hi] & 0xFFFF);
|
||||
off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off,
|
||||
" [%04X]%s", hid, hn[0] ? " " : "");
|
||||
if (hn[0]) off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, "%s", hn);
|
||||
}
|
||||
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void chat_core_collect_member_detail_trampoline(void* arg) {
|
||||
struct chat_node_arg* a = (struct chat_node_arg*)arg;
|
||||
if (!a) return;
|
||||
uint64_t node_id = a->node_id;
|
||||
collect_member_detail(a->inst, node_id);
|
||||
u_free(arg);
|
||||
struct chat_member_detail_req* req = arg;
|
||||
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: null request"); return; }
|
||||
collect_member_detail(req);
|
||||
u_free(req);
|
||||
}
|
||||
+494
-703
File diff suppressed because it is too large.
Load diff
+36
-27
@@ -1,10 +1,12 @@
|
||||
/*
|
||||
* chat_sync — связь чат-сервиса с транспортом и группами.
|
||||
* Отслеживает пиров, запускает member_sync, поддерживает кеш каналов и статус в GUI.
|
||||
* UP/REINIT/пробуждение восстанавливают только общие каналы соответствующего пира.
|
||||
* Отложенные запросы объединяются по (канал, пир); активными обменами и обновлениями владеет Merkle.
|
||||
* Вход по ссылке держит отдельный NCD handle до результата join, а не только до UP.
|
||||
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY; правила — в chat_join.h.
|
||||
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync,
|
||||
* кроме явно отмеченной обёртки, которая копирует аргументы и постит работу в этот поток.
|
||||
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync.
|
||||
* Вход из GUI/JNI через отдельный chat_join_port, который копирует ссылку и удерживает очередь до close.
|
||||
*/
|
||||
|
||||
#ifndef CHAT_SYNC_H
|
||||
@@ -20,7 +22,7 @@ extern "C" {
|
||||
struct UTUN_INSTANCE;
|
||||
struct UASYNC;
|
||||
|
||||
/* etcp service IDs (direct P2P): ETCP_RT_ID_CHAT_SYNC / ETCP_RT_ID_MEMBER_SYNC
|
||||
/* etcp service IDs (direct P2P): ETCP_ID_CHAT_SYNC / ETCP_ID_MEMBER_SYNC
|
||||
* (etcp_bind, см. etcp_api.h) */
|
||||
|
||||
/* Message types */
|
||||
@@ -32,12 +34,18 @@ struct UASYNC;
|
||||
#define CS_MSG_SYNC_DONE 0x06
|
||||
#define CS_MSG_ERROR 0x07
|
||||
#define CS_MSG_CHANNEL_INVITE 0x0F /* push-invite: inviter → joiner */
|
||||
#define CS_MSG_JOIN_INFO_REQ 0x10 /* joiner → connection: {join_key:8} */
|
||||
/* В JOIN_INFO_REQ pass_len обязателен даже при пустом пароле (0); password передаётся без NUL. */
|
||||
#define CS_MSG_JOIN_INFO_REQ 0x10 /* joiner → connection: {join_key:8, pass_len:1, password:pass_len} */
|
||||
#define CS_MSG_JOIN_INFO_RESP 0x11 /* connection → joiner: channel info + connection's member record */
|
||||
#define CS_MSG_JOIN_REQUEST 0x12 /* joiner → connection: {join_key:8, member...} */
|
||||
#define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */
|
||||
#define CS_MSG_SOUND_SETTING 0x14 /* peer → server: {sound_on:1} — per-chat play sound on message */
|
||||
|
||||
/* Управление JOIN (INFO_REQ/RESP, REQUEST, READY, ERROR): после type идёт attempt_id:8BE,
|
||||
* затем payload, описанный выше. Ненулевой случайный ID новый для каждой локальной попытки;
|
||||
* ответы проверяются по (peer, group, attempt_id, phase). CHANNEL_INVITE переносит предложение
|
||||
* с join_key; получатель создаёт собственный attempt_id при принятии предложения. */
|
||||
|
||||
/* Protocol constants */
|
||||
#define CS_SEND_DATA_MAX 32
|
||||
#define CS_MAX_SPARSE 16
|
||||
@@ -52,34 +60,34 @@ struct UASYNC;
|
||||
|
||||
/* Создать контекст, обработчики join и member_sync. 0/-1; повтор допустим. */
|
||||
int chat_sync_init(struct UTUN_INSTANCE* inst);
|
||||
/* Отменить ожидания join, синхронизацию и таймеры; не удаляет данные каналов. */
|
||||
/* uasync: закрыть приём команд и отменить локальный JOIN до остановки workers. */
|
||||
void chat_sync_quiesce(struct UTUN_INSTANCE* inst);
|
||||
/* uasync: отменить только активный request; чужая попытка сохраняется. */
|
||||
void chat_sync_cancel_join(struct UTUN_INSTANCE* inst, uint64_t request);
|
||||
/* Отменить ожидания, синхронизацию и таймеры, освободить контекст; данные каналов сохраняются. */
|
||||
void chat_sync_destroy(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Сейчас пустая функция. Для подключения использовать chat_core_connect_node с каналом. */
|
||||
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
|
||||
|
||||
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
|
||||
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
|
||||
password может быть NULL — тогда передаётся пустой пароль. */
|
||||
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
|
||||
const uint8_t* pubkey_bin,
|
||||
const uint8_t* addrs_data, int addr_count,
|
||||
int addrs_data_len,
|
||||
const char* password, uint64_t join_key);
|
||||
/* Локальная попытка JOIN: uasync. Отрицательный возврат — отказ запуска без события;
|
||||
* 0 — запущено, CHAT_EVT_CONNECT_RESULT ровно один раз, включая stop/delete.
|
||||
* request=0 — без адресата UI; ненулевые ID вызывающий не переиспользует до завершения.
|
||||
* link заимствован до возврата; request — корреляция интерфейса, не сетевой attempt_id. */
|
||||
#define CHAT_CONNECT_ERROR -1
|
||||
#define CHAT_CONNECT_TIMEOUT 2
|
||||
#define CHAT_CONNECT_INVALID -7
|
||||
#define CHAT_CONNECT_BUSY -8
|
||||
#define CHAT_CONNECT_CANCELLED -9
|
||||
int chat_sync_join_link(struct UTUN_INSTANCE* inst, uint64_t request, const char* link, size_t len);
|
||||
/* Единый формат результата: peer:8,result:4,channel:8,request:8, native; только uasync. */
|
||||
void chat_sync_join_result(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, uint64_t peer, int result);
|
||||
|
||||
/* Пригласить target_node_id в канал ch_id с адресами из invite-ссылки (inviter → joiner).
|
||||
Принимает pubkey и адреса целевого узла. Если соединения ещё нет — строит TOPO_NODE
|
||||
с адресами, удерживает NCD для join, и по UP отправляет CS_MSG_CHANNEL_INVITE.
|
||||
Если соединение уже есть — отправляет CHANNEL_INVITE сразу.
|
||||
Копирует pubkey_bin и addrs_data (вызывающий может освободить после возврата).
|
||||
Вызов из любого потока — внутри постит в uasync. */
|
||||
void chat_sync_invite_to_channel_with_addrs(
|
||||
struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id,
|
||||
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
|
||||
int addrs_data_len);
|
||||
/* uasync: пригласить адресата ссылки в свой channel; те же правила запуска/результата. */
|
||||
int chat_sync_offer_link(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, const char* link, size_t len);
|
||||
|
||||
/* Пригласить target_node_id в канал ch_id (inviter → joiner).
|
||||
Отправляет CS_MSG_CHANNEL_INVITE с полной информацией о канале и своих адресах */
|
||||
Отправляет CS_MSG_CHANNEL_INVITE с описанием канала, подписями/профилем инвайтера и join_key. */
|
||||
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
|
||||
|
||||
/* Принять входящее приглашение — продолжить протокол из chat_join.h. */
|
||||
@@ -95,12 +103,13 @@ void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
|
||||
это само по себе не даёт членства на других узлах. */
|
||||
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
|
||||
|
||||
/* Перезапустить групповые подключения для каналов без активных соединений.
|
||||
/* Снять паузу и сбросить задержки повторных попыток во всех кешированных каналах с автоматическим планировщиком.
|
||||
Планировщик пересматривает подключения; работающие соединения сохраняются.
|
||||
Вызывается из auto_socket при появлении/изменении сетевой связности. */
|
||||
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst);
|
||||
|
||||
/* Немедленно убрать канал из кеша chat_sync (локальное удаление канала),
|
||||
* чтобы cs_on_conn_status не запускал member_sync для удалённого ns до следующего refresh. */
|
||||
/* Отменить отложенные запросы канала и немедленно убрать его из кеша (локальное удаление).
|
||||
* Активные Merkle-сессии отменяет вызывающий код через member_sync_cancel_channel(). */
|
||||
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id);
|
||||
|
||||
/* Разослать всем подключённым пирам канала смену per-chat настройки «play sound on message». */
|
||||
|
||||
+4
-4
@@ -769,14 +769,14 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
|
||||
// Send
|
||||
// ============================================================
|
||||
|
||||
// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_RT_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send
|
||||
// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send
|
||||
static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group_id, const uint8_t* payload, size_t plen)
|
||||
{
|
||||
struct ll_entry* entry = queue_entry_new(0);
|
||||
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "queue_entry_new"); return -1; }
|
||||
uint8_t* buf = u_malloc(plen + 9);
|
||||
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync_send_gid: u_malloc(%zu) failed for dst=%016llx", plen + 9, (unsigned long long)node_id); queue_entry_free(entry); return -1; }
|
||||
buf[0] = ETCP_RT_ID_DB_SYNC;
|
||||
buf[0] = ETCP_ID_DB_SYNC;
|
||||
uint64_t gb = htobe64(group_id);
|
||||
memcpy(buf + 1, &gb, 8);
|
||||
memcpy(buf + 9, payload, plen);
|
||||
@@ -1750,7 +1750,7 @@ int db_sync_enable(struct UTUN_INSTANCE* inst) {
|
||||
}
|
||||
|
||||
db->enabled = 1;
|
||||
etcp_bind(inst, ETCP_RT_ID_DB_SYNC, db_sync_recv_cb);
|
||||
etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: initialized (enabled=%d)", db->enabled);
|
||||
return 0;
|
||||
@@ -1764,7 +1764,7 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst)
|
||||
struct DB_SYNC* db = inst->db_sync;
|
||||
inst->db_sync = NULL;
|
||||
|
||||
etcp_unbind(inst, ETCP_RT_ID_DB_SYNC);
|
||||
etcp_unbind(inst, ETCP_ID_DB_SYNC);
|
||||
|
||||
if (db->peer_check_timer) { uasync_cancel_timeout(inst->ua, db->peer_check_timer); db->peer_check_timer = NULL; }
|
||||
#ifdef UTUN_HAVE_STANDBY
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ extern "C" {
|
||||
struct UTUN_INSTANCE;
|
||||
struct DB_SYNC_INSTANCE;
|
||||
|
||||
// etcp service ID: ETCP_RT_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
|
||||
// etcp service ID: ETCP_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
|
||||
|
||||
// Message types
|
||||
#define DB_MSG_INIT_SYNC 0x01
|
||||
|
||||
@@ -99,8 +99,8 @@ chain_hash[0] = SHA256(0x00..00 [32] || id_0 || ts_0 || author_0 || sig_0)
|
||||
|
||||
### 1.4 Интеграция с ETCP
|
||||
|
||||
- **Service ID:** `ETCP_RT_ID_DB_SYNC = 0x20`
|
||||
- **Биндинг:** `etcp_bind(inst, 0x20, db_sync_recv_cb)` — приём сообщений
|
||||
- **Service ID:** `ETCP_ID_DB_SYNC = 0x20`
|
||||
- **Биндинг:** `etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb)` — приём сообщений
|
||||
- **Connection callbacks:** `etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL)` — отслеживание UP/DOWN соединений
|
||||
- **Отправка:** через `etcp_send()` в прямое P2P-соединение
|
||||
- **Маршрутизация:** каждый пакет начинается с `[svc:1][hash_be:8]`, hash идентифицирует инстанс на приёмной стороне
|
||||
|
||||
Loaded 100 of 608 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user