@@ -55,11 +55,57 @@ struct ServerProcess {
5555 base_url : String ,
5656}
5757
58+ fn rest_base_url ( ) -> String {
59+ format ! ( "http://127.0.0.1:{REST_PORT}" )
60+ }
61+
62+ async fn start_managed_server_process (
63+ codex_bin : Option < String > ,
64+ codex_args : Option < & str > ,
65+ ) -> Result < ServerProcess , String > {
66+ let base_url = rest_base_url ( ) ;
67+ let mut command = build_codex_command_with_bin (
68+ codex_bin,
69+ codex_args,
70+ vec ! [
71+ "serve" . to_string( ) ,
72+ "--port" . to_string( ) ,
73+ REST_PORT . to_string( ) ,
74+ ] ,
75+ ) ?;
76+ command. stdin ( std:: process:: Stdio :: null ( ) ) ;
77+ command. stdout ( std:: process:: Stdio :: null ( ) ) ;
78+ command. stderr ( std:: process:: Stdio :: null ( ) ) ;
79+
80+ let child = command. spawn ( ) . map_err ( |e| {
81+ if e. kind ( ) == ErrorKind :: NotFound {
82+ "OpenCode CLI not found. Install OpenCode and ensure `opencode` is on your PATH."
83+ . to_string ( )
84+ } else {
85+ e. to_string ( )
86+ }
87+ } ) ?;
88+
89+ let start = std:: time:: Instant :: now ( ) ;
90+ let health_timeout = Duration :: from_secs ( 30 ) ;
91+ loop {
92+ if start. elapsed ( ) > health_timeout {
93+ return Err ( "OpenCode server did not become healthy within 30 seconds." . to_string ( ) ) ;
94+ }
95+ if health_check ( & base_url) . await . is_ok ( ) {
96+ break ;
97+ }
98+ sleep ( Duration :: from_millis ( 200 ) ) . await ;
99+ }
100+
101+ Ok ( ServerProcess { child, base_url } )
102+ }
103+
58104async fn ensure_server_running (
59105 codex_bin : Option < String > ,
60106 codex_args : Option < & str > ,
61107) -> Result < String , String > {
62- let base_url = format ! ( "http://127.0.0.1:{REST_PORT}" ) ;
108+ let base_url = rest_base_url ( ) ;
63109
64110 // Fast path: if already initialized, just return the URL.
65111 if SERVER_PROCESS . get ( ) . is_some ( ) {
@@ -73,47 +119,8 @@ async fn ensure_server_running(
73119
74120 let init_result = SERVER_PROCESS
75121 . get_or_try_init ( || async {
76- let mut command = build_codex_command_with_bin (
77- codex_bin,
78- codex_args,
79- vec ! [
80- "serve" . to_string( ) ,
81- "--port" . to_string( ) ,
82- REST_PORT . to_string( ) ,
83- ] ,
84- ) ?;
85- command. stdin ( std:: process:: Stdio :: null ( ) ) ;
86- command. stdout ( std:: process:: Stdio :: null ( ) ) ;
87- command. stderr ( std:: process:: Stdio :: null ( ) ) ;
88-
89- let child = command. spawn ( ) . map_err ( |e| {
90- if e. kind ( ) == ErrorKind :: NotFound {
91- "OpenCode CLI not found. Install OpenCode and ensure `opencode` is on your PATH."
92- . to_string ( )
93- } else {
94- e. to_string ( )
95- }
96- } ) ?;
97-
98- // Poll health endpoint until ready.
99- let start = std:: time:: Instant :: now ( ) ;
100- let health_timeout = Duration :: from_secs ( 30 ) ;
101- loop {
102- if start. elapsed ( ) > health_timeout {
103- return Err (
104- "OpenCode server did not become healthy within 30 seconds." . to_string ( )
105- ) ;
106- }
107- if health_check ( & base_url) . await . is_ok ( ) {
108- break ;
109- }
110- sleep ( Duration :: from_millis ( 200 ) ) . await ;
111- }
112-
113- Ok ( Mutex :: new ( ServerProcess {
114- child,
115- base_url : base_url. clone ( ) ,
116- } ) )
122+ let server = start_managed_server_process ( codex_bin, codex_args) . await ?;
123+ Ok :: < Mutex < ServerProcess > , String > ( Mutex :: new ( server) )
117124 } )
118125 . await ;
119126
@@ -139,6 +146,89 @@ async fn health_check(base_url: &str) -> Result<Value, String> {
139146 resp. json :: < Value > ( ) . await . map_err ( |e| e. to_string ( ) )
140147}
141148
149+ pub ( crate ) async fn global_rest_get (
150+ codex_bin : Option < String > ,
151+ codex_args : Option < & str > ,
152+ path : & str ,
153+ directory : Option < & str > ,
154+ ) -> Result < Value , String > {
155+ let base_url = ensure_server_running ( codex_bin, codex_args) . await ?;
156+ let client = reqwest:: Client :: builder ( )
157+ . timeout ( Duration :: from_secs ( 300 ) )
158+ . build ( )
159+ . map_err ( |e| e. to_string ( ) ) ?;
160+ let mut url = format ! ( "{base_url}{path}" ) ;
161+ if let Some ( directory) = directory. filter ( |value| !value. trim ( ) . is_empty ( ) ) {
162+ let separator = if path. contains ( '?' ) { "&" } else { "?" } ;
163+ url = format ! ( "{url}{separator}directory={}" , urlencoding:: encode( directory) ) ;
164+ }
165+ let resp = client. get ( & url) . send ( ) . await . map_err ( |e| e. to_string ( ) ) ?;
166+ if !resp. status ( ) . is_success ( ) {
167+ let status = resp. status ( ) ;
168+ let body = resp. text ( ) . await . unwrap_or_default ( ) ;
169+ return Err ( format ! ( "REST GET {path} failed ({status}): {body}" ) ) ;
170+ }
171+ resp. json :: < Value > ( ) . await . map_err ( |e| e. to_string ( ) )
172+ }
173+
174+ pub ( crate ) async fn opencode_server_status ( ) -> Value {
175+ let base_url = rest_base_url ( ) ;
176+ let managed = SERVER_PROCESS . get ( ) . is_some ( ) ;
177+ match health_check ( & base_url) . await {
178+ Ok ( health) => json ! ( {
179+ "baseUrl" : base_url,
180+ "healthy" : true ,
181+ "managed" : managed,
182+ "source" : if managed { "managed" } else { "external" } ,
183+ "version" : health. get( "version" ) . cloned( ) . unwrap_or( Value :: Null ) ,
184+ "health" : health,
185+ } ) ,
186+ Err ( error) => json ! ( {
187+ "baseUrl" : base_url,
188+ "healthy" : false ,
189+ "managed" : managed,
190+ "source" : if managed { "managed" } else { "none" } ,
191+ "version" : Value :: Null ,
192+ "error" : error,
193+ } ) ,
194+ }
195+ }
196+
197+ pub ( crate ) async fn restart_opencode_server (
198+ codex_bin : Option < String > ,
199+ codex_args : Option < & str > ,
200+ ) -> Result < Value , String > {
201+ let base_url = rest_base_url ( ) ;
202+ if let Some ( server_mutex) = SERVER_PROCESS . get ( ) {
203+ let mut guard = server_mutex. lock ( ) . await ;
204+ let _ = kill_child_process_tree ( & mut guard. child ) . await ;
205+ let replacement = start_managed_server_process ( codex_bin, codex_args) . await ?;
206+ * guard = replacement;
207+ return Ok ( json ! ( {
208+ "restarted" : true ,
209+ "status" : opencode_server_status( ) . await ,
210+ } ) ) ;
211+ }
212+
213+ if health_check ( & base_url) . await . is_ok ( ) {
214+ return Err ( format ! (
215+ "OpenCode server at {base_url} is running but is not managed by OpenCode Monitor. Stop it manually, then retry."
216+ ) ) ;
217+ }
218+
219+ let _ = SERVER_PROCESS
220+ . get_or_try_init ( || async {
221+ let server = start_managed_server_process ( codex_bin, codex_args) . await ?;
222+ Ok :: < Mutex < ServerProcess > , String > ( Mutex :: new ( server) )
223+ } )
224+ . await ?;
225+
226+ Ok ( json ! ( {
227+ "restarted" : true ,
228+ "status" : opencode_server_status( ) . await ,
229+ } ) )
230+ }
231+
142232// ---------------------------------------------------------------------------
143233// WorkspaceSession (REST-based)
144234// ---------------------------------------------------------------------------
0 commit comments