frytrader 0.1.0__py3-none-any.whl
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.
- easytrader/__init__.py +11 -0
- easytrader/api.py +128 -0
- easytrader/background_focus.py +147 -0
- easytrader/background_runtime.py +133 -0
- easytrader/background_trader.py +542 -0
- easytrader/background_windows.py +1115 -0
- easytrader/clienttrader.py +684 -0
- easytrader/config/__init__.py +0 -0
- easytrader/config/background_controls.ahk +52 -0
- easytrader/config/client.py +193 -0
- easytrader/config/global.json +52 -0
- easytrader/config/xq.json +11 -0
- easytrader/exceptions.py +11 -0
- easytrader/follower.py +408 -0
- easytrader/gf_clienttrader.py +84 -0
- easytrader/gj_clienttrader.py +80 -0
- easytrader/grid_strategies.py +845 -0
- easytrader/ht_clienttrader.py +76 -0
- easytrader/htzq_clienttrader.py +61 -0
- easytrader/joinquant_follower.py +140 -0
- easytrader/log.py +14 -0
- easytrader/miniqmt/__init__.py +1 -0
- easytrader/miniqmt/miniqmt_trader.py +653 -0
- easytrader/native/bin/focus_guard32.dll +0 -0
- easytrader/native/bin/focus_host32.exe +0 -0
- easytrader/native/build.ps1 +38 -0
- easytrader/native/focus_guard.c +23 -0
- easytrader/native/focus_host.c +92 -0
- easytrader/native/kernel32.def +18 -0
- easytrader/native/user32.def +9 -0
- easytrader/pop_dialog_handler.py +112 -0
- easytrader/refresh_strategies.py +59 -0
- easytrader/remoteclient.py +135 -0
- easytrader/ricequant_follower.py +119 -0
- easytrader/screenshot.py +409 -0
- easytrader/server.py +135 -0
- easytrader/universal_clienttrader.py +60 -0
- easytrader/utils/__init__.py +1 -0
- easytrader/utils/captcha.py +250 -0
- easytrader/utils/misc.py +31 -0
- easytrader/utils/perf.py +48 -0
- easytrader/utils/stock.py +86 -0
- easytrader/utils/win_gui.py +3 -0
- easytrader/webtrader.py +245 -0
- easytrader/wk_clienttrader.py +56 -0
- easytrader/xq_follower.py +249 -0
- easytrader/xqtrader.py +644 -0
- easytrader/yh_clienttrader.py +129 -0
- frytrader-0.1.0.dist-info/METADATA +237 -0
- frytrader-0.1.0.dist-info/RECORD +53 -0
- frytrader-0.1.0.dist-info/WHEEL +5 -0
- frytrader-0.1.0.dist-info/licenses/LICENSE +22 -0
- frytrader-0.1.0.dist-info/top_level.txt +1 -0
easytrader/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import urllib3
|
|
3
|
+
|
|
4
|
+
from easytrader import exceptions
|
|
5
|
+
from easytrader.api import use, follower
|
|
6
|
+
from easytrader.log import logger
|
|
7
|
+
|
|
8
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
__author__ = "shidenggui, Fryt1"
|
easytrader/api.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import six
|
|
6
|
+
|
|
7
|
+
from easytrader.joinquant_follower import JoinQuantFollower
|
|
8
|
+
from easytrader.log import logger
|
|
9
|
+
from easytrader.ricequant_follower import RiceQuantFollower
|
|
10
|
+
from easytrader.xq_follower import XueQiuFollower
|
|
11
|
+
from easytrader.xqtrader import XueQiuTrader
|
|
12
|
+
|
|
13
|
+
if sys.version_info <= (3, 5):
|
|
14
|
+
raise TypeError("不支持 Python3.5 及以下版本,请升级")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def use(broker, debug=False, **kwargs):
|
|
18
|
+
"""用于生成特定的券商对象
|
|
19
|
+
:param broker: 券商名支持
|
|
20
|
+
例如 ['miniqmt', 'xq', '雪球', 'gj_client', '国金客户端', "universal_client", "通用同花顺客户端", "ths", "同花顺客户端"] 等
|
|
21
|
+
:param debug: 控制 debug 日志的显示, 默认为 False
|
|
22
|
+
:param initial_assets: [雪球参数] 控制雪球初始资金,默认为一百万
|
|
23
|
+
:return the class of trader
|
|
24
|
+
|
|
25
|
+
Usage::
|
|
26
|
+
|
|
27
|
+
>>> import easytrader
|
|
28
|
+
>>> user = easytrader.use('xq')
|
|
29
|
+
>>> user.prepare('xq.json')
|
|
30
|
+
"""
|
|
31
|
+
if "background" in kwargs:
|
|
32
|
+
if not isinstance(kwargs["background"], bool):
|
|
33
|
+
raise TypeError("background 必须是布尔值")
|
|
34
|
+
if kwargs["background"] and broker.lower() not in (
|
|
35
|
+
"ths", "同花顺客户端"
|
|
36
|
+
):
|
|
37
|
+
raise NotImplementedError("后台只读模式仅支持 ths 远航版")
|
|
38
|
+
|
|
39
|
+
if debug:
|
|
40
|
+
logger.setLevel(logging.DEBUG)
|
|
41
|
+
|
|
42
|
+
if broker.lower() in ["xq", "雪球"]:
|
|
43
|
+
return XueQiuTrader(**kwargs)
|
|
44
|
+
|
|
45
|
+
if broker.lower() in ["yh_client", "银河客户端"]:
|
|
46
|
+
from .yh_clienttrader import YHClientTrader
|
|
47
|
+
|
|
48
|
+
return YHClientTrader()
|
|
49
|
+
|
|
50
|
+
if broker.lower() in ["ht_client", "华泰客户端"]:
|
|
51
|
+
from .ht_clienttrader import HTClientTrader
|
|
52
|
+
|
|
53
|
+
return HTClientTrader()
|
|
54
|
+
|
|
55
|
+
if broker.lower() in ["wk_client", "五矿客户端"]:
|
|
56
|
+
from easytrader.wk_clienttrader import WKClientTrader
|
|
57
|
+
|
|
58
|
+
return WKClientTrader()
|
|
59
|
+
|
|
60
|
+
if broker.lower() in ["htzq_client", "海通证券客户端"]:
|
|
61
|
+
from easytrader.htzq_clienttrader import HTZQClientTrader
|
|
62
|
+
|
|
63
|
+
return HTZQClientTrader()
|
|
64
|
+
|
|
65
|
+
if broker.lower() in ["gj_client", "国金客户端"]:
|
|
66
|
+
from .gj_clienttrader import GJClientTrader
|
|
67
|
+
|
|
68
|
+
return GJClientTrader()
|
|
69
|
+
|
|
70
|
+
if broker.lower() in ["gf_client", "广发客户端"]:
|
|
71
|
+
from .gf_clienttrader import GFClientTrader
|
|
72
|
+
|
|
73
|
+
return GFClientTrader()
|
|
74
|
+
|
|
75
|
+
if broker.lower() in ["universal_client", "通用同花顺客户端"]:
|
|
76
|
+
from easytrader.universal_clienttrader import UniversalClientTrader
|
|
77
|
+
|
|
78
|
+
return UniversalClientTrader()
|
|
79
|
+
|
|
80
|
+
if broker.lower() in ["ths", "同花顺客户端"]:
|
|
81
|
+
background = kwargs.pop("background", False)
|
|
82
|
+
if not isinstance(background, bool):
|
|
83
|
+
raise TypeError("background 必须是布尔值")
|
|
84
|
+
if background:
|
|
85
|
+
from .background_trader import BackgroundThsTrader
|
|
86
|
+
|
|
87
|
+
return BackgroundThsTrader(**kwargs)
|
|
88
|
+
from .clienttrader import ClientTrader
|
|
89
|
+
|
|
90
|
+
return ClientTrader()
|
|
91
|
+
|
|
92
|
+
if broker.lower() in ["miniqmt"]:
|
|
93
|
+
try:
|
|
94
|
+
import xtquant
|
|
95
|
+
except:
|
|
96
|
+
logger.error("miniqmt 相关组件 xtqimt 未安装, 请执行 pip install easytrader[xtquant]安装")
|
|
97
|
+
from easytrader.miniqmt.miniqmt_trader import MiniqmtTrader
|
|
98
|
+
|
|
99
|
+
return MiniqmtTrader()
|
|
100
|
+
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def follower(platform, **kwargs):
|
|
105
|
+
"""用于生成特定的券商对象
|
|
106
|
+
:param platform:平台支持 ['jq', 'joinquant', '聚宽’]
|
|
107
|
+
:param initial_assets: [雪球参数] 控制雪球初始资金,默认为一万,
|
|
108
|
+
总资金由 initial_assets * 组合当前净值 得出
|
|
109
|
+
:param total_assets: [雪球参数] 控制雪球总资金,无默认值,
|
|
110
|
+
若设置则覆盖 initial_assets
|
|
111
|
+
:return the class of follower
|
|
112
|
+
|
|
113
|
+
Usage::
|
|
114
|
+
|
|
115
|
+
>>> import easytrader
|
|
116
|
+
>>> user = easytrader.use('xq')
|
|
117
|
+
>>> user.prepare('xq.json')
|
|
118
|
+
>>> jq = easytrader.follower('jq')
|
|
119
|
+
>>> jq.login(user='username', password='password')
|
|
120
|
+
>>> jq.follow(users=user, strategies=['strategies_link'])
|
|
121
|
+
"""
|
|
122
|
+
if platform.lower() in ["rq", "ricequant", "米筐"]:
|
|
123
|
+
return RiceQuantFollower()
|
|
124
|
+
if platform.lower() in ["jq", "joinquant", "聚宽"]:
|
|
125
|
+
return JoinQuantFollower()
|
|
126
|
+
if platform.lower() in ["xq", "xueqiu", "雪球"]:
|
|
127
|
+
return XueQiuFollower(**kwargs)
|
|
128
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Bounded thread-specific activation veto for the x86 Voyager UI thread.
|
|
2
|
+
|
|
3
|
+
The helper and hook are built from the included native sources. They never
|
|
4
|
+
patch executable files or send input; Windows removes the hook if the host
|
|
5
|
+
exits. Each query owns its helper and explicitly stops it in finally.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import struct
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from easytrader.background_runtime import BackgroundError
|
|
17
|
+
|
|
18
|
+
_HASHES = {
|
|
19
|
+
"focus_guard32.dll": (
|
|
20
|
+
"6f9143a13d75c0e2b874a6854bb1bede94f0408bae7e4dff086941e88481236b"
|
|
21
|
+
),
|
|
22
|
+
"focus_host32.exe": (
|
|
23
|
+
"5753bae4146a96b394f4981858e2d425330595c23b236b42bc0c90e19eb49f24"
|
|
24
|
+
),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pe_machine(path):
|
|
29
|
+
"""Reject a bitness mismatch before a window is moved or hooked."""
|
|
30
|
+
with Path(path).open("rb") as stream:
|
|
31
|
+
if stream.read(2) != b"MZ":
|
|
32
|
+
raise BackgroundError("原生保护文件不是 PE 可执行文件")
|
|
33
|
+
stream.seek(0x3C)
|
|
34
|
+
offset_bytes = stream.read(4)
|
|
35
|
+
if len(offset_bytes) != 4:
|
|
36
|
+
raise BackgroundError("PE 文件头不完整")
|
|
37
|
+
offset = struct.unpack("<I", offset_bytes)[0]
|
|
38
|
+
if not 64 <= offset <= 16 * 1024 * 1024:
|
|
39
|
+
raise BackgroundError("PE 文件头位置无效")
|
|
40
|
+
stream.seek(offset)
|
|
41
|
+
if stream.read(4) != b"PE\0\0":
|
|
42
|
+
raise BackgroundError("PE 文件签名无效")
|
|
43
|
+
machine = stream.read(2)
|
|
44
|
+
if len(machine) != 2:
|
|
45
|
+
raise BackgroundError("PE 架构信息缺失")
|
|
46
|
+
return struct.unpack("<H", machine)[0]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ThreadActivationGuard:
|
|
50
|
+
def __init__(self, directory=None):
|
|
51
|
+
self.directory = (
|
|
52
|
+
Path(directory)
|
|
53
|
+
if directory
|
|
54
|
+
else Path(__file__).resolve().parent / "native" / "bin"
|
|
55
|
+
)
|
|
56
|
+
for name, expected in _HASHES.items():
|
|
57
|
+
path = self.directory / name
|
|
58
|
+
if not path.is_file():
|
|
59
|
+
raise BackgroundError("缺少原生防激活保护,请重新安装完整包")
|
|
60
|
+
if hashlib.sha256(path.read_bytes()).hexdigest() != expected:
|
|
61
|
+
raise BackgroundError("原生防激活保护校验失败,请重新安装")
|
|
62
|
+
if pe_machine(path) != 0x14C:
|
|
63
|
+
raise BackgroundError("当前防激活保护仅支持 x86 下单窗口")
|
|
64
|
+
self.host = None
|
|
65
|
+
self.temp = None
|
|
66
|
+
self.log = None
|
|
67
|
+
self.folder = None
|
|
68
|
+
|
|
69
|
+
def start(self, backend, guard):
|
|
70
|
+
if self.host is not None:
|
|
71
|
+
raise BackgroundError("原生防激活保护已在运行")
|
|
72
|
+
guard.check()
|
|
73
|
+
hwnd, pid, thread = backend.hook_target()
|
|
74
|
+
self.temp = tempfile.TemporaryDirectory(prefix="frytrader-focus-")
|
|
75
|
+
self.folder = Path(self.temp.name)
|
|
76
|
+
self.log = (self.folder / "host.log").open("wb")
|
|
77
|
+
(self.folder / "target.txt").write_text(
|
|
78
|
+
"%d,0,%d,%d,%d" % (hwnd, pid, thread, os.getpid()),
|
|
79
|
+
encoding="ascii",
|
|
80
|
+
)
|
|
81
|
+
environment = os.environ.copy()
|
|
82
|
+
environment["FRYTRADER_FOCUS_DIRECTORY"] = str(self.folder)
|
|
83
|
+
environment["FRYTRADER_FOCUS_DLL"] = str(
|
|
84
|
+
self.directory / "focus_guard32.dll"
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
self.host = subprocess.Popen(
|
|
88
|
+
[str(self.directory / "focus_host32.exe")],
|
|
89
|
+
env=environment,
|
|
90
|
+
stdin=subprocess.DEVNULL,
|
|
91
|
+
stdout=self.log,
|
|
92
|
+
stderr=subprocess.STDOUT,
|
|
93
|
+
shell=False,
|
|
94
|
+
)
|
|
95
|
+
deadline = min(time.monotonic() + 2, guard.deadline)
|
|
96
|
+
ready = self.folder / "ready.txt"
|
|
97
|
+
while not ready.exists():
|
|
98
|
+
guard.check()
|
|
99
|
+
if self.host.poll() is not None:
|
|
100
|
+
raise BackgroundError(
|
|
101
|
+
"防激活宿主未就绪(exit=%s)" % self.host.returncode
|
|
102
|
+
)
|
|
103
|
+
if time.monotonic() >= deadline:
|
|
104
|
+
raise BackgroundError("防激活宿主启动超时")
|
|
105
|
+
time.sleep(0.01)
|
|
106
|
+
if ready.read_text(encoding="ascii") != "ok":
|
|
107
|
+
raise BackgroundError("防激活宿主就绪信号无效")
|
|
108
|
+
self.check()
|
|
109
|
+
guard.check()
|
|
110
|
+
except BaseException:
|
|
111
|
+
self.close()
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
def check(self):
|
|
115
|
+
if self.host is None or self.host.poll() is not None:
|
|
116
|
+
raise BackgroundError("防激活保护已退出,已停止后台操作")
|
|
117
|
+
|
|
118
|
+
def close(self):
|
|
119
|
+
failed = None
|
|
120
|
+
try:
|
|
121
|
+
if self.host is not None:
|
|
122
|
+
if self.host.poll() is None:
|
|
123
|
+
(self.folder / "stop.txt").write_text(
|
|
124
|
+
"stop", encoding="ascii"
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
self.host.wait(timeout=2)
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
# Only our own helper is terminated.
|
|
130
|
+
self.host.terminate()
|
|
131
|
+
self.host.wait(timeout=2)
|
|
132
|
+
failed = "防激活宿主未能正常卸载,已终止自己的宿主"
|
|
133
|
+
if self.host.returncode != 0 and failed is None:
|
|
134
|
+
failed = (
|
|
135
|
+
"防激活宿主退出异常(exit=%s)" % self.host.returncode
|
|
136
|
+
)
|
|
137
|
+
finally:
|
|
138
|
+
self.host = None
|
|
139
|
+
if self.log is not None:
|
|
140
|
+
self.log.close()
|
|
141
|
+
self.log = None
|
|
142
|
+
if self.temp is not None:
|
|
143
|
+
self.temp.cleanup()
|
|
144
|
+
self.temp = None
|
|
145
|
+
self.folder = None
|
|
146
|
+
if failed:
|
|
147
|
+
raise BackgroundError(failed)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Nonactivating AutoHotkey bridge, restricted to captcha controls."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BackgroundError(RuntimeError):
|
|
12
|
+
"""A background read could not be confirmed safely."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BackgroundInterference(BackgroundError):
|
|
16
|
+
"""The user desktop or foreground was unexpectedly taken by Voyager."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_ahk_path(configured=None):
|
|
20
|
+
"""Use a configured/runtime executable; never download or install here."""
|
|
21
|
+
candidates = []
|
|
22
|
+
if configured:
|
|
23
|
+
candidates.append(Path(configured).expanduser())
|
|
24
|
+
elif os.environ.get("FRYTRADER_AHK"):
|
|
25
|
+
candidates.append(Path(os.environ["FRYTRADER_AHK"]).expanduser())
|
|
26
|
+
else:
|
|
27
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
28
|
+
if local:
|
|
29
|
+
candidates.append(
|
|
30
|
+
Path(local) / "Frytrader" / "runtime" / "AutoHotkey64.exe"
|
|
31
|
+
)
|
|
32
|
+
discovered = shutil.which("AutoHotkey64.exe")
|
|
33
|
+
if discovered:
|
|
34
|
+
candidates.append(Path(discovered))
|
|
35
|
+
program_files = os.environ.get("ProgramFiles")
|
|
36
|
+
if program_files:
|
|
37
|
+
candidates.append(
|
|
38
|
+
Path(program_files) / "AutoHotkey" / "v2" / "AutoHotkey64.exe"
|
|
39
|
+
)
|
|
40
|
+
for candidate in candidates:
|
|
41
|
+
if candidate.is_file() and candidate.suffix.lower() == ".exe":
|
|
42
|
+
return candidate.resolve()
|
|
43
|
+
raise BackgroundError(
|
|
44
|
+
"未找到 AutoHotkey v2。请配置 ahk_path 或 FRYTRADER_AHK,"
|
|
45
|
+
"后台模式不会自动安装软件或退回真实键鼠输入。"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AutoHotkeyControls:
|
|
50
|
+
"""Only ControlSendText and ControlClick NA; no foreground fallback."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, ahk_path=None):
|
|
53
|
+
self.executable = resolve_ahk_path(ahk_path)
|
|
54
|
+
self.script = (
|
|
55
|
+
Path(__file__).resolve().parent
|
|
56
|
+
/ "config"
|
|
57
|
+
/ "background_controls.ahk"
|
|
58
|
+
)
|
|
59
|
+
if not self.script.is_file():
|
|
60
|
+
raise BackgroundError("后台控件脚本未随安装包提供")
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def validate_code(code):
|
|
64
|
+
if (
|
|
65
|
+
not isinstance(code, str)
|
|
66
|
+
or len(code) != 4
|
|
67
|
+
or not code.isascii()
|
|
68
|
+
or not code.isalnum()
|
|
69
|
+
):
|
|
70
|
+
raise BackgroundError("验证码必须是四位 ASCII 字母或数字")
|
|
71
|
+
|
|
72
|
+
def _run(self, action, controls, control, pid, guard, code=None):
|
|
73
|
+
guard.check()
|
|
74
|
+
with tempfile.TemporaryDirectory(prefix="frytrader-na-") as folder:
|
|
75
|
+
directory = Path(folder)
|
|
76
|
+
output = directory / "result.txt"
|
|
77
|
+
command = [
|
|
78
|
+
str(self.executable),
|
|
79
|
+
"/ErrorStdOut",
|
|
80
|
+
str(self.script),
|
|
81
|
+
action,
|
|
82
|
+
str(controls.dialog),
|
|
83
|
+
str(control),
|
|
84
|
+
str(pid),
|
|
85
|
+
str(output),
|
|
86
|
+
]
|
|
87
|
+
if code is not None:
|
|
88
|
+
command.append(code)
|
|
89
|
+
# Regular files/DEVNULL, not named-pipe stdio. No shell quoting.
|
|
90
|
+
with (directory / "helper.log").open("wb") as log:
|
|
91
|
+
process = subprocess.Popen(
|
|
92
|
+
command,
|
|
93
|
+
stdin=subprocess.DEVNULL,
|
|
94
|
+
stdout=log,
|
|
95
|
+
stderr=subprocess.STDOUT,
|
|
96
|
+
shell=False,
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
deadline = min(time.monotonic() + 5, guard.deadline)
|
|
100
|
+
while process.poll() is None:
|
|
101
|
+
guard.check()
|
|
102
|
+
if time.monotonic() >= deadline:
|
|
103
|
+
raise BackgroundError("NA 控件操作超时")
|
|
104
|
+
time.sleep(0.01)
|
|
105
|
+
guard.check()
|
|
106
|
+
result = (
|
|
107
|
+
output.read_text(encoding="utf-8-sig")
|
|
108
|
+
if output.exists()
|
|
109
|
+
else "missing result"
|
|
110
|
+
)
|
|
111
|
+
if process.returncode or result != "ok":
|
|
112
|
+
raise BackgroundError(
|
|
113
|
+
"NA 控件操作未确认(%s,exit=%s)"
|
|
114
|
+
% (action, process.returncode)
|
|
115
|
+
)
|
|
116
|
+
finally:
|
|
117
|
+
# This is our short-lived bridge, never the trader.
|
|
118
|
+
if process.poll() is None:
|
|
119
|
+
process.terminate()
|
|
120
|
+
try:
|
|
121
|
+
process.wait(timeout=1)
|
|
122
|
+
except subprocess.TimeoutExpired:
|
|
123
|
+
process.kill()
|
|
124
|
+
process.wait(timeout=1)
|
|
125
|
+
|
|
126
|
+
def fill(self, controls, code, pid, guard):
|
|
127
|
+
self.validate_code(code)
|
|
128
|
+
self._run("fill", controls, controls.editor, pid, guard, code)
|
|
129
|
+
|
|
130
|
+
def click(self, controls, role, pid, guard):
|
|
131
|
+
if role not in ("ok", "cancel", "image"):
|
|
132
|
+
raise BackgroundError("后台桥接只允许验证码对话框操作")
|
|
133
|
+
self._run("click", controls, getattr(controls, role), pid, guard)
|