File size: 4,230 Bytes
c817fe8
a1bab2d
 
c817fe8
 
 
a1bab2d
c817fe8
 
 
 
 
 
 
 
 
 
 
 
a1bab2d
c817fe8
 
 
 
 
a1bab2d
c817fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1bab2d
c817fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Platform-neutral lifecycle for the FCC desktop shell."""
from __future__ import annotations


import threading
from collections.abc import Callable
from typing import Protocol

from free_claude_code.cli.commands import (
    ServerStatus,
    ServerSupervisor,
    load_server_settings,
    open_admin_when_ready,
    schedule_open_admin_browser,
)
from free_claude_code.cli.launchers.common import preflight_proxy
from free_claude_code.config.paths import config_dir_path
from free_claude_code.config.server_urls import local_proxy_root_url
from free_claude_code.config.settings import get_settings
from free_claude_code.core.interprocess_lock import InterprocessFileLock


class DesktopTray(Protocol):
    """UI loop owned by the platform tray adapter."""


    def run(self) -> None: ...

    def stop(self) -> None: ...


class DesktopTrayFactory(Protocol):
    """Construct a tray adapter around a desktop controller."""

    def __call__(self, controller: DesktopController) -> DesktopTray: ...


class ServerOwner(Protocol):
    """Server lifecycle used by the desktop controller."""

    @property
    def status(self) -> ServerStatus: ...

    def schedule_run(self) -> bool: ...

    def run(self, *, open_admin_browser: bool | None = None) -> None: ...

    def request_restart(self) -> bool: ...

    def request_stop(self) -> None: ...


class DesktopController:
    """Coordinate one tray loop with one in-process FCC server owner."""

    def __init__(
        self,
        supervisor: ServerOwner,
        tray_factory: DesktopTrayFactory,
        open_admin: Callable[[], None],
    ) -> None:
        self._supervisor = supervisor
        self._open_admin = open_admin
        self._thread_lock = threading.Lock()
        self._server_thread: threading.Thread | None = None
        self._tray = tray_factory(self)

    @property
    def status(self) -> ServerStatus:
        return self._supervisor.status

    def run(self) -> None:
        """Run the tray on this thread and the FCC server on its owned worker."""

        self._start_server()
        try:
            self._tray.run()
        finally:
            self._supervisor.request_stop()
            self._tray.stop()
            with self._thread_lock:
                thread = self._server_thread
            if thread is not None:
                thread.join()

    def open_admin(self) -> None:
        self._open_admin()

    def restart_server(self) -> None:
        """Restart an active server or relaunch one that exited unexpectedly."""

        with self._thread_lock:
            thread = self._server_thread
        if thread is not None and thread.is_alive():
            self._supervisor.request_restart()
            return
        self._start_server()

    def quit(self) -> None:
        """Close the server gracefully and end the platform tray loop."""

        self._supervisor.request_stop()
        self._tray.stop()

    def _start_server(self) -> None:
        with self._thread_lock:
            if self._server_thread is not None and self._server_thread.is_alive():
                return
            if not self._supervisor.schedule_run():
                return
            self._server_thread = threading.Thread(
                target=self._run_server,
                name="fcc-desktop-server",
            )
            self._server_thread.start()

    def _run_server(self) -> None:
        self._supervisor.run(open_admin_browser=False)


def launch_desktop(tray_factory: DesktopTrayFactory) -> None:
    """Start the singleton desktop host or focus the already running FCC UI."""

    settings = load_server_settings()
    instance_lock = InterprocessFileLock(config_dir_path() / "desktop.lock")
    if not instance_lock.acquire():
        open_admin_when_ready(settings)
        return

    try:
        if preflight_proxy(local_proxy_root_url(settings)) is None:
            open_admin_when_ready(settings)
            return

        supervisor = ServerSupervisor(console_logging=False)

        def open_current_admin() -> None:
            schedule_open_admin_browser(get_settings())

        DesktopController(supervisor, tray_factory, open_current_admin).run()
    finally:
        instance_lock.release()