-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrunner.py
More file actions
216 lines (167 loc) · 7.47 KB
/
runner.py
File metadata and controls
216 lines (167 loc) · 7.47 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
213
214
215
216
import argparse
import logging
import os
import subprocess
import sys
import tempfile
from typing import Optional
from dotenv import load_dotenv
# Override system env vars with .env values for flexible device switching
load_dotenv(override=True)
ROOT = os.path.abspath(os.path.dirname(__file__))
DEVICE = os.getenv("DEVICE", "127.0.0.1:5555")
USERNAME = os.getenv("INSTAGRAM_USER_A") or os.getenv("INSTAGRAM_USER")
def resolve_gramaddict_executable() -> Optional[str]:
win_path = os.path.join(ROOT, ".venv", "Scripts", "gramaddict.exe")
if os.path.exists(win_path):
return win_path
nix_path = os.path.join(ROOT, ".venv", "bin", "gramaddict")
if os.path.exists(nix_path):
return nix_path
return None
def setup_logging(task_name: str) -> tuple[logging.Logger, str]:
logs_dir = os.path.join(ROOT, "logs")
os.makedirs(logs_dir, exist_ok=True)
log_path = os.path.join(logs_dir, f"gramaddict_{task_name}.log")
logger = logging.getLogger("gramaddict_runner")
logger.setLevel(logging.INFO)
logger.propagate = False
if logger.handlers:
logger.handlers.clear()
formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger, log_path
def check_adb(device: str, logger: logging.Logger) -> bool:
try:
result = subprocess.run(["adb", "devices"], capture_output=True, text=True, check=True)
except FileNotFoundError:
logger.error("ADB not installed or not in PATH.")
return False
except subprocess.CalledProcessError as exc:
logger.error("ADB command failed: %s", exc)
if exc.stdout:
logger.error("adb stdout: %s", exc.stdout.strip())
if exc.stderr:
logger.error("adb stderr: %s", exc.stderr.strip())
return False
output = (result.stdout or "") + (result.stderr or "")
if device not in output:
logger.error("Device %s not found. adb output:\n%s", device, output.strip())
logger.error("Ensure BlueStacks is running and ADB bridge is enabled.")
return False
logger.info("ADB reports device %s is connected.", device)
return True
def stream_process_output(proc: subprocess.Popen, log_path: str) -> None:
with open(log_path, "a", encoding="utf-8") as sink:
for line in proc.stdout or []:
sys.stdout.write(line)
sys.stdout.flush()
sink.write(line)
sink.flush()
def run_gramaddict(config_path: str, task_name: str, logger: logging.Logger, log_path: str) -> int:
executable = resolve_gramaddict_executable()
if not executable:
logger.error("GramAddict executable not found inside .venv.")
return 1
if not os.path.exists(config_path):
logger.error("Config file not found: %s", config_path)
return 1
abs_config = os.path.abspath(config_path)
temp_config_path = None
# Inject device and username from .env into config (flexible for USB/emulator switching and multi-account)
device = os.getenv("DEVICE")
username = USERNAME # Already loaded at module level from INSTAGRAM_USER_A or INSTAGRAM_USER
if device or username:
# Read original config as text to preserve formatting (yaml.dump mangles lists!)
with open(abs_config, 'r', encoding='utf-8') as f:
original_lines = f.readlines()
# Inject device and username at the beginning (after initial comments)
new_lines = []
injected = False
for line in original_lines:
# Add device and username before first non-comment, non-empty line
if not injected and line.strip() and not line.strip().startswith('#'):
if username:
new_lines.append(f"username: {username}\n")
if device:
new_lines.append(f"device: {device}\n")
injected = True
new_lines.append(line)
# If no non-comment line found, add at the end
if not injected:
if username:
new_lines.append(f"\nusername: {username}\n")
if device:
new_lines.append(f"device: {device}\n")
# Write to temporary config file
temp_config = tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False, encoding='utf-8')
temp_config.writelines(new_lines)
temp_config.close()
temp_config_path = temp_config.name
abs_config = temp_config_path
if username:
logger.info("Injected username %s into config", username)
if device:
logger.info("Injected device %s into config", device)
else:
logger.warning("DEVICE and USERNAME not set in .env; will use values from config YAML if present.")
cmd = [executable, "run", "--config", abs_config]
logger.info("Running command: %s", " ".join(cmd))
# Verify credentials are available
if not USERNAME:
logger.warning("INSTAGRAM_USER_A not set in .env; config must include username.")
env = os.environ.copy()
existing_pythonpath = env.get("PYTHONPATH", "")
env["PYTHONPATH"] = ROOT if not existing_pythonpath else ROOT + os.pathsep + existing_pythonpath
env.setdefault("PYTHONUNBUFFERED", "1")
env.setdefault("GRAMADDICT_LOG_LEVEL", "INFO")
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
env=env,
)
stream_process_output(proc, log_path)
proc.wait()
# Cleanup temp config file
if temp_config_path and os.path.exists(temp_config_path):
try:
os.unlink(temp_config_path)
except Exception as e:
logger.warning("Failed to cleanup temp config: %s", e)
if proc.returncode == 0:
logger.info("Task finished successfully.")
else:
logger.error("Task failed or stopped (exit code %s).", proc.returncode)
return proc.returncode
def main() -> int:
parser = argparse.ArgumentParser(description="GramAddict task runner")
parser.add_argument("mode", choices=["growth", "cleanup", "morning", "lunch", "evening", "extra"], help="Select which strategy to run")
args = parser.parse_args()
config_map = {
"growth": os.path.join(ROOT, "accounts", "strategy_growth.yml"),
"cleanup": os.path.join(ROOT, "accounts", "strategy_cleanup.yml"),
"morning": os.path.join(ROOT, "accounts", "session_morning.yml"),
"lunch": os.path.join(ROOT, "accounts", "session_lunch.yml"),
"evening": os.path.join(ROOT, "accounts", "session_evening.yml"),
"extra": os.path.join(ROOT, "accounts", "session_extra.yml"),
}
config_path = config_map[args.mode]
logger, log_path = setup_logging(args.mode)
logger.info("Starting %s task with config %s", args.mode, config_path)
filters_path = os.path.join(ROOT, "accounts", "filters.yml")
if not os.path.exists(filters_path):
logger.warning("filters.yml not found at %s; GramAddict will fall back to defaults.", filters_path)
# Check device from .env (not cached global variable)
device = os.getenv("DEVICE")
if device and not check_adb(device, logger):
return 1
return run_gramaddict(config_path, args.mode, logger, log_path)
if __name__ == "__main__":
sys.exit(main())