Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
830c7bfaa2
|
||
|
|
04f8b2fdc5
|
||
|
|
1ff838bdfa
|
||
|
|
292d1d1e3a
|
||
|
|
ad186c5d34
|
||
|
|
6c007c0e35
|
||
|
|
7e0f33be23
|
||
|
|
f905524fb4
|
||
|
|
47cf57771a
|
||
|
|
b431ab02d3
|
||
|
|
bb1df87280
|
||
|
|
58fc1a2be9
|
||
|
|
089ffc78f6
|
||
|
|
825f2409b9
|
||
|
|
bf3cbeaaba
|
||
|
|
0ca581f1e6
|
||
|
|
4b10dcc7f3
|
||
|
|
f3cecca1f8
|
||
|
|
1bb468c84e
|
||
|
|
41ac4a69f0
|
||
|
|
eaf806e02d
|
||
|
|
d033dd17c7
|
||
|
|
0c6c866142
|
||
|
|
9ca70847f9
|
||
|
|
a76d47b6cf
|
||
|
|
8ddce8d648
|
||
|
|
97ead3487e
|
||
|
|
04083cb309
|
||
|
|
3d32669431
|
||
|
|
eb5e07c9ff
|
||
|
|
6ad9b29ba1
|
||
|
|
9135b74484
|
||
|
|
d7277fb22d
|
||
|
|
7cfd24f042
|
||
|
|
58bd699c3f
|
||
|
|
bdc16b19e1
|
||
|
|
ea2de5d17c
|
||
|
|
380b1f9e6e
|
||
|
|
ed6370b9f9
|
||
|
|
e7b60ba3ea
|
||
|
|
fbc63d0a54
|
||
|
|
148a39b591
|
||
|
|
2073af147b
|
||
|
|
605fde2d7c
|
||
|
|
593799a0a6
|
||
|
|
56875e46b3
|
||
|
|
bfcbf268fc
|
||
|
|
cedc003f08
|
||
|
|
3dc24d56e1
|
||
|
|
6774c2070b
|
||
|
|
0942a9bba7
|
||
|
|
3d32cccc7e
|
||
|
|
30b77d2dbe
|
||
|
|
9922e25d38
|
||
|
|
5fbd775b9a
|
||
|
|
fe2998aea6
|
||
|
|
a2ab36408d
|
||
|
|
88cc9fbd8f
|
||
|
|
6e75e3d19c
|
||
|
|
2f4afcd5e8
|
||
|
|
dcd4b8f0f4
|
||
|
|
b854f1f858
|
||
|
|
576e73b5dc
|
||
|
|
ba4e63c401
|
||
|
|
748fb94562
|
||
|
|
86e0f30e58
|
||
|
|
c275f8cb7e
|
||
|
|
2c4f0bf848
|
||
|
|
744680a93a
|
||
|
|
e560f9fc58
|
||
|
|
237f42ef72
|
||
|
|
f9ac1f79f5
|
||
|
|
266ceac297
|
||
|
|
003ec83a87
|
||
|
|
9b388c554d
|
||
|
|
f420d72906
|
||
|
|
f1ea051b85
|
||
|
|
6d85dfc7ce
|
||
|
|
251b2eada4
|
||
|
|
e513fdde51
|
||
|
|
b95a45496b
|
||
|
|
844d390017
|
||
|
|
bbc0d1352d
|
||
|
|
4a59ec58bd
|
||
|
|
9c4e25fd36
|
||
|
|
2e610bc8ef
|
||
|
|
2789be852e
|
||
|
|
32da27567c
|
||
|
|
9b078b02d7
|
||
|
|
df34d2c1fe
|
||
|
|
5226a9542c
|
||
|
|
091a35009b
|
||
|
|
0f8df69f6b
|
||
|
|
3193c7c327
|
||
|
|
dcd4619d63
|
||
|
|
b9e0f0fc19
|
||
|
|
5a7428ac9b
|
||
|
|
6582754025
|
||
|
|
1e152c4641
|
||
|
|
2d745d41b6
|
||
|
|
bebb68d3b2
|
||
|
|
21a1088ea4
|
||
|
|
1799547606
|
||
|
|
d83ce3bdc3
|
||
|
|
892fabe501
|
||
|
|
fbf2970f88
|
||
|
|
cf52a2fc2d
|
||
|
|
4112509fe3
|
||
|
|
7c3ef72447
|
||
|
|
d817bb15e8
|
||
|
|
e69983ee85
|
||
|
|
dfd0a4086b
|
||
|
|
dda8e6b992
|
||
|
|
dbf39d025a
|
||
|
|
04ce2efbeb
|
||
|
|
1e8686c2a4
|
||
|
|
8c8dd37ce0
|
||
|
|
e45eb6082b
|
||
|
|
cc7905e6ee
|
||
|
|
643a66a0b9
|
||
|
|
dcb15deb4b
|
||
|
|
b535ee5f7e
|
||
|
|
0ad395b949
|
||
|
|
ae19dbeb0d
|
||
|
|
abdaa277f8
|
||
|
|
194043a892
|
||
|
|
9331d9fdf0
|
||
|
|
f4bbc2f2c3
|
||
|
|
47f75460f7
|
||
|
|
10ea7b1034
|
||
|
|
36d6831ce8
|
||
|
|
430d69695c
|
||
|
|
63d238171f
|
||
|
|
94ca975d56
|
||
|
|
c9edcd0a40
|
||
|
|
67bd1d9a1c
|
||
|
|
3d5c2ee537
|
||
|
|
dd8f28621f
|
||
|
|
4c0dd1b7d9
|
||
|
|
99f86e359f
|
||
|
|
07df89653c
|
||
|
|
8af0639490
|
||
|
|
b89637b195
|
||
|
|
b3f4f48251
|
||
|
|
a59cd12c01
|
||
|
|
e9f12b556c
|
||
|
|
07cb9480c2
|
||
|
|
c57d64040a
|
||
|
|
013ba8b749
|
||
|
|
3d8f06db85
|
||
|
|
a640960383
|
||
|
|
c45aad9f67
|
||
|
|
c793672933
|
||
|
|
2504a03680
|
||
|
|
4f56aece00
|
||
|
|
be70c88036
|
||
|
|
b029d9f732
|
||
|
|
feb1e1ae5c
|
||
|
|
415304b7a4
|
||
|
|
2bb2c2f4a4
|
||
|
|
e15eb7a9a5
|
||
|
|
f30deb0614
|
||
|
|
caf74d924d
|
||
|
|
05c30a2e75
|
||
|
|
3f38ac3827
|
||
|
|
4dfdcf42f5
|
||
|
|
5168e4edf0
|
||
|
|
c878ece7c8
|
||
|
|
1831356cee
|
||
|
|
7afc408587
|
||
|
|
77d4b19f9c
|
||
|
|
511a5b40ca
|
||
|
|
9637d6f9ee
|
||
|
|
5b6e25d73b
|
||
|
|
41d9c02331
|
||
|
|
b00676261d
|
||
|
|
db1a470d94
|
||
|
|
622f2a467d
|
||
|
|
090a141307
|
||
|
|
41d29438c1
|
||
|
|
eb59fdb477
|
||
|
|
de3d7b60d7
|
||
|
|
2616a0ebe6
|
||
|
|
3ae8ac3c7a
|
||
|
|
d674c7fd4d
|
||
|
|
b601c324b6
|
||
|
|
22e132db0a
|
||
|
|
4e01310b5f
|
||
|
|
8db00715b1
|
||
|
|
42459df920
|
||
|
|
8eed513497
|
||
|
|
f280648153
|
||
|
|
0b9da16ff3
|
||
|
|
70b784c2f4
|
||
|
|
38b6ce50c6
|
||
|
|
d6e52d7720
|
||
|
|
99862137f1
|
||
|
|
7415c7e217
|
||
|
|
6d2b536050
|
||
|
|
0481463c07
|
||
|
|
810e669118
|
||
|
|
63c464b51a
|
||
|
|
433931477e
|
||
|
|
dbc1789ae1
|
||
|
|
2f4eb5d047
|
||
|
|
ef9dde3da5 | ||
|
|
723aa66e69 | ||
|
|
0b28ed6f89
|
||
|
|
d54ff72565
|
||
|
|
a2bd36f570
|
||
|
|
50c4bc5bba
|
||
|
|
3cef2ee01b
|
||
|
|
0bd294582b
|
||
|
|
49149413d5
|
||
|
|
108f648517
|
||
|
|
aa4afbf6a2
|
||
|
|
d34c082ef3
|
||
|
|
d48427abb2
|
||
|
|
ce8eb1c452
|
||
|
|
d0e6c78cc1
|
||
|
|
75b8b78b24
|
||
|
|
cac19c10c3
|
||
|
|
d806c5e9e7
|
||
|
|
87975760ae
|
||
|
|
6a4b1998fc
|
||
|
|
d7db56eb5a
|
||
|
|
d017d9eefd
|
||
|
|
97c3ed30e9
|
||
|
|
52964c2ff0
|
||
|
|
be44b23d70
|
||
|
|
0590eebd44
|
||
|
|
11f5f1dd64
|
||
|
|
5ffaec0fb1
|
||
|
|
24e9bde5c7
|
||
|
|
64bb8e3a12
|
||
|
|
baddf23df5
|
||
|
|
fc5847d6d8
|
||
|
|
84ac3dc9b8
|
||
|
|
af3e178c88
|
||
|
|
7d084db107
|
||
|
|
fcab832be1
|
||
|
|
e8cf0a3517
|
||
|
|
808a02ddf5
|
||
|
|
5df288b577
|
||
|
|
0a10c61c59
|
||
|
|
247a9a8770
|
||
|
|
63d4b50461
|
||
|
|
8741cb6142
|
||
|
|
091b5c011a
|
||
|
|
c43f8e5458
|
||
|
|
6f6dab1c04
|
||
|
|
f6f2470e5b
|
||
|
|
2275133638
|
||
|
|
da34dc4b7d
|
||
|
|
07a7a19929
|
||
|
|
a7290f253e
|
||
|
|
0b1a4efd09
|
||
|
|
aaa56b928f
|
||
|
|
8dc527b06b
|
||
|
|
004af69d8f
|
||
|
|
a397539c69
|
||
|
|
864ac15282
|
||
|
|
ff2f226713
|
||
|
|
8177d0f56d
|
||
|
|
b606108871
|
||
|
|
be09b39bbe
|
||
|
|
fafb9d5a5c
|
||
|
|
998d52b56c
|
||
|
|
3e6a82764a
|
||
|
|
3cd8161afb
|
||
|
|
02b8de6d18
|
||
|
|
7a321eae48
|
||
|
|
ad19268e5a
|
||
|
|
8bc170e170
|
||
|
|
1d618d1624
|
||
|
|
9eb3bde649
|
||
|
|
62ef4c733c
|
||
|
|
27dbd31434
|
||
|
|
74ef1a7b4d
|
||
|
|
43282354cd
|
||
|
|
f92d92aec0
|
||
|
|
3a00c47125
|
||
|
|
bca3395cb9
|
||
|
|
766cb78377
|
||
|
|
84acb67061
|
||
|
|
9c07251669
|
||
|
|
19272d6cbe
|
||
|
|
f268cfeda8
|
||
|
|
37b5d4e4d2
|
||
|
|
29bd71ccc8
|
||
|
|
d5f4891d4e
|
||
|
|
9a500b9191
|
||
|
|
180fa6aa7d
|
||
|
|
4325118562
|
||
|
|
2adf3f9efe
|
||
|
|
648bbb8a63
|
||
|
|
f73bba3e20
|
||
|
|
e321fd7e28
|
||
|
|
6215f061c8
|
||
|
|
72a948da68
|
||
|
|
c69a8543c6
|
||
|
|
c16e50157e
|
||
|
|
2cd705b5ba
|
||
|
|
f70e75f1a6
|
||
|
|
861cf9a2da
|
||
|
|
437977ef63
|
||
|
|
123c248f43
|
||
|
|
1461cc41c6
|
||
|
|
34af75245e
|
||
|
|
6e3d5fc4c5
|
||
|
|
16d5f214d8
|
||
|
|
f669130e33
|
@@ -21,6 +21,14 @@ test.py
|
||||
test/
|
||||
note.md
|
||||
MCE/config.json
|
||||
resources/
|
||||
locales/
|
||||
*.pak
|
||||
*.dll
|
||||
*.dat
|
||||
v8_context_snapshot.bin
|
||||
snapshot_blob.bin
|
||||
vk_swiftshader_icd.json
|
||||
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
|
||||
from tasks.login.login import Login
|
||||
Login(self.config, device=self.device).app_start()
|
||||
|
||||
def stop(self):
|
||||
from tasks.login.login import Login
|
||||
Login(self.config, device=self.device).app_stop()
|
||||
|
||||
def goto_main(self):
|
||||
from tasks.login.login import Login
|
||||
from tasks.base.ui import UI
|
||||
@@ -56,7 +60,7 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
|
||||
|
||||
def momotalk(self):
|
||||
from tasks.momotalk.momotalk import MomoTalk
|
||||
MomoTalk(config=self.config, device=self.device).run()
|
||||
MomoTalk(config=self.config, device=self.device, task="Momotalk").run()
|
||||
|
||||
def mission(self):
|
||||
from tasks.mission.mission import Mission
|
||||
@@ -70,6 +74,10 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
|
||||
from tasks.item.data_update import DataUpdate
|
||||
DataUpdate(config=self.config, device=self.device).run()
|
||||
|
||||
def benchmark(self):
|
||||
from module.daemon.benchmark import run_benchmark
|
||||
run_benchmark(config=self.config)
|
||||
|
||||
if __name__ == '__main__':
|
||||
aas = ArisuAutoSweeper('aas')
|
||||
aas.loop()
|
||||
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -315,6 +315,16 @@ pre.rich-traceback-code {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.anim-rotate {
|
||||
animation: rotate-keyframes 0.4s linear infinite
|
||||
}
|
||||
|
||||
@keyframes rotate-keyframes {
|
||||
100% {
|
||||
transform: rotate(360deg)
|
||||
}
|
||||
}
|
||||
|
||||
#pywebio-scope-contents {
|
||||
margin-top: 0;
|
||||
overflow-y: auto;
|
||||
@@ -377,6 +387,7 @@ pre.rich-traceback-code {
|
||||
#pywebio-scope-scheduler-bar,
|
||||
#pywebio-scope-log-bar,
|
||||
#pywebio-scope-log,
|
||||
#pywebio-scope-daemon-log-bar,
|
||||
#pywebio-scope-daemon-overview #pywebio-scope-groups {
|
||||
font-weight: 500;
|
||||
margin: 0.3125rem;
|
||||
@@ -407,13 +418,13 @@ pre.rich-traceback-code {
|
||||
#pywebio-scope-dashboard > i {
|
||||
flex-grow: 1;
|
||||
align-self: flex-end;
|
||||
width: 10rem;
|
||||
width: 9.5rem;
|
||||
}
|
||||
|
||||
[id^="pywebio-scope-dashboard-row-"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
width: 10rem;
|
||||
width: 9.5rem;
|
||||
}
|
||||
|
||||
.dashboard-icon {
|
||||
@@ -435,16 +446,26 @@ pre.rich-traceback-code {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
*[style*="--dashboard-bold--"] {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
color: #7a77bb;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
[id^="pywebio-scope-dashboard-row-"] p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
[id^="pywebio-scope-dashboard-value-"] {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
align-items: baseline;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
[id^="pywebio-scope-arg_stored-stored-value-"] p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#pywebio-scope-log {
|
||||
line-height: 1.2;
|
||||
@@ -549,19 +570,19 @@ pre.rich-traceback-code {
|
||||
padding-right: .25rem;
|
||||
}
|
||||
|
||||
*[style*="--loading-grow--"] {
|
||||
*[style*="--loading-grow--"] > div {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
*[style*="--loading-border--"] {
|
||||
*[style*="--loading-border--"] > div {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: .2em solid currentColor;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
*[style*="--loading-border-fill--"] {
|
||||
*[style*="--loading-border-fill--"] > div {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: .2em solid currentColor;
|
||||
|
||||
@@ -63,18 +63,18 @@ select {
|
||||
background-image: url("data:image/png;base64,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");
|
||||
}
|
||||
|
||||
.state > select {
|
||||
select.state {
|
||||
border-bottom: 0;
|
||||
background-image: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.state-bold > select {
|
||||
select.state-bold {
|
||||
font-weight: bold;
|
||||
color: #7a77bb;
|
||||
}
|
||||
|
||||
.state-light > select {
|
||||
select.state-light {
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
@@ -133,12 +133,13 @@ pre.rich-traceback-code {
|
||||
color: #c9d1d9;
|
||||
}
|
||||
|
||||
#pywebio-scope-scheduler-bar,
|
||||
#pywebio-scope-log-bar,
|
||||
#pywebio-scope-log,
|
||||
#pywebio-scope-running,
|
||||
#pywebio-scope-pending,
|
||||
#pywebio-scope-waiting,
|
||||
#pywebio-scope-scheduler-bar,
|
||||
#pywebio-scope-log-bar,
|
||||
#pywebio-scope-log,
|
||||
#pywebio-scope-daemon-log-bar,
|
||||
#pywebio-scope-daemon-overview #pywebio-scope-groups {
|
||||
background-color: #2f3136;
|
||||
border: 1px solid #21262d;
|
||||
|
||||
@@ -64,18 +64,18 @@ select {
|
||||
background-image: url("data:image/png;base64,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");
|
||||
}
|
||||
|
||||
.state > select {
|
||||
select.state {
|
||||
border-bottom: 0;
|
||||
background-image: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.state-bold > select {
|
||||
select.state-bold {
|
||||
font-weight: bold;
|
||||
color: #7a77bb;
|
||||
}
|
||||
|
||||
.state-light > select {
|
||||
select.state-light {
|
||||
color: #777777;
|
||||
}
|
||||
|
||||
@@ -133,12 +133,13 @@ pre.rich-traceback-code {
|
||||
border: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
#pywebio-scope-scheduler-bar,
|
||||
#pywebio-scope-log-bar,
|
||||
#pywebio-scope-log,
|
||||
#pywebio-scope-running,
|
||||
#pywebio-scope-pending,
|
||||
#pywebio-scope-waiting,
|
||||
#pywebio-scope-scheduler-bar,
|
||||
#pywebio-scope-log-bar,
|
||||
#pywebio-scope-log,
|
||||
#pywebio-scope-daemon-log-bar,
|
||||
#pywebio-scope-daemon-overview #pywebio-scope-groups {
|
||||
background-color: white;
|
||||
border: 1px solid lightgrey;
|
||||
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -157,6 +157,14 @@ Deploy:
|
||||
# '["alas"]' specified "alas" config
|
||||
# '["alas","alas2"]' specified "alas" "alas2" configs
|
||||
Run: null
|
||||
# SSL support
|
||||
# Only effective when both parameters below are set
|
||||
# --ssl-key. Path to SSL key file
|
||||
# [Default] null (no SSL)
|
||||
WebuiSSLKey: null
|
||||
# --ssl-cert. Path to SSL cert file
|
||||
# [Default] null (no SSL)
|
||||
WebuiSSLCert: null
|
||||
# To update app.asar
|
||||
# [In most cases] true
|
||||
AppAsarUpdate: true
|
||||
|
||||
@@ -157,6 +157,14 @@ Deploy:
|
||||
# '["alas"]' specified "alas" config
|
||||
# '["alas","alas2"]' specified "alas" "alas2" configs
|
||||
Run: null
|
||||
# SSL support
|
||||
# Only effective when both parameters below are set
|
||||
# --ssl-key. Path to SSL key file
|
||||
# [Default] null (no SSL)
|
||||
WebuiSSLKey: null
|
||||
# --ssl-cert. Path to SSL cert file
|
||||
# [Default] null (no SSL)
|
||||
WebuiSSLCert: null
|
||||
# To update app.asar
|
||||
# [In most cases] true
|
||||
AppAsarUpdate: true
|
||||
|
||||
@@ -250,11 +250,8 @@
|
||||
}
|
||||
},
|
||||
"Momotalk": {
|
||||
"Scheduler": {
|
||||
"Enable": false,
|
||||
"NextRun": "2020-01-01 00:00:00",
|
||||
"Command": "Momotalk",
|
||||
"ServerUpdate": "04:00"
|
||||
"Momotalk": {
|
||||
"Enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import os
|
||||
import time
|
||||
import typing as t
|
||||
from typing import Iterable
|
||||
|
||||
from deploy.Windows.config import DeployConfig
|
||||
from deploy.Windows.logger import Progress, logger
|
||||
from deploy.Windows.utils import DataProcessInfo, cached_property, iter_process
|
||||
from deploy.Windows.utils import cached_property, iter_process
|
||||
|
||||
|
||||
class AlasManager(DeployConfig):
|
||||
@@ -19,43 +19,45 @@ class AlasManager(DeployConfig):
|
||||
def self_pid(self):
|
||||
return os.getpid()
|
||||
|
||||
def list_process(self) -> t.List[DataProcessInfo]:
|
||||
def list_process(self) -> "list[tuple[int, list[str]]]":
|
||||
logger.info('List process')
|
||||
process = list(iter_process())
|
||||
logger.info(f'Found {len(process)} processes')
|
||||
return process
|
||||
process_data = list(iter_process())
|
||||
logger.info(f'Found {len(process_data)} processes')
|
||||
return process_data
|
||||
|
||||
def iter_process_by_names(self, names, in_alas=False) -> t.Iterable[DataProcessInfo]:
|
||||
def iter_process_by_names(self, names, in_alas=False) -> "Iterable[int]":
|
||||
"""
|
||||
Args:
|
||||
names (str, list[str]): process name, such as 'alas.exe'
|
||||
in_alas (bool): If the output process must in Alas
|
||||
|
||||
Yields:
|
||||
DataProcessInfo:
|
||||
pid:
|
||||
"""
|
||||
if not isinstance(names, list):
|
||||
names = [names]
|
||||
try:
|
||||
for proc in self.list_process():
|
||||
for pid, cmdline in self.list_process():
|
||||
if pid == self.self_pid:
|
||||
continue
|
||||
exe = cmdline[0]
|
||||
name = os.path.basename(exe)
|
||||
if not (name and name in names):
|
||||
continue
|
||||
|
||||
if not (proc.name and proc.name in names):
|
||||
continue
|
||||
if proc.pid == self.self_pid:
|
||||
continue
|
||||
if in_alas:
|
||||
cmdline = proc.cmdline.replace(r"\\", "/").replace("\\", "/")
|
||||
exe = exe.replace(r"\\", "/").replace("\\", "/")
|
||||
for folder in self.alas_folder:
|
||||
if folder in cmdline:
|
||||
yield proc
|
||||
if folder in exe:
|
||||
yield pid
|
||||
else:
|
||||
yield proc
|
||||
yield pid
|
||||
except Exception as e:
|
||||
logger.info(str(e))
|
||||
return False
|
||||
|
||||
def kill_process(self, process: DataProcessInfo):
|
||||
self.execute(f'taskkill /f /t /pid {process.pid}', allow_failure=True, output=False)
|
||||
def kill_process(self, pid: int):
|
||||
self.execute(f'taskkill /f /t /pid {pid}', allow_failure=True, output=False)
|
||||
|
||||
def alas_kill(self):
|
||||
for _ in range(10):
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
from typing import Iterable, Union
|
||||
|
||||
IS_WINDOWS = os.name == 'nt'
|
||||
# Max attempt if another process is reading/writing, effective only on Windows
|
||||
WINDOWS_MAX_ATTEMPT = 5
|
||||
# Base time to wait between retries (seconds)
|
||||
WINDOWS_RETRY_DELAY = 0.05
|
||||
|
||||
|
||||
def random_id():
|
||||
"""
|
||||
Returns:
|
||||
str: Random ID, like "sTD2kF"
|
||||
"""
|
||||
# 6 random letter (62^6 combinations) would be enough
|
||||
return ''.join(random.sample(string.ascii_letters + string.digits, 6))
|
||||
|
||||
|
||||
def is_tmp_file(file: str) -> bool:
|
||||
"""
|
||||
Check if a filename is tmp file
|
||||
"""
|
||||
# Check suffix first to reduce regex calls
|
||||
if not file.endswith('.tmp'):
|
||||
return False
|
||||
# Check temp file format
|
||||
dot = file[-11:-10]
|
||||
if not dot:
|
||||
return False
|
||||
rid = file[-10:-4]
|
||||
return rid.isalnum()
|
||||
|
||||
|
||||
def to_tmp_file(file: str) -> str:
|
||||
"""
|
||||
Convert a filename or directory name to tmp
|
||||
filename -> filename.sTD2kF.tmp
|
||||
"""
|
||||
suffix = random_id()
|
||||
return f'{file}.{suffix}.tmp'
|
||||
|
||||
|
||||
def to_nontmp_file(file: str) -> str:
|
||||
"""
|
||||
Convert a tmp filename or directory name to original file
|
||||
filename.sTD2kF.tmp -> filename
|
||||
"""
|
||||
if is_tmp_file(file):
|
||||
return file[:-11]
|
||||
else:
|
||||
return file
|
||||
|
||||
|
||||
def windows_attempt_delay(attempt: int) -> float:
|
||||
"""
|
||||
Exponential Backoff if file is in use on Windows
|
||||
|
||||
Args:
|
||||
attempt: Current attempt, starting from 0
|
||||
|
||||
Returns:
|
||||
float: Seconds to wait
|
||||
"""
|
||||
return 2 ** attempt * WINDOWS_RETRY_DELAY
|
||||
|
||||
|
||||
def replace_tmp(tmp: str, file: str):
|
||||
"""
|
||||
Replace temp file to file
|
||||
|
||||
Raises:
|
||||
PermissionError: (Windows only) If another process is still reading the file and all retries failed
|
||||
FileNotFoundError: If tmp file gets deleted unexpectedly
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is reading
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
# Atomic operation
|
||||
os.replace(tmp, file)
|
||||
# success
|
||||
return
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
# tmp file gets deleted unexpectedly
|
||||
raise
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
break
|
||||
else:
|
||||
# Linux and Mac allow existing reading
|
||||
try:
|
||||
# Atomic operation
|
||||
os.replace(tmp, file)
|
||||
# success
|
||||
return
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
|
||||
# Clean up tmp file on failure
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except FileNotFoundError:
|
||||
# tmp file already get deleted
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
|
||||
|
||||
def atomic_replace(replace_from: str, replace_to: str):
|
||||
"""
|
||||
Replace file or directory
|
||||
|
||||
Raises:
|
||||
PermissionError: (Windows only) If another process is still reading the file and all retries failed
|
||||
FileNotFoundError:
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is reading
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
# Atomic operation
|
||||
os.replace(replace_from, replace_to)
|
||||
# success
|
||||
return
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
break
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
else:
|
||||
# Linux and Mac
|
||||
os.replace(replace_from, replace_to)
|
||||
|
||||
|
||||
def file_write(file: str, data: Union[str, bytes]):
|
||||
"""
|
||||
Write data into file, auto create directory
|
||||
Auto determines write mode based on the type of data.
|
||||
"""
|
||||
if isinstance(data, str):
|
||||
mode = 'w'
|
||||
encoding = 'utf-8'
|
||||
newline = ''
|
||||
elif isinstance(data, bytes):
|
||||
mode = 'wb'
|
||||
encoding = None
|
||||
newline = None
|
||||
# Create memoryview as Pathlib do
|
||||
data = memoryview(data)
|
||||
else:
|
||||
typename = str(type(data))
|
||||
if typename == "<class 'numpy.ndarray'>":
|
||||
mode = 'wb'
|
||||
encoding = None
|
||||
newline = None
|
||||
else:
|
||||
mode = 'w'
|
||||
encoding = 'utf-8'
|
||||
newline = ''
|
||||
|
||||
try:
|
||||
# Write temp file
|
||||
with open(file, mode=mode, encoding=encoding, newline=newline) as f:
|
||||
f.write(data)
|
||||
# Ensure data flush to disk
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
except FileNotFoundError:
|
||||
# Create parent directory
|
||||
directory = os.path.dirname(file)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
# Write again
|
||||
with open(file, mode=mode, encoding=encoding, newline=newline) as f:
|
||||
f.write(data)
|
||||
# Ensure data flush to disk
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
|
||||
def file_write_stream(file: str, data_generator):
|
||||
"""
|
||||
Only creates a file if the generator yields at least one data chunk.
|
||||
Auto determines write mode based on the type of first chunk.
|
||||
|
||||
Args:
|
||||
file: Target file path
|
||||
data_generator: An iterable that yields data chunks (str or bytes)
|
||||
"""
|
||||
# Convert generator to iterator to ensure we can peek at first chunk
|
||||
data_iter = iter(data_generator)
|
||||
|
||||
# Try to get the first chunk
|
||||
try:
|
||||
first_chunk = next(data_iter)
|
||||
except StopIteration:
|
||||
# Generator is empty, no file will be created
|
||||
return
|
||||
|
||||
# Determine mode, encoding and newline from first chunk
|
||||
if isinstance(first_chunk, str):
|
||||
mode = 'w'
|
||||
encoding = 'utf-8'
|
||||
newline = ''
|
||||
elif isinstance(first_chunk, bytes):
|
||||
mode = 'wb'
|
||||
encoding = None
|
||||
newline = None
|
||||
else:
|
||||
# Default to text mode for other types
|
||||
mode = 'w'
|
||||
encoding = 'utf-8'
|
||||
newline = ''
|
||||
|
||||
try:
|
||||
# Write temp file
|
||||
with open(file, mode=mode, encoding=encoding, newline=newline) as f:
|
||||
f.write(first_chunk)
|
||||
for chunk in data_iter:
|
||||
f.write(chunk)
|
||||
# Ensure data flush to disk
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
except FileNotFoundError:
|
||||
# Create parent directory
|
||||
directory = os.path.dirname(file)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
# Write again
|
||||
with open(file, mode=mode, encoding=encoding, newline=newline) as f:
|
||||
f.write(first_chunk)
|
||||
for chunk in data_iter:
|
||||
f.write(chunk)
|
||||
# Ensure data flush to disk
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
|
||||
def atomic_write(
|
||||
file: str,
|
||||
data: Union[str, bytes],
|
||||
):
|
||||
"""
|
||||
Atomic file write with minimal IO operation
|
||||
and handles cases where file might be read by another process.
|
||||
|
||||
os.replace() is an atomic operation among all OS,
|
||||
we write to temp file then do os.replace()
|
||||
|
||||
Args:
|
||||
file:
|
||||
data:
|
||||
"""
|
||||
tmp = to_tmp_file(file)
|
||||
file_write(tmp, data)
|
||||
replace_tmp(tmp, file)
|
||||
|
||||
|
||||
def atomic_write_stream(
|
||||
file: str,
|
||||
data_generator,
|
||||
):
|
||||
"""
|
||||
Atomic file write with streaming data support.
|
||||
Handles cases where file might be read by another process.
|
||||
|
||||
os.replace() is an atomic operation among all OS,
|
||||
we write to temp file then do os.replace()
|
||||
|
||||
Args:
|
||||
file: Target file path
|
||||
data_generator: An iterable that yields data chunks (str or bytes)
|
||||
"""
|
||||
tmp = to_tmp_file(file)
|
||||
file_write_stream(tmp, data_generator)
|
||||
replace_tmp(tmp, file)
|
||||
|
||||
|
||||
def file_read_text(
|
||||
file: str,
|
||||
encoding: str = 'utf-8',
|
||||
errors: str = 'strict'
|
||||
) -> str:
|
||||
"""
|
||||
Args:
|
||||
file:
|
||||
encoding:
|
||||
errors: 'strict', 'ignore', 'replace' and any other errors mode in open()
|
||||
"""
|
||||
try:
|
||||
with open(file, mode='r', encoding=encoding, errors=errors) as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return ''
|
||||
|
||||
|
||||
def file_read_text_stream(
|
||||
file: str,
|
||||
encoding: str = 'utf-8',
|
||||
errors: str = 'strict',
|
||||
chunk_size: int = 8192
|
||||
) -> Iterable[str]:
|
||||
"""
|
||||
Args:
|
||||
file:
|
||||
encoding:
|
||||
errors: 'strict', 'ignore', 'replace' and any other errors mode in open()
|
||||
chunk_size:
|
||||
"""
|
||||
try:
|
||||
with open(file, mode='r', encoding=encoding, errors=errors) as f:
|
||||
while 1:
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
return
|
||||
yield chunk
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
|
||||
def file_read_bytes(file: str) -> bytes:
|
||||
"""
|
||||
Args:
|
||||
file:
|
||||
"""
|
||||
try:
|
||||
# No python-side buffering when reading the entire file to speedup reading
|
||||
# https://github.com/python/cpython/pull/122111
|
||||
with open(file, mode='rb', buffering=0) as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return b''
|
||||
|
||||
|
||||
def file_read_bytes_stream(file: str, chunk_size: int = 8192) -> Iterable[bytes]:
|
||||
"""
|
||||
Args:
|
||||
file:
|
||||
chunk_size:
|
||||
"""
|
||||
try:
|
||||
with open(file, mode='rb') as f:
|
||||
while 1:
|
||||
chunk = f.read(chunk_size)
|
||||
if not chunk:
|
||||
return
|
||||
yield chunk
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
|
||||
def atomic_read_text(
|
||||
file: str,
|
||||
encoding: str = 'utf-8',
|
||||
errors: str = 'strict'
|
||||
) -> str:
|
||||
"""
|
||||
Atomic file read with minimal IO operation
|
||||
|
||||
Args:
|
||||
file:
|
||||
encoding:
|
||||
errors: 'strict', 'ignore', 'replace' and any other errors mode in open()
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is replacing
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
return file_read_text(file, encoding=encoding, errors=errors)
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
else:
|
||||
# Linux and Mac allow reading while replacing
|
||||
return file_read_text(file, encoding=encoding, errors=errors)
|
||||
|
||||
|
||||
def atomic_read_text_stream(
|
||||
file: str,
|
||||
encoding: str = 'utf-8',
|
||||
errors: str = 'strict',
|
||||
chunk_size: int = 8192
|
||||
) -> Iterable[str]:
|
||||
"""
|
||||
Args:
|
||||
file:
|
||||
encoding:
|
||||
errors: 'strict', 'ignore', 'replace' and any other errors mode in open()
|
||||
chunk_size:
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is replacing
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
yield from file_read_text_stream(file, encoding=encoding, errors=errors, chunk_size=chunk_size)
|
||||
return
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
else:
|
||||
# Linux and Mac allow reading while replacing
|
||||
yield from file_read_text_stream(file, encoding=encoding, errors=errors, chunk_size=chunk_size)
|
||||
return
|
||||
|
||||
|
||||
def atomic_read_bytes(file: str) -> bytes:
|
||||
"""
|
||||
Atomic file read with minimal IO operation
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is replacing
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
return file_read_bytes(file)
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
else:
|
||||
# Linux and Mac allow reading while replacing
|
||||
return file_read_bytes(file)
|
||||
|
||||
|
||||
def atomic_read_bytes_stream(file: str, chunk_size: int = 8192) -> Iterable[bytes]:
|
||||
"""
|
||||
Args:
|
||||
file:
|
||||
chunk_size:
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is replacing
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
yield from file_read_bytes_stream(file, chunk_size=chunk_size)
|
||||
return
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
else:
|
||||
# Linux and Mac allow reading while replacing
|
||||
yield from file_read_bytes_stream(file, chunk_size=chunk_size)
|
||||
return
|
||||
|
||||
|
||||
def file_remove(file: str):
|
||||
"""
|
||||
Remove a file non-atomic
|
||||
"""
|
||||
try:
|
||||
os.unlink(file)
|
||||
except FileNotFoundError:
|
||||
# If file not exist, just no need to remove
|
||||
pass
|
||||
|
||||
|
||||
def atomic_remove(file: str):
|
||||
"""
|
||||
Atomic file remove
|
||||
|
||||
Args:
|
||||
file:
|
||||
"""
|
||||
if IS_WINDOWS:
|
||||
# PermissionError on Windows if another process is replacing
|
||||
last_error = None
|
||||
for attempt in range(WINDOWS_MAX_ATTEMPT):
|
||||
try:
|
||||
return file_remove(file)
|
||||
except PermissionError as e:
|
||||
last_error = e
|
||||
delay = windows_attempt_delay(attempt)
|
||||
time.sleep(delay)
|
||||
continue
|
||||
if last_error is not None:
|
||||
raise last_error from None
|
||||
else:
|
||||
# Linux and Mac allow deleting while another process is reading
|
||||
# The directory entry is removed but the storage allocated to the file is not made available
|
||||
# until the original file is no longer in use.
|
||||
return file_remove(file)
|
||||
|
||||
|
||||
def folder_rmtree(folder, may_symlinks=True):
|
||||
"""
|
||||
Recursively remove a folder and its content
|
||||
|
||||
Args:
|
||||
folder:
|
||||
may_symlinks: Default to True
|
||||
False if you already know it's not a symlink
|
||||
|
||||
Returns:
|
||||
bool: If success
|
||||
"""
|
||||
try:
|
||||
# If it's a symlinks, unlink it
|
||||
if may_symlinks and os.path.islink(folder):
|
||||
file_remove(folder)
|
||||
return True
|
||||
# Iter folder
|
||||
with os.scandir(folder) as entries:
|
||||
for entry in entries:
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
folder_rmtree(entry.path, may_symlinks=False)
|
||||
else:
|
||||
# File or symlink
|
||||
# Just remove the symlink, not what it points to
|
||||
try:
|
||||
file_remove(entry.path)
|
||||
except PermissionError:
|
||||
# Another process is reading/writing
|
||||
pass
|
||||
|
||||
except FileNotFoundError:
|
||||
# directory to clean up does not exist, no need to clean up
|
||||
return True
|
||||
except NotADirectoryError:
|
||||
file_remove(folder)
|
||||
return True
|
||||
|
||||
# Remove empty folder
|
||||
# May raise OSError if it's still not empty
|
||||
try:
|
||||
os.rmdir(folder)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
return True
|
||||
except NotADirectoryError:
|
||||
file_remove(folder)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def atomic_rmtree(folder: str):
|
||||
"""
|
||||
Atomic folder rmtree
|
||||
Rename folder as temp folder and remove it,
|
||||
folder can be removed by atomic_failure_cleanup at next startup if remove gets interrupted
|
||||
"""
|
||||
tmp = to_tmp_file(folder)
|
||||
try:
|
||||
atomic_replace(folder, tmp)
|
||||
except FileNotFoundError:
|
||||
# Folder not exist, no need to rmtree
|
||||
return
|
||||
folder_rmtree(tmp)
|
||||
|
||||
|
||||
def atomic_failure_cleanup(folder: str, recursive: bool = False):
|
||||
"""
|
||||
Cleanup remaining temp file under given path.
|
||||
In most cases there should be no remaining temp files unless write process get interrupted.
|
||||
|
||||
This method should only be called at startup
|
||||
to avoid deleting temp files that another process is writing.
|
||||
"""
|
||||
try:
|
||||
with os.scandir(folder) as entries:
|
||||
for entry in entries:
|
||||
if is_tmp_file(entry.name):
|
||||
try:
|
||||
# Delete temp file or directory
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
folder_rmtree(entry.path, may_symlinks=False)
|
||||
else:
|
||||
file_remove(entry.path)
|
||||
except PermissionError:
|
||||
# Another process is reading/writing
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if recursive:
|
||||
try:
|
||||
if entry.is_dir(follow_symlinks=False):
|
||||
# Normal directory
|
||||
atomic_failure_cleanup(entry.path, recursive=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
except FileNotFoundError:
|
||||
# directory to clean up does not exist, no need to clean up
|
||||
pass
|
||||
except NotADirectoryError:
|
||||
file_remove(folder)
|
||||
except:
|
||||
# Ignore all failures, it doesn't matter if tmp files still exist
|
||||
pass
|
||||
@@ -1,6 +1,7 @@
|
||||
import copy
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Optional, Union
|
||||
|
||||
from deploy.Windows.logger import logger
|
||||
@@ -56,6 +57,8 @@ class ConfigModel:
|
||||
# Webui
|
||||
WebuiHost: str = "0.0.0.0"
|
||||
WebuiPort: int = 23467
|
||||
WebuiSSLKey: Optional[str] = None
|
||||
WebuiSSLCert: Optional[str] = None
|
||||
Language: str = "en-US"
|
||||
Theme: str = "default"
|
||||
DpiScaling: bool = True
|
||||
@@ -77,42 +80,52 @@ class DeployConfig(ConfigModel):
|
||||
self.config_template = {}
|
||||
self.read()
|
||||
|
||||
self.set_repo()
|
||||
|
||||
self.write()
|
||||
self.show_config()
|
||||
|
||||
def set_repo(self):
|
||||
# Bypass webui.config.DeployConfig.__setattr__()
|
||||
# Don't write these into deploy.yaml
|
||||
if self.Repository == 'cn':
|
||||
super().__setattr__('Repository', 'https://git.yoursfunny.top/YoursFunny/ArisuAutoSweeper.git')
|
||||
if self.Repository == 'global':
|
||||
super().__setattr__('Repository', 'https://github.com/TheFunny/ArisuAutoSweeper')
|
||||
|
||||
def show_config(self):
|
||||
logger.hr("Show deploy config", 1)
|
||||
for k, v in self.config.items():
|
||||
if k in ("Password", "SSHUser"):
|
||||
continue
|
||||
if self.config_template[k] == v:
|
||||
if self.config_template.get(k) == v:
|
||||
continue
|
||||
logger.info(f"{k}: {v}")
|
||||
|
||||
logger.info(f"Rest of the configs are the same as default")
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Read and update deploy config, copy `self.configs` to properties.
|
||||
"""
|
||||
self.config = poor_yaml_read(DEPLOY_TEMPLATE)
|
||||
self.config_template = copy.deepcopy(self.config)
|
||||
self.config.update(poor_yaml_read(self.file))
|
||||
origin = poor_yaml_read(self.file)
|
||||
self.config.update(origin)
|
||||
|
||||
for key, value in self.config.items():
|
||||
if hasattr(self, key):
|
||||
super().__setattr__(key, value)
|
||||
|
||||
self.config_redirect()
|
||||
|
||||
if self.config != origin:
|
||||
self.write()
|
||||
|
||||
def write(self):
|
||||
poor_yaml_write(self.config, self.file)
|
||||
|
||||
def config_redirect(self):
|
||||
"""
|
||||
Redirect deploy config, must be called after each `read()`
|
||||
"""
|
||||
# Bypass webui.config.DeployConfig.__setattr__()
|
||||
# Don't write these into deploy.yaml
|
||||
if self.Repository == 'cn':
|
||||
super().__setattr__('Repository', 'https://git.yoursfunny.top/YoursFunny/ArisuAutoSweeper.git')
|
||||
if self.Repository == 'global':
|
||||
super().__setattr__('Repository', 'https://github.com/TheFunny/ArisuAutoSweeper')
|
||||
|
||||
def filepath(self, path):
|
||||
"""
|
||||
Args:
|
||||
@@ -144,7 +157,7 @@ class DeployConfig(ConfigModel):
|
||||
if os.path.exists(exe):
|
||||
return exe
|
||||
|
||||
logger.warning(f'AdbExecutable: {exe} does not exists, use `adb` instead')
|
||||
logger.warning(f'AdbExecutable: {exe} does not exist, use `adb` instead')
|
||||
return 'adb'
|
||||
|
||||
@cached_property
|
||||
@@ -153,12 +166,21 @@ class DeployConfig(ConfigModel):
|
||||
if os.path.exists(exe):
|
||||
return exe
|
||||
|
||||
logger.warning(f'GitExecutable: {exe} does not exists, use `git` instead')
|
||||
logger.warning(f'GitExecutable: {exe} does not exist, use `git` instead')
|
||||
return 'git'
|
||||
|
||||
@cached_property
|
||||
def python(self) -> str:
|
||||
return self.filepath(self.PythonExecutable)
|
||||
# No need to read PythonExecutable
|
||||
# since you run this code with python, current python is the python
|
||||
|
||||
# exe = self.filepath(self.PythonExecutable)
|
||||
# if os.path.exists(exe):
|
||||
# return exe
|
||||
|
||||
current = sys.executable.replace("\\", "/")
|
||||
# logger.warning(f'PythonExecutable: {exe} does not exist, use current python instead: {current}')
|
||||
return current
|
||||
|
||||
@cached_property
|
||||
def requirements_file(self) -> str:
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import filecmp
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -9,7 +10,8 @@ from deploy.Windows.alas import AlasManager
|
||||
from deploy.Windows.logger import logger
|
||||
from deploy.Windows.utils import cached_property
|
||||
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||
if sys.platform.startswith('win'):
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -140,10 +142,13 @@ class EmulatorManager(AlasManager):
|
||||
for adb in replace:
|
||||
logger.info(f'Replacing {adb}')
|
||||
bak = self.adb_path_to_backup(adb, new_backup=True)
|
||||
logger.info(f'{adb} -----> {bak}')
|
||||
shutil.move(adb, bak)
|
||||
logger.info(f'{self.adb} -----> {adb}')
|
||||
shutil.copy(self.adb, adb)
|
||||
try:
|
||||
logger.info(f'{adb} -----> {bak}')
|
||||
shutil.move(adb, bak)
|
||||
logger.info(f'{self.adb} -----> {adb}')
|
||||
shutil.copy(self.adb, adb)
|
||||
except OSError as e:
|
||||
logger.warning(f'Failed to replace {adb}, {e}')
|
||||
|
||||
def adb_recover(self):
|
||||
"""
|
||||
|
||||
@@ -59,17 +59,34 @@ def patch_uiautomator2():
|
||||
https://tool.appetizer.io is added to bypass the wall in China but https://tool.appetizer.io is slow outside of CN
|
||||
plus some CN users cannot access it for unknown reason.
|
||||
|
||||
So we patch `uiautomator2/init.py` to a local assets cache `uiautomator2cache/cache`.
|
||||
1. So we patch `uiautomator2/init.py` to a local assets cache `uiautomator2cache/cache`.
|
||||
appdir = os.path.join(os.path.expanduser('~'), '.uiautomator2')
|
||||
to:
|
||||
appdir = os.path.join(__file__, '../../uiautomator2cache')
|
||||
|
||||
And we also remove minicap installations since emulators doesn't need it.
|
||||
2. And we also remove minicap installations since emulators doesn't need it.
|
||||
for url in self.minicap_urls:
|
||||
self.push_url(url)
|
||||
to:
|
||||
for url in []:
|
||||
self.push_url(url)
|
||||
|
||||
3. Fix atx_agent_url so ARM Mac can have correct ATX installed
|
||||
```
|
||||
@property
|
||||
def atx_agent_url(self):
|
||||
files = {
|
||||
'armeabi-v7a': 'atx-agent_{v}_linux_armv7.tar.gz',
|
||||
'arm64-v8a': 'atx-agent_{v}_linux_armv7.tar.gz',
|
||||
'armeabi': 'atx-agent_{v}_linux_armv6.tar.gz',
|
||||
'x86': 'atx-agent_{v}_linux_386.tar.gz',
|
||||
'x86_64': 'atx-agent_{v}_linux_386.tar.gz',
|
||||
}
|
||||
```
|
||||
where
|
||||
'arm64-v8a': 'atx-agent_{v}_linux_armv7.tar.gz',
|
||||
to
|
||||
'arm64-v8a': 'atx-agent_{v}_linux_arm64.tar.gz',
|
||||
"""
|
||||
init_file = './toolkit/Lib/site-packages/uiautomator2/init.py'
|
||||
cache_dir = './toolkit/Lib/site-packages/uiautomator2cache/cache'
|
||||
@@ -92,6 +109,17 @@ def patch_uiautomator2():
|
||||
else:
|
||||
logger.info(f'{init_file} minicap_urls no need to patch')
|
||||
|
||||
# Patch atx_agent_url
|
||||
res = re.search(r"'arm64-v8a': 'atx-agent_\{v}_linux_armv7.tar.gz'", content)
|
||||
if res:
|
||||
content = re.sub(r"'arm64-v8a': 'atx-agent_\{v}_linux_armv7.tar.gz'",
|
||||
"'arm64-v8a': 'atx-agent_{v}_linux_arm64.tar.gz'",
|
||||
content)
|
||||
modified = True
|
||||
logger.info(f'{init_file} atx_agent_url patched')
|
||||
else:
|
||||
logger.info(f'{init_file} atx_agent_url no need to patch')
|
||||
|
||||
# Patch appdir
|
||||
if os.path.exists(cache_dir):
|
||||
res = re.search(r'appdir ?=(.*)\n', content)
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from deploy.Windows.config import DeployConfig
|
||||
from deploy.Windows.logger import logger, Progress
|
||||
from deploy.Windows.logger import Progress, logger
|
||||
from deploy.Windows.utils import cached_property
|
||||
|
||||
|
||||
@@ -46,9 +46,15 @@ class PipManager(DeployConfig):
|
||||
return f'"{self.python}" -m pip'
|
||||
|
||||
@cached_property
|
||||
def python_site_packages(self):
|
||||
return os.path.abspath(os.path.join(self.python, '../Lib/site-packages')) \
|
||||
.replace(r"\\", "/").replace("\\", "/")
|
||||
def python_site_packages(self) -> str:
|
||||
import site
|
||||
paths = site.getsitepackages()
|
||||
# site-packages should be site-packages folder
|
||||
for path in paths:
|
||||
if path.endswith('site-packages'):
|
||||
return path
|
||||
# Otherwise pick first
|
||||
return paths[0]
|
||||
|
||||
@cached_property
|
||||
def set_installed_dependency(self) -> t.Set[DataDependency]:
|
||||
|
||||
@@ -157,6 +157,14 @@ Deploy:
|
||||
# '["alas"]' specified "alas" config
|
||||
# '["alas","alas2"]' specified "alas" "alas2" configs
|
||||
Run: null
|
||||
# SSL support
|
||||
# Only effective when both parameters below are set
|
||||
# --ssl-key. Path to SSL key file
|
||||
# [Default] null (no SSL)
|
||||
WebuiSSLKey: null
|
||||
# --ssl-cert. Path to SSL cert file
|
||||
# [Default] null (no SSL)
|
||||
WebuiSSLCert: null
|
||||
# To update app.asar
|
||||
# [In most cases] true
|
||||
AppAsarUpdate: true
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Generic, Iterable, TypeVar
|
||||
|
||||
from deploy.Windows.atomic import atomic_read_text, atomic_write
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
DEPLOY_CONFIG = './config/deploy.yaml'
|
||||
@@ -64,29 +65,26 @@ def poor_yaml_read(file):
|
||||
Returns:
|
||||
dict:
|
||||
"""
|
||||
if not os.path.exists(file):
|
||||
return {}
|
||||
|
||||
content = atomic_read_text(file)
|
||||
data = {}
|
||||
regex = re.compile(r'^(.*?):(.*?)$')
|
||||
with open(file, 'r', encoding='utf-8') as f:
|
||||
for line in f.readlines():
|
||||
line = line.strip('\n\r\t ').replace('\\', '/')
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
result = re.match(regex, line)
|
||||
if result:
|
||||
k, v = result.group(1), result.group(2).strip('\n\r\t\' ')
|
||||
if v:
|
||||
if v.lower() == 'null':
|
||||
v = None
|
||||
elif v.lower() == 'false':
|
||||
v = False
|
||||
elif v.lower() == 'true':
|
||||
v = True
|
||||
elif v.isdigit():
|
||||
v = int(v)
|
||||
data[k] = v
|
||||
for line in content.splitlines():
|
||||
line = line.strip('\n\r\t ').replace('\\', '/')
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
result = re.match(regex, line)
|
||||
if result:
|
||||
k, v = result.group(1), result.group(2).strip('\n\r\t\' ')
|
||||
if v:
|
||||
if v.lower() == 'null':
|
||||
v = None
|
||||
elif v.lower() == 'false':
|
||||
v = False
|
||||
elif v.lower() == 'true':
|
||||
v = True
|
||||
elif v.isdigit():
|
||||
v = int(v)
|
||||
data[k] = v
|
||||
|
||||
return data
|
||||
|
||||
@@ -98,8 +96,8 @@ def poor_yaml_write(data, file, template_file=DEPLOY_TEMPLATE):
|
||||
file (str):
|
||||
template_file (str):
|
||||
"""
|
||||
with open(template_file, 'r', encoding='utf-8') as f:
|
||||
text = f.read().replace('\\', '/')
|
||||
text = atomic_read_text(template_file)
|
||||
text = text.replace('\\', '/')
|
||||
|
||||
for key, value in data.items():
|
||||
if value is None:
|
||||
@@ -110,38 +108,15 @@ def poor_yaml_write(data, file, template_file=DEPLOY_TEMPLATE):
|
||||
value = "false"
|
||||
text = re.sub(f'{key}:.*?\n', f'{key}: {value}\n', text)
|
||||
|
||||
with open(file, 'w', encoding='utf-8', newline='') as f:
|
||||
f.write(text)
|
||||
atomic_write(file, text)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataProcessInfo:
|
||||
proc: object # psutil.Process or psutil._pswindows.Process
|
||||
pid: int
|
||||
|
||||
@cached_property
|
||||
def name(self):
|
||||
name = self.proc.name()
|
||||
return name
|
||||
|
||||
@cached_property
|
||||
def cmdline(self):
|
||||
try:
|
||||
cmdline = self.proc.cmdline()
|
||||
except:
|
||||
# psutil.AccessDenied
|
||||
cmdline = []
|
||||
cmdline = ' '.join(cmdline).replace(r'\\', '/').replace('\\', '/')
|
||||
return cmdline
|
||||
|
||||
def __str__(self):
|
||||
# Don't print `proc`, it will take some time to get process properties
|
||||
return f'DataProcessInfo(name="{self.name}", pid={self.pid}, cmdline="{self.cmdline}")'
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
def iter_process() -> Iterable[DataProcessInfo]:
|
||||
def iter_process() -> "Iterable[tuple[int, list[str]]]":
|
||||
"""
|
||||
Yields:
|
||||
int: pid
|
||||
list[str]: cmdline, and it's guaranteed to have at least one element
|
||||
"""
|
||||
try:
|
||||
import psutil
|
||||
except ModuleNotFoundError:
|
||||
@@ -151,16 +126,48 @@ def iter_process() -> Iterable[DataProcessInfo]:
|
||||
# Since this is a one-time-usage, we access psutil._psplatform.Process directly
|
||||
# to bypass the call of psutil.Process.is_running().
|
||||
# This only costs about 0.017s.
|
||||
# If you do psutil.process_iter(['pid', 'cmdline']) it will take over 1s
|
||||
import psutil._psutil_windows as cetx
|
||||
for pid in psutil.pids():
|
||||
# 0 and 4 are always represented in taskmgr and process-hacker
|
||||
if pid == 0 or pid == 4:
|
||||
continue
|
||||
try:
|
||||
# This would be fast on psutil<=5.9.8 taking overall time 0.027s
|
||||
# but taking 0.39s on psutil>=6.0.0
|
||||
cmdline = cetx.proc_cmdline(pid, use_peb=True)
|
||||
except (psutil.AccessDenied, psutil.NoSuchProcess, OSError):
|
||||
# psutil.AccessDenied
|
||||
# NoSuchProcess: process no longer exists (pid=xxx)
|
||||
# ProcessLookupError: [Errno 3] assume no such process (originated from psutil_pid_is_running -> 0)
|
||||
# OSError: [WinError 87] 参数错误。: '(originated from ReadProcessMemory)'
|
||||
continue
|
||||
|
||||
# Validate cmdline
|
||||
if not cmdline:
|
||||
continue
|
||||
try:
|
||||
exe = cmdline[0]
|
||||
except IndexError:
|
||||
continue
|
||||
# \??\C:\Windows\system32\conhost.exe
|
||||
if exe.startswith(r'\??'):
|
||||
continue
|
||||
yield pid, cmdline
|
||||
else:
|
||||
# No optimizations yet
|
||||
for pid in psutil.pids():
|
||||
proc = psutil._psplatform.Process(pid)
|
||||
yield DataProcessInfo(
|
||||
proc=proc,
|
||||
pid=proc.pid,
|
||||
)
|
||||
else:
|
||||
# This will cost about 0.45s, even `attr` is given.
|
||||
for proc in psutil.process_iter():
|
||||
yield DataProcessInfo(
|
||||
proc=proc,
|
||||
pid=proc.pid,
|
||||
)
|
||||
try:
|
||||
cmdline = proc.cmdline()
|
||||
except (psutil.AccessDenied, psutil.NoSuchProcess, OSError):
|
||||
continue
|
||||
|
||||
# Validate cmdline
|
||||
if not cmdline:
|
||||
continue
|
||||
try:
|
||||
cmdline[0]
|
||||
except IndexError:
|
||||
continue
|
||||
yield pid, cmdline
|
||||
|
||||
@@ -13,6 +13,8 @@ from deploy.Windows.pip import PipManager
|
||||
|
||||
class Installer(GitManager, PipManager, AdbManager, AppManager, AlasManager):
|
||||
def install(self):
|
||||
from deploy.Windows.atomic import atomic_failure_cleanup
|
||||
atomic_failure_cleanup('./config')
|
||||
try:
|
||||
self.git_install()
|
||||
self.alas_kill()
|
||||
|
||||
@@ -3,20 +3,61 @@ import re
|
||||
import typing as t
|
||||
from dataclasses import dataclass
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from module.base.code_generator import CodeGenerator
|
||||
from module.base.utils import SelectedGrids, area_limit, area_pad, get_bbox, get_color, image_size, load_image
|
||||
from module.base.utils import SelectedGrids, area_center, area_limit, area_pad, corner2area, get_bbox, get_color, \
|
||||
image_size, load_image
|
||||
from module.config.config_manual import ManualConfig as AzurLaneConfig
|
||||
from module.config.deep import deep_get, deep_iter, deep_set
|
||||
from module.config.server import VALID_LANG
|
||||
from module.config.utils import deep_get, deep_iter, deep_set, iter_folder
|
||||
from module.config.utils import iter_folder
|
||||
from module.logger import logger
|
||||
|
||||
SHARE_SERVER = 'share'
|
||||
ASSET_SERVER = [SHARE_SERVER] + VALID_LANG
|
||||
|
||||
|
||||
def parse_grid(image):
|
||||
"""
|
||||
Args:
|
||||
image:
|
||||
|
||||
Returns:
|
||||
dict: Key: Grid position (x, y)
|
||||
Value: Area on image
|
||||
"""
|
||||
image = cv2.inRange(image, (127, 127, 127), (255, 255, 255))
|
||||
contours, _ = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
||||
dic_rect = {}
|
||||
for corners in contours:
|
||||
area = corner2area(corners.reshape(4, 2)) + (0, 0, 1, 1)
|
||||
center = area_center(area)
|
||||
dic_rect[center] = area
|
||||
# for k, v in dic_rect.items():
|
||||
# print(k, v)
|
||||
|
||||
dic_grid = {}
|
||||
prev_y = -100
|
||||
grid_y = -1
|
||||
stack_center = []
|
||||
for center in sorted(dic_rect.keys(), key=lambda x: x[1]):
|
||||
if center[1] > prev_y + 3:
|
||||
for x, c in enumerate(sorted(stack_center, key=lambda x: x[0])):
|
||||
dic_grid[(x, grid_y)] = tuple(dic_rect[c].astype(int))
|
||||
grid_y += 1
|
||||
stack_center = []
|
||||
stack_center.append(center)
|
||||
prev_y = center[1]
|
||||
for x, c in enumerate(sorted(stack_center, key=lambda x: x[0])):
|
||||
dic_grid[(x, grid_y)] = tuple(dic_rect[c].astype(int))
|
||||
# for k, v in dic_grid.items():
|
||||
# print(k, v)
|
||||
return dic_grid
|
||||
|
||||
|
||||
class AssetsImage:
|
||||
REGEX_ASSETS = re.compile(
|
||||
f'^{AzurLaneConfig.ASSETS_FOLDER}/'
|
||||
@@ -24,7 +65,7 @@ class AssetsImage:
|
||||
f'(?P<module>[a-zA-Z0-9_/]+?)/'
|
||||
f'(?P<assets>\w+)'
|
||||
f'(?P<frame>\.\d+)?'
|
||||
f'(?P<attr>\.AREA|\.SEARCH|\.COLOR|\.BUTTON)?'
|
||||
f'(?P<attr>\.AREA|\.SEARCH|\.COLOR|\.BUTTON|\.GRID)?'
|
||||
f'\.png$'
|
||||
)
|
||||
|
||||
@@ -46,6 +87,7 @@ class AssetsImage:
|
||||
self.assets = ''
|
||||
self.frame = 1
|
||||
self.attr = ''
|
||||
self.posi = None
|
||||
|
||||
if res:
|
||||
self.valid = True
|
||||
@@ -66,6 +108,7 @@ class AssetsImage:
|
||||
|
||||
self.bbox: t.Tuple = ()
|
||||
self.mean: t.Tuple = ()
|
||||
self.grids = {}
|
||||
|
||||
def parse(self):
|
||||
image = load_image(self.file)
|
||||
@@ -79,6 +122,10 @@ class AssetsImage:
|
||||
mean = tuple(np.rint(mean).astype(int))
|
||||
self.bbox = bbox
|
||||
self.mean = mean
|
||||
|
||||
if self.attr == 'GRID':
|
||||
self.grids = parse_grid(image)
|
||||
|
||||
return bbox, mean
|
||||
|
||||
def __str__(self):
|
||||
@@ -87,13 +134,40 @@ class AssetsImage:
|
||||
else:
|
||||
return f'AssetsImage(file={self.file}, valid={self.valid})'
|
||||
|
||||
@property
|
||||
def is_GRID(self):
|
||||
return self.attr == 'GRID'
|
||||
|
||||
@property
|
||||
def is_base(self):
|
||||
return self.attr == ''
|
||||
|
||||
def iter_grids(self):
|
||||
frame = 0
|
||||
for posi, rect in self.grids.items():
|
||||
frame += 1
|
||||
image = AssetsImage(self.file)
|
||||
image.attr = ''
|
||||
image.bbox = rect
|
||||
image.mean = self.mean
|
||||
image.frame = frame
|
||||
image.posi = posi
|
||||
yield image
|
||||
|
||||
|
||||
def iter_images():
|
||||
for server in ASSET_SERVER:
|
||||
for path, folders, files in os.walk(os.path.join(AzurLaneConfig.ASSETS_FOLDER, server)):
|
||||
for file in files:
|
||||
file = os.path.join(path, file).replace('\\', '/')
|
||||
yield AssetsImage(file)
|
||||
if not file.startswith('.'):
|
||||
file = os.path.join(path, file).replace('\\', '/')
|
||||
yield AssetsImage(file)
|
||||
|
||||
|
||||
def iter_grids(images):
|
||||
for image in images:
|
||||
for grid in image.iter_grids():
|
||||
yield grid
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -103,11 +177,17 @@ class DataAssets:
|
||||
server: str
|
||||
frame: int
|
||||
file: str = ''
|
||||
posi = None
|
||||
area: t.Tuple[int, int, int, int] = ()
|
||||
search: t.Tuple[int, int, int, int] = ()
|
||||
color: t.Tuple[int, int, int] = ()
|
||||
button: t.Tuple[int, int, int, int] = ()
|
||||
|
||||
has_raw_area = False
|
||||
has_raw_search = False
|
||||
has_raw_color = False
|
||||
has_raw_button = False
|
||||
|
||||
@staticmethod
|
||||
def area_to_search(area):
|
||||
area = area_pad(area, pad=-20)
|
||||
@@ -120,7 +200,6 @@ class DataAssets:
|
||||
Product DataAssets from AssetsImage with attr=""
|
||||
"""
|
||||
data = cls(module=image.module, assets=image.assets, server=image.server, frame=image.frame, file=image.file)
|
||||
data.load_image(image)
|
||||
return data
|
||||
|
||||
def load_image(self, image: AssetsImage):
|
||||
@@ -129,14 +208,21 @@ class DataAssets:
|
||||
self.area = image.bbox
|
||||
self.color = image.mean
|
||||
self.button = image.bbox
|
||||
self.posi = image.posi
|
||||
elif image.attr == 'AREA':
|
||||
self.area = image.bbox
|
||||
self.has_raw_area = True
|
||||
elif image.attr == 'SEARCH':
|
||||
self.search = image.bbox
|
||||
self.has_raw_search = True
|
||||
elif image.attr == 'COLOR':
|
||||
self.color = image.mean
|
||||
self.has_raw_color = True
|
||||
elif image.attr == 'BUTTON':
|
||||
self.button = image.bbox
|
||||
self.has_raw_button = True
|
||||
elif image.attr == 'GRID':
|
||||
pass
|
||||
else:
|
||||
logger.warning(f'Trying to load an image with unknown attribute: {image}')
|
||||
|
||||
@@ -151,17 +237,22 @@ def iter_assets():
|
||||
for image in tqdm(images):
|
||||
image.parse()
|
||||
|
||||
images += list(iter_grids(images))
|
||||
|
||||
# Validate images
|
||||
images = SelectedGrids(images).select(valid=True)
|
||||
images.create_index('module', 'assets', 'server', 'frame', 'attr')
|
||||
for image in images.filter(lambda x: bool(x.attr)):
|
||||
image: AssetsImage = image
|
||||
if not images.indexed_select(image.module, image.assets, image.server, image.frame, ''):
|
||||
logger.warning(f'Attribute assets has no parent assets: {image.file}')
|
||||
image.valid = False
|
||||
if not images.indexed_select(image.module, image.assets, image.server, 1, ''):
|
||||
logger.warning(f'Attribute assets has no first frame: {image.file}')
|
||||
image.valid = False
|
||||
if image.is_GRID:
|
||||
pass
|
||||
else:
|
||||
if not images.indexed_select(image.module, image.assets, image.server, image.frame, ''):
|
||||
logger.warning(f'Attribute assets has no parent assets: {image.file}')
|
||||
image.valid = False
|
||||
if not images.indexed_select(image.module, image.assets, image.server, 1, ''):
|
||||
logger.warning(f'Attribute assets has no first frame: {image.file}')
|
||||
image.valid = False
|
||||
if image.attr == 'SEARCH' and image.frame > 1:
|
||||
logger.warning(f'Attribute SEARCH with frame > 1 is not allowed: {image.file}')
|
||||
image.valid = False
|
||||
@@ -170,31 +261,34 @@ def iter_assets():
|
||||
# Convert to DataAssets
|
||||
data = {}
|
||||
for image in images:
|
||||
if image.attr == '':
|
||||
if image.is_base:
|
||||
row = DataAssets.product(image)
|
||||
row.load_image(image)
|
||||
deep_set(data, keys=[image.module, image.assets, image.server, image.frame], value=row)
|
||||
# Load attribute images
|
||||
for image in images:
|
||||
if image.attr != '':
|
||||
if not image.is_base:
|
||||
row = deep_get(data, keys=[image.module, image.assets, image.server, image.frame])
|
||||
row.load_image(image)
|
||||
# Set `search`
|
||||
for path, frames in deep_iter(data, depth=3):
|
||||
print(path, frames)
|
||||
# If `search` attribute is set in the first frame, apply to all
|
||||
first = frames[1]
|
||||
if first.search:
|
||||
for frame in frames.values():
|
||||
# print(path, frames)
|
||||
for frame in frames.values():
|
||||
# Generate `search` from `area`
|
||||
if not frame.has_raw_search:
|
||||
frame.search = DataAssets.area_to_search(frame.area)
|
||||
# If an attribute is set in the first frame, apply to all
|
||||
first: DataAssets = frames[1]
|
||||
for frame in frames.values():
|
||||
# frame: DataAssets = frame
|
||||
if not frame.has_raw_area and first.has_raw_area:
|
||||
frame.area = first.area
|
||||
if not frame.has_raw_search and first.has_raw_search:
|
||||
frame.search = first.search
|
||||
else:
|
||||
for frame in frames.values():
|
||||
if frame.search:
|
||||
# Follow frame specific `search`
|
||||
pass
|
||||
else:
|
||||
# Generate `search` from `area`
|
||||
frame.search = DataAssets.area_to_search(frame.area)
|
||||
if not frame.has_raw_color and first.has_raw_color:
|
||||
frame.color = first.color
|
||||
if not frame.has_raw_button and first.has_raw_button:
|
||||
frame.button = first.button
|
||||
|
||||
return data
|
||||
|
||||
@@ -240,6 +334,8 @@ def generate_code():
|
||||
gen.ObjectAttr(key='search', value=frame.search)
|
||||
gen.ObjectAttr(key='color', value=frame.color)
|
||||
gen.ObjectAttr(key='button', value=frame.button)
|
||||
if frame.posi is not None:
|
||||
gen.ObjectAttr(key='posi', value=frame.posi)
|
||||
elif len(frames) == 1:
|
||||
frame = frames[0]
|
||||
with gen.ObjectAttr(key=server, value=gen.Object(object_class='Button')):
|
||||
@@ -248,6 +344,8 @@ def generate_code():
|
||||
gen.ObjectAttr(key='search', value=frame.search)
|
||||
gen.ObjectAttr(key='color', value=frame.color)
|
||||
gen.ObjectAttr(key='button', value=frame.button)
|
||||
if frame.posi is not None:
|
||||
gen.ObjectAttr(key='posi', value=frame.posi)
|
||||
else:
|
||||
gen.ObjectAttr(key=server, value=None)
|
||||
gen.write(os.path.join(output, f'assets_{module.replace("/", "_")}.py'))
|
||||
|
||||
@@ -2,8 +2,8 @@ import os
|
||||
from datetime import datetime
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from pynput import keyboard
|
||||
|
||||
from module.config.config import AzurLaneConfig
|
||||
from module.config.utils import alas_instance
|
||||
from module.device.connection import Connection, ConnectionAttr
|
||||
@@ -50,12 +50,12 @@ def handle_sensitive_info(image):
|
||||
|
||||
_ = EmptyConnection()
|
||||
name = input(
|
||||
'输入alas配置文件名称,或者模拟器serial,或者模拟器端口号: (默认输入 "alas"):\n'
|
||||
'例如:"alas", "127.0.0.1:16384", "7555"\n'
|
||||
'输入aas配置文件名称,或者模拟器serial,或者模拟器端口号: (默认输入 "aas"):\n'
|
||||
'例如:"aas", "127.0.0.1:16384", "7555"\n'
|
||||
)
|
||||
name = name.strip().strip('"').strip()
|
||||
if not name:
|
||||
name = 'alas'
|
||||
name = 'aas'
|
||||
if name.isdigit():
|
||||
name = f'127.0.0.1:{name}'
|
||||
if name in alas_instance():
|
||||
|
||||
@@ -40,6 +40,12 @@ def func(ev: threading.Event):
|
||||
parser.add_argument(
|
||||
"--electron", action="store_true", help="Runs by electron client."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ssl-key", dest="ssl_key", type=str, help="SSL key file path for HTTPS support"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ssl-cert", type=str, help="SSL certificate file path for HTTPS support"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--run",
|
||||
nargs="+",
|
||||
@@ -50,11 +56,15 @@ def func(ev: threading.Event):
|
||||
|
||||
host = args.host or State.deploy_config.WebuiHost or "0.0.0.0"
|
||||
port = args.port or int(State.deploy_config.WebuiPort) or 23467
|
||||
ssl_key = args.ssl_key or State.deploy_config.WebuiSSLKey
|
||||
ssl_cert = args.ssl_cert or State.deploy_config.WebuiSSLCert
|
||||
ssl = ssl_key is not None and ssl_cert is not None
|
||||
State.electron = args.electron
|
||||
|
||||
logger.hr("Launcher config")
|
||||
logger.attr("Host", host)
|
||||
logger.attr("Port", port)
|
||||
logger.attr("SSL", ssl)
|
||||
logger.attr("Electron", args.electron)
|
||||
logger.attr("Reload", ev is not None)
|
||||
|
||||
@@ -64,7 +74,16 @@ def func(ev: threading.Event):
|
||||
from module.logger.logger import console_hdlr
|
||||
logger.removeHandler(console_hdlr)
|
||||
|
||||
uvicorn.run("module.webui.app:app", host=host, port=port, factory=True)
|
||||
if ssl_cert is None and ssl_key is not None:
|
||||
logger.error("SSL key provided without certificate. Please provide both SSL key and certificate.")
|
||||
elif ssl_key is None and ssl_cert is not None:
|
||||
logger.error("SSL certificate provided without key. Please provide both SSL key and certificate.")
|
||||
|
||||
if ssl:
|
||||
uvicorn.run("module.webui.app:app", host=host, port=port, factory=True, ssl_keyfile=ssl_key,
|
||||
ssl_certfile=ssl_cert)
|
||||
else:
|
||||
uvicorn.run("module.webui.app:app", host=host, port=port, factory=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -20,7 +20,7 @@ def run_print_test():
|
||||
run()
|
||||
|
||||
|
||||
def run_set(modify=t.List[str]) -> t.Dict[str, str]:
|
||||
def run_set(modify: t.List[str]) -> t.Dict[str, str]:
|
||||
data = {}
|
||||
for kv in modify:
|
||||
if "=" in kv:
|
||||
|
||||