remove the annoying HOST_PROJECT_DIR and DOCKER env variables

This commit is contained in:
fccview
2025-11-05 19:52:20 +00:00
parent ce379a8cc9
commit 2ba9cdc622
21 changed files with 107 additions and 88 deletions

View File

@@ -22,7 +22,8 @@ import { CloneTaskModal } from "@/app/_components/FeatureComponents/Modals/Clone
import { UserFilter } from "@/app/_components/FeatureComponents/User/UserFilter";
import { ErrorBadge } from "@/app/_components/GlobalComponents/Badges/ErrorBadge";
import { ErrorDetailsModal } from "@/app/_components/FeatureComponents/Modals/ErrorDetailsModal";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
import {
getJobErrorsByJobId,
JobError,

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { CronJobList } from "@/app/_components/FeatureComponents/Cronjobs/CronJobList";
import { ScriptsManager } from "@/app/_components/FeatureComponents/Scripts/ScriptsManager";
import { CronJob } from "@/app/_utils/system";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
import { Clock, FileText } from "lucide-react";
interface TabbedInterfaceProps {

View File

@@ -5,7 +5,7 @@ import { Copy } from "lucide-react";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal";
import { Input } from "@/app/_components/GlobalComponents/FormElements/Input";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
interface CloneScriptModalProps {
script: Script | null;

View File

@@ -3,7 +3,7 @@
import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { FileText, AlertCircle, Trash2 } from "lucide-react";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
interface DeleteScriptModalProps {
script: Script | null;

View File

@@ -1,7 +1,7 @@
"use client";
import { Edit } from "lucide-react";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
import { ScriptModal } from "@/app/_components/FeatureComponents/Modals/ScriptModal";
interface EditScriptModalProps {

View File

@@ -5,7 +5,7 @@ import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal";
import { Button } from "@/app/_components/GlobalComponents/UIElements/Button";
import { Input } from "@/app/_components/GlobalComponents/FormElements/Input";
import { FileText, Search, Check, Terminal } from "lucide-react";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
import { getScriptContent } from "@/app/_server/actions/scripts";
import { getHostScriptPath } from "@/app/_utils/scripts";

View File

@@ -13,7 +13,7 @@ import {
CheckCircle,
Files,
} from "lucide-react";
import { type Script } from "@/app/_server/actions/scripts";
import { Script } from "@/app/_utils/scriptScanner";
import {
createScript,
updateScript,

5
app/_consts/file.ts Normal file
View File

@@ -0,0 +1,5 @@
import path from "path";
export const SCRIPTS_DIR = path.join("scripts");
export const SNIPPETS_DIR = path.join("snippets");
export const DATA_DIR = path.join("data");

3
app/_consts/nsenter.ts Normal file
View File

@@ -0,0 +1,3 @@
export const NSENTER_RUN_JOB = (executionUser: string, escapedCommand: string) => `nsenter -t 1 -m -u -i -n -p su - ${executionUser} -c '${escapedCommand}'`;
export const NSENTER_HOST_CRONTAB = (command: string) => `nsenter -t 1 -m -u -i -n -p sh -c "${command}"`;

View File

@@ -18,6 +18,8 @@ import { revalidatePath } from "next/cache";
import { getScriptPath } from "@/app/_utils/scripts";
import { exec } from "child_process";
import { promisify } from "util";
import { isDocker } from "@/app/_server/actions/global";
import { NSENTER_RUN_JOB } from "@/app/_consts/nsenter";
const execAsync = promisify(exec);
@@ -260,11 +262,25 @@ export const runCronJob = async (
return { success: false, message: "Cannot run paused cron job" };
}
const userInfo = await getUserInfo(job.user);
const executionUser = userInfo ? userInfo.username : "root";
const escapedCommand = job.command.replace(/'/g, "'\\''");
let command: string;
const docker = await isDocker();
const command = `nsenter -t 1 -m -u -i -n -p su - ${executionUser} -c '${escapedCommand}'`;
if (docker) {
const userInfo = await getUserInfo(job.user);
const executionUser = userInfo ? userInfo.username : "root";
const escapedCommand = job.command.replace(/'/g, "'\\''");
command = NSENTER_RUN_JOB(executionUser, escapedCommand);
} else {
command = job.command;
const appUser = process.env.USER || "unknown";
if (job.user !== appUser) {
console.warn(
`[Native Mode] Running job ${job.id} as current user (${appUser}) instead of target user (${job.user}).`
);
}
}
const { stdout, stderr } = await execAsync(command, {
timeout: 30000,
@@ -285,7 +301,7 @@ export const runCronJob = async (
return {
success: false,
message: "Failed to execute cron job",
output: errorMessage,
output: errorMessage.trim(),
details: error.stack,
};
}

View File

@@ -0,0 +1,20 @@
"use server";
import { existsSync, readFileSync } from "fs";
export const isDocker = async (): Promise<boolean> => {
try {
if (existsSync("/.dockerenv")) {
return true;
}
if (existsSync("/proc/1/cgroup")) {
const cgroupContent = readFileSync("/proc/1/cgroup", "utf8");
return cgroupContent.includes("/docker/");
}
return false;
} catch (error) {
return false;
}
};

View File

@@ -6,13 +6,13 @@ import { join } from "path";
import { existsSync } from "fs";
import { exec } from "child_process";
import { promisify } from "util";
import { SCRIPTS_DIR, normalizeLineEndings } from "@/app/_utils/scripts";
import { loadAllScripts, type Script } from "@/app/_utils/scriptScanner";
import { normalizeLineEndings } from "@/app/_utils/scripts";
import { SCRIPTS_DIR } from "@/app/_consts/file";
import { loadAllScripts, Script } from "@/app/_utils/scriptScanner";
import { isDocker } from "@/app/_server/actions/global";
const execAsync = promisify(exec);
export type { Script } from "@/app/_utils/scriptScanner";
const sanitizeScriptName = (name: string): string => {
return name
.toLowerCase()
@@ -37,24 +37,22 @@ const generateUniqueFilename = async (baseName: string): Promise<string> => {
};
const ensureScriptsDirectory = async () => {
const scriptsDir = await SCRIPTS_DIR();
const scriptsDir = join(process.cwd(), SCRIPTS_DIR);
if (!existsSync(scriptsDir)) {
await mkdir(scriptsDir, { recursive: true });
}
};
const ensureHostScriptsDirectory = async () => {
const hostProjectDir = process.env.HOST_PROJECT_DIR || process.cwd();
const hostScriptsDir = join(hostProjectDir, "scripts");
const hostScriptsDir = join(process.cwd(), SCRIPTS_DIR);
if (!existsSync(hostScriptsDir)) {
await mkdir(hostScriptsDir, { recursive: true });
}
};
const saveScriptFile = async (filename: string, content: string) => {
const isDocker = process.env.DOCKER === "true";
const scriptsDir = isDocker ? "/app/scripts" : await SCRIPTS_DIR();
const docker = await isDocker();
const scriptsDir = docker ? "/app/scripts" : join(process.cwd(), SCRIPTS_DIR);
await ensureScriptsDirectory();
const scriptPath = join(scriptsDir, filename);
@@ -68,8 +66,8 @@ const saveScriptFile = async (filename: string, content: string) => {
};
const deleteScriptFile = async (filename: string) => {
const isDocker = process.env.DOCKER === "true";
const scriptsDir = isDocker ? "/app/scripts" : await SCRIPTS_DIR();
const docker = await isDocker();
const scriptsDir = docker ? "/app/scripts" : join(process.cwd(), SCRIPTS_DIR);
const scriptPath = join(scriptsDir, filename);
if (existsSync(scriptPath)) {
await unlink(scriptPath);
@@ -240,8 +238,8 @@ export const cloneScript = async (
export const getScriptContent = async (filename: string): Promise<string> => {
try {
const isDocker = process.env.DOCKER === "true";
const scriptPath = isDocker
const docker = await isDocker();
const scriptPath = docker
? join("/app/scripts", filename)
: join(process.cwd(), "scripts", filename);
@@ -280,8 +278,8 @@ export const executeScript = async (
}> => {
try {
await ensureHostScriptsDirectory();
const isDocker = process.env.DOCKER === "true";
const hostScriptPath = isDocker
const docker = await isDocker();
const hostScriptPath = docker
? join("/app/scripts", filename)
: join(process.cwd(), "scripts", filename);

View File

@@ -3,6 +3,7 @@
import { exec } from "child_process";
import { promisify } from "util";
import { readHostCrontab, writeHostCrontab } from "@/app/_utils/system/hostCrontab";
import { isDocker } from "@/app/_server/actions/global";
const execAsync = promisify(exec);
@@ -27,9 +28,9 @@ export const cleanCrontabContent = async (content: string): Promise<string> => {
}
export const readCronFiles = async (): Promise<string> => {
const isDocker = process.env.DOCKER === "true";
const docker = await isDocker();
if (!isDocker) {
if (!docker) {
try {
const { stdout } = await execAsync('crontab -l 2>/dev/null || echo ""');
return stdout;
@@ -43,9 +44,9 @@ export const readCronFiles = async (): Promise<string> => {
}
export const writeCronFiles = async (content: string): Promise<boolean> => {
const isDocker = process.env.DOCKER === "true";
const docker = await isDocker();
if (!isDocker) {
if (!docker) {
try {
await execAsync('echo "' + content + '" | crontab -');
return true;

View File

@@ -1,5 +1,6 @@
import { promises as fs } from "fs";
import path from "path";
import { isDocker } from "../_server/actions/global";
export interface Script {
id: string;
@@ -67,8 +68,8 @@ const scanScriptsDirectory = async (dirPath: string): Promise<Script[]> => {
}
export const loadAllScripts = async (): Promise<Script[]> => {
const isDocker = process.env.DOCKER === "true";
const scriptsDir = isDocker
const docker = await isDocker();
const scriptsDir = docker
? "/app/scripts"
: path.join(process.cwd(), "scripts");
return await scanScriptsDirectory(scriptsDir);

View File

@@ -1,28 +1,16 @@
"use server";
import { join } from "path";
import { SCRIPTS_DIR } from "@/app/_consts/file";
const isDocker = process.env.DOCKER === "true";
const SCRIPTS_DIR = async () => {
if (isDocker && process.env.HOST_PROJECT_DIR) {
return `${process.env.HOST_PROJECT_DIR}/scripts`;
}
return join(process.cwd(), "scripts");
export const getScriptPath = (filename: string): string => {
return join(process.cwd(), SCRIPTS_DIR, filename);
};
export const getScriptPath = async (filename: string): Promise<string> => {
return join(await SCRIPTS_DIR(), filename);
}
export const getHostScriptPath = async (filename: string): Promise<string> => {
const hostProjectDir = process.env.HOST_PROJECT_DIR || process.cwd();
const hostScriptsDir = join(hostProjectDir, "scripts");
return `bash ${join(hostScriptsDir, filename)}`;
}
export const getHostScriptPath = (filename: string): string => {
return `bash ${join(process.cwd(), SCRIPTS_DIR, filename)}`;
};
export const normalizeLineEndings = (content: string): string => {
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
};
export { SCRIPTS_DIR };

View File

@@ -1,5 +1,6 @@
import { promises as fs } from "fs";
import path from "path";
import { isDocker } from "../_server/actions/global";
export interface BashSnippet {
id: string;
@@ -118,12 +119,12 @@ const scanSnippetDirectory = async (
}
export const loadAllSnippets = async (): Promise<BashSnippet[]> => {
const isDocker = process.env.DOCKER === "true";
const docker = await isDocker();
let builtinSnippetsPath: string;
let userSnippetsPath: string;
if (isDocker) {
if (docker) {
builtinSnippetsPath = "/app/app/_utils/snippets";
userSnippetsPath = "/app/snippets";
} else {

View File

@@ -6,6 +6,7 @@ import {
} from "@/app/_utils/system/hostCrontab";
import { parseJobsFromLines, deleteJobInLines, updateJobInLines, pauseJobInLines, resumeJobInLines } from "@/app/_utils/cron/line-manipulation";
import { cleanCrontabContent, readCronFiles, writeCronFiles } from "@/app/_utils/cron/files-manipulation";
import { isDocker } from "@/app/_server/actions/global";
const execAsync = promisify(exec);
@@ -18,10 +19,8 @@ export interface CronJob {
paused?: boolean;
}
const isDocker = (): boolean => process.env.DOCKER === "true";
const readUserCrontab = async (user: string): Promise<string> => {
if (isDocker()) {
if (await isDocker()) {
const userCrontabs = await readAllHostCrontabs();
const targetUserCrontab = userCrontabs.find((uc) => uc.user === user);
return targetUserCrontab?.content || "";
@@ -34,7 +33,7 @@ const readUserCrontab = async (user: string): Promise<string> => {
};
const writeUserCrontab = async (user: string, content: string): Promise<boolean> => {
if (isDocker()) {
if (await isDocker()) {
return await writeHostCrontabForUser(user, content);
} else {
try {
@@ -48,7 +47,7 @@ const writeUserCrontab = async (user: string, content: string): Promise<boolean>
};
const getAllUsers = async (): Promise<{ user: string; content: string }[]> => {
if (isDocker()) {
if (await isDocker()) {
return await readAllHostCrontabs();
} else {
const { getAllTargetUsers } = await import("@/app/_utils/system/hostCrontab");

View File

@@ -1,3 +1,4 @@
import { NSENTER_HOST_CRONTAB } from "@/app/_consts/nsenter";
import { exec } from "child_process";
import { promisify } from "util";
@@ -11,15 +12,13 @@ export interface UserInfo {
const execHostCrontab = async (command: string): Promise<string> => {
try {
const { stdout } = await execAsync(
`nsenter -t 1 -m -u -i -n -p sh -c "${command}"`
);
const { stdout } = await execAsync(NSENTER_HOST_CRONTAB(command?.trim()));
return stdout;
} catch (error: any) {
console.error("Error executing host crontab command:", error);
throw error;
}
}
};
const getTargetUser = async (): Promise<string> => {
try {
@@ -31,19 +30,6 @@ const getTargetUser = async (): Promise<string> => {
const dockerSocketOwner = stdout.trim();
if (dockerSocketOwner === "root") {
try {
const projectDir = process.env.HOST_PROJECT_DIR;
if (projectDir) {
const dirOwner = await execHostCrontab(
`stat -c "%U" "${projectDir}"`
);
return dirOwner.trim();
}
} catch (error) {
console.warn("Could not detect user from project directory:", error);
}
try {
const users = await execHostCrontab(
'getent passwd | grep ":/home/" | head -1 | cut -d: -f1'
@@ -64,7 +50,7 @@ const getTargetUser = async (): Promise<string> => {
console.error("Error detecting target user:", error);
return "root";
}
}
};
export const getAllTargetUsers = async (): Promise<string[]> => {
try {
@@ -91,7 +77,7 @@ export const getAllTargetUsers = async (): Promise<string[]> => {
console.error("Error getting all target users:", error);
return ["root"];
}
}
};
export const readHostCrontab = async (): Promise<string> => {
try {
@@ -103,7 +89,7 @@ export const readHostCrontab = async (): Promise<string> => {
console.error("Error reading host crontab:", error);
return "";
}
}
};
export const readAllHostCrontabs = async (): Promise<
{ user: string; content: string }[]
@@ -129,7 +115,7 @@ export const readAllHostCrontabs = async (): Promise<
console.error("Error reading all host crontabs:", error);
return [];
}
}
};
export const writeHostCrontab = async (content: string): Promise<boolean> => {
try {
@@ -148,7 +134,7 @@ export const writeHostCrontab = async (content: string): Promise<boolean> => {
console.error("Error writing host crontab:", error);
return false;
}
}
};
export const writeHostCrontabForUser = async (
user: string,
@@ -169,7 +155,7 @@ export const writeHostCrontabForUser = async (
console.error(`Error writing host crontab for user ${user}:`, error);
return false;
}
}
};
export async function getUserInfo(username: string): Promise<UserInfo | null> {
try {

View File

@@ -37,7 +37,7 @@
"react": "^18",
"react-dom": "^18",
"react-syntax-highlighter": "^15.6.1",
"systeminformation": "^5.27.8",
"systeminformation": "^5.27.11",
"tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.0",
"typescript": "^5"

View File

@@ -1,5 +1,5 @@
# @id: demo-script
# @title: Hi, this is a demo script
# @title: Hi, this is a demo scripttttt
# @description: This script logs a "hello world" to teach you how scripts work.
#!/bin/bash

View File

@@ -4708,10 +4708,10 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
systeminformation@^5.27.8:
version "5.27.8"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.27.8.tgz#f13d180104a0df2e7222c5d4aa85aea147428de5"
integrity sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==
systeminformation@^5.27.11:
version "5.27.11"
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.27.11.tgz#286c8eca3947cfde0da684221ee9afb2ae313441"
integrity sha512-K3Lto/2m3K2twmKHdgx5B+0in9qhXK4YnoT9rIlgwN/4v7OV5c8IjbeAUkuky/6VzCQC7iKCAqi8rZathCdjHg==
tailwind-merge@^2.0.0:
version "2.6.0"