14
14
15
15
16
16
FRONTEND_DIR = Path (os .getenv ("FRONTEND_DIR" , ".frontend" ))
17
- DEFAULT_FRONTEND_PORT = 3000
17
+ APP_HOST = os .getenv ("APP_HOST" , "localhost" )
18
+ APP_PORT = int (
19
+ os .getenv ("APP_PORT" , 8000 )
20
+ ) # Allocated to backend but also for access to the app, please change it in .env
21
+ DEFAULT_FRONTEND_PORT = (
22
+ 3000 # Not for access directly, but for proxying to the backend in development
23
+ )
18
24
STATIC_DIR = Path (os .getenv ("STATIC_DIR" , "static" ))
19
25
20
26
27
+ class NodePackageManager (str ):
28
+ def __new__ (cls , value : str ) -> "NodePackageManager" :
29
+ return super ().__new__ (cls , value )
30
+
31
+ @property
32
+ def name (self ) -> str :
33
+ return Path (self ).stem
34
+
35
+ @property
36
+ def is_pnpm (self ) -> bool :
37
+ return self .name == "pnpm"
38
+
39
+ @property
40
+ def is_npm (self ) -> bool :
41
+ return self .name == "npm"
42
+
43
+
21
44
def build ():
22
45
"""
23
46
Build the frontend and copy the static files to the backend.
@@ -146,22 +169,20 @@ async def _run_frontend(
146
169
package_manager ,
147
170
"run" ,
148
171
"dev" ,
172
+ "--" if package_manager .is_npm else "" ,
149
173
"-p" ,
150
174
str (port ),
151
175
cwd = FRONTEND_DIR ,
152
176
)
153
- rich .print (
154
- f"\n [bold]Waiting for frontend to start, port: { port } , process id: { frontend_process .pid } [/bold]"
155
- )
177
+ rich .print ("\n [bold]Waiting for frontend to start..." )
156
178
# Block until the frontend is accessible
157
179
for _ in range (timeout ):
158
180
await asyncio .sleep (1 )
159
- # Check if the frontend is accessible (port is open) or frontend_process is running
160
181
if frontend_process .returncode is not None :
161
182
raise RuntimeError ("Could not start frontend dev server" )
162
- if not _is_bindable_port (port ):
183
+ if _is_server_running (port ):
163
184
rich .print (
164
- f "\n [bold green ]Frontend dev server is running on port { port } [/bold green ]"
185
+ "\n [bold]Frontend dev server is running. Please wait a while for the app to be ready... [/bold]"
165
186
)
166
187
return frontend_process , port
167
188
raise TimeoutError (f"Frontend dev server failed to start within { timeout } seconds" )
@@ -173,33 +194,50 @@ async def _run_backend(
173
194
"""
174
195
Start the backend development server.
175
196
176
- Args:
177
- frontend_port: The port number the frontend is running on
178
197
Returns:
179
198
Process: The backend process
180
199
"""
181
200
# Merge environment variables
182
201
envs = {** os .environ , ** (envs or {})}
183
- rich .print ("\n [bold]Starting backend FastAPI server...[/bold]" )
202
+ # Check if the port is free
203
+ if not _is_port_available (APP_PORT ):
204
+ raise SystemError (
205
+ f"Port { APP_PORT } is not available! Please change the port in .env file or kill the process running on this port."
206
+ )
207
+ rich .print (f"\n [bold]Starting app on port { APP_PORT } ...[/bold]" )
184
208
poetry_executable = _get_poetry_executable ()
185
- return await asyncio .create_subprocess_exec (
209
+ process = await asyncio .create_subprocess_exec (
186
210
poetry_executable ,
187
211
"run" ,
188
212
"python" ,
189
213
"main.py" ,
190
214
env = envs ,
191
215
)
216
+ # Wait for port is started
217
+ timeout = 30
218
+ for _ in range (timeout ):
219
+ await asyncio .sleep (1 )
220
+ if process .returncode is not None :
221
+ raise RuntimeError ("Could not start backend dev server" )
222
+ if _is_server_running (APP_PORT ):
223
+ rich .print (
224
+ f"\n [bold green]App is running. You now can access it at http://{ APP_HOST } :{ APP_PORT } [/bold green]"
225
+ )
226
+ return process
227
+ # Timeout, kill the process
228
+ process .terminate ()
229
+ raise TimeoutError (f"Backend dev server failed to start within { timeout } seconds" )
192
230
193
231
194
232
def _install_frontend_dependencies ():
195
233
package_manager = _get_node_package_manager ()
196
234
rich .print (
197
- f"\n [bold]Installing frontend dependencies using { Path ( package_manager ) .name } . It might take a while...[/bold]"
235
+ f"\n [bold]Installing frontend dependencies using { package_manager .name } . It might take a while...[/bold]"
198
236
)
199
237
run ([package_manager , "install" ], cwd = ".frontend" , check = True )
200
238
201
239
202
- def _get_node_package_manager () -> str :
240
+ def _get_node_package_manager () -> NodePackageManager :
203
241
"""
204
242
Check for available package managers and return the preferred one.
205
243
Returns 'pnpm' if installed, falls back to 'npm'.
@@ -215,12 +253,12 @@ def _get_node_package_manager() -> str:
215
253
for cmd in pnpm_cmds :
216
254
cmd_path = which (cmd )
217
255
if cmd_path is not None :
218
- return cmd_path
256
+ return NodePackageManager ( cmd_path )
219
257
220
258
for cmd in npm_cmds :
221
259
cmd_path = which (cmd )
222
260
if cmd_path is not None :
223
- return cmd_path
261
+ return NodePackageManager ( cmd_path )
224
262
225
263
raise SystemError (
226
264
"Neither pnpm nor npm is installed. Please install Node.js and a package manager first."
@@ -244,28 +282,27 @@ def _get_poetry_executable() -> str:
244
282
raise SystemError ("Poetry is not installed. Please install Poetry first." )
245
283
246
284
247
- def _is_bindable_port (port : int ) -> bool :
248
- """Check if a port is available by attempting to connect to it ."""
285
+ def _is_port_available (port : int ) -> bool :
286
+ """Check if a port is available for binding ."""
249
287
with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as s :
250
288
try :
251
- # Try to connect to the port
252
289
s .connect (("localhost" , port ))
253
- # If we can connect, port is in use
254
- return False
290
+ return False # Port is in use, so not available
255
291
except ConnectionRefusedError :
256
- # Connection refused means port is available
257
- return True
292
+ return True # Port is available
258
293
except socket .error :
259
- # Other socket errors also likely mean port is available
260
- return True
294
+ return True # Other socket errors likely mean port is available
295
+
296
+
297
+ def _is_server_running (port : int ) -> bool :
298
+ """Check if a server is running on the specified port."""
299
+ return not _is_port_available (port )
261
300
262
301
263
302
def _find_free_port (start_port : int ) -> int :
264
- """
265
- Find a free port starting from the given port number.
266
- """
303
+ """Find a free port starting from the given port number."""
267
304
for port in range (start_port , 65535 ):
268
- if _is_bindable_port (port ):
305
+ if _is_port_available (port ):
269
306
return port
270
307
raise SystemError ("No free port found" )
271
308
0 commit comments