@follenfang/fupload 0.0.0-bootstrap.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -3
- package/fupload/SKILL.md +129 -0
- package/fupload/agents/openai.yaml +4 -0
- package/fupload/examples/dd-config-delete.json +5 -0
- package/fupload/examples/dd-config-update.json +25 -0
- package/fupload/examples/dd-plugin-delete.json +5 -0
- package/fupload/examples/dd-plugin-update.json +9 -0
- package/fupload/examples/dd-wa-delete.json +5 -0
- package/fupload/examples/dd-wa-edit.json +9 -0
- package/fupload/examples/newbee-config-delete.json +5 -0
- package/fupload/examples/newbee-config-update.json +10 -0
- package/fupload/examples/newbee-plugin-create.json +14 -0
- package/fupload/examples/newbee-plugin-delete.json +5 -0
- package/fupload/examples/newbee-wa-delete.json +5 -0
- package/fupload/examples/newbee-wa-update.json +8 -0
- package/fupload/references/dd.md +105 -0
- package/fupload/references/newbee-official-cli.md +288 -0
- package/fupload/references/newbee.md +80 -0
- package/fupload/references/workflow.md +67 -0
- package/fupload/scripts/fupload.py +17 -0
- package/fupload/scripts/fupload_cli/__init__.py +3 -0
- package/fupload/scripts/fupload_cli/cli.py +266 -0
- package/fupload/scripts/fupload_cli/dd.py +2406 -0
- package/fupload/scripts/fupload_cli/dd_broker.py +634 -0
- package/fupload/scripts/fupload_cli/dd_sidecar.py +860 -0
- package/fupload/scripts/fupload_cli/errors.py +94 -0
- package/fupload/scripts/fupload_cli/io.py +125 -0
- package/fupload/scripts/fupload_cli/newbee.py +1412 -0
- package/fupload/scripts/fupload_cli/newbee_auth.py +135 -0
- package/fupload/scripts/fupload_cli/schema.py +539 -0
- package/fupload/scripts/fupload_cli/transport.py +125 -0
- package/fupload/scripts/fupload_cli/trust.py +207 -0
- package/npm/bin/fupload.mjs +90 -0
- package/npm/lib/managed-install.mjs +86 -0
- package/npm/lib/options.mjs +38 -0
- package/npm/lib/python.mjs +45 -0
- package/npm/lib/skill-installer.mjs +228 -0
- package/npm/lib/uninstall.mjs +211 -0
- package/npm/lib/update.mjs +99 -0
- package/npm/lib/versions.mjs +63 -0
- package/npm/postinstall.mjs +18 -0
- package/npm/skill-manifest.json +164 -0
- package/package.json +50 -6
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
"""DD-native JSONL sidecar. Run only with the version-matched netease_dd.exe."""
|
|
2
|
+
|
|
3
|
+
from __future__ import print_function
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DD_DIR = os.environ["NETEASE_DD_DIR"]
|
|
18
|
+
DEVICE_STATE = os.environ["FUPLOAD_DD_DEVICE_STATE"]
|
|
19
|
+
USER_AGENT = (
|
|
20
|
+
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 "
|
|
21
|
+
"(KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 app/df_client dfVersion/%s"
|
|
22
|
+
% os.path.basename(DD_DIR.rstrip("\\/"))
|
|
23
|
+
)
|
|
24
|
+
_SENSITIVE_NAME = (
|
|
25
|
+
r"(?:x-amz-(?:credential|signature|security-token)|access[_-]?token|refresh[_-]?token|"
|
|
26
|
+
r"resource[_-]?token|token|jwt(?:token)?|authorization|authentication|cookie|set-cookie|"
|
|
27
|
+
r"credential|client[_-]?(?:no|id)|clientno|clientid|device[_-]?(?:id|proof)|"
|
|
28
|
+
r"client[_-]?secret|api[_-]?key|auth[_-]?key|password|secret|signed[_-]?url|upload[_-]?url|"
|
|
29
|
+
r"presigned[_-]?(?:uri|url)|signature)"
|
|
30
|
+
)
|
|
31
|
+
_SENSITIVE_ERROR_VALUE = re.compile(
|
|
32
|
+
r'''(?i)((?:["']?''' + _SENSITIVE_NAME + r'''["']?)\s*(?:=|:)\s*["']?)[^"',}&\s]+'''
|
|
33
|
+
)
|
|
34
|
+
_BEARER_VALUE = re.compile(r"(?i)Bearer\s+[A-Za-z0-9._~+/=-]+")
|
|
35
|
+
_JWT_VALUE = re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+\b")
|
|
36
|
+
_WA_VALUE = re.compile(r"!WA:\d+![^\s\"']+")
|
|
37
|
+
_HTTP_STATUS = re.compile(r"\bHTTP\s+(\d{3})\b", re.IGNORECASE)
|
|
38
|
+
_SENSITIVE_KEYS = {
|
|
39
|
+
"token", "access_token", "refresh_token", "resource_token", "jwt", "jwttoken",
|
|
40
|
+
"cookie", "set-cookie", "authorization", "authentication", "credential",
|
|
41
|
+
"clientno", "client_no", "clientid", "client_id", "device_id", "device_proof", "signed_url", "upload_url",
|
|
42
|
+
"presigneduri", "presigned_uri", "signature", "client_secret", "api_key", "auth_key", "password", "secret",
|
|
43
|
+
}
|
|
44
|
+
_MAX_LOG_BODY = 1024 * 1024
|
|
45
|
+
_CONFIG_CONTENT_KEYS = {
|
|
46
|
+
"known_addon", "unknown_addon", "wtf", "material", "font",
|
|
47
|
+
"known_wa", "unknown_wa", "retail_ui_config",
|
|
48
|
+
}
|
|
49
|
+
_PRIVATE_BUSINESS_KEYS = _CONFIG_CONTENT_KEYS | {
|
|
50
|
+
"associated_acts", "brief_desc", "channel_id", "content", "cover",
|
|
51
|
+
"creation_statement", "desc", "description", "detail_imgs", "detail_url",
|
|
52
|
+
"display_imgs", "file_install_path", "file_path", "html_desc", "images",
|
|
53
|
+
"import_string", "logo", "name", "parse_wa_id", "parse_wa_uid", "room_id",
|
|
54
|
+
"share_sn", "sn", "t_wa_str", "title", "update_desc", "wa_str",
|
|
55
|
+
}
|
|
56
|
+
_SAFE_REQUEST_VALUE_KEYS = {
|
|
57
|
+
"act_type", "addon_type", "body_bytes", "body_sha256", "business_id",
|
|
58
|
+
"buy_life_type", "category_ids", "channel_type", "creation_statement",
|
|
59
|
+
"file_type", "game_type", "game_version", "game_versions", "headers",
|
|
60
|
+
"jump_room", "mime_type", "need_anchor_vip", "need_buy", "price_fen",
|
|
61
|
+
"primary_category_id", "release_type", "scope", "second_category_ids",
|
|
62
|
+
"share_code_life_type", "sync_room", "version", "vip_levels",
|
|
63
|
+
"with_associate", "with_file",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def safe_exception_message(exc):
|
|
68
|
+
value = _SENSITIVE_ERROR_VALUE.sub(r"\1[REDACTED]", str(exc).strip())
|
|
69
|
+
value = _BEARER_VALUE.sub("Bearer [REDACTED]", value)
|
|
70
|
+
value = _JWT_VALUE.sub("[REDACTED_JWT]", value)
|
|
71
|
+
return _WA_VALUE.sub("[REDACTED_WA]", value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _sanitize_log_value(value, key=None, depth=0):
|
|
75
|
+
if depth > 12:
|
|
76
|
+
return "[MAX_DEPTH]"
|
|
77
|
+
normalized_key = str(key or "").replace("-", "_").lower()
|
|
78
|
+
if normalized_key in _SENSITIVE_KEYS or any(
|
|
79
|
+
marker in normalized_key for marker in ("token", "cookie", "credential", "signature", "password", "secret")
|
|
80
|
+
):
|
|
81
|
+
return "[REDACTED]"
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
return {
|
|
84
|
+
str(child_key): _sanitize_log_value(child_value, child_key, depth + 1)
|
|
85
|
+
for child_key, child_value in value.items()
|
|
86
|
+
}
|
|
87
|
+
if isinstance(value, (list, tuple)):
|
|
88
|
+
return [_sanitize_log_value(item, None, depth + 1) for item in value]
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
return safe_exception_message(value)
|
|
91
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
92
|
+
return value
|
|
93
|
+
return safe_exception_message(str(value))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _private_value_summary(value):
|
|
97
|
+
try:
|
|
98
|
+
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
99
|
+
except (TypeError, ValueError):
|
|
100
|
+
encoded = str(value).encode("utf-8", "replace")
|
|
101
|
+
return {
|
|
102
|
+
"redacted": True,
|
|
103
|
+
"type": type(value).__name__,
|
|
104
|
+
"bytes": len(encoded),
|
|
105
|
+
"sha256": hashlib.sha256(encoded).hexdigest(),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _sanitize_business_payload(value, endpoint, direction, depth=0):
|
|
110
|
+
if depth > 12:
|
|
111
|
+
return "[MAX_DEPTH]"
|
|
112
|
+
if isinstance(value, dict):
|
|
113
|
+
result = {}
|
|
114
|
+
for key, item in value.items():
|
|
115
|
+
normalized = str(key).replace("-", "_").lower()
|
|
116
|
+
if direction == "request" and normalized in _SAFE_REQUEST_VALUE_KEYS:
|
|
117
|
+
result[str(key)] = _sanitize_log_value(item, key, depth + 1)
|
|
118
|
+
elif direction == "request" and normalized == "associated_acts":
|
|
119
|
+
result[str(key)] = _sanitize_business_payload(item, endpoint, direction, depth + 1)
|
|
120
|
+
elif normalized in _PRIVATE_BUSINESS_KEYS:
|
|
121
|
+
result[str(key)] = _private_value_summary(item)
|
|
122
|
+
elif endpoint == "/file/upload" and normalized in ("url", "d_url"):
|
|
123
|
+
result[str(key)] = _private_value_summary(item)
|
|
124
|
+
elif direction == "request" and isinstance(item, str):
|
|
125
|
+
result[str(key)] = _private_value_summary(item)
|
|
126
|
+
else:
|
|
127
|
+
result[str(key)] = _sanitize_business_payload(item, endpoint, direction, depth + 1)
|
|
128
|
+
return result
|
|
129
|
+
if isinstance(value, (list, tuple)):
|
|
130
|
+
return [_sanitize_business_payload(item, endpoint, direction, depth + 1) for item in value]
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _request_shape(value, depth=0):
|
|
135
|
+
if depth > 12:
|
|
136
|
+
return {"type": "max_depth"}
|
|
137
|
+
if isinstance(value, dict):
|
|
138
|
+
return {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"fields": {
|
|
141
|
+
str(key): _request_shape(item, depth + 1)
|
|
142
|
+
for key, item in sorted(value.items(), key=lambda pair: str(pair[0]))
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
if isinstance(value, (list, tuple)):
|
|
146
|
+
return {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"length": len(value),
|
|
149
|
+
"item_types": sorted({type(item).__name__ for item in value}),
|
|
150
|
+
}
|
|
151
|
+
if isinstance(value, str):
|
|
152
|
+
return {"type": "string", "bytes": len(value.encode("utf-8"))}
|
|
153
|
+
if value is None:
|
|
154
|
+
return {"type": "null"}
|
|
155
|
+
return {"type": type(value).__name__}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _request_log_content(value, endpoint=""):
|
|
159
|
+
try:
|
|
160
|
+
encoded = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
161
|
+
except (TypeError, ValueError):
|
|
162
|
+
encoded = str(value).encode("utf-8", "replace")
|
|
163
|
+
projected = _sanitize_business_payload(value, str(endpoint or ""), "request")
|
|
164
|
+
sanitized = _sanitize_log_value(projected)
|
|
165
|
+
sanitized_encoded = json.dumps(
|
|
166
|
+
sanitized, ensure_ascii=True, sort_keys=True, separators=(",", ":"),
|
|
167
|
+
)
|
|
168
|
+
sanitized_size = len(sanitized_encoded.encode("utf-8"))
|
|
169
|
+
result = {
|
|
170
|
+
"request_bytes": len(encoded),
|
|
171
|
+
"request_sha256": hashlib.sha256(encoded).hexdigest(),
|
|
172
|
+
"request_shape": _request_shape(value),
|
|
173
|
+
"request_log_bytes": sanitized_size,
|
|
174
|
+
"request_truncated": sanitized_size > _MAX_LOG_BODY,
|
|
175
|
+
}
|
|
176
|
+
if sanitized_size <= _MAX_LOG_BODY:
|
|
177
|
+
result["request_json"] = sanitized
|
|
178
|
+
else:
|
|
179
|
+
result["request_body"] = sanitized_encoded[:_MAX_LOG_BODY]
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _bounded_json_fields(prefix, value, endpoint="", direction=""):
|
|
184
|
+
projected = _sanitize_business_payload(value, str(endpoint or ""), direction)
|
|
185
|
+
sanitized = _sanitize_log_value(projected)
|
|
186
|
+
encoded = json.dumps(sanitized, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
187
|
+
size = len(encoded.encode("utf-8"))
|
|
188
|
+
fields = {prefix + "_bytes": size, prefix + "_truncated": size > _MAX_LOG_BODY}
|
|
189
|
+
if size <= _MAX_LOG_BODY:
|
|
190
|
+
fields[prefix + "_json"] = sanitized
|
|
191
|
+
else:
|
|
192
|
+
fields[prefix + "_body"] = encoded[:_MAX_LOG_BODY]
|
|
193
|
+
return fields
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _bounded_text(value):
|
|
197
|
+
encoded = str(value).encode("utf-8")
|
|
198
|
+
original_size = len(encoded)
|
|
199
|
+
truncated = len(encoded) > _MAX_LOG_BODY
|
|
200
|
+
if truncated:
|
|
201
|
+
encoded = encoded[:_MAX_LOG_BODY]
|
|
202
|
+
return encoded.decode("utf-8", "ignore"), original_size, truncated
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _response_probe(status, body):
|
|
206
|
+
if isinstance(body, bytes):
|
|
207
|
+
text = body.decode("utf-8", "replace")
|
|
208
|
+
else:
|
|
209
|
+
text = str(body or "")
|
|
210
|
+
bounded, size, truncated = _bounded_text(text)
|
|
211
|
+
return {
|
|
212
|
+
"status": int(status or 0),
|
|
213
|
+
"body": bounded,
|
|
214
|
+
"body_bytes": size,
|
|
215
|
+
"body_truncated": truncated,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _response_status(payload):
|
|
220
|
+
if not isinstance(payload, dict):
|
|
221
|
+
return None
|
|
222
|
+
for key in ("http_status", "status_code", "status", "httpStatus"):
|
|
223
|
+
value = payload.get(key)
|
|
224
|
+
if value not in (None, ""):
|
|
225
|
+
try:
|
|
226
|
+
return int(value)
|
|
227
|
+
except (TypeError, ValueError):
|
|
228
|
+
pass
|
|
229
|
+
for key in ("error", "data", "result"):
|
|
230
|
+
nested = payload.get(key)
|
|
231
|
+
status = _response_status(nested)
|
|
232
|
+
if status is not None:
|
|
233
|
+
return status
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _response_log_content(probe, payload=None, endpoint=""):
|
|
238
|
+
if payload is not None:
|
|
239
|
+
return _bounded_json_fields("response", payload, endpoint, "response")
|
|
240
|
+
if not isinstance(probe, dict) or not probe.get("body"):
|
|
241
|
+
return {}
|
|
242
|
+
body = str(probe["body"])
|
|
243
|
+
original_size = int(probe.get("body_bytes") or len(body.encode("utf-8")))
|
|
244
|
+
originally_truncated = bool(probe.get("body_truncated"))
|
|
245
|
+
if originally_truncated:
|
|
246
|
+
sanitized, _stored_size, _ = _bounded_text(safe_exception_message(body))
|
|
247
|
+
return {
|
|
248
|
+
"response_body": sanitized,
|
|
249
|
+
"response_bytes": original_size,
|
|
250
|
+
"response_truncated": True,
|
|
251
|
+
}
|
|
252
|
+
try:
|
|
253
|
+
parsed = json.loads(body)
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
sanitized, size, truncated = _bounded_text(safe_exception_message(body))
|
|
256
|
+
return {
|
|
257
|
+
"response_body": sanitized,
|
|
258
|
+
"response_bytes": size,
|
|
259
|
+
"response_truncated": truncated,
|
|
260
|
+
}
|
|
261
|
+
return _bounded_json_fields("response", parsed, endpoint, "response")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def write_error_log(failure, command, probe=None, payload=None):
|
|
265
|
+
log_dir = os.path.join(DD_DIR, "Fupload", "logs")
|
|
266
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
267
|
+
path = os.path.join(log_dir, "dd-errors-%s.jsonl" % time.strftime("%Y%m%d"))
|
|
268
|
+
request = command.get("wire_request") if isinstance(command, dict) else None
|
|
269
|
+
if request is None and isinstance(command, dict):
|
|
270
|
+
request = command.get("payload")
|
|
271
|
+
if request is None and isinstance(command, dict) and command.get("action") == "upload":
|
|
272
|
+
request = command.get("meta")
|
|
273
|
+
endpoint = command.get("path") if isinstance(command, dict) else None
|
|
274
|
+
if isinstance(command, dict) and command.get("action") == "upload":
|
|
275
|
+
if failure.stage == "upload_authorize":
|
|
276
|
+
endpoint = "/file/upload"
|
|
277
|
+
elif failure.stage == "object_put":
|
|
278
|
+
endpoint = "object-store-put"
|
|
279
|
+
record = {
|
|
280
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
|
|
281
|
+
"action": command.get("action") if isinstance(command, dict) else None,
|
|
282
|
+
"method": command.get("method") if isinstance(command, dict) else None,
|
|
283
|
+
"endpoint": endpoint,
|
|
284
|
+
"stage": failure.stage,
|
|
285
|
+
"message": safe_exception_message(failure),
|
|
286
|
+
"http_status": failure.http_status,
|
|
287
|
+
"business_code": failure.business_code,
|
|
288
|
+
"verification_required": bool(failure.verification_required),
|
|
289
|
+
"request_fields": sorted(str(key) for key in request) if isinstance(request, dict) else [],
|
|
290
|
+
"validation": _sanitize_log_value(failure.details),
|
|
291
|
+
}
|
|
292
|
+
if request is not None:
|
|
293
|
+
record.update(_request_log_content(request, endpoint))
|
|
294
|
+
record.update(_response_log_content(probe, payload, endpoint))
|
|
295
|
+
with open(path, "a", encoding="utf-8") as handle:
|
|
296
|
+
handle.write(json.dumps(record, ensure_ascii=True, sort_keys=True, separators=(",", ":")) + "\n")
|
|
297
|
+
return os.path.abspath(path)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def output(payload):
|
|
301
|
+
print("FUPLOAD_RESULT " + json.dumps(payload, ensure_ascii=True, sort_keys=True))
|
|
302
|
+
sys.stdout.flush()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class SidecarFailure(Exception):
|
|
306
|
+
def __init__(self, message, stage, http_status=None, business_code=None, verification_required=False, details=None):
|
|
307
|
+
Exception.__init__(self, message)
|
|
308
|
+
self.stage = stage
|
|
309
|
+
self.http_status = http_status
|
|
310
|
+
self.business_code = business_code
|
|
311
|
+
self.verification_required = verification_required
|
|
312
|
+
self.details = details or {}
|
|
313
|
+
|
|
314
|
+
def as_dict(self):
|
|
315
|
+
result = {
|
|
316
|
+
"kind": "platform_error",
|
|
317
|
+
"stage": self.stage,
|
|
318
|
+
"message": safe_exception_message(self.args[0]),
|
|
319
|
+
"verification_required": bool(self.verification_required),
|
|
320
|
+
}
|
|
321
|
+
if self.http_status is not None:
|
|
322
|
+
result["http_status"] = self.http_status
|
|
323
|
+
if self.business_code is not None:
|
|
324
|
+
result["business_code"] = self.business_code
|
|
325
|
+
if self.details:
|
|
326
|
+
result["details"] = self.details
|
|
327
|
+
return result
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _validation_details(probe):
|
|
331
|
+
if not isinstance(probe, dict):
|
|
332
|
+
return {}
|
|
333
|
+
body = probe.get("body")
|
|
334
|
+
if not body:
|
|
335
|
+
return {}
|
|
336
|
+
try:
|
|
337
|
+
parsed = json.loads(body)
|
|
338
|
+
except (TypeError, ValueError):
|
|
339
|
+
return {"server_message": safe_exception_message(str(body))[:400]}
|
|
340
|
+
if not isinstance(parsed, dict):
|
|
341
|
+
return {"server_response": safe_exception_message(str(parsed))[:400]}
|
|
342
|
+
result = {}
|
|
343
|
+
for key in ("code", "msg", "message", "error", "field", "fields", "type"):
|
|
344
|
+
if key in parsed and parsed[key] not in (None, ""):
|
|
345
|
+
result["server_" + key] = safe_exception_message(str(parsed[key]))[:400]
|
|
346
|
+
detail = parsed.get("detail")
|
|
347
|
+
if isinstance(detail, list):
|
|
348
|
+
items = []
|
|
349
|
+
for item in detail[:20]:
|
|
350
|
+
if not isinstance(item, dict):
|
|
351
|
+
continue
|
|
352
|
+
entry = {}
|
|
353
|
+
for key in ("loc", "msg", "type"):
|
|
354
|
+
if key in item:
|
|
355
|
+
entry[key] = safe_exception_message(str(item[key]))[:200]
|
|
356
|
+
if entry:
|
|
357
|
+
items.append(entry)
|
|
358
|
+
if items:
|
|
359
|
+
result["server_detail"] = items
|
|
360
|
+
elif detail not in (None, ""):
|
|
361
|
+
result["server_detail"] = safe_exception_message(str(detail))[:400]
|
|
362
|
+
return result
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def install_response_probe(client):
|
|
366
|
+
"""Capture response objects and HTTPError bodies without changing DD behavior."""
|
|
367
|
+
session = getattr(client, "_session", None)
|
|
368
|
+
restore = getattr(client, "_fupload_restore_response_probe", None)
|
|
369
|
+
if callable(restore):
|
|
370
|
+
restore()
|
|
371
|
+
original_opener_open = urllib.request.OpenerDirector.open
|
|
372
|
+
|
|
373
|
+
def opener_open(opener, *args, **kwargs):
|
|
374
|
+
try:
|
|
375
|
+
return original_opener_open(opener, *args, **kwargs)
|
|
376
|
+
except urllib.error.HTTPError as error:
|
|
377
|
+
original_read = error.read
|
|
378
|
+
|
|
379
|
+
def error_read(*read_args, _error=error, **read_kwargs):
|
|
380
|
+
body = original_read(*read_args, **read_kwargs)
|
|
381
|
+
try:
|
|
382
|
+
text = bytes(body or b"").decode("utf-8", "replace")
|
|
383
|
+
except Exception:
|
|
384
|
+
text = ""
|
|
385
|
+
bounded, size, truncated = _bounded_text(text)
|
|
386
|
+
client._fupload_last_response_error = {
|
|
387
|
+
"status": int(getattr(_error, "code", 0) or 0),
|
|
388
|
+
"body": bounded,
|
|
389
|
+
"body_bytes": size,
|
|
390
|
+
"body_truncated": truncated,
|
|
391
|
+
}
|
|
392
|
+
return body
|
|
393
|
+
|
|
394
|
+
error.read = error_read
|
|
395
|
+
raise
|
|
396
|
+
|
|
397
|
+
urllib.request.OpenerDirector.open = opener_open
|
|
398
|
+
|
|
399
|
+
def restore_probe():
|
|
400
|
+
if getattr(urllib.request.OpenerDirector, "open", None) is opener_open:
|
|
401
|
+
urllib.request.OpenerDirector.open = original_opener_open
|
|
402
|
+
client._fupload_restore_response_probe = None
|
|
403
|
+
|
|
404
|
+
client._fupload_restore_response_probe = restore_probe
|
|
405
|
+
for method_name in ("get", "post"):
|
|
406
|
+
original = getattr(session, method_name, None)
|
|
407
|
+
if not callable(original):
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
def wrapped_factory(original_method):
|
|
411
|
+
def wrapped(*args, **kwargs):
|
|
412
|
+
client._fupload_last_response_error = None
|
|
413
|
+
response = original_method(*args, **kwargs)
|
|
414
|
+
try:
|
|
415
|
+
status = int(getattr(response, "status_code", 0) or 0)
|
|
416
|
+
except (TypeError, ValueError):
|
|
417
|
+
status = 0
|
|
418
|
+
if status >= 400:
|
|
419
|
+
try:
|
|
420
|
+
body = str(getattr(response, "text", "") or "")
|
|
421
|
+
except Exception:
|
|
422
|
+
body = ""
|
|
423
|
+
bounded, size, truncated = _bounded_text(body)
|
|
424
|
+
client._fupload_last_response_error = {
|
|
425
|
+
"status": status,
|
|
426
|
+
"body": bounded,
|
|
427
|
+
"body_bytes": size,
|
|
428
|
+
"body_truncated": truncated,
|
|
429
|
+
}
|
|
430
|
+
return response
|
|
431
|
+
return wrapped
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
setattr(session, method_name, wrapped_factory(original))
|
|
435
|
+
except (AttributeError, TypeError):
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def failure_from_exception(exc, stage, response=None):
|
|
440
|
+
if isinstance(exc, SidecarFailure):
|
|
441
|
+
return exc
|
|
442
|
+
if isinstance(exc, urllib.error.HTTPError):
|
|
443
|
+
probe = dict(response) if isinstance(response, dict) else {}
|
|
444
|
+
if not probe.get("body"):
|
|
445
|
+
try:
|
|
446
|
+
body = exc.read()
|
|
447
|
+
except Exception:
|
|
448
|
+
body = b""
|
|
449
|
+
if body:
|
|
450
|
+
text = bytes(body).decode("utf-8", "replace")
|
|
451
|
+
bounded, size, truncated = _bounded_text(text)
|
|
452
|
+
probe.update({
|
|
453
|
+
"status": int(exc.code),
|
|
454
|
+
"body": bounded,
|
|
455
|
+
"body_bytes": size,
|
|
456
|
+
"body_truncated": truncated,
|
|
457
|
+
})
|
|
458
|
+
business_code = None
|
|
459
|
+
if probe.get("body"):
|
|
460
|
+
try:
|
|
461
|
+
body_value = json.loads(probe["body"])
|
|
462
|
+
if isinstance(body_value, dict):
|
|
463
|
+
business_code = body_value.get("code")
|
|
464
|
+
if business_code is None:
|
|
465
|
+
business_code = body_value.get("error_code")
|
|
466
|
+
except (TypeError, ValueError):
|
|
467
|
+
pass
|
|
468
|
+
return SidecarFailure(
|
|
469
|
+
"DD %s request returned HTTP %d" % (stage, exc.code), stage,
|
|
470
|
+
http_status=exc.code,
|
|
471
|
+
business_code=business_code,
|
|
472
|
+
verification_required=stage in ("object_put", "mutation") and exc.code >= 500,
|
|
473
|
+
details=_validation_details(probe),
|
|
474
|
+
)
|
|
475
|
+
uncertain = stage in ("object_put", "mutation")
|
|
476
|
+
message = safe_exception_message(exc)
|
|
477
|
+
probe = response if isinstance(response, dict) else {}
|
|
478
|
+
status = probe.get("status")
|
|
479
|
+
match = _HTTP_STATUS.search(message)
|
|
480
|
+
if status is None and match:
|
|
481
|
+
status = int(match.group(1))
|
|
482
|
+
try:
|
|
483
|
+
status = int(status) if status is not None else None
|
|
484
|
+
except (TypeError, ValueError):
|
|
485
|
+
status = None
|
|
486
|
+
business_code = getattr(exc, "code", None)
|
|
487
|
+
if business_code is None:
|
|
488
|
+
business_code = getattr(exc, "error_code", None)
|
|
489
|
+
if business_code is None and isinstance(probe, dict) and probe.get("body"):
|
|
490
|
+
try:
|
|
491
|
+
body_value = json.loads(probe["body"])
|
|
492
|
+
if isinstance(body_value, dict):
|
|
493
|
+
business_code = body_value.get("code")
|
|
494
|
+
if business_code is None:
|
|
495
|
+
business_code = body_value.get("error_code")
|
|
496
|
+
except (TypeError, ValueError):
|
|
497
|
+
pass
|
|
498
|
+
if not message:
|
|
499
|
+
message = type(exc).__name__
|
|
500
|
+
return SidecarFailure(
|
|
501
|
+
"DD %s request failed (%s): %s" % (stage, type(exc).__name__, message[:400]),
|
|
502
|
+
stage,
|
|
503
|
+
http_status=status,
|
|
504
|
+
business_code=business_code,
|
|
505
|
+
verification_required=uncertain and not (status is not None and 400 <= status < 500),
|
|
506
|
+
details=_validation_details(probe),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def bootstrap():
|
|
511
|
+
resource = os.path.join(DD_DIR, "ccvoicehub.res")
|
|
512
|
+
pyqt_dir = os.path.join(DD_DIR, "ccsub64", "PyQt5")
|
|
513
|
+
support_dir = os.path.join(DD_DIR, "ccsub64")
|
|
514
|
+
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
|
515
|
+
os.environ["PATH"] = os.pathsep.join([pyqt_dir, DD_DIR, support_dir, os.environ.get("PATH", "")])
|
|
516
|
+
ctypes.windll.kernel32.SetDllDirectoryW(pyqt_dir)
|
|
517
|
+
sys.path.insert(0, resource)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def load_client_no(machine_data):
|
|
521
|
+
if os.path.exists(DEVICE_STATE):
|
|
522
|
+
try:
|
|
523
|
+
with open(DEVICE_STATE, "r", encoding="utf-8") as handle:
|
|
524
|
+
state = json.load(handle)
|
|
525
|
+
value = state.get("client_no", "")
|
|
526
|
+
except Exception as exc:
|
|
527
|
+
raise RuntimeError("sidecar-device.json is unreadable") from exc
|
|
528
|
+
if not re.match(r"^[0-9a-f]{32}$", value):
|
|
529
|
+
raise RuntimeError("sidecar-device.json contains an invalid client_no")
|
|
530
|
+
return value, False
|
|
531
|
+
value = machine_data.generateClientNo()
|
|
532
|
+
if not re.match(r"^[0-9a-f]{32}$", value):
|
|
533
|
+
raise RuntimeError("DD generated an invalid client_no")
|
|
534
|
+
directory = os.path.dirname(DEVICE_STATE)
|
|
535
|
+
if not os.path.isdir(directory):
|
|
536
|
+
os.makedirs(directory)
|
|
537
|
+
temporary = DEVICE_STATE + ".tmp.%d" % os.getpid()
|
|
538
|
+
with open(temporary, "w", encoding="utf-8") as handle:
|
|
539
|
+
json.dump({"version": 1, "client_no": value}, handle, sort_keys=True)
|
|
540
|
+
os.replace(temporary, DEVICE_STATE)
|
|
541
|
+
return value, True
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def wait_until(qt, predicate, timeout):
|
|
545
|
+
deadline = time.time() + timeout
|
|
546
|
+
while time.time() < deadline:
|
|
547
|
+
qt.process_events(100)
|
|
548
|
+
if predicate():
|
|
549
|
+
return True
|
|
550
|
+
time.sleep(0.02)
|
|
551
|
+
return bool(predicate())
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def open_session(timeout=45):
|
|
555
|
+
from cli_anything.ccvoicehub.core.container import ContainerManager
|
|
556
|
+
from cli_anything.ccvoicehub.core.qt_runtime import QtRuntime
|
|
557
|
+
from components.login.flow import MobileReLoginFlow
|
|
558
|
+
import components.login.login_controller as login_controller_module
|
|
559
|
+
import logger.cclogger as cclogger
|
|
560
|
+
|
|
561
|
+
qt = QtRuntime.get_instance()
|
|
562
|
+
container = ContainerManager.get_container()
|
|
563
|
+
storage = container.get_instance("AccountCredStorage")
|
|
564
|
+
account = storage.getAutoAccount()
|
|
565
|
+
credential = storage.getCred(account)
|
|
566
|
+
if not account or not credential:
|
|
567
|
+
raise RuntimeError("DD has no persisted credential; sign in with the desktop client first")
|
|
568
|
+
cclogger.renameLogAfterLogin = lambda *_args, **_kwargs: None
|
|
569
|
+
login_controller_module.renameLogAfterLogin = lambda *_args, **_kwargs: None
|
|
570
|
+
controller = container.get_instance("LoginController")
|
|
571
|
+
jwt_helper = container.get_instance("JwtHelper")
|
|
572
|
+
state = {"done": False, "ok": False, "jwt": False, "error": ""}
|
|
573
|
+
|
|
574
|
+
def login_result(*args):
|
|
575
|
+
state["done"] = True
|
|
576
|
+
state["ok"] = bool(args and args[0])
|
|
577
|
+
if len(args) > 1 and args[1]:
|
|
578
|
+
state["error"] = str(args[1])[:160]
|
|
579
|
+
|
|
580
|
+
def jwt_result(*_args):
|
|
581
|
+
state["jwt"] = True
|
|
582
|
+
|
|
583
|
+
jwt_helper.sigJwtUpdated.connect(jwt_result)
|
|
584
|
+
flow = MobileReLoginFlow(
|
|
585
|
+
controller, container.get_instance("UrsSDK"), container.get_instance("CgiHelper"),
|
|
586
|
+
container.get_instance("NetConfig"), credential.value,
|
|
587
|
+
credential.modifier.name == "mobile_password", account.name,
|
|
588
|
+
)
|
|
589
|
+
flow.sigResult.connect(login_result)
|
|
590
|
+
flow.start()
|
|
591
|
+
if not wait_until(qt, lambda: state["done"], timeout) or not state["ok"]:
|
|
592
|
+
raise RuntimeError(state["error"] or "DD native login failed or timed out")
|
|
593
|
+
jwt_helper.startRequestJwt()
|
|
594
|
+
if not wait_until(qt, lambda: state["jwt"] and bool(jwt_helper.getJwt()), timeout):
|
|
595
|
+
raise RuntimeError("DD JWT refresh failed or timed out")
|
|
596
|
+
from components.wow_ui.nep_wrapper import NepWrapper
|
|
597
|
+
from cli_anything.ccvoicehub.core.ui_api_client import UiApiClient
|
|
598
|
+
nep = NepWrapper(None)
|
|
599
|
+
if not nep.isDllInited():
|
|
600
|
+
raise RuntimeError("DD NEP module is not initialized")
|
|
601
|
+
client = UiApiClient(nep, login_cookie=getattr(controller, "_cookie", None))
|
|
602
|
+
client._session.headers["User-Agent"] = USER_AGENT
|
|
603
|
+
if not client.login():
|
|
604
|
+
raise RuntimeError("DD author API login failed")
|
|
605
|
+
install_response_probe(client)
|
|
606
|
+
return qt, container, flow, jwt_helper, client
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def api_result(payload):
|
|
610
|
+
return payload.get("result") if isinstance(payload, dict) else None
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _native_json(value):
|
|
614
|
+
to_json = getattr(value, "toJson", None)
|
|
615
|
+
if callable(to_json):
|
|
616
|
+
value = to_json()
|
|
617
|
+
if isinstance(value, bytes):
|
|
618
|
+
value = value.decode("utf-8", "replace")
|
|
619
|
+
if isinstance(value, str):
|
|
620
|
+
value = json.loads(value)
|
|
621
|
+
return value
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _native_wa_ids(value):
|
|
625
|
+
parsed = _native_json(value)
|
|
626
|
+
if isinstance(parsed, dict):
|
|
627
|
+
code = parsed.get("code")
|
|
628
|
+
if code not in (None, 0, 200):
|
|
629
|
+
raise RuntimeError(str(parsed.get("msg") or parsed.get("message") or "DD WA parser rejected the content"))
|
|
630
|
+
nested = _native_json(parsed.get("result")) if parsed.get("result") is not None else parsed
|
|
631
|
+
if isinstance(nested, dict):
|
|
632
|
+
uid = nested.get("parse_wa_uid") or nested.get("uid") or nested.get("wa_uid")
|
|
633
|
+
ident = nested.get("parse_wa_id") or nested.get("id") or nested.get("wa_id")
|
|
634
|
+
else:
|
|
635
|
+
uid = ident = None
|
|
636
|
+
elif isinstance(parsed, (list, tuple)) and len(parsed) >= 2:
|
|
637
|
+
uid, ident = parsed[0], parsed[1]
|
|
638
|
+
else:
|
|
639
|
+
uid = ident = None
|
|
640
|
+
if not uid or not ident:
|
|
641
|
+
raise RuntimeError("DD native WA parser did not return uid and id")
|
|
642
|
+
return {"parse_wa_uid": str(uid), "parse_wa_id": str(ident)}
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def parse_native_wa(session, content):
|
|
646
|
+
"""Follow the installed DD bridge and parser chain; never synthesize IDs."""
|
|
647
|
+
container = session[1]
|
|
648
|
+
try:
|
|
649
|
+
interface = container.get_instance("WowUIInterface")
|
|
650
|
+
except Exception:
|
|
651
|
+
interface = None
|
|
652
|
+
bridge = getattr(interface, "parseWa", None)
|
|
653
|
+
if callable(bridge):
|
|
654
|
+
return _native_wa_ids(bridge({"waStr": content}))
|
|
655
|
+
|
|
656
|
+
try:
|
|
657
|
+
from components.wow_ui.wa.wa_parser import WaParser
|
|
658
|
+
parser = WaParser(None)
|
|
659
|
+
return _native_wa_ids(parser.parseWa(content))
|
|
660
|
+
except ImportError:
|
|
661
|
+
pass
|
|
662
|
+
|
|
663
|
+
raise RuntimeError("installed DD runtime does not expose a native WA parser")
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def run_command(session, command):
|
|
667
|
+
client = session[-1]
|
|
668
|
+
client._fupload_last_response_error = None
|
|
669
|
+
action = command.get("action")
|
|
670
|
+
rejected_payload = None
|
|
671
|
+
try:
|
|
672
|
+
if action == "request":
|
|
673
|
+
method = command.get("method", "GET").upper()
|
|
674
|
+
path = command["path"]
|
|
675
|
+
params = command.get("payload") or {}
|
|
676
|
+
response_payload = client.get(path, params) if method == "GET" else client.post(path, params)
|
|
677
|
+
if isinstance(response_payload, dict) and response_payload.get("code") not in (None, 0):
|
|
678
|
+
rejected_payload = response_payload
|
|
679
|
+
response_probe = getattr(client, "_fupload_last_response_error", None)
|
|
680
|
+
raise SidecarFailure(
|
|
681
|
+
str(response_payload.get("msg") or response_payload.get("message") or "DD operation failed"),
|
|
682
|
+
command.get("request_stage") or ("dependency_get" if method == "GET" else "mutation"),
|
|
683
|
+
http_status=(response_probe or {}).get("status") if isinstance(response_probe, dict) else _response_status(response_payload),
|
|
684
|
+
business_code=response_payload.get("code"),
|
|
685
|
+
details=_validation_details({"body": json.dumps(response_payload, ensure_ascii=False)}),
|
|
686
|
+
)
|
|
687
|
+
return response_payload
|
|
688
|
+
if action == "upload":
|
|
689
|
+
path = command["file"]
|
|
690
|
+
meta = command["meta"]
|
|
691
|
+
command["wire_request"] = {"upload_authorize": meta}
|
|
692
|
+
try:
|
|
693
|
+
auth = client.get("/file/upload", meta)
|
|
694
|
+
except Exception as exc:
|
|
695
|
+
raise failure_from_exception(exc, "upload_authorize", getattr(client, "_fupload_last_response_error", None))
|
|
696
|
+
if isinstance(auth, dict) and auth.get("code") not in (None, 0):
|
|
697
|
+
rejected_payload = auth
|
|
698
|
+
response_probe = getattr(client, "_fupload_last_response_error", None)
|
|
699
|
+
raise SidecarFailure(
|
|
700
|
+
"DD upload authorization was rejected",
|
|
701
|
+
"upload_authorize",
|
|
702
|
+
http_status=(response_probe or {}).get("status") if isinstance(response_probe, dict) else _response_status(auth),
|
|
703
|
+
business_code=auth.get("code"),
|
|
704
|
+
details=_validation_details({"body": json.dumps(auth, ensure_ascii=False)}),
|
|
705
|
+
)
|
|
706
|
+
info = api_result(auth)
|
|
707
|
+
if not isinstance(info, dict) or not info.get("url") or not info.get("d_url"):
|
|
708
|
+
rejected_payload = auth
|
|
709
|
+
raise SidecarFailure("DD upload authorization did not return a usable upload target", "upload_authorize")
|
|
710
|
+
size = os.path.getsize(path)
|
|
711
|
+
maximum = int(info.get("maxSize") or 0)
|
|
712
|
+
if maximum and size > maximum:
|
|
713
|
+
raise SidecarFailure("file exceeds DD server size limit", "upload_authorize")
|
|
714
|
+
try:
|
|
715
|
+
with open(path, "rb") as handle:
|
|
716
|
+
data = handle.read()
|
|
717
|
+
headers = {"Content-Type": meta["mime_type"], "X-Amz-Acl": "public-read"}
|
|
718
|
+
command["wire_request"]["object_put"] = {
|
|
719
|
+
"headers": headers,
|
|
720
|
+
"body_bytes": size,
|
|
721
|
+
"body_sha256": hashlib.sha256(data).hexdigest(),
|
|
722
|
+
}
|
|
723
|
+
request = urllib.request.Request(
|
|
724
|
+
info["url"], data=data, method="PUT", headers=headers,
|
|
725
|
+
)
|
|
726
|
+
with urllib.request.urlopen(request, timeout=180) as response:
|
|
727
|
+
if response.status != 200:
|
|
728
|
+
try:
|
|
729
|
+
response_body = response.read()
|
|
730
|
+
except Exception:
|
|
731
|
+
response_body = b""
|
|
732
|
+
response_probe = _response_probe(response.status, response_body)
|
|
733
|
+
raise SidecarFailure(
|
|
734
|
+
"DD object upload returned HTTP %d" % response.status,
|
|
735
|
+
"object_put",
|
|
736
|
+
http_status=response.status,
|
|
737
|
+
details=_validation_details(response_probe),
|
|
738
|
+
)
|
|
739
|
+
except urllib.error.HTTPError as exc:
|
|
740
|
+
response_probe = _response_probe(exc.code, exc.read())
|
|
741
|
+
raise failure_from_exception(exc, "object_put", response_probe)
|
|
742
|
+
except Exception as exc:
|
|
743
|
+
raise failure_from_exception(exc, "object_put")
|
|
744
|
+
return {"d_url": info["d_url"], "size": size}
|
|
745
|
+
if action == "cc_get":
|
|
746
|
+
jwt_value = session[3].getJwt()
|
|
747
|
+
url = command["url"]
|
|
748
|
+
request = urllib.request.Request(
|
|
749
|
+
url,
|
|
750
|
+
headers={
|
|
751
|
+
"Authentication": jwt_value,
|
|
752
|
+
"User-Agent": USER_AGENT,
|
|
753
|
+
},
|
|
754
|
+
)
|
|
755
|
+
with urllib.request.urlopen(request, timeout=30) as response:
|
|
756
|
+
return json.loads(response.read().decode("utf-8"))
|
|
757
|
+
if action == "parse_wa":
|
|
758
|
+
return parse_native_wa(session, command["content"])
|
|
759
|
+
raise SidecarFailure("unsupported sidecar action", "session")
|
|
760
|
+
except Exception as exc:
|
|
761
|
+
if action == "request":
|
|
762
|
+
stage = command.get("request_stage")
|
|
763
|
+
if stage not in ("dependency_get", "mutation"):
|
|
764
|
+
stage = "dependency_get" if command.get("method", "GET").upper() == "GET" else "mutation"
|
|
765
|
+
elif action == "cc_get":
|
|
766
|
+
stage = "dependency_get"
|
|
767
|
+
elif action == "parse_wa":
|
|
768
|
+
stage = "native_parser"
|
|
769
|
+
else:
|
|
770
|
+
stage = "session"
|
|
771
|
+
response_probe = locals().get("response_probe")
|
|
772
|
+
if not isinstance(response_probe, dict):
|
|
773
|
+
response_probe = getattr(client, "_fupload_last_response_error", None)
|
|
774
|
+
failure = failure_from_exception(exc, stage, response_probe)
|
|
775
|
+
try:
|
|
776
|
+
log_path = write_error_log(
|
|
777
|
+
failure, command,
|
|
778
|
+
response_probe,
|
|
779
|
+
rejected_payload,
|
|
780
|
+
)
|
|
781
|
+
failure.details["log_path"] = log_path
|
|
782
|
+
except Exception as log_exc:
|
|
783
|
+
failure.details["log_write_error"] = type(log_exc).__name__
|
|
784
|
+
raise failure
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def close_session(session):
|
|
788
|
+
if not session:
|
|
789
|
+
return
|
|
790
|
+
qt, container, _flow, _jwt_helper, client = session
|
|
791
|
+
restore_probe = getattr(client, "_fupload_restore_response_probe", None)
|
|
792
|
+
if callable(restore_probe):
|
|
793
|
+
restore_probe()
|
|
794
|
+
try:
|
|
795
|
+
message_center = container.get_instance("MessageCenter")
|
|
796
|
+
message_center.logout()
|
|
797
|
+
try:
|
|
798
|
+
from components.net_server.messagecenter import State
|
|
799
|
+
|
|
800
|
+
def stopped():
|
|
801
|
+
state = message_center.state
|
|
802
|
+
checker = getattr(state, "isStopped", None)
|
|
803
|
+
if callable(checker):
|
|
804
|
+
return bool(checker())
|
|
805
|
+
checker = getattr(State, "isStopped", None)
|
|
806
|
+
return bool(checker(state)) if callable(checker) else False
|
|
807
|
+
|
|
808
|
+
wait_until(qt, stopped, 5)
|
|
809
|
+
except Exception:
|
|
810
|
+
pass
|
|
811
|
+
except Exception:
|
|
812
|
+
pass
|
|
813
|
+
try:
|
|
814
|
+
client._token = ""
|
|
815
|
+
client.close()
|
|
816
|
+
except Exception:
|
|
817
|
+
pass
|
|
818
|
+
try:
|
|
819
|
+
container.shutdown()
|
|
820
|
+
finally:
|
|
821
|
+
qt.shutdown()
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def main():
|
|
825
|
+
logging.disable(logging.CRITICAL)
|
|
826
|
+
bootstrap()
|
|
827
|
+
import datacenter.local_data.machine_data as machine_data
|
|
828
|
+
original = machine_data.clientNo
|
|
829
|
+
value, created = load_client_no(machine_data)
|
|
830
|
+
machine_data.clientNo = value
|
|
831
|
+
session = None
|
|
832
|
+
try:
|
|
833
|
+
session = open_session()
|
|
834
|
+
output({"ready": True, "device_state_created": created})
|
|
835
|
+
for line in sys.stdin:
|
|
836
|
+
if not line.strip():
|
|
837
|
+
continue
|
|
838
|
+
command = None
|
|
839
|
+
try:
|
|
840
|
+
command = json.loads(line)
|
|
841
|
+
result = run_command(session, command)
|
|
842
|
+
output({"id": command.get("id"), "ok": True, "data": result})
|
|
843
|
+
except Exception as exc:
|
|
844
|
+
error = exc.as_dict() if isinstance(exc, SidecarFailure) else failure_from_exception(exc, "session").as_dict()
|
|
845
|
+
output({"id": command.get("id") if isinstance(command, dict) else None, "ok": False,
|
|
846
|
+
"error": error})
|
|
847
|
+
return 0
|
|
848
|
+
except Exception as exc:
|
|
849
|
+
output({"ready": False, "error": {
|
|
850
|
+
"type": type(exc).__name__,
|
|
851
|
+
"message": safe_exception_message(exc)[:400],
|
|
852
|
+
}})
|
|
853
|
+
return 1
|
|
854
|
+
finally:
|
|
855
|
+
machine_data.clientNo = original
|
|
856
|
+
close_session(session)
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
if __name__ == "__main__":
|
|
860
|
+
sys.exit(main())
|