138 lines
7.6 KiB
Python
138 lines
7.6 KiB
Python
from pathlib import Path
|
|
import shutil
|
|
|
|
root=Path('C:/wechat_rpa')
|
|
backup=root/'backups/loading-performance-20260917'
|
|
backup.mkdir(parents=True,exist_ok=True)
|
|
|
|
def edit(name, replacements):
|
|
path=root/name
|
|
text=path.read_text(encoding='utf-8')
|
|
for old,new in replacements:
|
|
assert text.count(old)==1,(name,old[:90],text.count(old))
|
|
text=text.replace(old,new)
|
|
compile(text,str(path),'exec')
|
|
assert not (backup/name).exists(),f'Backup exists: {name}'
|
|
shutil.copy2(path,backup/name)
|
|
path.write_text(text,encoding='utf-8')
|
|
|
|
edit('wecom_native_sender.py',[
|
|
('from contextlib import contextmanager','from contextlib import contextmanager\nfrom contextvars import ContextVar'),
|
|
("class NativeClient:\n",'''_readonly_build_validation = ContextVar('wecom_readonly_build_validation', default=None)
|
|
|
|
|
|
@contextmanager
|
|
def readonly_build_validation_scope():
|
|
"""Reuse only file hashes within one probe, never live account/send readiness."""
|
|
state={'entries':{},'hashedFiles':0,'reusedFiles':0,'bytesHashed':0}
|
|
token=_readonly_build_validation.set(state)
|
|
try:yield state
|
|
finally:_readonly_build_validation.reset(token)
|
|
|
|
|
|
def _file_identity(info):
|
|
return (info.st_dev,info.st_ino,info.st_size,info.st_mtime_ns,info.st_ctime_ns)
|
|
|
|
|
|
def _hash_client_file(path):
|
|
"""Bound memory and reject a replacement/change during the complete hash."""
|
|
with Path(path).open('rb') as handle:
|
|
before=_file_identity(os.fstat(handle.fileno()))
|
|
digest=hashlib.sha256()
|
|
while chunk:=handle.read(1024*1024):digest.update(chunk)
|
|
after=_file_identity(os.fstat(handle.fileno()))
|
|
if before!=after or before!=_file_identity(Path(path).stat()):
|
|
raise NativeUnavailable('企业微信文件在校验期间发生变化,请重新检测')
|
|
return digest.hexdigest(),before
|
|
|
|
|
|
def _process_creation_id(pid,kernel32):
|
|
"""PID reuse must not reuse the earlier process's build validation."""
|
|
handle=kernel32.OpenProcess(0x1000,False,int(pid))
|
|
if not handle:return None
|
|
try:
|
|
created=w.FILETIME();exited=w.FILETIME();system=w.FILETIME();user=w.FILETIME()
|
|
if not kernel32.GetProcessTimes(handle,c.byref(created),c.byref(exited),c.byref(system),c.byref(user)):
|
|
return None
|
|
return (created.dwHighDateTime<<32)|created.dwLowDateTime
|
|
finally:kernel32.CloseHandle(handle)
|
|
|
|
|
|
def _client_file_digest(path,pid,kernel32,*,readonly):
|
|
state=_readonly_build_validation.get() if readonly else None
|
|
if state is None:
|
|
return _hash_client_file(path)[0]
|
|
created=_process_creation_id(pid,kernel32)
|
|
before=_file_identity(Path(path).stat())
|
|
identity=(int(pid),created,os.path.normcase(os.path.abspath(path)),before)
|
|
if created is not None and identity in state['entries']:
|
|
state['reusedFiles']+=1
|
|
return state['entries'][identity]
|
|
digest,verified=_hash_client_file(path)
|
|
state['hashedFiles']+=1
|
|
state['bytesHashed']+=verified[2]
|
|
if before!=verified:
|
|
raise NativeUnavailable('企业微信文件在校验期间发生变化,请重新检测')
|
|
if created is not None:state['entries'][identity]=digest
|
|
return digest
|
|
|
|
|
|
class NativeClient:
|
|
'''),
|
|
("('GetExitCodeThread',w.BOOL,[w.HANDLE,c.POINTER(w.DWORD)])]", "('GetExitCodeThread',w.BOOL,[w.HANDLE,c.POINTER(w.DWORD)]),\n ('GetProcessTimes',w.BOOL,[w.HANDLE,c.POINTER(w.FILETIME),c.POINTER(w.FILETIME),c.POINTER(w.FILETIME),c.POINTER(w.FILETIME)])]"),
|
|
("self.profile=build_for_hash(hashlib.sha256(self.path.read_bytes()).hexdigest())", "self.profile=build_for_hash(_client_file_digest(self.path,self.pid,self.k,readonly=not write))"),
|
|
])
|
|
|
|
edit('wxwork_protocol_probe.py',[
|
|
('import argparse','import argparse\nfrom contextlib import contextmanager'),
|
|
(' result = []\n for pid in find_wxwork_pids', ' result = []\n binary_metadata = {}\n for pid in find_wxwork_pids'),
|
|
(' entry.update(pe_header(path))\n entry["version"] = file_version(path)', ''' identity = os.path.normcase(os.path.abspath(path))
|
|
metadata = binary_metadata.get(identity)
|
|
if metadata is None:
|
|
metadata = {**pe_header(path), "version": file_version(path), "size": Path(path).stat().st_size}
|
|
binary_metadata[identity] = metadata
|
|
entry.update(metadata)'''),
|
|
('def probe_installed_client(*, deep=False, initialize_database=False):', '''@contextmanager
|
|
def _probe_phase(timings, name):
|
|
started=time.perf_counter()
|
|
try:yield
|
|
finally:timings[name]=round((time.perf_counter()-started)*1000,3)
|
|
|
|
|
|
def probe_installed_client(*, deep=False, initialize_database=False):
|
|
"""Run fresh identity checks; static build hashes live for this call only."""
|
|
from wecom_native_sender import readonly_build_validation_scope
|
|
started=time.perf_counter()
|
|
timings={}
|
|
with readonly_build_validation_scope() as builds:
|
|
result=_probe_installed_client(deep=deep,initialize_database=initialize_database,timings=timings)
|
|
timings['total']=round((time.perf_counter()-started)*1000,3)
|
|
result['timingsMs']=timings
|
|
result['buildValidation']={key:builds[key] for key in ('hashedFiles','reusedFiles','bytesHashed')}
|
|
return result
|
|
|
|
|
|
def _probe_installed_client(*, deep=False, initialize_database=False, timings):'''),
|
|
(' result["clients"] = running_clients()', ' with _probe_phase(timings,"processes"):\n result["clients"] = running_clients()'),
|
|
(' result["binaries"].append(inspect_binary(target, deep=deep))', ''' metadata=next((item for item in result['clients'] if item.get('path')==str(target)),{})
|
|
if not deep and all(key in metadata for key in ('size','architecture','machine','sections','buildTimestamp','isDll','version')):
|
|
result['binaries'].append({'name':target.name,'path':str(target),**{
|
|
key:metadata[key] for key in ('size','architecture','machine','sections','buildTimestamp','isDll','version')}})
|
|
else:
|
|
result["binaries"].append(inspect_binary(target, deep=deep))'''),
|
|
(' live = discover(result["clients"])', ' with _probe_phase(timings,"clientValidation"):\n live = discover(result["clients"])'),
|
|
(' database = protocol_database_status(live["accountId"], initialize=initialize_database)', ' with _probe_phase(timings,"databaseValidation"):\n database = protocol_database_status(live["accountId"], initialize=initialize_database)'),
|
|
(' current = discover()', ' with _probe_phase(timings,"identityRecheck"):\n current = discover()'),
|
|
])
|
|
|
|
edit('reply_database.py',[
|
|
(' def _open_database(*, account="", initialize=True):', ' def _open_database(*, account="", initialize=True, source_directory=None):'),
|
|
(' source = detect_wxwork_dir(account=account) if account else detect_wxwork_dir()', ' # The protocol probe already located this account; reuse only that path,\n # never a readiness result. health_check below still verifies the account.\n source = source_directory if source_directory is not None else (detect_wxwork_dir(account=account) if account else detect_wxwork_dir())'),
|
|
])
|
|
|
|
edit('wecom_environment.py',[
|
|
(' if not detect_wxwork_dir(account=account):', ' source=detect_wxwork_dir(account=account)\n if not source:'),
|
|
(' database = LiveReplyDatabase._open_database(account=account, initialize=initialize)', ' database = LiveReplyDatabase._open_database(account=account, initialize=initialize, source_directory=source)'),
|
|
])
|
|
print('Patched bounded same-probe hashes, phase timings, metadata reuse and one-time data-directory lookup.')
|