Skip to content

Commit

Permalink
fix: fixing bug of cannot running docker container after container is… (
Browse files Browse the repository at this point in the history
#147)

… shut down
  • Loading branch information
NarwhalChen authored Mar 5, 2025
1 parent 90ebd1f commit f92c788
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 45 deletions.
2 changes: 2 additions & 0 deletions frontend/src/app/api/file/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function getFileType(filePath: string): string {
const extension = filePath.split('.').pop()?.toLowerCase() || '';

const typeMap: { [key: string]: string } = {
//TODO: Add more file types
tsx: 'typescriptreact',
txt: 'text',
md: 'markdown',
json: 'json',
Expand Down
95 changes: 83 additions & 12 deletions frontend/src/app/api/runProject/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ async function buildAndRunDocker(

return new Promise((resolve, reject) => {
// 2. Build the Docker image
console.log(
`Starting Docker build for image: ${imageName} in directory: ${directory}`
);
exec(
`docker build -t ${imageName} ${directory}`,
(buildErr, buildStdout, buildStderr) => {
Expand All @@ -141,19 +144,61 @@ async function buildAndRunDocker(

// 3. Run the Docker container
const runCommand = `docker run -d --name ${containerName} -l "traefik.enable=true" \
-l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \
-l "traefik.http.services.${subdomain}.loadbalancer.server.port=5173" \
--network=codefox_traefik_network -p ${exposedPort}:5173 \
-v "${directory}:/app" \
${imageName}`;
-l "traefik.http.routers.${subdomain}.rule=Host(\\"${domain}\\")" \
-l "traefik.http.services.${subdomain}.loadbalancer.server.port=5173" \
--network=codefox_traefik_network -p ${exposedPort}:5173 \
-v "${directory}:/app" \
${imageName}`;

console.log(runCommand);
console.log(`Executing run command: ${runCommand}`);

exec(runCommand, (runErr, runStdout, runStderr) => {
if (runErr) {
// If the container name already exists
console.error(`Error during Docker run: ${runStderr}`);
if (runStderr.includes('Conflict. The container name')) {
resolve({ domain, containerId: containerName });
console.log(
`Container name conflict detected. Removing existing container ${containerName}.`
);
// Remove the existing container
exec(
`docker rm -f ${containerName}`,
(removeErr, removeStdout, removeStderr) => {
if (removeErr) {
console.error(
`Error removing existing container: ${removeStderr}`
);
return reject(removeErr);
}
console.log(
`Existing container ${containerName} removed. Retrying to run the container.`
);

// Retry running the Docker container
exec(
runCommand,
(retryRunErr, retryRunStdout, retryRunStderr) => {
if (retryRunErr) {
console.error(
`Error during Docker run: ${retryRunStderr}`
);
return reject(retryRunErr);
}

const containerActualId = retryRunStdout.trim();
runningContainers.set(projectPath, {
domain,
containerId: containerActualId,
});

console.log(
`Container ${containerName} is now running at http://${domain}`
);
resolve({ domain, containerId: containerActualId });
}
);
}
);
return;
}
console.error(`Error during Docker run: ${runStderr}`);
Expand All @@ -169,7 +214,6 @@ async function buildAndRunDocker(
console.log(
`Container ${containerName} is now running at http://${domain}`
);

resolve({ domain, containerId: containerActualId });
});
}
Expand Down Expand Up @@ -204,11 +248,38 @@ export async function GET(req: Request) {
// Check if a container is already running
const existingContainer = runningContainers.get(projectPath);
if (existingContainer) {
return NextResponse.json({
message: 'Docker container already running',
domain: existingContainer.domain,
containerId: existingContainer.containerId,
// Check if the container is running
const containerStatus = await new Promise<string>((resolve) => {
exec(
`docker inspect -f "{{.State.Running}}" ${existingContainer.containerId}`,
(err, stdout) => {
if (err) {
resolve('not found');
} else {
resolve(stdout.trim());
}
}
);
});

if (containerStatus === 'true') {
return NextResponse.json({
message: 'Docker container already running',
domain: existingContainer.domain,
containerId: existingContainer.containerId,
});
} else {
// Remove the existing container if it's not running
exec(`docker rm -f ${existingContainer.containerId}`, (removeErr) => {
if (removeErr) {
console.error(`Error removing existing container: ${removeErr}`);
} else {
console.log(
`Removed existing container: ${existingContainer.containerId}`
);
}
});
}
}

// Prevent duplicate builds
Expand Down
15 changes: 6 additions & 9 deletions frontend/src/components/chat/chat-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import ProjectModal from '@/components/chat/project-modal';
import { useQuery } from '@apollo/client';
import { GET_USER_PROJECTS } from '@/graphql/request';
import { useAuthContext } from '@/providers/AuthProvider';
import { ProjectProvider } from './code-engine/project-context';

export default function ChatLayout({
children,
Expand All @@ -30,14 +29,12 @@ export default function ChatLayout({

return (
<main className="flex h-[calc(100dvh)] flex-col items-center">
<ProjectProvider>
<ProjectModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
refetchProjects={refetch}
/>
<div className="w-full h-full">{children}</div>
</ProjectProvider>
<ProjectModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
refetchProjects={refetch}
/>
<div className="w-full h-full">{children}</div>
</main>
);
}
12 changes: 8 additions & 4 deletions frontend/src/components/chat/code-engine/code-engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,16 @@ export function CodeEngine({
// Effect: Fetch file structure when projectId changes
useEffect(() => {
async function fetchFiles() {
if (!curProject?.projectPath) return;
if (!curProject?.projectPath) {
console.log('no project path found');
return;
}

try {
const response = await fetch(
`/api/project?path=${curProject.projectPath}`
);
console.log('loading file structure');
const data = await response.json();
setFileStructureData(data.res || {});
} catch (error) {
Expand Down Expand Up @@ -270,12 +274,12 @@ export function CodeEngine({

// Render the CodeEngine layout
return (
<div className="rounded-lg border shadow-sm overflow-hidden h-full">
<div className="rounded-lg border shadow-sm overflow-scroll h-full">
{/* Header Bar */}
<ResponsiveToolbar isLoading={!isProjectReady} />

{/* Main Content Area with Loading */}
<div className="relative h-[calc(100vh-48px-2rem)]">
<div className="relative h-[calc(100vh-48px-4rem)]">
<AnimatePresence>
{!isProjectReady && (
<motion.div
Expand Down Expand Up @@ -311,7 +315,7 @@ export function CodeEngine({
<Editor
height="100%"
width="100%"
defaultLanguage="typescript"
defaultLanguage="typescriptreact"
value={newCode}
language={type}
loading={isLoading}
Expand Down
88 changes: 83 additions & 5 deletions frontend/src/components/chat/code-engine/web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
ChevronRight,
Maximize,
ExternalLink,
RefreshCcw,
ZoomIn,
ZoomOut,
} from 'lucide-react';

export default function WebPreview() {
Expand All @@ -15,6 +18,7 @@ export default function WebPreview() {
const [displayPath, setDisplayPath] = useState('/');
const [history, setHistory] = useState<string[]>(['/']);
const [currentIndex, setCurrentIndex] = useState(0);
const [scale, setScale] = useState(0.7);
const iframeRef = useRef(null);
const containerRef = useRef<{ projectPath: string; domain: string } | null>(
null
Expand Down Expand Up @@ -49,13 +53,34 @@ export default function WebPreview() {
);
const json = await response.json();

await new Promise((resolve) => setTimeout(resolve, 200));
await new Promise((resolve) => setTimeout(resolve, 100));

containerRef.current = {
projectPath,
domain: json.domain,
};
setBaseUrl(`http://${json.domain}`);

const checkUrlStatus = async (url: string) => {
let status = 0;
while (status !== 200) {
try {
const res = await fetch(url, { method: 'HEAD' });
status = res.status;
if (status !== 200) {
console.log(`URL status: ${status}. Retrying...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
} catch (err) {
console.error('Error checking URL status:', err);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
};

const baseUrl = `http://${json.domain}`;
await checkUrlStatus(baseUrl);

setBaseUrl(baseUrl);
setDisplayPath('/');
} catch (error) {
console.error('fetching url error:', error);
Expand Down Expand Up @@ -109,6 +134,25 @@ export default function WebPreview() {
setDisplayPath(history[currentIndex + 1]);
}
};
const reloadIframe = () => {
const iframe = document.getElementById('myIframe') as HTMLIFrameElement;
if (iframe) {
const src = iframe.src;
iframe.src = 'about:blank';
setTimeout(() => {
iframe.src = src;
setScale(0.7);
}, 50);
}
};

const zoomIn = () => {
setScale((prevScale) => Math.min(prevScale + 0.1, 2)); // 最大缩放比例为 2
};

const zoomOut = () => {
setScale((prevScale) => Math.max(prevScale - 0.1, 0.5)); // 最小缩放比例为 0.5
};

return (
<div className="flex flex-col w-full h-full">
Expand All @@ -119,7 +163,7 @@ export default function WebPreview() {
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="h-6 w-6"
onClick={goBack}
disabled={!baseUrl || currentIndex === 0}
>
Expand All @@ -128,12 +172,20 @@ export default function WebPreview() {
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
className="h-6 w-6"
onClick={goForward}
disabled={!baseUrl || currentIndex >= history.length - 1}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={reloadIframe}
>
<RefreshCcw />
</Button>
</div>

{/* URL Input */}
Expand All @@ -150,6 +202,24 @@ export default function WebPreview() {

{/* Actions */}
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={zoomOut}
className="h-8 w-8"
disabled={!baseUrl}
>
<ZoomOut className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={zoomIn}
className="h-8 w-8"
disabled={!baseUrl}
>
<ZoomIn className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
Expand All @@ -175,9 +245,17 @@ export default function WebPreview() {
<div className="relative flex-1 w-full h-full">
{baseUrl ? (
<iframe
id="myIframe"
ref={iframeRef}
src={`${baseUrl}${displayPath}`}
className="absolute inset-0 w-full h-full border-none bg-background"
className="absolute inset-0 w-full h-80% border-none bg-background"
style={{
transform: `scale(${scale})`,
transformOrigin: 'top left',
width: `calc(100% / ${scale})`,
height: `calc(100% / ${scale})`,
border: 'none',
}}
/>
) : (
<div className="absolute inset-0 w-full h-full flex items-center justify-center bg-background">
Expand Down
Loading

0 comments on commit f92c788

Please sign in to comment.