2 Commits
Author SHA1 Message Date
YoursFunny dda9a54f86 fix: update assets file 2024-09-03 14:45:22 +08:00
YoursFunny 247034a0ea fix(circle): update ui assets 2024-09-03 14:44:04 +08:00
196 changed files with 1937 additions and 8334 deletions
-8
View File
@@ -21,14 +21,6 @@ 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)
+1 -9
View File
@@ -11,10 +11,6 @@ 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
@@ -60,7 +56,7 @@ class ArisuAutoSweeper(AzurLaneAutoScript):
def momotalk(self):
from tasks.momotalk.momotalk import MomoTalk
MomoTalk(config=self.config, device=self.device, task="Momotalk").run()
MomoTalk(config=self.config, device=self.device).run()
def mission(self):
from tasks.mission.mission import Mission
@@ -74,10 +70,6 @@ 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()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

+6 -27
View File
@@ -315,16 +315,6 @@ 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;
@@ -387,7 +377,6 @@ 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;
@@ -418,13 +407,13 @@ pre.rich-traceback-code {
#pywebio-scope-dashboard > i {
flex-grow: 1;
align-self: flex-end;
width: 9.5rem;
width: 10rem;
}
[id^="pywebio-scope-dashboard-row-"] {
display: flex;
flex-grow: 1;
width: 9.5rem;
width: 10rem;
}
.dashboard-icon {
@@ -446,26 +435,16 @@ 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: baseline;
align-items: flex-end;
height: 1.5rem;
}
[id^="pywebio-scope-arg_stored-stored-value-"] p {
margin-bottom: 0;
}
#pywebio-scope-log {
line-height: 1.2;
@@ -570,19 +549,19 @@ pre.rich-traceback-code {
padding-right: .25rem;
}
*[style*="--loading-grow--"] > div {
*[style*="--loading-grow--"] {
width: 1.5rem;
height: 1.5rem;
}
*[style*="--loading-border--"] > div {
*[style*="--loading-border--"] {
width: 1.5rem;
height: 1.5rem;
border: .2em solid currentColor;
border-right-color: transparent;
}
*[style*="--loading-border-fill--"] > div {
*[style*="--loading-border-fill--"] {
width: 1.5rem;
height: 1.5rem;
border: .2em solid currentColor;
+6 -7
View File
@@ -63,18 +63,18 @@ select {
background-image: url("data:image/png;base64,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");
}
select.state {
.state > select {
border-bottom: 0;
background-image: none;
pointer-events: none;
}
select.state-bold {
.state-bold > select {
font-weight: bold;
color: #7a77bb;
}
select.state-light {
.state-light > select {
color: #777777;
}
@@ -133,13 +133,12 @@ pre.rich-traceback-code {
color: #c9d1d9;
}
#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-running,
#pywebio-scope-pending,
#pywebio-scope-waiting,
#pywebio-scope-daemon-overview #pywebio-scope-groups {
background-color: #2f3136;
border: 1px solid #21262d;
+6 -7
View File
@@ -64,18 +64,18 @@ select {
background-image: url("data:image/png;base64,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");
}
select.state {
.state > select {
border-bottom: 0;
background-image: none;
pointer-events: none;
}
select.state-bold {
.state-bold > select {
font-weight: bold;
color: #7a77bb;
}
select.state-light {
.state-light > select {
color: #777777;
}
@@ -133,13 +133,12 @@ pre.rich-traceback-code {
border: 1px solid lightgrey;
}
#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-running,
#pywebio-scope-pending,
#pywebio-scope-waiting,
#pywebio-scope-daemon-overview #pywebio-scope-groups {
background-color: white;
border: 1px solid lightgrey;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.
-8
View File
@@ -157,14 +157,6 @@ 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
-8
View File
@@ -157,14 +157,6 @@ 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
+5 -2
View File
@@ -250,8 +250,11 @@
}
},
"Momotalk": {
"Momotalk": {
"Enable": true
"Scheduler": {
"Enable": false,
"NextRun": "2020-01-01 00:00:00",
"Command": "Momotalk",
"ServerUpdate": "04:00"
}
}
}
+19 -21
View File
@@ -1,10 +1,10 @@
import os
import time
from typing import Iterable
import typing as t
from deploy.Windows.config import DeployConfig
from deploy.Windows.logger import Progress, logger
from deploy.Windows.utils import cached_property, iter_process
from deploy.Windows.utils import DataProcessInfo, cached_property, iter_process
class AlasManager(DeployConfig):
@@ -19,45 +19,43 @@ class AlasManager(DeployConfig):
def self_pid(self):
return os.getpid()
def list_process(self) -> "list[tuple[int, list[str]]]":
def list_process(self) -> t.List[DataProcessInfo]:
logger.info('List process')
process_data = list(iter_process())
logger.info(f'Found {len(process_data)} processes')
return process_data
process = list(iter_process())
logger.info(f'Found {len(process)} processes')
return process
def iter_process_by_names(self, names, in_alas=False) -> "Iterable[int]":
def iter_process_by_names(self, names, in_alas=False) -> t.Iterable[DataProcessInfo]:
"""
Args:
names (str, list[str]): process name, such as 'alas.exe'
in_alas (bool): If the output process must in Alas
Yields:
pid:
DataProcessInfo:
"""
if not isinstance(names, list):
names = [names]
try:
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
for proc in self.list_process():
if not (proc.name and proc.name in names):
continue
if proc.pid == self.self_pid:
continue
if in_alas:
exe = exe.replace(r"\\", "/").replace("\\", "/")
cmdline = proc.cmdline.replace(r"\\", "/").replace("\\", "/")
for folder in self.alas_folder:
if folder in exe:
yield pid
if folder in cmdline:
yield proc
else:
yield pid
yield proc
except Exception as e:
logger.info(str(e))
return False
def kill_process(self, pid: int):
self.execute(f'taskkill /f /t /pid {pid}', allow_failure=True, output=False)
def kill_process(self, process: DataProcessInfo):
self.execute(f'taskkill /f /t /pid {process.pid}', allow_failure=True, output=False)
def alas_kill(self):
for _ in range(10):
-630
View File
@@ -1,630 +0,0 @@
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
+29 -51
View File
@@ -1,7 +1,6 @@
import copy
import os
import subprocess
import sys
from typing import Optional, Union
from deploy.Windows.logger import logger
@@ -57,8 +56,6 @@ 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
@@ -80,45 +77,12 @@ class DeployConfig(ConfigModel):
self.config_template = {}
self.read()
self.set_repo()
self.write()
self.show_config()
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.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)
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()`
"""
def set_repo(self):
# Bypass webui.config.DeployConfig.__setattr__()
# Don't write these into deploy.yaml
if self.Repository == 'cn':
@@ -126,6 +90,29 @@ class DeployConfig(ConfigModel):
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:
continue
logger.info(f"{k}: {v}")
logger.info(f"Rest of the configs are the same as default")
def read(self):
self.config = poor_yaml_read(DEPLOY_TEMPLATE)
self.config_template = copy.deepcopy(self.config)
self.config.update(poor_yaml_read(self.file))
for key, value in self.config.items():
if hasattr(self, key):
super().__setattr__(key, value)
def write(self):
poor_yaml_write(self.config, self.file)
def filepath(self, path):
"""
Args:
@@ -157,7 +144,7 @@ class DeployConfig(ConfigModel):
if os.path.exists(exe):
return exe
logger.warning(f'AdbExecutable: {exe} does not exist, use `adb` instead')
logger.warning(f'AdbExecutable: {exe} does not exists, use `adb` instead')
return 'adb'
@cached_property
@@ -166,21 +153,12 @@ class DeployConfig(ConfigModel):
if os.path.exists(exe):
return exe
logger.warning(f'GitExecutable: {exe} does not exist, use `git` instead')
logger.warning(f'GitExecutable: {exe} does not exists, use `git` instead')
return 'git'
@cached_property
def python(self) -> str:
# 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
return self.filepath(self.PythonExecutable)
@cached_property
def requirements_file(self) -> str:
+5 -10
View File
@@ -2,7 +2,6 @@ import asyncio
import filecmp
import os
import shutil
import sys
import typing as t
from dataclasses import dataclass
@@ -10,8 +9,7 @@ from deploy.Windows.alas import AlasManager
from deploy.Windows.logger import logger
from deploy.Windows.utils import cached_property
if sys.platform.startswith('win'):
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
@dataclass
@@ -142,13 +140,10 @@ class EmulatorManager(AlasManager):
for adb in replace:
logger.info(f'Replacing {adb}')
bak = self.adb_path_to_backup(adb, new_backup=True)
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}')
logger.info(f'{adb} -----> {bak}')
shutil.move(adb, bak)
logger.info(f'{self.adb} -----> {adb}')
shutil.copy(self.adb, adb)
def adb_recover(self):
"""
+2 -30
View File
@@ -59,34 +59,17 @@ 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.
1. So we patch `uiautomator2/init.py` to a local assets cache `uiautomator2cache/cache`.
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')
2. And we also remove minicap installations since emulators doesn't need it.
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'
@@ -109,17 +92,6 @@ 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)
+4 -10
View File
@@ -5,7 +5,7 @@ from dataclasses import dataclass
from urllib.parse import urlparse
from deploy.Windows.config import DeployConfig
from deploy.Windows.logger import Progress, logger
from deploy.Windows.logger import logger, Progress
from deploy.Windows.utils import cached_property
@@ -46,15 +46,9 @@ class PipManager(DeployConfig):
return f'"{self.python}" -m pip'
@cached_property
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]
def python_site_packages(self):
return os.path.abspath(os.path.join(self.python, '../Lib/site-packages')) \
.replace(r"\\", "/").replace("\\", "/")
@cached_property
def set_installed_dependency(self) -> t.Set[DataDependency]:
-8
View File
@@ -157,14 +157,6 @@ 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
+65 -72
View File
@@ -1,9 +1,8 @@
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'
@@ -65,26 +64,29 @@ def poor_yaml_read(file):
Returns:
dict:
"""
content = atomic_read_text(file)
if not os.path.exists(file):
return {}
data = {}
regex = re.compile(r'^(.*?):(.*?)$')
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
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
return data
@@ -96,8 +98,8 @@ def poor_yaml_write(data, file, template_file=DEPLOY_TEMPLATE):
file (str):
template_file (str):
"""
text = atomic_read_text(template_file)
text = text.replace('\\', '/')
with open(template_file, 'r', encoding='utf-8') as f:
text = f.read().replace('\\', '/')
for key, value in data.items():
if value is None:
@@ -108,15 +110,38 @@ def poor_yaml_write(data, file, template_file=DEPLOY_TEMPLATE):
value = "false"
text = re.sub(f'{key}:.*?\n', f'{key}: {value}\n', text)
atomic_write(file, text)
with open(file, 'w', encoding='utf-8', newline='') as f:
f.write(text)
def iter_process() -> "Iterable[tuple[int, list[str]]]":
"""
Yields:
int: pid
list[str]: cmdline, and it's guaranteed to have at least one element
"""
@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]:
try:
import psutil
except ModuleNotFoundError:
@@ -126,48 +151,16 @@ def iter_process() -> "Iterable[tuple[int, list[str]]]":
# 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)
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
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,
)
-2
View File
@@ -13,8 +13,6 @@ 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()
+27 -125
View File
@@ -3,61 +3,20 @@ 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_center, area_limit, area_pad, corner2area, get_bbox, get_color, \
image_size, load_image
from module.base.utils import SelectedGrids, area_limit, area_pad, 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 iter_folder
from module.config.utils import deep_get, deep_iter, deep_set, 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}/'
@@ -65,7 +24,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|\.GRID)?'
f'(?P<attr>\.AREA|\.SEARCH|\.COLOR|\.BUTTON)?'
f'\.png$'
)
@@ -87,7 +46,6 @@ class AssetsImage:
self.assets = ''
self.frame = 1
self.attr = ''
self.posi = None
if res:
self.valid = True
@@ -108,7 +66,6 @@ class AssetsImage:
self.bbox: t.Tuple = ()
self.mean: t.Tuple = ()
self.grids = {}
def parse(self):
image = load_image(self.file)
@@ -122,10 +79,6 @@ 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):
@@ -134,40 +87,13 @@ 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:
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
file = os.path.join(path, file).replace('\\', '/')
yield AssetsImage(file)
@dataclass
@@ -177,17 +103,11 @@ 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)
@@ -200,6 +120,7 @@ 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):
@@ -208,21 +129,14 @@ 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}')
@@ -237,22 +151,17 @@ 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 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 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
@@ -261,34 +170,31 @@ def iter_assets():
# Convert to DataAssets
data = {}
for image in images:
if image.is_base:
if image.attr == '':
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 not image.is_base:
if image.attr != '':
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)
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:
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():
frame.search = first.search
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
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)
return data
@@ -334,8 +240,6 @@ 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')):
@@ -344,8 +248,6 @@ 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'))
+4 -4
View File
@@ -2,8 +2,8 @@ import os
from datetime import datetime
from PIL import Image
from pynput import keyboard
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(
'输入aas配置文件名称,或者模拟器serial,或者模拟器端口号: (默认输入 "aas"):\n'
'例如:"aas", "127.0.0.1:16384", "7555"\n'
'输入alas配置文件名称,或者模拟器serial,或者模拟器端口号: (默认输入 "alas"):\n'
'例如:"alas", "127.0.0.1:16384", "7555"\n'
)
name = name.strip().strip('"').strip()
if not name:
name = 'aas'
name = 'alas'
if name.isdigit():
name = f'127.0.0.1:{name}'
if name in alas_instance():
+1 -20
View File
@@ -40,12 +40,6 @@ 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="+",
@@ -56,15 +50,11 @@ 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)
@@ -74,16 +64,7 @@ def func(ev: threading.Event):
from module.logger.logger import console_hdlr
logger.removeHandler(console_hdlr)
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)
uvicorn.run("module.webui.app:app", host=host, port=port, factory=True)
if __name__ == "__main__":
+1 -1
View File
@@ -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:
+43 -74
View File
@@ -1,5 +1,5 @@
import platform
import subprocess
import os
import re
import threading
import time
from datetime import datetime, timedelta
@@ -7,14 +7,16 @@ from datetime import datetime, timedelta
import inflection
from cached_property import cached_property
from MCE.custom_widgets.ctkmessagebox import CTkMessagebox
from module.base.decorator import del_cached_property
from module.config.config import AzurLaneConfig, TaskEnd
from module.config.deep import deep_get, deep_set
from module.config.utils import deep_get, deep_set
from module.exception import *
from module.logger import logger, save_error_log
from module.logger import logger
from module.notify import handle_notify
from MCE.custom_widgets.ctkmessagebox import CTkMessagebox
import subprocess
import platform
class AzurLaneAutoScript:
stop_event: threading.Event = None
@@ -64,22 +66,9 @@ class AzurLaneAutoScript:
logger.exception(e)
exit(1)
def restart(self):
raise NotImplemented
def start(self):
raise NotImplemented
def stop(self):
raise NotImplemented
def goto_main(self):
raise NotImplemented
def run(self, command):
try:
self.device.screenshot()
self.device.screenshot_tracking.clear()
self.__getattribute__(command)()
return True
except TaskEnd:
@@ -87,68 +76,61 @@ class AzurLaneAutoScript:
except GameNotRunningError as e:
logger.warning(e)
self.config.task_call('Restart')
return False
return True
except (GameStuckError, GameTooManyClickError) as e:
logger.error(e)
self.save_error_log()
logger.warning(f'Game stuck, {self.device.package} will be restarted in 10 seconds')
logger.warning('If you are playing by hand, please stop AAS')
logger.warning('If you are playing by hand, please stop Alas')
self.config.task_call('Restart')
self.device.sleep(10)
return False
except GameBugError as e:
logger.warning(e)
self.save_error_log()
logger.warning('An error has occurred in Blue Archive game client, AAS is unable to handle')
logger.warning('An error has occurred in Azur Lane game client, Alas is unable to handle')
logger.warning(f'Restarting {self.device.package} to fix it')
self.config.task_call('Restart')
self.device.sleep(10)
return False
except GamePageUnknownError:
# logger.info('Game server may be under maintenance or network may be broken, check server status now')
logger.info('Game server may be under maintenance or network may be broken, check server status now')
self.checker.check_now()
if self.checker.is_available():
logger.critical('Game page unknown')
self.save_error_log()
handle_notify(
self.config.Error_OnePushConfig,
title=f"AAS <{self.config_name}> crashed",
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> GamePageUnknownError",
)
exit(1)
else:
self.checker.wait_until_available()
return False
except HandledError as e:
logger.error(e)
return False
except ScriptError as e:
logger.exception(e)
self.error_postprocess()
logger.critical(e)
logger.critical('This is likely to be a mistake of developers, but sometimes just random issues')
self.save_error_log()
handle_notify(
self.config.Error_OnePushConfig,
title=f"AAS <{self.config_name}> crashed",
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> ScriptError",
)
exit(1)
except RequestHumanTakeover:
logger.critical('Request human takeover')
self.error_postprocess()
handle_notify(
self.config.Error_OnePushConfig,
title=f"AAS <{self.config_name}> crashed",
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> RequestHumanTakeover",
)
exit(1)
except Exception as e:
logger.exception(e)
self.error_postprocess()
self.save_error_log()
handle_notify(
self.config.Error_OnePushConfig,
title=f"AAS <{self.config_name}> crashed",
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> Exception occured",
)
exit(1)
@@ -158,13 +140,29 @@ class AzurLaneAutoScript:
Save last 60 screenshots in ./log/error/<timestamp>
Save logs to ./log/error/<timestamp>/log.txt
"""
save_error_log(config=self.config, device=self.device)
def error_postprocess(self):
"""
Do something when error occurred
"""
pass
from module.base.utils import save_image
from module.handler.sensitive_info import (handle_sensitive_image, handle_sensitive_logs)
if self.config.Error_SaveError:
if not os.path.exists('./log/error'):
os.mkdir('./log/error')
folder = f'./log/error/{int(time.time() * 1000)}'
logger.warning(f'Saving error: {folder}')
os.mkdir(folder)
for data in self.device.screenshot_deque:
image_time = datetime.strftime(data['time'], '%Y-%m-%d_%H-%M-%S-%f')
image = handle_sensitive_image(data['image'])
save_image(image, f'{folder}/{image_time}.png')
with open(logger.log_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
start = 0
for index, line in enumerate(lines):
line = line.strip(' \r\t\n')
if re.match('^═{15,}$', line):
start = index
lines = lines[start - 2:]
lines = handle_sensitive_logs(lines)
with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f:
f.writelines(lines)
def wait_until(self, future):
"""
@@ -212,16 +210,13 @@ class AzurLaneAutoScript:
method = self.config.Optimization_WhenTaskQueueEmpty
if method == 'close_game':
logger.info('Close game during wait')
self.run('stop')
self.device.app_stop()
release_resources()
self.device.release_during_wait()
if not self.wait_until(task.next_run):
del_cached_property(self, 'config')
continue
if task.command != 'Restart':
self.config.task_call('Restart')
del_cached_property(self, 'config')
continue
self.run('start')
elif method == 'goto_main':
logger.info('Goto main page during wait')
self.run('goto_main')
@@ -237,31 +232,6 @@ class AzurLaneAutoScript:
if not self.wait_until(task.next_run):
del_cached_property(self, 'config')
continue
elif method == 'close_emulator':
logger.info('Close emulator during wait')
self.run('stop')
release_resources()
self.device.release_during_wait()
# 关闭模拟器
try:
self.device.emulator_stop()
logger.info('Emulator stopped successfully')
except Exception as e:
logger.warning(f'Failed to stop emulator: {e}')
if not self.wait_until(task.next_run):
del_cached_property(self, 'config')
del_cached_property(self, 'device')
continue
if task.command == 'Restart':
del_cached_property(self, 'config')
del_cached_property(self, 'device')
continue
# 重新启动模拟器
if task.command != 'Restart':
self.config.task_call('Restart')
del_cached_property(self, 'config')
del_cached_property(self, 'device')
continue
elif method == 'exit_aas':
if abs(task.next_run - datetime.now()) >= timedelta(minutes=2): # ensure tactical challenge is fully ran
self.config.Optimization_WhenTaskQueueEmpty = 'goto_main'
@@ -323,7 +293,7 @@ class AzurLaneAutoScript:
if self.stop_event is not None:
if self.stop_event.is_set():
logger.info("Update event detected")
logger.info(f"AAS [{self.config_name}] exited.")
logger.info(f"Alas [{self.config_name}] exited.")
break
# Check game server maintenance
self.checker.wait_until_available()
@@ -339,7 +309,6 @@ class AzurLaneAutoScript:
task = self.get_next_task()
# Init device and change server
_ = self.device
self.device.config = self.config
# Skip first restart
if self.is_first_task and task == 'Restart':
logger.info('Skip task `Restart` at scheduler start')
@@ -369,7 +338,7 @@ class AzurLaneAutoScript:
logger.critical('Request human takeover')
handle_notify(
self.config.Error_OnePushConfig,
title=f"AAS <{self.config_name}> crashed",
title=f"Alas <{self.config_name}> crashed",
content=f"<{self.config_name}> RequestHumanTakeover\nTask `{task}` failed 3 or more times.",
)
exit(1)

Some files were not shown because too many files have changed in this diff Show More