Updates
This commit is contained in:
parent
f26227c7a8
commit
921375c53d
113
src/components/general/ttyd-iframe.tsx
Normal file
113
src/components/general/ttyd-iframe.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import Border from "@/twui/components/elements/Border";
|
||||||
|
import Loading from "@/twui/components/elements/Loading";
|
||||||
|
import LucideIcon from "@/twui/components/elements/lucide-icon";
|
||||||
|
import useStatus from "@/twui/components/hooks/useStatus";
|
||||||
|
import Button from "@/twui/components/layout/Button";
|
||||||
|
import Center from "@/twui/components/layout/Center";
|
||||||
|
import Row from "@/twui/components/layout/Row";
|
||||||
|
import Span from "@/twui/components/layout/Span";
|
||||||
|
import Stack from "@/twui/components/layout/Stack";
|
||||||
|
import {
|
||||||
|
ComponentProps,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
Fragment,
|
||||||
|
IframeHTMLAttributes,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type Props = Omit<
|
||||||
|
DetailedHTMLProps<
|
||||||
|
IframeHTMLAttributes<HTMLIFrameElement>,
|
||||||
|
HTMLIFrameElement
|
||||||
|
>,
|
||||||
|
"title"
|
||||||
|
> & {
|
||||||
|
url: string;
|
||||||
|
wrapperProps?: ComponentProps<typeof Border>;
|
||||||
|
title?: string | ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TtydIframe({
|
||||||
|
url,
|
||||||
|
wrapperProps,
|
||||||
|
title,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const { loading, setLoading } = useStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Border
|
||||||
|
{...wrapperProps}
|
||||||
|
className={twMerge("p-0", wrapperProps?.className)}
|
||||||
|
>
|
||||||
|
<Stack className="gap-0">
|
||||||
|
<Row className="p-4 w-full justify-between">
|
||||||
|
<Row>
|
||||||
|
{title ? (
|
||||||
|
<Fragment>
|
||||||
|
<Span size="small" variant="faded">
|
||||||
|
{title}
|
||||||
|
</Span>
|
||||||
|
<LucideIcon
|
||||||
|
name="ChevronRight"
|
||||||
|
size={15}
|
||||||
|
opacity={0.3}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Span size="small" variant="faded">
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
className="dotted-link"
|
||||||
|
>
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
</Span>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button
|
||||||
|
title="Open Full Screen"
|
||||||
|
variant="ghost"
|
||||||
|
className="p-1!"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(url, "__blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LucideIcon name="ArrowUpRight" size={20} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title="Refresh Iframe"
|
||||||
|
variant="ghost"
|
||||||
|
className="p-1!"
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => {
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
loadingProps={{ size: "smaller" }}
|
||||||
|
>
|
||||||
|
<LucideIcon name="RotateCcw" size={18} />
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
<hr />
|
||||||
|
{loading ? (
|
||||||
|
<Center className="w-full p-10 h-[400px]">
|
||||||
|
<Loading />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<iframe
|
||||||
|
{...props}
|
||||||
|
src={url}
|
||||||
|
className={twMerge("w-full h-[400px]", props.className)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Border>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,15 +1,18 @@
|
|||||||
import Stack from "@/twui/components/layout/Stack";
|
import Stack from "@/twui/components/layout/Stack";
|
||||||
import { useContext, useEffect, useRef } from "react";
|
import { RefObject, useContext, useEffect, useRef, useState } from "react";
|
||||||
import { AppContext } from "@/src/pages/_app";
|
import { AppContext } from "@/src/pages/_app";
|
||||||
import {
|
import {
|
||||||
NormalizedServerObject,
|
NormalizedServerObject,
|
||||||
ParsedDeploymentServiceConfig,
|
ParsedDeploymentServiceConfig,
|
||||||
|
TtydInfoObject,
|
||||||
WebSocketDataType,
|
WebSocketDataType,
|
||||||
} from "@/src/types";
|
} from "@/src/types";
|
||||||
import useIntersectionObserver from "@/twui/components/hooks/useIntersectionObserver";
|
|
||||||
import Center from "@/twui/components/layout/Center";
|
|
||||||
import Loading from "@/twui/components/elements/Loading";
|
|
||||||
import useWebSocketEventHandler from "@/twui/components/hooks/useWebSocketEventHandler";
|
import useWebSocketEventHandler from "@/twui/components/hooks/useWebSocketEventHandler";
|
||||||
|
import _ from "lodash";
|
||||||
|
import Loading from "@/twui/components/elements/Loading";
|
||||||
|
import Center from "@/twui/components/layout/Center";
|
||||||
|
import TtydIframe from "@/src/components/general/ttyd-iframe";
|
||||||
|
import useIntersectionObserver from "@/twui/components/hooks/useIntersectionObserver";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
service: ParsedDeploymentServiceConfig;
|
service: ParsedDeploymentServiceConfig;
|
||||||
@ -19,23 +22,97 @@ type Props = {
|
|||||||
export default function ServiceClusterServerViews({ service, server }: Props) {
|
export default function ServiceClusterServerViews({ service, server }: Props) {
|
||||||
const { pageProps, ws } = useContext(AppContext);
|
const { pageProps, ws } = useContext(AppContext);
|
||||||
|
|
||||||
|
const viewRef = useRef<HTMLDivElement>(undefined);
|
||||||
|
|
||||||
const { data } = useWebSocketEventHandler<WebSocketDataType>();
|
const { data } = useWebSocketEventHandler<WebSocketDataType>();
|
||||||
|
const { isIntersecting } = useIntersectionObserver({ elementRef: viewRef });
|
||||||
|
|
||||||
|
const [ttydLogs, setTtydLogs] = useState<TtydInfoObject>();
|
||||||
|
|
||||||
|
const WsReqSentRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ws?.socket) {
|
if (!ws?.socket || WsReqSentRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.sendData({
|
ws.sendData({
|
||||||
event: "client:ping",
|
event: "client:service-server-logs",
|
||||||
server,
|
server,
|
||||||
service,
|
service: _.omit(service, ["servers"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
WsReqSentRef.current = true;
|
||||||
}, [ws]);
|
}, [ws]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("data", data);
|
if (ttydLogs) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
data?.event == "server:service-server-logs" &&
|
||||||
|
data?.ttyd &&
|
||||||
|
data.server?.private_ip == server.private_ip
|
||||||
|
) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setTtydLogs(data.ttyd);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
return <Stack className="gap-0 w-full"></Stack>;
|
console.log("isIntersecting", isIntersecting);
|
||||||
|
console.log("ttydLogs", ttydLogs);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ttydLogs?.port) return;
|
||||||
|
|
||||||
|
if (!isIntersecting) {
|
||||||
|
ws.sendData({
|
||||||
|
event: "client:kill-port",
|
||||||
|
server,
|
||||||
|
service: _.omit(service, ["servers"]),
|
||||||
|
port: ttydLogs.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isIntersecting]);
|
||||||
|
|
||||||
|
const dev_logs_url = ttydLogs?.port
|
||||||
|
? `http://localhost:${ttydLogs.port}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const title = (
|
||||||
|
<>
|
||||||
|
{service.service_name} service <code>{server.private_ip}</code> Logs
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack className="gap-0 w-full" componentRef={viewRef as any}>
|
||||||
|
{isIntersecting && ttydLogs?.url && ttydLogs.port ? (
|
||||||
|
<Stack className="gap-0">
|
||||||
|
{/* {dev_logs_url ? (
|
||||||
|
<TtydIframe
|
||||||
|
url={dev_logs_url}
|
||||||
|
title={title}
|
||||||
|
wrapperProps={{
|
||||||
|
className: "border-none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<hr /> */}
|
||||||
|
<TtydIframe
|
||||||
|
url={ttydLogs?.url}
|
||||||
|
title={title}
|
||||||
|
wrapperProps={{
|
||||||
|
className: "border-none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Center className="p-10 h-[400px]">
|
||||||
|
<Loading />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<hr />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,9 @@ export default async function defaultAdminProps({
|
|||||||
const deployment = grabTurboCiConfig();
|
const deployment = grabTurboCiConfig();
|
||||||
const deployment_id = readFileSync(TURBOCI_DEPLOYMENT_ID_FILE, "utf-8");
|
const deployment_id = readFileSync(TURBOCI_DEPLOYMENT_ID_FILE, "utf-8");
|
||||||
|
|
||||||
|
const host = process.env.HOST || null;
|
||||||
|
const ws_url = `${host?.replace(/^http/, "ws")}/ws`;
|
||||||
|
|
||||||
const service = query.service_name
|
const service = query.service_name
|
||||||
? deployment.services.find(
|
? deployment.services.find(
|
||||||
(srv) => srv.service_name == query.service_name,
|
(srv) => srv.service_name == query.service_name,
|
||||||
@ -98,6 +101,8 @@ export default async function defaultAdminProps({
|
|||||||
deployment_id,
|
deployment_id,
|
||||||
service,
|
service,
|
||||||
children_services,
|
children_services,
|
||||||
|
ws_url,
|
||||||
|
host,
|
||||||
};
|
};
|
||||||
|
|
||||||
let finalProps = _.merge(props, propsFnProps, defaultPageProps);
|
let finalProps = _.merge(props, propsFnProps, defaultPageProps);
|
||||||
|
|||||||
72
src/functions/ttyd/grab-ttyd-service-info.ts
Normal file
72
src/functions/ttyd/grab-ttyd-service-info.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { AppData } from "@/src/data/app-data";
|
||||||
|
import {
|
||||||
|
NormalizedServerObject,
|
||||||
|
ParsedDeploymentServiceConfig,
|
||||||
|
PrivateServerTtydParadigms,
|
||||||
|
TtydInfoObject,
|
||||||
|
User,
|
||||||
|
} from "@/src/types";
|
||||||
|
import grabDirNames from "@/src/utils/grab-dir-names";
|
||||||
|
import getNextAvailablePort from "@/src/utils/grab-next-available-port";
|
||||||
|
import grabSSHPrefix from "@/src/utils/grab-ssh-prefix";
|
||||||
|
import grabTtydCmd from "@/src/utils/grab-ttyd-cmd";
|
||||||
|
import grabConnectedWebsocketUserdata from "@/src/websocket/(utils)/grab-connected-websocket-user-data";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
|
||||||
|
const { TURBOCI_SSH_KEY_FILE } = grabDirNames();
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
user: User;
|
||||||
|
service?: Omit<ParsedDeploymentServiceConfig, "servers">;
|
||||||
|
server?: NormalizedServerObject;
|
||||||
|
paradigm: (typeof PrivateServerTtydParadigms)[number]["name"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function grabTtydServerInfo({
|
||||||
|
server,
|
||||||
|
service,
|
||||||
|
user,
|
||||||
|
paradigm,
|
||||||
|
}: Params): Promise<TtydInfoObject> {
|
||||||
|
const first_log = service?.logs?.[0];
|
||||||
|
const final_first_log =
|
||||||
|
typeof first_log == "string" ? first_log : first_log?.cmd;
|
||||||
|
|
||||||
|
if (paradigm == "logs" && !final_first_log) {
|
||||||
|
throw new Error(`Service Doesn't have logs.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const available_port = getNextAvailablePort();
|
||||||
|
|
||||||
|
let url = `${process.env.HOST}/ttyd/${available_port}`;
|
||||||
|
|
||||||
|
let cmd = ``;
|
||||||
|
|
||||||
|
if (paradigm == "logs") {
|
||||||
|
cmd += ` ${grabSSHPrefix()}`;
|
||||||
|
} else {
|
||||||
|
cmd += ` ssh -i ${TURBOCI_SSH_KEY_FILE}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd += ` root@${server?.private_ip}`;
|
||||||
|
|
||||||
|
if (final_first_log) {
|
||||||
|
cmd += ` ${final_first_log}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttyd_cmd = grabTtydCmd({
|
||||||
|
cmd,
|
||||||
|
port: available_port,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ttyd_exec = exec(ttyd_cmd.cmd);
|
||||||
|
|
||||||
|
await Bun.sleep(2000);
|
||||||
|
|
||||||
|
const connected_user_data = grabConnectedWebsocketUserdata({ user });
|
||||||
|
|
||||||
|
connected_user_data.child_processes.push(ttyd_exec);
|
||||||
|
connected_user_data.ports.push(available_port);
|
||||||
|
|
||||||
|
return { port: available_port, url };
|
||||||
|
}
|
||||||
@ -2,9 +2,11 @@ import React, { useEffect } from "react";
|
|||||||
import { PagePropsType, ToastType, WebSocketDataType } from "../types";
|
import { PagePropsType, ToastType, WebSocketDataType } from "../types";
|
||||||
import useWebSocket from "@/twui/components/hooks/useWebSocket";
|
import useWebSocket from "@/twui/components/hooks/useWebSocket";
|
||||||
import useStatus from "@/twui/components/hooks/useStatus";
|
import useStatus from "@/twui/components/hooks/useStatus";
|
||||||
|
import useWebSocketEventHandler from "@/twui/components/hooks/useWebSocketEventHandler";
|
||||||
|
import { date } from "zod";
|
||||||
|
|
||||||
export default function useAppInit(pageProps: PagePropsType) {
|
export default function useAppInit(pageProps: PagePropsType) {
|
||||||
const wsURL = process.env.NEXT_PUBLIC_WEBSOCKET_URL || "";
|
const wsURL = pageProps.ws_url || "";
|
||||||
|
|
||||||
const { user } = pageProps;
|
const { user } = pageProps;
|
||||||
|
|
||||||
@ -22,6 +24,32 @@ export default function useAppInit(pageProps: PagePropsType) {
|
|||||||
|
|
||||||
const ws = { socket, sendData };
|
const ws = { socket, sendData };
|
||||||
|
|
||||||
|
const { data } = useWebSocketEventHandler<WebSocketDataType>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.event == "server:error") {
|
||||||
|
setToast({
|
||||||
|
toastOpen: true,
|
||||||
|
toastMessage: data.message,
|
||||||
|
toastStyle: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data?.event == "server:update") {
|
||||||
|
setToast({
|
||||||
|
toastOpen: true,
|
||||||
|
toastMessage: data.message,
|
||||||
|
toastStyle: "normal",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (data?.event == "server:success") {
|
||||||
|
setToast({
|
||||||
|
toastOpen: true,
|
||||||
|
toastMessage: data.message,
|
||||||
|
toastStyle: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
socket,
|
socket,
|
||||||
sendData,
|
sendData,
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import Stack from "@/twui/components/layout/Stack";
|
|||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
import { AdminAsideLinks } from "./(data)/links";
|
import { AdminAsideLinks } from "./(data)/links";
|
||||||
import Header from "./header";
|
import Header from "./header";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import Spacer from "@/twui/components/layout/Spacer";
|
||||||
|
|
||||||
type Props = PropsWithChildren & {};
|
type Props = PropsWithChildren & {};
|
||||||
|
|
||||||
@ -25,8 +27,17 @@ export default function Layout({ children }: Props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack className="grid-cell col-span-6 xl:col-span-5 gap-0">
|
<Stack
|
||||||
|
className={twMerge(
|
||||||
|
"grid-cell col-span-6 xl:col-span-5 gap-0",
|
||||||
|
"overflow-auto pb-[200px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
|
<div
|
||||||
|
className="h-[400px] w-full block"
|
||||||
|
style={{ height: "400px" }}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Main>
|
</Main>
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { AppProps } from "next/app";
|
|||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
import { PagePropsType, TurboCIAdminAppContextType } from "../types";
|
import { PagePropsType, TurboCIAdminAppContextType } from "../types";
|
||||||
import useAppInit from "../hooks/use-app-init";
|
import useAppInit from "../hooks/use-app-init";
|
||||||
|
import Toast from "@/twui/components/elements/Toast";
|
||||||
|
|
||||||
export const AppContext = createContext<TurboCIAdminAppContextType>(
|
export const AppContext = createContext<TurboCIAdminAppContextType>(
|
||||||
{} as TurboCIAdminAppContextType,
|
{} as TurboCIAdminAppContextType,
|
||||||
@ -11,9 +12,21 @@ export const AppContext = createContext<TurboCIAdminAppContextType>(
|
|||||||
export default function App({ Component, pageProps }: AppProps<PagePropsType>) {
|
export default function App({ Component, pageProps }: AppProps<PagePropsType>) {
|
||||||
const init = useAppInit(pageProps);
|
const init = useAppInit(pageProps);
|
||||||
|
|
||||||
|
const { toast, setToast } = init;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{ ...init }}>
|
<AppContext.Provider value={{ ...init }}>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
<Toast
|
||||||
|
open={toast.toastOpen}
|
||||||
|
closeDispatch={(open) => {
|
||||||
|
setToast((prev) => ({ ...prev, toastOpen: false }));
|
||||||
|
}}
|
||||||
|
color={toast.toastStyle}
|
||||||
|
closeDelay={toast.closeDelay}
|
||||||
|
>
|
||||||
|
{toast.toastMessage}
|
||||||
|
</Toast>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ app.prepare().then(() => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const full_href = `${process.env.NEXT_PUBLIC_HOST}${req.url}`;
|
const full_href = `${process.env.HOST}${req.url}`;
|
||||||
|
|
||||||
const url = new URL(full_href);
|
const url = new URL(full_href);
|
||||||
|
|
||||||
|
|||||||
@ -119,3 +119,7 @@ code {
|
|||||||
hr {
|
hr {
|
||||||
@apply border-foreground-light/10 dark:border-foreground-dark/10;
|
@apply border-foreground-light/10 dark:border-foreground-dark/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dotted-link {
|
||||||
|
@apply border-b border-dashed border-foreground-light/40 dark:border-foreground-dark/40;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { ToastStyles } from "@/twui/components/elements/Toast";
|
import { ToastStyles } from "@/twui/components/elements/Toast";
|
||||||
import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/dist/package-shared/types";
|
import { DATASQUIREL_LoggedInUser } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||||
import useAppInit from "../hooks/use-app-init";
|
import useAppInit from "../hooks/use-app-init";
|
||||||
|
import { ServerWebSocket } from "bun";
|
||||||
|
import { ChildProcess } from "child_process";
|
||||||
|
|
||||||
export type User = DATASQUIREL_LoggedInUser & {};
|
export type User = DATASQUIREL_LoggedInUser & {};
|
||||||
|
|
||||||
@ -119,6 +121,13 @@ export type TCIConfigServiceConfig = {
|
|||||||
* Commoands to Run on first run
|
* Commoands to Run on first run
|
||||||
*/
|
*/
|
||||||
init?: string[];
|
init?: string[];
|
||||||
|
logs?: TCIConfigServiceConfigLog[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCIConfigServiceConfigLog =
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
cmd: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCIConfigServiceHealthcheck = {
|
export type TCIConfigServiceHealthcheck = {
|
||||||
@ -183,6 +192,8 @@ export type PagePropsType = {
|
|||||||
deployment_id?: string | null;
|
deployment_id?: string | null;
|
||||||
service?: ParsedDeploymentServiceConfig | null;
|
service?: ParsedDeploymentServiceConfig | null;
|
||||||
children_services?: ParsedDeploymentServiceConfig[] | null;
|
children_services?: ParsedDeploymentServiceConfig[] | null;
|
||||||
|
ws_url?: string | null;
|
||||||
|
host?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type APIReqObject = {
|
export type APIReqObject = {
|
||||||
@ -219,6 +230,8 @@ export type TurboCIAdminAppContextType = ReturnType<typeof useAppInit>;
|
|||||||
|
|
||||||
export const WebSocketEvents = [
|
export const WebSocketEvents = [
|
||||||
"client:ping",
|
"client:ping",
|
||||||
|
"client:service-server-logs",
|
||||||
|
"client:kill-port",
|
||||||
|
|
||||||
"server:ping",
|
"server:ping",
|
||||||
"server:error",
|
"server:error",
|
||||||
@ -226,13 +239,23 @@ export const WebSocketEvents = [
|
|||||||
"server:ready",
|
"server:ready",
|
||||||
"server:success",
|
"server:success",
|
||||||
"server:update",
|
"server:update",
|
||||||
|
"server:service-server-logs",
|
||||||
|
"server:killed-port",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type WebSocketDataType = {
|
export type WebSocketDataType = {
|
||||||
event: (typeof WebSocketEvents)[number];
|
event: (typeof WebSocketEvents)[number];
|
||||||
message?: string;
|
message?: string;
|
||||||
service?: ParsedDeploymentServiceConfig;
|
service?: Omit<ParsedDeploymentServiceConfig, "servers">;
|
||||||
server?: NormalizedServerObject;
|
server?: NormalizedServerObject;
|
||||||
|
ttyd?: TtydInfoObject;
|
||||||
|
port?: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WebSocketMessageParam = {
|
||||||
|
ws: ServerWebSocket<WebSocketData>;
|
||||||
|
message?: string | Buffer;
|
||||||
|
data?: WebSocketDataType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebSocketType = {
|
export type WebSocketType = {
|
||||||
@ -248,3 +271,23 @@ export type ToastType = {
|
|||||||
toastOpen: boolean;
|
toastOpen: boolean;
|
||||||
closeDelay?: number;
|
closeDelay?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TtydInfoObject = {
|
||||||
|
url: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PrivateServerTtydParadigms = [
|
||||||
|
{
|
||||||
|
name: "logs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "terminal",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type WebSocketConnectedUserData = {
|
||||||
|
user: User;
|
||||||
|
child_processes: ChildProcess[];
|
||||||
|
ports: (string | number)[];
|
||||||
|
};
|
||||||
|
|||||||
@ -4,14 +4,19 @@ type Params = {
|
|||||||
cmd: string;
|
cmd: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
flags?: string[];
|
flags?: string[];
|
||||||
|
port: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function grabTtydCmd({ cmd: ttydCmd, cwd, flags }: Params) {
|
export default function grabTtydCmd({
|
||||||
const port = 8080;
|
cmd: ttydCmd,
|
||||||
|
cwd,
|
||||||
|
flags,
|
||||||
|
port,
|
||||||
|
}: Params) {
|
||||||
let cmd = ``;
|
let cmd = ``;
|
||||||
cmd += `${AppData["TerminalBinName"]}`;
|
cmd += `${AppData["TerminalBinName"]}`;
|
||||||
cmd += ` --writable --max-clients 1`;
|
cmd += ` --writable`;
|
||||||
|
// cmd += ` --max-clients 1`;
|
||||||
cmd += ` --client-option 'theme={"background":"#0c0e11"}'`;
|
cmd += ` --client-option 'theme={"background":"#0c0e11"}'`;
|
||||||
cmd += ` --client-option fontSize=14`;
|
cmd += ` --client-option fontSize=14`;
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { User } from "@/src/types";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function grabConnectedWebsocketUserdataIndex({ user }: Params) {
|
||||||
|
const user_index = global.WEBSOCKET_CONNECTED_USERS_DATA.findIndex(
|
||||||
|
(u) => u.user.id == user.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof user_index == "number" && user_index >= 0) {
|
||||||
|
return user_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
21
src/websocket/(utils)/grab-connected-websocket-user-data.ts
Normal file
21
src/websocket/(utils)/grab-connected-websocket-user-data.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { User } from "@/src/types";
|
||||||
|
import grabConnectedWebsocketUserdataIndex from "./grab-connected-websocket-user-data-index";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function grabConnectedWebsocketUserdata({ user }: Params) {
|
||||||
|
const connected_user_data_index = grabConnectedWebsocketUserdataIndex({
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof connected_user_data_index !== "number") {
|
||||||
|
throw new Error(`User Connection Data not found!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connected_user_data =
|
||||||
|
global.WEBSOCKET_CONNECTED_USERS_DATA[connected_user_data_index];
|
||||||
|
|
||||||
|
return connected_user_data;
|
||||||
|
}
|
||||||
20
src/websocket/(utils)/kill-all-ttyd-ports.ts
Normal file
20
src/websocket/(utils)/kill-all-ttyd-ports.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { AppData } from "@/src/data/app-data";
|
||||||
|
import { execSync, ExecSyncOptions } from "child_process";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
exec_options?: ExecSyncOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function killAllTtydPorts(params?: Params) {
|
||||||
|
const start_port = AppData["DynamicPortStart"];
|
||||||
|
const end_port = start_port + 100;
|
||||||
|
const kill_ports_cmd = `ss -tlnp | awk 'NR>1 {split($4, a, ":"); port=a[length(a)]; if (port >= ${start_port} && port <= ${end_port}) print $6}' | grep -oP 'pid=\\K[0-9]+' | xargs -r kill -9`;
|
||||||
|
|
||||||
|
execSync(kill_ports_cmd, {
|
||||||
|
...params?.exec_options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// sudo ss -tlnp | awk 'NR>1 {split($4, a, ":"); port=a[length(a)]; if (port >= 4700 && port <= 4800) print $6}' | grep -oP 'pid=\K[0-9]+' | xargs -r sudo kill -9
|
||||||
|
|
||||||
|
// ss -tlnp | awk 'NR>1 {split($4, a, ":"); port=a[length(a)]; if (port >= 4700 && port <= 4800) print $6}' | grep -oP 'pid=\K[0-9]+' | xargs -r sudo kill -9
|
||||||
14
src/websocket/(utils)/kill-port.ts
Normal file
14
src/websocket/(utils)/kill-port.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { execSync, ExecSyncOptions } from "child_process";
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
port: string | number;
|
||||||
|
exec_options?: ExecSyncOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function killPort({ port, exec_options }: Params) {
|
||||||
|
const kill_ports_cmd = `ss -tlnp | awk -v p=${port} 'NR>1 {split($4,a,":"); if(a[length(a)]==p) print $6}' | grep -oP 'pid=\K[0-9]+' | xargs -r kill -9`;
|
||||||
|
|
||||||
|
execSync(kill_ports_cmd, {
|
||||||
|
...exec_options,
|
||||||
|
});
|
||||||
|
}
|
||||||
26
src/websocket/events/client-kill-port.ts
Normal file
26
src/websocket/events/client-kill-port.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { WebSocketMessageParam } from "@/src/types";
|
||||||
|
import sendData from "../(utils)/send-data";
|
||||||
|
import sendError from "../(utils)/send-error";
|
||||||
|
import grabConnectedWebsocketUserdata from "../(utils)/grab-connected-websocket-user-data";
|
||||||
|
|
||||||
|
export default async function socketClientKillPort({
|
||||||
|
ws,
|
||||||
|
data,
|
||||||
|
}: WebSocketMessageParam) {
|
||||||
|
try {
|
||||||
|
const user = ws.data.user;
|
||||||
|
const service = data?.service;
|
||||||
|
const server = data?.server;
|
||||||
|
const port = data?.port;
|
||||||
|
|
||||||
|
const connected_user_data = grabConnectedWebsocketUserdata({ user });
|
||||||
|
|
||||||
|
console.log("connected_user_data", connected_user_data);
|
||||||
|
|
||||||
|
sendData(ws, {
|
||||||
|
event: "server:killed-port",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
sendError(ws, "Service Server Logs Error! " + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,18 @@
|
|||||||
|
import { WebSocketMessageParam } from "@/src/types";
|
||||||
import sendData from "../(utils)/send-data";
|
import sendData from "../(utils)/send-data";
|
||||||
import sendError from "../(utils)/send-error";
|
import sendError from "../(utils)/send-error";
|
||||||
import { WebSocketMessageParam } from "../socket-message";
|
import grabConnectedWebsocketUserdata from "../(utils)/grab-connected-websocket-user-data";
|
||||||
|
|
||||||
export default async function socketClientPing({
|
export default async function socketClientPing({
|
||||||
ws,
|
ws,
|
||||||
data,
|
data,
|
||||||
}: WebSocketMessageParam) {
|
}: WebSocketMessageParam) {
|
||||||
try {
|
try {
|
||||||
|
const user = ws.data.user;
|
||||||
|
const connected_user_data = grabConnectedWebsocketUserdata({ user });
|
||||||
|
|
||||||
|
console.log("connected_user_data", connected_user_data);
|
||||||
|
|
||||||
sendData(ws, {
|
sendData(ws, {
|
||||||
event: "server:ping",
|
event: "server:ping",
|
||||||
});
|
});
|
||||||
|
|||||||
32
src/websocket/events/client-service-server-logs.ts
Normal file
32
src/websocket/events/client-service-server-logs.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { WebSocketMessageParam } from "@/src/types";
|
||||||
|
import sendData from "../(utils)/send-data";
|
||||||
|
import sendError from "../(utils)/send-error";
|
||||||
|
import grabTtydServerInfo from "@/src/functions/ttyd/grab-ttyd-service-info";
|
||||||
|
|
||||||
|
export default async function socketClientServiceServerLogs({
|
||||||
|
ws,
|
||||||
|
data,
|
||||||
|
}: WebSocketMessageParam) {
|
||||||
|
try {
|
||||||
|
const user = ws.data.user;
|
||||||
|
const service = data?.service;
|
||||||
|
const server = data?.server;
|
||||||
|
|
||||||
|
const ttyd = await grabTtydServerInfo({
|
||||||
|
server,
|
||||||
|
service,
|
||||||
|
user,
|
||||||
|
paradigm: "logs",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("ttyd", ttyd);
|
||||||
|
|
||||||
|
sendData(ws, {
|
||||||
|
event: "server:service-server-logs",
|
||||||
|
ttyd,
|
||||||
|
server,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
sendError(ws, "Service Server Logs Error! " + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,14 @@
|
|||||||
import { WebSocketData } from "../types";
|
import { WebSocketConnectedUserData, WebSocketData } from "../types";
|
||||||
|
import socketClose from "./socket-close";
|
||||||
import socketInit from "./socket-init";
|
import socketInit from "./socket-init";
|
||||||
import socketMessage from "./socket-message";
|
import socketMessage from "./socket-message";
|
||||||
|
import socketOpen from "./socket-open";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var WEBSOCKET_CONNECTED_USERS_DATA: WebSocketConnectedUserData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
global.WEBSOCKET_CONNECTED_USERS_DATA = [];
|
||||||
|
|
||||||
const server = Bun.serve<WebSocketData>({
|
const server = Bun.serve<WebSocketData>({
|
||||||
async fetch(req, server) {
|
async fetch(req, server) {
|
||||||
@ -27,12 +35,10 @@ const server = Bun.serve<WebSocketData>({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async open(ws) {
|
async open(ws) {
|
||||||
const user = ws.data.user;
|
await socketOpen({ ws });
|
||||||
console.log(`Web Socket Opened by ${user.first_name}`);
|
|
||||||
},
|
},
|
||||||
async close(ws, code, message) {
|
async close(ws, code, message) {
|
||||||
const user = ws.data.user;
|
await socketClose({ ws });
|
||||||
console.log(`Socket Closed by ${user.first_name}`);
|
|
||||||
},
|
},
|
||||||
idleTimeout: 600,
|
idleTimeout: 600,
|
||||||
maxPayloadLength: 1024 * 1024 * 10,
|
maxPayloadLength: 1024 * 1024 * 10,
|
||||||
|
|||||||
33
src/websocket/socket-close.ts
Normal file
33
src/websocket/socket-close.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ServerWebSocket } from "bun";
|
||||||
|
import { WebSocketData } from "../types";
|
||||||
|
import killAllTtydPorts from "./(utils)/kill-all-ttyd-ports";
|
||||||
|
|
||||||
|
type Param = {
|
||||||
|
ws: ServerWebSocket<WebSocketData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function socketClose({ ws }: Param) {
|
||||||
|
const user = ws.data.user;
|
||||||
|
console.log(`Web Socket Closed by ${user.first_name}`);
|
||||||
|
|
||||||
|
const existing_connected_user_data =
|
||||||
|
global.WEBSOCKET_CONNECTED_USERS_DATA.find((u) => u.user.id == user.id);
|
||||||
|
|
||||||
|
if (existing_connected_user_data) {
|
||||||
|
for (
|
||||||
|
let i = 0;
|
||||||
|
i < existing_connected_user_data.child_processes.length;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
const child_process =
|
||||||
|
existing_connected_user_data.child_processes[i];
|
||||||
|
child_process.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
existing_connected_user_data.child_processes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
killAllTtydPorts({
|
||||||
|
exec_options: { stdio: "inherit" },
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,20 +1,20 @@
|
|||||||
import { ServerWebSocket } from "bun";
|
import { ServerWebSocket } from "bun";
|
||||||
import { WebSocketData, WebSocketDataType } from "../types";
|
import {
|
||||||
|
WebSocketData,
|
||||||
|
WebSocketDataType,
|
||||||
|
WebSocketMessageParam,
|
||||||
|
} from "../types";
|
||||||
import { EJSON } from "../exports/client-exports";
|
import { EJSON } from "../exports/client-exports";
|
||||||
import socketClientPing from "./events/client-ping";
|
import socketClientPing from "./events/client-ping";
|
||||||
import debugLog from "@moduletrace/datasquirel/dist/package-shared/utils/logging/debug-log";
|
import debugLog from "@moduletrace/datasquirel/dist/package-shared/utils/logging/debug-log";
|
||||||
|
import socketClientServiceServerLogs from "./events/client-service-server-logs";
|
||||||
|
import socketClientKillPort from "./events/client-kill-port";
|
||||||
|
|
||||||
type Param = {
|
type Param = {
|
||||||
ws: ServerWebSocket<WebSocketData>;
|
ws: ServerWebSocket<WebSocketData>;
|
||||||
message: string | Buffer;
|
message: string | Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebSocketMessageParam = {
|
|
||||||
ws: ServerWebSocket<WebSocketData>;
|
|
||||||
message?: string | Buffer;
|
|
||||||
data?: WebSocketDataType;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function socketMessage({ ws, message }: Param) {
|
export default async function socketMessage({ ws, message }: Param) {
|
||||||
const user = ws.data.user;
|
const user = ws.data.user;
|
||||||
const data = EJSON.parse(message.toString()) as
|
const data = EJSON.parse(message.toString()) as
|
||||||
@ -39,6 +39,22 @@ export default async function socketMessage({ ws, message }: Param) {
|
|||||||
});
|
});
|
||||||
await socketClientPing(websocketMessageParams);
|
await socketClientPing(websocketMessageParams);
|
||||||
break;
|
break;
|
||||||
|
case "client:service-server-logs":
|
||||||
|
debugLog({
|
||||||
|
log: `${userRef} Getting Service Server Logs ...`,
|
||||||
|
addTime: true,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
await socketClientServiceServerLogs(websocketMessageParams);
|
||||||
|
break;
|
||||||
|
case "client:kill-port":
|
||||||
|
debugLog({
|
||||||
|
log: `${userRef} Killing Port ${data.port} ...`,
|
||||||
|
addTime: true,
|
||||||
|
label,
|
||||||
|
});
|
||||||
|
await socketClientKillPort(websocketMessageParams);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
22
src/websocket/socket-open.ts
Normal file
22
src/websocket/socket-open.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ServerWebSocket } from "bun";
|
||||||
|
import { WebSocketData } from "../types";
|
||||||
|
|
||||||
|
type Param = {
|
||||||
|
ws: ServerWebSocket<WebSocketData>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function socketOpen({ ws }: Param) {
|
||||||
|
const user = ws.data.user;
|
||||||
|
console.log(`Web Socket Opened by ${user.first_name}`);
|
||||||
|
|
||||||
|
const existing_connected_user_data =
|
||||||
|
global.WEBSOCKET_CONNECTED_USERS_DATA.find((u) => u.user.id == user.id);
|
||||||
|
|
||||||
|
if (!existing_connected_user_data) {
|
||||||
|
global.WEBSOCKET_CONNECTED_USERS_DATA.push({
|
||||||
|
user,
|
||||||
|
child_processes: [],
|
||||||
|
ports: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user