mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-07 15:05:23 +02:00
Merge branch 'canary' into feature/add-custom-webhook-notification-provider
This commit is contained in:
@@ -15,7 +15,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can create an organization",
|
||||
@@ -96,7 +96,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can update it",
|
||||
@@ -119,7 +119,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can delete it",
|
||||
@@ -194,6 +194,65 @@ export const organizationRouter = createTRPCRouter({
|
||||
.delete(invitation)
|
||||
.where(eq(invitation.id, input.invitationId));
|
||||
}),
|
||||
updateMemberRole: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
role: z.enum(["admin", "member"]),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Fetch the target member
|
||||
const target = await db.query.member.findFirst({
|
||||
where: eq(member.id, input.memberId),
|
||||
with: { user: true },
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
|
||||
}
|
||||
|
||||
if (target.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to update this member's role",
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent users from changing their own role
|
||||
if (target.userId === ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You cannot change your own role",
|
||||
});
|
||||
}
|
||||
|
||||
// Owner role is intransferible - cannot change to or from owner
|
||||
if (target.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "The owner role is intransferible",
|
||||
});
|
||||
}
|
||||
|
||||
// Only owners can change admin roles
|
||||
// Admins can only change member roles
|
||||
if (ctx.user.role === "admin" && target.role === "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only the organization owner can change admin roles. Admins can only modify member roles.",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the target member's role
|
||||
await db
|
||||
.update(member)
|
||||
.set({ role: input.role })
|
||||
.where(eq(member.id, input.memberId));
|
||||
|
||||
return true;
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
canAccessToTraefikFiles,
|
||||
checkGPUStatus,
|
||||
cleanStoppedContainers,
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
cleanUpUnusedVolumes,
|
||||
checkPortInUse,
|
||||
cleanupAll,
|
||||
cleanupBuilders,
|
||||
cleanupContainers,
|
||||
cleanupImages,
|
||||
cleanupSystem,
|
||||
cleanupVolumes,
|
||||
DEFAULT_UPDATE_DATA,
|
||||
execAsync,
|
||||
findServerById,
|
||||
@@ -130,6 +132,17 @@ export const settingsRouter = createTRPCRouter({
|
||||
let newPorts = ports;
|
||||
// If receive true, add 8080 to ports
|
||||
if (input.enableDashboard) {
|
||||
// Check if port 8080 is already in use before enabling dashboard
|
||||
const portCheck = await checkPortInUse(8080, input.serverId);
|
||||
if (portCheck.isInUse) {
|
||||
const conflictingContainer = portCheck.conflictingContainer
|
||||
? ` by container "${portCheck.conflictingContainer}"`
|
||||
: "";
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
|
||||
});
|
||||
}
|
||||
newPorts.push({
|
||||
targetPort: 8080,
|
||||
publishedPort: 8080,
|
||||
@@ -149,41 +162,38 @@ export const settingsRouter = createTRPCRouter({
|
||||
cleanUnusedImages: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
await cleanupImages(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedVolumes: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedVolumes(input?.serverId);
|
||||
await cleanupVolumes(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanStoppedContainers: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
await cleanupContainers(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanDockerBuilder: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanupBuilders(input?.serverId);
|
||||
}),
|
||||
cleanDockerPrune: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanupSystem(input?.serverId);
|
||||
await cleanupBuilders(input?.serverId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
cleanAll: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
await cleanupAll(input?.serverId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
@@ -201,7 +211,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateUser(ctx.user.id, {
|
||||
await updateUser(ctx.user.ownerId, {
|
||||
sshPrivateKey: input.sshPrivateKey,
|
||||
});
|
||||
|
||||
@@ -213,7 +223,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const user = await updateUser(ctx.user.id, {
|
||||
const user = await updateUser(ctx.user.ownerId, {
|
||||
host: input.host,
|
||||
...(input.letsEncryptEmail && {
|
||||
letsEncryptEmail: input.letsEncryptEmail,
|
||||
@@ -240,7 +250,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateUser(ctx.user.id, {
|
||||
await updateUser(ctx.user.ownerId, {
|
||||
sshPrivateKey: null,
|
||||
});
|
||||
return true;
|
||||
@@ -281,9 +291,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages(server.serverId);
|
||||
await cleanUpDockerBuilder(server.serverId);
|
||||
await cleanUpSystemPrune(server.serverId);
|
||||
|
||||
await cleanupAll(server.serverId);
|
||||
|
||||
await sendDockerCleanupNotifications(server.organizationId);
|
||||
});
|
||||
}
|
||||
@@ -300,7 +310,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
} else if (!IS_CLOUD) {
|
||||
const userUpdated = await updateUser(ctx.user.id, {
|
||||
const userUpdated = await updateUser(ctx.user.ownerId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
@@ -309,9 +319,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
|
||||
await cleanupAll();
|
||||
|
||||
await sendDockerCleanupNotifications(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -810,6 +820,19 @@ export const settingsRouter = createTRPCRouter({
|
||||
"dokploy-traefik",
|
||||
input?.serverId,
|
||||
);
|
||||
|
||||
for (const port of input.additionalPorts) {
|
||||
const portCheck = await checkPortInUse(
|
||||
port.publishedPort,
|
||||
input.serverId,
|
||||
);
|
||||
if (portCheck.isInUse) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `Port ${port.targetPort} is already in use by ${portCheck.conflictingContainer}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const preparedEnv = prepareEnvironmentVariables(env);
|
||||
|
||||
await writeTraefikSetup({
|
||||
|
||||
@@ -56,15 +56,16 @@ export const stripeRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||
const user = await findUserById(ctx.user.id);
|
||||
// Always operate on the organization owner's Stripe customer
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
let stripeCustomerId = user.stripeCustomerId;
|
||||
let stripeCustomerId = owner.stripeCustomerId;
|
||||
|
||||
if (stripeCustomerId) {
|
||||
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||
|
||||
if (customer.deleted) {
|
||||
await updateUser(user.id, {
|
||||
await updateUser(owner.id, {
|
||||
stripeCustomerId: null,
|
||||
});
|
||||
stripeCustomerId = null;
|
||||
@@ -78,7 +79,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
customer: stripeCustomerId,
|
||||
}),
|
||||
metadata: {
|
||||
adminId: user.id,
|
||||
adminId: owner.id,
|
||||
},
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
||||
@@ -88,15 +89,16 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { sessionId: session.id };
|
||||
}),
|
||||
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.id);
|
||||
// Use the organization's owner account for billing portal
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
if (!owner.stripeCustomerId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Stripe Customer ID not found",
|
||||
});
|
||||
}
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
const stripeCustomerId = owner.stripeCustomerId;
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
@@ -87,7 +86,11 @@ export const userRouter = createTRPCRouter({
|
||||
// Allow access if:
|
||||
// 1. User is requesting their own information
|
||||
// 2. User has owner role (admin permissions) AND user is in the same organization
|
||||
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
|
||||
if (
|
||||
memberResult.userId !== ctx.user.id &&
|
||||
ctx.user.role !== "owner" &&
|
||||
ctx.user.role !== "admin"
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this user",
|
||||
@@ -223,10 +226,61 @@ export const userRouter = createTRPCRouter({
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure the acting user has admin privileges in the active organization
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only owners or admins can delete users",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch target member within the active organization
|
||||
const targetMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, input.userId),
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
});
|
||||
|
||||
if (!targetMember) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Target user is not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Never allow deleting the organization owner via this endpoint
|
||||
if (targetMember.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You cannot delete the organization owner",
|
||||
});
|
||||
}
|
||||
|
||||
// Admin self-protection: an admin cannot delete themselves
|
||||
if (targetMember.role === "admin" && input.userId === ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Admins cannot delete themselves. Ask the owner or another admin.",
|
||||
});
|
||||
}
|
||||
|
||||
// Only owners can delete admins
|
||||
// Admins can only delete members
|
||||
if (ctx.user.role === "admin" && targetMember.role === "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only the organization owner can delete admins. Admins can only delete members.",
|
||||
});
|
||||
}
|
||||
|
||||
return await removeUserById(input.userId);
|
||||
}),
|
||||
assignPermissions: adminProcedure
|
||||
|
||||
@@ -183,7 +183,11 @@ export const uploadProcedure = async (opts: any) => {
|
||||
};
|
||||
|
||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||
) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
@@ -197,7 +201,11 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
|
||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||
) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
|
||||
@@ -26,6 +26,16 @@ config({ path: ".env" });
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
|
||||
// Initialize critical directories and Traefik config BEFORE Next.js starts
|
||||
// This prevents race conditions with the install script
|
||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
console.log("✅ Critical initialization complete");
|
||||
}
|
||||
|
||||
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
|
||||
const handle = app.getRequestHandler();
|
||||
void app.prepare().then(async () => {
|
||||
@@ -45,11 +55,8 @@ void app.prepare().then(async () => {
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
createDefaultMiddlewares();
|
||||
await initializeNetwork();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
await migration();
|
||||
await initCronJobs();
|
||||
await initSchedules();
|
||||
|
||||
@@ -71,7 +71,9 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
const command = search
|
||||
? `${baseCommand} 2>&1 | grep --line-buffered -iF "${escapedSearch}"`
|
||||
: baseCommand;
|
||||
client.exec(command, (err, stream) => {
|
||||
// Use pty: true to ensure the remote process receives SIGHUP when SSH connection closes
|
||||
// This is crucial for terminating docker logs processes when the connection is closed
|
||||
client.exec(command, { pty: true }, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("Execution error:", err);
|
||||
ws.close();
|
||||
|
||||
@@ -58,7 +58,12 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
`docker exec -it -w / ${containerId} ${activeWay}`,
|
||||
{ pty: true },
|
||||
(err, stream) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.error("SSH exec error:", err);
|
||||
ws.close();
|
||||
conn.end();
|
||||
return;
|
||||
}
|
||||
|
||||
stream
|
||||
.on("close", (code: number, _signal: string) => {
|
||||
@@ -93,10 +98,20 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
|
||||
ws.on("close", () => {
|
||||
stream.end();
|
||||
// Ensure SSH connection is closed when WebSocket closes
|
||||
conn.end();
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
ws.close();
|
||||
}
|
||||
conn.end();
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
|
||||
@@ -31,8 +31,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
// Generate unique connection ID for tracking
|
||||
const connectionId = `deployment-logs-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
if (!logPath) {
|
||||
console.log("logPath no provided");
|
||||
console.log(`[${connectionId}] logPath no provided`);
|
||||
ws.close(4000, "logPath no provided");
|
||||
return;
|
||||
}
|
||||
@@ -42,40 +45,55 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
let tailProcess: ReturnType<typeof spawn> | null = null;
|
||||
let sshClient: Client | null = null;
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (!server.sshKeyId) return;
|
||||
const client = new Client();
|
||||
client
|
||||
if (!server.sshKeyId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
sshClient = new Client();
|
||||
sshClient
|
||||
.on("ready", () => {
|
||||
const command = `
|
||||
tail -n +1 -f ${logPath};
|
||||
`;
|
||||
client.exec(command, (err, stream) => {
|
||||
sshClient!.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("Execution error:", err);
|
||||
sshClient!.end();
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
sshClient!.end();
|
||||
ws.close();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
ws.send(data.toString());
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data.toString());
|
||||
}
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
ws.close(); // Cierra el WebSocket si hay un error con SSH
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
ws.close();
|
||||
}
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
@@ -85,26 +103,75 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
client.end();
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||
tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||
|
||||
tail.stdout.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
});
|
||||
const stdout = tailProcess.stdout;
|
||||
const stderr = tailProcess.stderr;
|
||||
|
||||
tail.stderr.on("data", (data) => {
|
||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||
});
|
||||
tail.on("close", () => {
|
||||
if (stdout) {
|
||||
stdout.on("data", (data) => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
stderr.on("data", (data) => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tailProcess.on("close", () => {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
tailProcess.on("error", () => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGTERM");
|
||||
// Force kill after a timeout if it doesn't terminate
|
||||
setTimeout(() => {
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGKILL");
|
||||
} else {
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Clean up resources on error
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGTERM");
|
||||
setTimeout(() => {
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGKILL");
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
}
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
ws.send(errorMessage || "An error occurred");
|
||||
ws.close();
|
||||
}
|
||||
} catch {
|
||||
// @ts-ignore
|
||||
// const errorMessage = error?.message as unknown as string;
|
||||
// ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user