mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-29 00:25:28 +00:00
Starts backend from react and monitors health
This commit is contained in:
parent
125c223b2d
commit
2a4d8c0ea4
@ -20,12 +20,6 @@ fn add_log(message: String) {
|
|||||||
println!("{}", message); // Also print to console
|
println!("{}", message); // Also print to console
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to get backend logs
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_backend_logs() -> Result<Vec<String>, String> {
|
|
||||||
let logs = BACKEND_LOGS.lock().unwrap();
|
|
||||||
Ok(logs.iter().cloned().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command to start the backend with bundled JRE
|
// Command to start the backend with bundled JRE
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -253,7 +247,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
|
|||||||
|
|
||||||
// Wait for the backend to start
|
// Wait for the backend to start
|
||||||
println!("⏳ Waiting for backend startup...");
|
println!("⏳ Waiting for backend startup...");
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(5000)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(10000)).await;
|
||||||
|
|
||||||
Ok("Backend startup initiated successfully with bundled JRE".to_string())
|
Ok("Backend startup initiated successfully with bundled JRE".to_string())
|
||||||
}
|
}
|
||||||
@ -261,20 +255,18 @@ async fn start_backend(app: tauri::AppHandle) -> Result<String, String> {
|
|||||||
// Command to check if backend is healthy
|
// Command to check if backend is healthy
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn check_backend_health() -> Result<bool, String> {
|
async fn check_backend_health() -> Result<bool, String> {
|
||||||
println!("🔍 Checking backend health...");
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(5))
|
.timeout(std::time::Duration::from_secs(5))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
||||||
|
|
||||||
match client.get("http://localhost:8080/actuator/health").send().await {
|
match client.get("http://localhost:8080/api/v1/info/status").send().await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
println!("💓 Health check response status: {}", status);
|
println!("💓 Health check response status: {}", status);
|
||||||
if status.is_success() {
|
if status.is_success() {
|
||||||
match response.text().await {
|
match response.text().await {
|
||||||
Ok(body) => {
|
Ok(body) => {
|
||||||
println!("💓 Health check response: {}", body);
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -294,41 +286,7 @@ async fn check_backend_health() -> Result<bool, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to get backend process status
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_backend_status() -> Result<String, String> {
|
|
||||||
let process_guard = BACKEND_PROCESS.lock().unwrap();
|
|
||||||
match process_guard.as_ref() {
|
|
||||||
Some(child) => {
|
|
||||||
// Try to check if process is still alive
|
|
||||||
let pid = child.pid();
|
|
||||||
println!("🔍 Checking backend process status, PID: {}", pid);
|
|
||||||
Ok(format!("Backend process is running with bundled JRE (PID: {})", pid))
|
|
||||||
},
|
|
||||||
None => Ok("Backend process is not running".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command to check if backend port is accessible
|
|
||||||
#[tauri::command]
|
|
||||||
async fn check_backend_port() -> Result<bool, String> {
|
|
||||||
println!("🔍 Checking if port 8080 is accessible...");
|
|
||||||
let client = reqwest::Client::builder()
|
|
||||||
.timeout(std::time::Duration::from_secs(3))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
|
|
||||||
|
|
||||||
match client.head("http://localhost:8080/").send().await {
|
|
||||||
Ok(response) => {
|
|
||||||
println!("✅ Port 8080 responded with status: {}", response.status());
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("❌ Port 8080 not accessible: {}", e);
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command to check bundled runtime and JAR
|
// Command to check bundled runtime and JAR
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -387,99 +345,8 @@ async fn check_jar_exists(app: tauri::AppHandle) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command to test sidecar binary directly
|
|
||||||
#[tauri::command]
|
|
||||||
async fn test_sidecar_binary(app: tauri::AppHandle) -> Result<String, String> {
|
|
||||||
println!("🔍 Testing sidecar binary availability...");
|
|
||||||
|
|
||||||
// Test if we can create the sidecar command (this validates the binary exists)
|
|
||||||
match app.shell().sidecar("stirling-pdf-backend") {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("✅ Sidecar binary 'stirling-pdf-backend' is available");
|
|
||||||
Ok("Sidecar binary 'stirling-pdf-backend' is available and can be executed".to_string())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("❌ Failed to access sidecar binary: {}", e);
|
|
||||||
Ok(format!("Sidecar binary not available: {}. Make sure the binary exists in the binaries/ directory with correct permissions.", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command to stop the backend process
|
|
||||||
#[tauri::command]
|
|
||||||
async fn stop_backend() -> Result<String, String> {
|
|
||||||
add_log("🛑 stop_backend() called - Attempting to stop backend...".to_string());
|
|
||||||
|
|
||||||
let mut process_guard = BACKEND_PROCESS.lock().unwrap();
|
|
||||||
match process_guard.take() {
|
|
||||||
Some(child) => {
|
|
||||||
let pid = child.pid();
|
|
||||||
add_log(format!("🔄 Terminating backend process (PID: {})", pid));
|
|
||||||
|
|
||||||
// Kill the process
|
|
||||||
match child.kill() {
|
|
||||||
Ok(_) => {
|
|
||||||
add_log(format!("✅ Backend process (PID: {}) terminated successfully", pid));
|
|
||||||
Ok(format!("Backend process (PID: {}) terminated successfully", pid))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let error_msg = format!("❌ Failed to terminate backend process: {}", e);
|
|
||||||
add_log(error_msg.clone());
|
|
||||||
Err(error_msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
add_log("⚠️ No backend process running to stop".to_string());
|
|
||||||
Ok("No backend process running".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command to check Java environment (bundled version)
|
|
||||||
#[tauri::command]
|
|
||||||
async fn check_java_environment(app: tauri::AppHandle) -> Result<String, String> {
|
|
||||||
println!("🔍 Checking bundled Java environment...");
|
|
||||||
|
|
||||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
|
||||||
let jre_dir = resource_dir.join("runtime").join("jre");
|
|
||||||
let java_executable = if cfg!(windows) {
|
|
||||||
jre_dir.join("bin").join("java.exe")
|
|
||||||
} else {
|
|
||||||
jre_dir.join("bin").join("java")
|
|
||||||
};
|
|
||||||
|
|
||||||
if java_executable.exists() {
|
|
||||||
let output = std::process::Command::new(&java_executable)
|
|
||||||
.arg("--version")
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match output {
|
|
||||||
Ok(output) => {
|
|
||||||
if output.status.success() {
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
let version_info = if !stdout.is_empty() { stdout } else { stderr };
|
|
||||||
println!("✅ Bundled Java found: {}", version_info);
|
|
||||||
Ok(format!("Bundled Java available: {}", version_info.trim()))
|
|
||||||
} else {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
println!("❌ Bundled Java command failed: {}", stderr);
|
|
||||||
Ok(format!("Bundled Java command failed: {}", stderr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
println!("❌ Failed to execute bundled Java: {}", e);
|
|
||||||
Ok(format!("Failed to execute bundled Java: {}", e))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok("❌ Bundled JRE not found".to_string())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Ok("❌ Could not access bundled resources".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function to stop backend on app exit
|
// Cleanup function to stop backend on app exit
|
||||||
fn cleanup_backend() {
|
fn cleanup_backend() {
|
||||||
@ -505,34 +372,25 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Disable file logging in debug mode to prevent dev server restart loops
|
|
||||||
// if cfg!(debug_assertions) {
|
|
||||||
// app.handle().plugin(
|
|
||||||
// tauri_plugin_log::Builder::default()
|
|
||||||
// .level(log::LevelFilter::Info)
|
|
||||||
// .build(),
|
|
||||||
// )?;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Automatically start the backend when Tauri starts
|
// Automatically start the backend when Tauri starts
|
||||||
let app_handle = app.handle().clone();
|
// let app_handle = app.handle().clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
// tauri::async_runtime::spawn(
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await; // Small delay to ensure app is ready
|
// async move {
|
||||||
add_log("🔄 Tauri app ready, starting backend...".to_string());
|
// match start_backend(app_handle).await {
|
||||||
|
// Ok(result) => {
|
||||||
match start_backend(app_handle).await {
|
// add_log(format!("🚀 Auto-started backend on Tauri startup: {}", result));
|
||||||
Ok(result) => {
|
// }
|
||||||
add_log(format!("🚀 Auto-started backend on Tauri startup: {}", result));
|
// Err(error) => {
|
||||||
}
|
// add_log(format!("❌ Failed to auto-start backend: {}", error));
|
||||||
Err(error) => {
|
// }
|
||||||
add_log(format!("❌ Failed to auto-start backend: {}", error));
|
// }
|
||||||
}
|
// });
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
}
|
||||||
.invoke_handler(tauri::generate_handler![start_backend, stop_backend, check_backend_health, check_jar_exists, test_sidecar_binary, get_backend_status, check_backend_port, check_java_environment, get_backend_logs])
|
)
|
||||||
|
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, check_jar_exists])
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app_handle, event| {
|
.run(|app_handle, event| {
|
||||||
|
@ -1,26 +1,37 @@
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
import { SidecarTest } from './components/SidecarTest';
|
import { BackendHealthIndicator } from './components/BackendHealthIndicator';
|
||||||
|
import { backendService } from './services/backendService';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [showTests, setShowTests] = useState(false); // Start with app visible
|
useEffect(() => {
|
||||||
|
// Only start backend if running in Tauri
|
||||||
|
const initializeBackend = async () => {
|
||||||
|
try {
|
||||||
|
// Check if we're running in Tauri environment
|
||||||
|
if (typeof window !== 'undefined' && window.__TAURI__) {
|
||||||
|
console.log('Running in Tauri - Starting backend on React app startup...');
|
||||||
|
await backendService.startBackend();
|
||||||
|
console.log('Backend started successfully');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start backend on app startup:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeBackend();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100">
|
<div className="min-h-screen bg-gray-100">
|
||||||
<div className="bg-white shadow-sm border-b">
|
<div className="bg-white shadow-sm border-b relative">
|
||||||
<div className="max-w-4xl mx-auto px-4 py-3 flex justify-between items-center">
|
<BackendHealthIndicator className="absolute top-3 left-3 z-10" />
|
||||||
<h1 className="text-xl font-bold">Stirling PDF - Tauri Integration</h1>
|
<div className="max-w-4xl mx-auto px-4 py-3">
|
||||||
<button
|
<h1 className="text-xl font-bold">Stirling PDF</h1>
|
||||||
onClick={() => setShowTests(!showTests)}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
{showTests ? 'Show App' : 'Show Tests'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showTests ? <SidecarTest /> : <HomePage />}
|
<HomePage />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
28
frontend/src/components/BackendHealthIndicator.tsx
Normal file
28
frontend/src/components/BackendHealthIndicator.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import '../index.css';
|
||||||
|
import { useBackendHealth } from '../hooks/useBackendHealth';
|
||||||
|
|
||||||
|
interface BackendHealthIndicatorProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
const { isHealthy, isChecking, error, checkHealth } = useBackendHealth();
|
||||||
|
|
||||||
|
let statusColor = 'bg-red-500'; // offline
|
||||||
|
if (isChecking || (!isHealthy && error === 'Backend starting up...')) {
|
||||||
|
statusColor = 'bg-yellow-500'; // starting/checking
|
||||||
|
} else if (isHealthy) {
|
||||||
|
statusColor = 'bg-green-500'; // online
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-2xs h-2xs ${statusColor} rounded-full cursor-pointer ${isChecking ? 'animate-pulse' : ''} ${className}`}
|
||||||
|
onClick={checkHealth}
|
||||||
|
title={isHealthy ? 'Backend Online' : isChecking ? 'Checking...' : 'Backend Offline'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
70
frontend/src/hooks/useBackendHealth.ts
Normal file
70
frontend/src/hooks/useBackendHealth.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { backendService } from '../services/backendService';
|
||||||
|
|
||||||
|
export interface BackendHealthState {
|
||||||
|
isHealthy: boolean;
|
||||||
|
isChecking: boolean;
|
||||||
|
lastChecked: Date | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBackendHealth = (checkInterval: number = 2000) => {
|
||||||
|
const [healthState, setHealthState] = useState<BackendHealthState>({
|
||||||
|
isHealthy: false,
|
||||||
|
isChecking: false,
|
||||||
|
lastChecked: null,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [startupTime] = useState<Date>(new Date());
|
||||||
|
const [attemptCount, setAttemptCount] = useState<number>(0);
|
||||||
|
|
||||||
|
const checkHealth = useCallback(async () => {
|
||||||
|
setHealthState(prev => ({ ...prev, isChecking: true, error: null }));
|
||||||
|
setAttemptCount(prev => prev + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isHealthy = await backendService.checkHealth();
|
||||||
|
setHealthState({
|
||||||
|
isHealthy,
|
||||||
|
isChecking: false,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
if (isHealthy) {
|
||||||
|
setAttemptCount(0); // Reset attempt count on success
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const now = new Date();
|
||||||
|
const timeSinceStartup = now.getTime() - startupTime.getTime();
|
||||||
|
const isWithinStartupPeriod = timeSinceStartup < 60000; // 60 seconds
|
||||||
|
|
||||||
|
// Don't show error during initial startup period
|
||||||
|
const errorMessage = isWithinStartupPeriod
|
||||||
|
? 'Backend starting up...'
|
||||||
|
: (error instanceof Error ? error.message : 'Health check failed');
|
||||||
|
|
||||||
|
setHealthState({
|
||||||
|
isHealthy: false,
|
||||||
|
isChecking: false,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [startupTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initial health check
|
||||||
|
checkHealth();
|
||||||
|
|
||||||
|
// Set up periodic health checks
|
||||||
|
const interval = setInterval(checkHealth, checkInterval);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [checkHealth, checkInterval]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...healthState,
|
||||||
|
checkHealth,
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { getApiBaseUrl } from '../utils/api';
|
|
||||||
|
|
||||||
export class BackendService {
|
export class BackendService {
|
||||||
private static instance: BackendService;
|
private static instance: BackendService;
|
||||||
@ -31,6 +30,9 @@ export class BackendService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async checkHealth(): Promise<boolean> {
|
async checkHealth(): Promise<boolean> {
|
||||||
|
if (!this.backendStarted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return await invoke('check_backend_health');
|
return await invoke('check_backend_health');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -39,7 +41,7 @@ export class BackendService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async waitForHealthy(maxAttempts = 30): Promise<void> {
|
private async waitForHealthy(maxAttempts = 60): Promise<void> {
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
const isHealthy = await this.checkHealth();
|
const isHealthy = await this.checkHealth();
|
||||||
if (isHealthy) {
|
if (isHealthy) {
|
||||||
@ -48,26 +50,7 @@ export class BackendService {
|
|||||||
}
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
}
|
}
|
||||||
throw new Error('Backend failed to become healthy after 30 seconds');
|
throw new Error('Backend failed to become healthy after 60 seconds');
|
||||||
}
|
|
||||||
|
|
||||||
getBackendUrl(): string {
|
|
||||||
return getApiBaseUrl() || 'http://localhost:8080';
|
|
||||||
}
|
|
||||||
|
|
||||||
async makeApiCall(endpoint: string, options?: RequestInit): Promise<Response> {
|
|
||||||
if (!this.backendStarted) {
|
|
||||||
await this.startBackend();
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${this.getBackendUrl()}${endpoint}`;
|
|
||||||
return fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options?.headers,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +1,8 @@
|
|||||||
export const getApiBaseUrl = (): string => {
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
|
||||||
const envUrl = import.meta.env.VITE_API_BASE_URL;
|
|
||||||
|
|
||||||
// In development, use empty string to leverage Vite proxy
|
|
||||||
// In production/Tauri, use localhost:8080 directly
|
|
||||||
// if (envUrl !== undefined) {
|
|
||||||
// console.log(`Using API base URL from environment: ${envUrl}`);
|
|
||||||
// return envUrl;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Fallback for development
|
|
||||||
console.log('Using default API base URL: http://localhost:8080');
|
|
||||||
return 'http://localhost:8080';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const makeApiUrl = (endpoint: string): string => {
|
export const makeApiUrl = (endpoint: string): string => {
|
||||||
const baseUrl = getApiBaseUrl();
|
const baseUrl = apiBaseUrl;
|
||||||
|
|
||||||
// If baseUrl is empty (development), return endpoint as-is for proxy
|
// If baseUrl is empty (development), return endpoint as-is for proxy
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user