raw9 commited on
Commit
668180c
·
verified ·
1 Parent(s): e9961f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +11 -629
app.py CHANGED
@@ -1,635 +1,17 @@
1
- """
2
- Service Orchestrator — Pro Linux Workspace
3
-
4
- Starts and health-monitors: Filebrowser, ttyd, code-server, Glances, Caddy.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
  import os
10
- import sys
11
- import time
12
- import signal
13
- import shutil
14
- import logging
15
  import subprocess
16
- from typing import Optional
17
-
18
- # ── Logging ──────────────────────────────────────────────────────────────────
19
- logging.basicConfig(
20
- level=logging.INFO,
21
- format="%(asctime)s - %(levelname)s - %(message)s",
22
- )
23
- log = logging.getLogger(__name__)
24
-
25
- # ── Constants ────────────────────────────────────────────────────────────────
26
- HEALTH_CHECK_INTERVAL = 10 # seconds
27
- DRIVE_DIR = "/home/user/Drive"
28
- DB_DIR = f"{DRIVE_DIR}/system_data"
29
- DB_PATH = f"{DB_DIR}/filebrowser.db"
30
- CADDYFILE = "/tmp/Caddyfile"
31
-
32
- # Service ports (localhost only)
33
- PORT_FB = 8080
34
- PORT_TTYD = 8081
35
- PORT_CODE = 8082
36
- PORT_GLANCES = 8083
37
- PORT_CADDY = 7860
38
-
39
-
40
- # ─────────────────────────────────────────────────────────────────────────────
41
- # Helpers
42
- # ─────────────────────────────────────────────────────────────────────────────
43
-
44
- def which(name: str) -> Optional[str]:
45
- """Return full path of *name* if found on PATH, else None."""
46
- return shutil.which(name)
47
-
48
-
49
- def run_checked(
50
- cmd: list[str],
51
- name: str,
52
- *,
53
- silent: bool = True,
54
- critical: bool = False,
55
- capture: bool = False,
56
- ) -> subprocess.CompletedProcess:
57
- """
58
- Run a command synchronously and check its return code.
59
- Calls sys.exit(1) if critical=True and command fails.
60
- Returns the CompletedProcess object.
61
- """
62
- kwargs: dict = {}
63
- if silent and not capture:
64
- kwargs["stdout"] = subprocess.DEVNULL
65
- kwargs["stderr"] = subprocess.DEVNULL
66
- if capture:
67
- kwargs["capture_output"] = True
68
- kwargs["text"] = True
69
-
70
- try:
71
- result = subprocess.run(cmd, **kwargs)
72
- if result.returncode != 0:
73
- log.error(
74
- "%s exited with code %d (cmd: %s)",
75
- name,
76
- result.returncode,
77
- " ".join(cmd),
78
- )
79
- if critical:
80
- sys.exit(1)
81
- return result
82
- except Exception as exc:
83
- log.error("%s failed to run: %s", name, exc)
84
- if critical:
85
- sys.exit(1)
86
- return subprocess.CompletedProcess(cmd, returncode=1, stdout="", stderr=str(exc))
87
-
88
-
89
- def launch(
90
- cmd: Optional[list[str]],
91
- name: str,
92
- *,
93
- env: Optional[dict] = None,
94
- silent: bool = False,
95
- critical: bool = False,
96
- ) -> Optional[subprocess.Popen]:
97
- """
98
- Start *cmd* as a background process.
99
-
100
- Parameters
101
- ----------
102
- cmd : Command + arguments. None -> skip gracefully.
103
- name : Human-readable service label (for logging).
104
- env : Override environment dict (defaults to inherited env).
105
- silent : Redirect stdout/stderr to /dev/null when True.
106
- critical : Call sys.exit(1) if the process cannot be launched.
107
- """
108
- if cmd is None:
109
- log.warning("%s: skipped (binary not found).", name)
110
- return None
111
-
112
- kwargs: dict = {}
113
- if env is not None:
114
- kwargs["env"] = env
115
- if silent:
116
- kwargs["stdout"] = subprocess.DEVNULL
117
- kwargs["stderr"] = subprocess.DEVNULL
118
-
119
- try:
120
- proc = subprocess.Popen(cmd, **kwargs)
121
- log.info("%s started (PID %d)", name, proc.pid)
122
- return proc
123
- except Exception as exc:
124
- log.error("Failed to start %s: %s", name, exc)
125
- if critical:
126
- log.critical("Critical service '%s' failed — aborting.", name)
127
- sys.exit(1)
128
- return None
129
-
130
-
131
- # ─────────────────────────────────────────────────────────────────────────────
132
- # Setup
133
- # ─────────────────────────────────────────────────────────────────────────────
134
-
135
- def validate_auth(env: dict) -> tuple:
136
- """
137
- Parse and validate TERMINAL_AUTH from environment.
138
- Returns (username, password) or exits the process.
139
- """
140
- auth = env.get("TERMINAL_AUTH", "").strip()
141
- if not auth:
142
- log.critical(
143
- "TERMINAL_AUTH is not set. "
144
- "Add it to your Space secrets as 'username:strongpassword'. "
145
- "Refusing to start without authentication."
146
- )
147
- sys.exit(1)
148
-
149
- if ":" not in auth:
150
- log.critical(
151
- "TERMINAL_AUTH format invalid. Expected 'username:password'."
152
- )
153
- sys.exit(1)
154
-
155
- username, password = auth.split(":", 1)
156
- username = username.strip()
157
- password = password.strip()
158
-
159
- if not username or not password:
160
- log.critical(
161
- "TERMINAL_AUTH has empty username or password. "
162
- "Expected 'username:password'."
163
- )
164
- sys.exit(1)
165
-
166
- return username, password
167
-
168
-
169
- def restore_cloud_data(env: dict) -> None:
170
- """Restore previous session data from cloud backup."""
171
- os.makedirs(DRIVE_DIR, exist_ok=True)
172
- log.info("Restoring previous session from cloud…")
173
- result = subprocess.run(
174
- ["python3", "autobackup.py", "--restore"],
175
- env=env,
176
- capture_output=True,
177
- text=True,
178
- )
179
- if result.returncode == 0:
180
- log.info("Cloud data restored into %s", DRIVE_DIR)
181
- else:
182
- log.warning(
183
- "Restore exited %d (first run or no credentials). Continuing…",
184
- result.returncode,
185
- )
186
- if result.stderr:
187
- log.debug("Restore stderr: %s", result.stderr.strip()[:500])
188
-
189
-
190
- def _parse_fb_user_table(output: str) -> Optional[dict]:
191
- """
192
- Parse the tabular output of 'filebrowser users ls'.
193
-
194
- Example output:
195
- ID Username Scope Locale ...
196
- 1 admin / en ...
197
-
198
- Returns dict with keys {id, username} of first data row,
199
- or None if no user found.
200
- """
201
- lines = output.strip().splitlines()
202
- if len(lines) < 2:
203
- return None
204
-
205
- # First line is always the header
206
- header_line = lines[0]
207
-
208
- # Find column positions from header
209
- # We need "ID" and "Username" column start positions
210
- id_start = header_line.find("ID")
211
- username_start = header_line.find("Username")
212
-
213
- if id_start == -1 or username_start == -1:
214
- return None
215
-
216
- # Find next column after Username to know its end boundary
217
- # Look for the next column header after "Username"
218
- remaining_header = header_line[username_start + len("Username"):]
219
- # Find the start of the next non-space column
220
- next_col_offset = 0
221
- found_space = False
222
- for i, ch in enumerate(remaining_header):
223
- if ch == " ":
224
- found_space = True
225
- elif found_space:
226
- next_col_offset = i
227
- break
228
- username_end = username_start + len("Username") + next_col_offset if next_col_offset else len(header_line)
229
-
230
- # Parse first data row (line index 1)
231
- data_line = lines[1]
232
- if len(data_line) < username_start:
233
- return None
234
-
235
- # Extract ID (everything from id_start to username_start, stripped)
236
- raw_id = data_line[id_start:username_start].strip()
237
-
238
- # Extract Username (from username_start to username_end, stripped)
239
- raw_username = data_line[username_start:username_end].strip()
240
-
241
- if not raw_id or not raw_username:
242
- return None
243
 
 
 
 
 
 
 
244
  try:
245
- user_id = int(raw_id)
246
- except ValueError:
247
- return None
248
-
249
- return {"id": user_id, "username": raw_username}
250
-
251
-
252
- def setup_filebrowser(username: str, password: str) -> None:
253
- """
254
- Initialise or update the Filebrowser database.
255
-
256
- Handles all scenarios:
257
- - Fresh install: create DB + user
258
- - Same username: update password only (by numeric ID)
259
- - Changed username: update both username and password (by numeric ID)
260
- - Corrupted/unreadable DB: delete and recreate
261
- """
262
- fb_bin = which("filebrowser")
263
- if not fb_bin:
264
- log.critical("filebrowser binary not found — aborting.")
265
- sys.exit(1)
266
-
267
- os.makedirs(DB_DIR, exist_ok=True)
268
-
269
- if not os.path.exists(DB_PATH):
270
- # ── Fresh database ───────────────────────────────────────────────
271
- _init_fresh_fb_db(username, password)
272
- return
273
-
274
- # ── Existing database — read current user ────────────────────────────
275
- result = run_checked(
276
- ["filebrowser", "users", "ls", "-d", DB_PATH],
277
- "Filebrowser user list",
278
- capture=True,
279
- )
280
-
281
- if result.returncode != 0 or not result.stdout or not result.stdout.strip():
282
- # Cannot read DB — recreate from scratch
283
- log.warning("Filebrowser: cannot read existing DB. Recreating…")
284
- _recreate_fb_db(username, password)
285
- return
286
-
287
- existing = _parse_fb_user_table(result.stdout)
288
-
289
- if existing is None:
290
- # No users in DB or parse failure — recreate
291
- log.warning("Filebrowser: no users found in DB. Recreating…")
292
- _recreate_fb_db(username, password)
293
- return
294
-
295
- user_id = existing["id"]
296
- existing_username = existing["username"]
297
-
298
- if existing_username == username:
299
- # Same username → update password only, using numeric ID
300
- log.info("Filebrowser: updating password for '%s' (ID %d).", username, user_id)
301
- run_checked(
302
- [
303
- "filebrowser", "users", "update", str(user_id),
304
- "-p", password,
305
- "-d", DB_PATH,
306
- ],
307
- "Filebrowser password update",
308
- )
309
- else:
310
- # Username changed → update username + password using numeric ID
311
- log.info(
312
- "Filebrowser: username changed '%s' -> '%s' (ID %d). Updating…",
313
- existing_username, username, user_id,
314
- )
315
- run_checked(
316
- [
317
- "filebrowser", "users", "update", str(user_id),
318
- "--username", username,
319
- "-p", password,
320
- "-d", DB_PATH,
321
- ],
322
- "Filebrowser user update (username + password)",
323
- )
324
-
325
- log.info("Filebrowser: existing database loaded, credentials synced.")
326
-
327
-
328
- def _init_fresh_fb_db(username: str, password: str) -> None:
329
- """Create a brand-new Filebrowser database with one admin user."""
330
- run_checked(
331
- [
332
- "filebrowser", "config", "init",
333
- "-d", DB_PATH,
334
- "-a", "127.0.0.1",
335
- "-p", str(PORT_FB),
336
- "-r", "/home/user",
337
- ],
338
- "Filebrowser config init",
339
- critical=True,
340
- )
341
- run_checked(
342
- [
343
- "filebrowser", "config", "set",
344
- "--auth.method", "json",
345
- "--auth.header", "",
346
- "-d", DB_PATH,
347
- ],
348
- "Filebrowser config set",
349
- )
350
- run_checked(
351
- [
352
- "filebrowser", "users", "add",
353
- username, password,
354
- "--perm.admin",
355
- "-d", DB_PATH,
356
- ],
357
- "Filebrowser user add",
358
- critical=True,
359
- )
360
- log.info("Filebrowser: new persistent database initialised for '%s'.", username)
361
-
362
-
363
- def _recreate_fb_db(username: str, password: str) -> None:
364
- """Delete the existing Filebrowser DB and create a fresh one."""
365
- try:
366
- os.remove(DB_PATH)
367
- log.info("Filebrowser: removed corrupted database.")
368
- except OSError as exc:
369
- log.warning("Filebrowser: could not remove old DB: %s", exc)
370
- _init_fresh_fb_db(username, password)
371
-
372
-
373
- def write_caddyfile() -> str:
374
- """
375
- Generate and write the Caddyfile.
376
-
377
- Routing rules:
378
- - /code/* → handle_path (strips prefix, code-server expects /)
379
- - /terminal/* → handle (keeps prefix, ttyd uses -b /terminal)
380
- - /status/* → handle_path (strips prefix for Glances)
381
- - / → Filebrowser (default)
382
-
383
- flush_interval -1 disables response buffering for WebSocket services.
384
- """
385
- content = f"""\
386
- {{
387
- admin off
388
- log {{
389
- level ERROR
390
- }}
391
- }}
392
-
393
- :{PORT_CADDY} {{
394
- # ── Suppress Filebrowser 401 noise on token renewal ──
395
- @renew path /api/renew
396
- handle @renew {{
397
- reverse_proxy 127.0.0.1:{PORT_FB} {{
398
- @err status 401
399
- handle_response @err {{
400
- respond "" 200
401
- }}
402
- }}
403
- }}
404
-
405
- # ── Health Dashboard (Glances) ──
406
- redir /status /status/
407
- handle_path /status/* {{
408
- reverse_proxy 127.0.0.1:{PORT_GLANCES}
409
- }}
410
-
411
- # ── VS Code IDE ──
412
- # handle_path strips /code/ so code-server receives / as expected
413
- redir /code /code/
414
- handle_path /code/* {{
415
- reverse_proxy 127.0.0.1:{PORT_CODE} {{
416
- flush_interval -1
417
- }}
418
- }}
419
-
420
- # ── Web Terminal (ttyd) ──
421
- # handle preserves /terminal/ because ttyd uses -b /terminal
422
- redir /terminal /terminal/
423
- handle /terminal/* {{
424
- reverse_proxy 127.0.0.1:{PORT_TTYD} {{
425
- flush_interval -1
426
- }}
427
- }}
428
-
429
- # ── File Manager (default) ──
430
- handle {{
431
- reverse_proxy 127.0.0.1:{PORT_FB}
432
- }}
433
- }}
434
- """
435
- with open(CADDYFILE, "w", encoding="utf-8") as fh:
436
- fh.write(content)
437
-
438
- log.info("Caddyfile written to %s", CADDYFILE)
439
- return CADDYFILE
440
-
441
-
442
- # ─────────────────────────────────────────────────────────────────────────────
443
- # Service definitions
444
- # ─────────────────────────────────────────────────────────────────────────────
445
-
446
- def build_services(
447
- username: str,
448
- password: str,
449
- env: dict,
450
- ) -> dict:
451
- """
452
- Build command lists and environments for each service.
453
- Returns dict: service_name -> (cmd, service_env, is_critical).
454
- """
455
- services: dict = {}
456
-
457
- # ── Filebrowser ──
458
- fb_bin = which("filebrowser")
459
- if fb_bin:
460
- services["File Manager"] = (
461
- [fb_bin, "-d", DB_PATH, "-a", "127.0.0.1", "-p", str(PORT_FB)],
462
- env,
463
- True,
464
- )
465
- else:
466
- log.critical("filebrowser not found — cannot start File Manager.")
467
- sys.exit(1)
468
-
469
- # ── ttyd (Web Terminal) ──
470
- ttyd_bin = which("ttyd")
471
- if ttyd_bin:
472
- services["Web Terminal"] = (
473
- [
474
- ttyd_bin,
475
- "-p", str(PORT_TTYD),
476
- "-W",
477
- "-c", f"{username}:{password}",
478
- "-b", "/terminal",
479
- "-t", "fontSize=14",
480
- "tmux", "new-session", "-A", "-s", "main", "zsh",
481
- ],
482
- env,
483
- False,
484
- )
485
- else:
486
- log.warning("ttyd not found — Web Terminal will be unavailable.")
487
-
488
- # ── code-server (VS Code IDE) ──
489
- code_bin = which("code-server")
490
- if code_bin:
491
- code_env = env.copy()
492
- code_env["PASSWORD"] = password
493
- services["VS Code IDE"] = (
494
- [
495
- code_bin,
496
- "--bind-addr", f"127.0.0.1:{PORT_CODE}",
497
- "--auth", "password",
498
- "--disable-telemetry",
499
- "--disable-update-check",
500
- "/home/user",
501
- ],
502
- code_env,
503
- False,
504
- )
505
- else:
506
- log.warning("code-server not found — VS Code IDE will be unavailable.")
507
-
508
- # ── Glances (Health Dashboard) ──
509
- glances_bin = which("glances")
510
- if glances_bin:
511
- services["Health Dashboard"] = (
512
- [
513
- glances_bin,
514
- "-w",
515
- "--port", str(PORT_GLANCES),
516
- "--disable-plugin", "cloud",
517
- ],
518
- env,
519
- False,
520
- )
521
- else:
522
- log.warning("glances not found — Health Dashboard will be unavailable.")
523
-
524
- # ── Auto-backup ──
525
- services["Auto-backup"] = (
526
- ["python3", "autobackup.py", "--backup"],
527
- env,
528
- False,
529
- )
530
-
531
- # ── Caddy (Router — critical) ──
532
- caddy_bin = which("caddy")
533
- if not caddy_bin:
534
- log.critical("caddy not found — cannot start router. Aborting.")
535
- sys.exit(1)
536
- services["Caddy Router"] = (
537
- [caddy_bin, "run", "--config", CADDYFILE],
538
- env,
539
- True,
540
- )
541
-
542
- return services
543
-
544
-
545
- # ─────────────────────────────────────────────────────────────────────────────
546
- # Main
547
- # ─────────────────────────────────────────────────────────────────────────────
548
-
549
- def main() -> None:
550
- # ── 1. Build runtime environment ─────────────────────────────────────────
551
- env = os.environ.copy()
552
- env["PATH"] = f"/home/user/.local/bin:{env.get('PATH', '')}"
553
-
554
- # ── 2. Validate authentication ───────────────────────────────────────────
555
- username, password = validate_auth(env)
556
- log.info("Starting Pro Linux Workspace — user: '%s'", username)
557
-
558
- # ── 3. Restore cloud backup ──────────────────────────────────────────────
559
- restore_cloud_data(env)
560
-
561
- # ── 4. Initialise Filebrowser ────────────────────────────────────────────
562
- setup_filebrowser(username, password)
563
-
564
- # ── 5. Write Caddyfile ───────────────────────────────────────────────────
565
- write_caddyfile()
566
-
567
- # ── 6. Build and launch all services ─────────────────────────────────────
568
- service_defs = build_services(username, password, env)
569
-
570
- # Registry: name -> (proc, cmd, svc_env, is_critical)
571
- registry: dict = {}
572
- for svc_name, (cmd, svc_env, is_critical) in service_defs.items():
573
- proc = launch(
574
- cmd, svc_name,
575
- env=svc_env,
576
- silent=True,
577
- critical=is_critical,
578
- )
579
- registry[svc_name] = (proc, cmd, svc_env, is_critical)
580
-
581
- # ── 7. Graceful shutdown handler ─────────────────────────────────────────
582
- def _shutdown(signum, _frame) -> None:
583
- log.info("Shutting down all services…")
584
- for svc_name, (proc, _, _svc_env, _crit) in registry.items():
585
- if proc is None or proc.poll() is not None:
586
- continue
587
- try:
588
- proc.terminate()
589
- proc.wait(timeout=5)
590
- log.info(" Stopped %s (PID %d)", svc_name, proc.pid)
591
- except subprocess.TimeoutExpired:
592
- log.warning(" Force-killing %s (PID %d)", svc_name, proc.pid)
593
- proc.kill()
594
- proc.wait(timeout=3)
595
- except Exception as exc:
596
- log.error(" Error stopping %s: %s", svc_name, exc)
597
- sys.exit(0)
598
-
599
- signal.signal(signal.SIGINT, _shutdown)
600
- signal.signal(signal.SIGTERM, _shutdown)
601
-
602
- # ── 8. Health-monitor loop ───────────────────────────────────────────────
603
- log.info("All services launched. Entering health-monitor loop…")
604
- try:
605
- while True:
606
- time.sleep(HEALTH_CHECK_INTERVAL)
607
- for svc_name in list(registry.keys()):
608
- proc, cmd, svc_env, is_critical = registry[svc_name]
609
- if proc is None or proc.poll() is None:
610
- continue
611
-
612
- log.warning(
613
- "%s (PID %d) exited with code %d — restarting…",
614
- svc_name,
615
- proc.pid,
616
- proc.returncode,
617
- )
618
- if cmd is None:
619
- log.warning(" No command for %s — skipping.", svc_name)
620
- continue
621
-
622
- new_proc = launch(
623
- cmd, svc_name,
624
- env=svc_env,
625
- silent=True,
626
- critical=is_critical,
627
- )
628
- registry[svc_name] = (new_proc, cmd, svc_env, is_critical)
629
-
630
- except KeyboardInterrupt:
631
- _shutdown(None, None)
632
-
633
 
634
  if __name__ == "__main__":
635
- main()
 
 
 
 
 
 
 
 
 
1
  import os
 
 
 
 
 
2
  import subprocess
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ def start_terminal():
5
+ print("🚀 Starting Professional Linux Web Terminal on Port 7860...")
6
+
7
+ # ttyd কমান্ড: -p 7860 (পোর্ট), -W (টাইপ করার পারমিশন), bash (লিনাক্স শেল)
8
+ command = ["ttyd", "-p", "7860", "-W", "bash"]
9
+
10
  try:
11
+ # প্রসেস রান করা
12
+ subprocess.run(command, check=True)
13
+ except Exception as e:
14
+ print(f"Error starting terminal: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  if __name__ == "__main__":
17
+ start_terminal()