Merge pull request #2105 from Dokploy/fix/cancel-schedules

feat: add kill process functionality to schedules
This commit is contained in:
Mauricio Siu
2025-06-29 21:13:37 -06:00
committed by GitHub
10 changed files with 5940 additions and 11 deletions

View File

@@ -62,6 +62,8 @@ export const ShowDeployments = ({
const { mutateAsync: rollback, isLoading: isRollingBack } =
api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isLoading: isKillingProcess } =
api.deployment.killProcess.useMutation();
const [url, setUrl] = React.useState("");
useEffect(() => {
@@ -170,6 +172,32 @@ export const ShowDeployments = ({
</div>
<div className="flex flex-row items-center gap-2">
{deployment.pid && deployment.status === "running" && (
<DialogAction
title="Kill Process"
description="Are you sure you want to kill the process?"
type="default"
onClick={async () => {
await killProcess({
deploymentId: deployment.deploymentId,
})
.then(() => {
toast.success("Process killed successfully");
})
.catch(() => {
toast.error("Error killing process");
});
}}
>
<Button
variant="destructive"
size="sm"
isLoading={isKillingProcess}
>
Kill Process
</Button>
</DialogAction>
)}
<Button
onClick={() => {
setActiveLog(deployment);

View File

@@ -166,12 +166,16 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
await runManually({
scheduleId: schedule.scheduleId,
}).then(async () => {
await new Promise((resolve) =>
setTimeout(resolve, 1500),
);
refetchSchedules();
});
})
.then(async () => {
await new Promise((resolve) =>
setTimeout(resolve, 1500),
);
refetchSchedules();
})
.catch(() => {
toast.error("Error running schedule");
});
}}
>
<Play className="size-4 transition-colors" />

View File

@@ -0,0 +1 @@
ALTER TABLE "deployment" ADD COLUMN "pid" text;

File diff suppressed because it is too large Load Diff

View File

@@ -687,6 +687,13 @@
"when": 1750567641441,
"tag": "0097_hard_lizard",
"breakpoints": true
},
{
"idx": 98,
"version": "7",
"when": 1751233265357,
"tag": "0098_conscious_chat",
"breakpoints": true
}
]
}

View File

@@ -7,16 +7,21 @@ import {
deployments,
} from "@/server/db/schema";
import {
execAsync,
execAsyncRemote,
findAllDeploymentsByApplicationId,
findAllDeploymentsByComposeId,
findAllDeploymentsByServerId,
findApplicationById,
findComposeById,
findDeploymentById,
findServerById,
updateDeploymentStatus,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure
@@ -72,4 +77,30 @@ export const deploymentRouter = createTRPCRouter({
return deploymentsList;
}),
killProcess: protectedProcedure
.input(
z.object({
deploymentId: z.string().min(1),
}),
)
.mutation(async ({ input }) => {
const deployment = await findDeploymentById(input.deploymentId);
if (!deployment.pid) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Deployment is not running",
});
}
const command = `kill -9 ${deployment.pid}`;
if (deployment.schedule?.serverId) {
await execAsyncRemote(deployment.schedule.serverId, command);
} else {
await execAsync(command);
}
await updateDeploymentStatus(deployment.deploymentId, "error");
}),
});

View File

@@ -31,6 +31,7 @@ export const deployments = pgTable("deployment", {
description: text("description"),
status: deploymentStatus("status").default("running"),
logPath: text("logPath").notNull(),
pid: text("pid"),
applicationId: text("applicationId").references(
() => applications.applicationId,
{ onDelete: "cascade" },

View File

@@ -40,6 +40,7 @@ export const findDeploymentById = async (deploymentId: string) => {
where: eq(deployments.deploymentId, deploymentId),
with: {
application: true,
schedule: true,
},
});
if (!deployment) {

View File

@@ -109,7 +109,12 @@ export const updateSchedule = async (
const handleScript = async (schedule: Schedule) => {
const { SCHEDULES_PATH } = paths(!!schedule?.serverId);
const fullPath = path.join(SCHEDULES_PATH, schedule?.appName || "");
const encodedContent = encodeBase64(schedule?.script || "");
// Add PID and Schedule ID echo by default to all scripts
const scriptWithPid = `echo "PID: $$ | Schedule ID: ${schedule.scheduleId}"
${schedule?.script || ""}`;
const encodedContent = encodeBase64(scriptWithPid);
const script = `
mkdir -p ${fullPath}
rm -f ${fullPath}/script.sh

View File

@@ -2,7 +2,10 @@ import { createWriteStream } from "node:fs";
import path from "node:path";
import { paths } from "@dokploy/server/constants";
import type { Schedule } from "@dokploy/server/db/schema/schedule";
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
import {
createDeploymentSchedule,
updateDeployment,
} from "@dokploy/server/services/deployment";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduleJob as scheduleJobNode, scheduledJobs } from "node-schedule";
@@ -113,8 +116,16 @@ export const runCommand = async (scheduleId: string) => {
await spawnAsync(
"bash",
["-c", "./script.sh"],
(data) => {
async (data) => {
if (writeStream.writable) {
// we need to extract the PID and Schedule ID from the data
const pid = data?.match(/PID: (\d+)/)?.[1];
if (pid) {
await updateDeployment(deployment.deploymentId, {
pid,
});
}
writeStream.write(data);
}
},
@@ -133,13 +144,21 @@ export const runCommand = async (scheduleId: string) => {
const command = `
set -e
echo "Running script" >> ${deployment.logPath};
bash -c ${fullPath}/script.sh >> ${deployment.logPath} 2>> ${deployment.logPath} || {
bash -c ${fullPath}/script.sh 2>&1 | tee -a ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
echo "✅ Command executed successfully" >> ${deployment.logPath};
`;
await execAsyncRemote(serverId, command);
await execAsyncRemote(serverId, command, async (data) => {
// we need to extract the PID and Schedule ID from the data
const pid = data?.match(/PID: (\d+)/)?.[1];
if (pid) {
await updateDeployment(deployment.deploymentId, {
pid,
});
}
});
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;