-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrun_wrapper.py
More file actions
212 lines (187 loc) · 7.97 KB
/
Copy pathrun_wrapper.py
File metadata and controls
212 lines (187 loc) · 7.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env python3
"""
VPS wrapper for the LinkedIn connection bot.
Handles:
- File-based OTP flow (for headless VPS where terminal input is unavailable)
- Chrome option patching (removes problematic --remote-debugging-pipe)
- Chrome profile lifecycle (clean create, consistent path, cleanup on exit)
- Runs run.py via subprocess
"""
import sys
import os
import time
import shutil
import signal
import subprocess
import builtins
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
OTP_REQUEST_FILE = os.path.join(PROJECT_DIR, 'otp_request.txt')
OTP_RESPONSE_FILE = os.path.join(PROJECT_DIR, 'otp_response.txt')
CHROME_PROFILE = os.path.join(PROJECT_DIR, '.chrome-profile')
# -------------------------------------------------------------------------
# Chrome process management
# -------------------------------------------------------------------------
def kill_chrome_processes():
"""Kill any existing Chrome/Chromium processes."""
try:
subprocess.run(['pkill', '-f', 'chrome'], capture_output=True)
subprocess.run(['pkill', '-f', 'chromium'], capture_output=True)
time.sleep(1)
except Exception:
pass
def cleanup_chrome_profile():
"""Remove the Chrome profile directory."""
if os.path.exists(CHROME_PROFILE):
try:
shutil.rmtree(CHROME_PROFILE)
print(f"[WRAPPER] Cleaned up Chrome profile: {CHROME_PROFILE}", flush=True)
except Exception as e:
print(f"[WRAPPER] Warning: could not remove Chrome profile: {e}", flush=True)
# -------------------------------------------------------------------------
# OTP flow (file-based for VPS)
# -------------------------------------------------------------------------
_driver_ref = [None]
def request_otp(prompt="Enter verification code"):
"""Write OTP request file and wait for response file (up to 600s)."""
print(f"[OTP] Requesting input: {prompt}", flush=True)
with open(OTP_REQUEST_FILE, 'w') as f:
f.write(f"{time.time()}\n{prompt}")
for _ in range(600):
if os.path.exists(OTP_RESPONSE_FILE):
with open(OTP_RESPONSE_FILE, 'r') as f:
response = f.read().strip()
os.remove(OTP_RESPONSE_FILE)
try:
os.remove(OTP_REQUEST_FILE)
except OSError:
pass
print("[OTP] Got response", flush=True)
return response
time.sleep(1)
try:
os.remove(OTP_REQUEST_FILE)
except OSError:
pass
return ""
def _submit_otp_code(driver, code):
"""Submit an OTP code into the verification form."""
from selenium.webdriver.common.by import By
try:
input_field = driver.find_element(By.CSS_SELECTOR,
'input[name="pin"], input#input__email_verification_pin, input[type="text"]')
input_field.clear()
input_field.send_keys(code)
time.sleep(1)
submit_btn = driver.find_element(By.CSS_SELECTOR,
'button[type="submit"], button#email-pin-submit-button')
submit_btn.click()
time.sleep(5)
print(f"[VERIFY] Code submitted. URL: {driver.current_url}", flush=True)
except Exception as e:
print(f"[VERIFY] Error submitting code: {e}", flush=True)
def smart_input(prompt=""):
"""
Replacement for builtins.input() on VPS.
When a verification challenge is detected:
1. Try automatic OTP retrieval from email (IMAP)
2. Fall back to file-based OTP exchange if IMAP is not configured
"""
driver = _driver_ref[0]
if driver is not None:
try:
current_url = driver.current_url
if 'checkpoint' in current_url or 'challenge' in current_url:
print("[VERIFY] Verification challenge detected!", flush=True)
screenshot_path = os.path.join(PROJECT_DIR, 'verify-screenshot.png')
driver.save_screenshot(screenshot_path)
# Strategy 1: Auto-read OTP from email via IMAP
code = None
try:
from otp_email import fetch_otp
code = fetch_otp()
except ImportError:
print("[VERIFY] otp_email module not available, skipping auto-read.", flush=True)
except Exception as e:
print(f"[VERIFY] Auto-read failed: {e}", flush=True)
# Strategy 2: File-based OTP exchange (fallback)
if not code:
print("[VERIFY] Falling back to file-based OTP exchange...", flush=True)
code = request_otp("LinkedIn verification code needed. Check your email.")
if code:
_submit_otp_code(driver, code)
return ""
except Exception as e:
print(f"[VERIFY] Check failed: {e}", flush=True)
# Non-challenge input() calls also try file-based exchange
return request_otp(prompt)
# -------------------------------------------------------------------------
# Monkey-patches (applied before importing run.py)
# -------------------------------------------------------------------------
# Replace input() with file-based OTP handler
builtins.input = smart_input
# Patch Chrome options to remove problematic flags
import selenium.webdriver.chrome.options
_OrigOpts = selenium.webdriver.chrome.options.Options
class PatchedOptions(_OrigOpts):
def add_argument(self, arg):
if '--remote-debugging-pipe' in arg:
return
super().add_argument(arg)
selenium.webdriver.chrome.options.Options = PatchedOptions
# Patch Chrome init to force our profile path and capture driver ref
import selenium.webdriver
_orig_init = selenium.webdriver.Chrome.__init__
def _patched_init(self, *args, **kwargs):
if 'options' in kwargs and kwargs['options'] is not None:
opts = kwargs['options']
if not any('--user-data-dir' in a for a in opts.arguments):
opts.add_argument(f'--user-data-dir={CHROME_PROFILE}')
_orig_init(self, *args, **kwargs)
_driver_ref[0] = self
selenium.webdriver.Chrome.__init__ = _patched_init
# -------------------------------------------------------------------------
# Main execution
# -------------------------------------------------------------------------
if __name__ == '__main__':
os.chdir(PROJECT_DIR)
# 1. Kill existing Chrome processes
print("[WRAPPER] Killing existing Chrome processes...", flush=True)
kill_chrome_processes()
# 2. Clean existing Chrome profile
cleanup_chrome_profile()
os.makedirs(CHROME_PROFILE, exist_ok=True)
print(f"[WRAPPER] Created fresh Chrome profile: {CHROME_PROFILE}", flush=True)
# 3. Bypass env_bootstrap in subprocess (venv already active on VPS)
sys.modules['env_bootstrap'] = type(sys)('env_bootstrap')
sys.modules['env_bootstrap'].ensure_venv = lambda: None
# 4. Run the bot
exit_code = 1
try:
# Import and execute run.py directly (patches must be in same process)
with open('run.py', 'r') as f:
source = f.read()
# Remove the env_bootstrap import lines since we bypassed it
source = source.replace('from env_bootstrap import ensure_venv\nensure_venv()', '')
exec(compile(source, 'run.py', 'exec'), {'__name__': '__main__', '__file__': 'run.py'})
exit_code = 0
except Exception as e:
print(f"[WRAPPER] Bot execution error: {e}", flush=True)
raise
finally:
# 5. Cleanup: kill Chrome, delete profile, clean verification emails
print("[WRAPPER] Cleaning up...", flush=True)
kill_chrome_processes()
cleanup_chrome_profile()
# Remove OTP files
for f in [OTP_REQUEST_FILE, OTP_RESPONSE_FILE]:
try:
os.remove(f)
except OSError:
pass
# Clean up LinkedIn verification emails from inbox
try:
from otp_email import cleanup_linkedin_emails
cleanup_linkedin_emails()
except Exception:
pass
print(f"[WRAPPER] Done. Exit code: {exit_code}", flush=True)