diff --git a/client/src/pages/servapps/containers/terminal.jsx b/client/src/pages/servapps/containers/terminal.jsx index 63bfb78..a583e0f 100644 --- a/client/src/pages/servapps/containers/terminal.jsx +++ b/client/src/pages/servapps/containers/terminal.jsx @@ -6,9 +6,9 @@ import { Alert, Input, Stack, useMediaQuery, useTheme } from '@mui/material'; import { ApiOutlined, SendOutlined } from '@ant-design/icons'; import ResponsiveButton from '../../../components/responseiveButton'; -import { Terminal, ITerminalOptions, ITerminalAddon } from 'xterm' -import { FitAddon } from 'xterm-addon-fit'; -import 'xterm/css/xterm.css' +import { Terminal } from '@xterm/xterm' +import '@xterm/xterm/css/xterm.css' +import { FitAddon } from '@xterm/addon-fit'; const DockerTerminal = ({containerInfo, refresh}) => { const { Name, Config, NetworkSettings, State } = containerInfo; @@ -76,14 +76,14 @@ const DockerTerminal = ({containerInfo, refresh}) => { setIsConnected(false); let terminalBoldRed = '\x1b[1;31m'; let terminalReset = '\x1b[0m'; - terminal.write(terminalBoldRed + 'Disconnected from ' + (newProc ? 'bash' : 'main process TTY') + '\r\n' + terminalReset); + terminal.write(terminalBoldRed + 'Disconnected from ' + (newProc ? 'shell' : 'main process TTY') + '\r\n' + terminalReset); }; ws.current.onopen = () => { setIsConnected(true); let terminalBoldGreen = '\x1b[1;32m'; let terminalReset = '\x1b[0m'; - terminal.write(terminalBoldGreen + 'Connected to ' + (newProc ? 'bash' : 'main process TTY') + '\r\n' + terminalReset); + terminal.write(terminalBoldGreen + 'Connected to ' + (newProc ? 'shell' : 'main process TTY') + '\r\n' + terminalReset); // focus terminal terminal.focus(); }; @@ -97,44 +97,20 @@ const DockerTerminal = ({containerInfo, refresh}) => { const [SelectedText, setSelectedText] = useState(''); useEffect(() => { - xtermRef.current.innerHTML = ''; + // xtermRef.current.innerHTML = ''; terminal.open(xtermRef.current); // const fitAddon = new FitAddon(); // terminal.loadAddon(fitAddon); // fitAddon.fit(); - if (navigator.clipboard) { - navigator.permissions.query({ name: "clipboard-read" }) - navigator.permissions.query({ name: "clipboard-write" }) - } else { - console.error('Clipboard API not available'); - return; - } + // const onFocus = () => { + // terminal.focus(); + // } + // xtermRef.current.removeEventListener('touchstart', onFocus); - // remove all listener from xtermRef - const contextMenuListener = async (e) => { - e.preventDefault(); - console.log('context menu', SelectedText) - if(SelectedText && SelectedText.length > 0 && SelectedText != "") { - await navigator.clipboard.writeText(SelectedText); - } else { - const toWrite = await navigator.clipboard.readText(); - ws.current.send(toWrite); - } - return false; - } - - const onFocus = () => { - terminal.focus(); - } - - xtermRef.current.removeEventListener('contextmenu', contextMenuListener); - xtermRef.current.removeEventListener('touchstart', onFocus); - - xtermRef.current.addEventListener('contextmenu', contextMenuListener); - xtermRef.current.addEventListener('touchstart', onFocus); + // xtermRef.current.addEventListener('touchstart', onFocus); terminal.onSelectionChange((e) => { let sel = terminal.getSelection(); @@ -144,6 +120,12 @@ const DockerTerminal = ({containerInfo, refresh}) => { } }); + terminal.onData((data) => { + if (data.startsWith("\x1b[200~") && data.endsWith("\x1b[201~")) { + ws.current.send(data); + } + }); + terminal.attachCustomKeyEventHandler((e) => { const codes = { 'Enter': '\r', @@ -193,13 +175,16 @@ const DockerTerminal = ({containerInfo, refresh}) => { } return true; - }); - - + }); }, []); return ( -
+
{(!isInteractive) && ( This container is not interactive. @@ -207,25 +192,25 @@ const DockerTerminal = ({containerInfo, refresh}) => { )} - -
+

- - +
{ isConnected ? ( @@ -248,9 +233,6 @@ const DockerTerminal = ({containerInfo, refresh}) => { } - - -
); }; diff --git a/client/src/pages/storage/disks.jsx b/client/src/pages/storage/disks.jsx index dd2e4b4..faf0fdb 100644 --- a/client/src/pages/storage/disks.jsx +++ b/client/src/pages/storage/disks.jsx @@ -3,7 +3,7 @@ import { useEffect, useState } from "react"; import * as API from "../../api"; import PrettyTableView from "../../components/tableView/prettyTableView"; import { DeleteButton } from "../../components/delete"; -import { CloudOutlined, CompassOutlined, DesktopOutlined, ExpandOutlined, LaptopOutlined, MenuFoldOutlined, MenuOutlined, MinusCircleFilled, MobileOutlined, NodeCollapseOutlined, PlusCircleFilled, PlusCircleOutlined, SettingFilled, TabletOutlined, WarningFilled, WarningOutlined } from "@ant-design/icons"; +import { CloudOutlined, CompassOutlined, DesktopOutlined, ExpandOutlined, LaptopOutlined, MenuFoldOutlined, MenuOutlined, MinusCircleFilled, MobileOutlined, NodeCollapseOutlined, PlusCircleFilled, PlusCircleOutlined, ReloadOutlined, SettingFilled, TabletOutlined, WarningFilled, WarningOutlined } from "@ant-design/icons"; import { Alert, Button, CircularProgress, InputLabel, LinearProgress, ListItemIcon, ListItemText, MenuItem, Stack, Tooltip } from "@mui/material"; import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts"; import MainCard from "../../components/MainCard"; @@ -21,10 +21,11 @@ import lockIcon from '../../assets/images/icons/lock.svg'; import raidIcon from '../../assets/images/icons/database.svg'; import { simplifyNumber } from "../dashboard/components/utils"; import LogsInModal from "../../components/logsInModal"; -import MountDialog from "./mountDialog"; +import MountDiskDialog from "./mountDiskDialog"; import PasswordModal from "../../components/passwordModal"; import FormatModal from "./FormatModal"; import MenuButton from "../../components/MenuButton"; +import ResponsiveButton from "../../components/responseiveButton"; const diskStyle = { width: "100%", @@ -173,14 +174,14 @@ const Disk = ({disk, refresh}) => { {(disk.type == "disk" || disk.type == "part") ? : ""} - {disk.mountpoint ? : ""} + {disk.mountpoint ? : ""} {( (disk.type == "part" || (disk.type == "disk" && (!disk.children || !disk.children.length))) && disk.fstype && disk.fstype !== "swap" && !disk.mountpoint - ) ? : ""} + ) ? : ""} @@ -218,7 +219,9 @@ export const StorageDisks = () => { {containerized && You are running Cosmos inside a Docker container. As such, it will only have limited access to your disks and their informations.}
- + } onClick={() => { + refresh(); + }}>Refresh
{ +const MergerDialogInternal = ({ refresh, open, setOpen, data }) => { const formik = useFormik({ initialValues: { - path: '/mnt/storage', - permanent: false, + path: data ? data.path : '/mnt/storage', + permanent: data ? data.permanent : false, branches: [], - chown: '1000:1000', - opts: '', + chown: data ? data.chown : '1000:1000', + opts: data ? data.opts.join(',') : '', }, validateOnChange: false, validationSchema: yup.object({ @@ -128,12 +130,14 @@ const MergerDialog = ({ refresh }) => { return <> {open && } - + >Create Merge } -export default MergerDialog; \ No newline at end of file +export default MergerDialog; +export { MergerDialogInternal }; \ No newline at end of file diff --git a/client/src/pages/storage/merges.jsx b/client/src/pages/storage/merges.jsx index 16f488f..c556e56 100644 --- a/client/src/pages/storage/merges.jsx +++ b/client/src/pages/storage/merges.jsx @@ -3,8 +3,8 @@ import { useEffect, useState } from "react"; import * as API from "../../api"; import PrettyTableView from "../../components/tableView/prettyTableView"; import { DeleteButton } from "../../components/delete"; -import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons"; -import { Alert, Button, CircularProgress, InputLabel, Stack } from "@mui/material"; +import { CloudOutlined, CloudServerOutlined, CompassOutlined, DeleteOutlined, DesktopOutlined, EditOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, ReloadOutlined, TabletOutlined } from "@ant-design/icons"; +import { Alert, Button, CircularProgress, InputLabel, ListItemIcon, ListItemText, MenuItem, Stack } from "@mui/material"; import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts"; import MainCard from "../../components/MainCard"; import { Formik } from "formik"; @@ -13,19 +13,25 @@ import ApiModal from "../../components/apiModal"; import ConfirmModal from "../../components/confirmModal"; import { isDomain } from "../../utils/indexs"; import UploadButtons from "../../components/fileUpload"; -import MergerDialog from "./mergerDialog"; +import MergerDialog, { MergerDialogInternal } from "./mergerDialog"; +import ResponsiveButton from "../../components/responseiveButton"; +import MenuButton from "../../components/MenuButton"; export const StorageMerges = () => { const [isAdmin, setIsAdmin] = useState(false); const [config, setConfig] = useState(null); const [mounts, setMounts] = useState([]); + const [mergeDialog, setMergeDialog] = useState(null); + const [loading, setLoading] = useState(false); const refresh = async () => { + setLoading(true); let mountsData = await API.storage.mounts.list(); let configAsync = await API.config.get(); setConfig(configAsync.data); setIsAdmin(configAsync.isAdmin); setMounts(mountsData.data); + setLoading(false); }; useEffect(() => { @@ -34,15 +40,18 @@ export const StorageMerges = () => { return <> {(config) ? <> - + {mergeDialog && } +
- -
-
- mount.type === 'fuse.mergerfs')} getKey={(r) => `${r.device} - ${refresh.path}`} + buttons={[ + , + } onClick={() => { + refresh(); + }}>Refresh + ]} columns={[ { title: 'Device', @@ -58,7 +67,20 @@ export const StorageMerges = () => { }, { title: 'Options', + screenMin: 'md', field: (r) => JSON.stringify(r.opts), + + }, + { + title: '', + clickable:true, + field: (r) => <> + } onClick={() => { + API.storage.mounts.unmount({ mountPoint: r.path, permanent: true }).then(() => { + refresh(); + }); + }}>Delete + }, ]} /> diff --git a/client/src/pages/storage/mountDialog.jsx b/client/src/pages/storage/mountDialog.jsx index 0c6dafa..d9f37bb 100644 --- a/client/src/pages/storage/mountDialog.jsx +++ b/client/src/pages/storage/mountDialog.jsx @@ -7,25 +7,37 @@ import * as yup from "yup"; import * as API from '../../api'; -const MountDialogInternal = ({disk, unmount, refresh, open, setOpen }) => { +const MountDialogInternal = ({ unmount, refresh, open, setOpen, data }) => { const formik = useFormik({ initialValues: { - path: '/mnt/' + disk.name.replace('/dev/', ''), + device: data ? data.device : '', + path: data ? data.path : '', permanent: false, chown: '1000:1000', }, validationSchema: yup.object({ // should start with /mnt/ or /var/mnt + device: yup.string().required('Required'), path: yup.string().required('Required').matches(/^\/(mnt|var\/mnt)\/.{1,}$/, 'Path should start with /mnt/ or /var/mnt'), }), - onSubmit: (values, { setErrors, setStatus, setSubmitting }) => { + onSubmit: async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); + if(data && !unmount) { + try { + await API.storage.mounts.unmount({ + mountPoint: data.path, + permanent: true, + }); + } catch (err) { + console.error(err); + } + } return (unmount ? API.storage.mounts.unmount({ - mountPoint: disk.mountpoint, + mountPoint: values.path, chown: values.chown, permanent: values.permanent, }) : API.storage.mounts.mount({ - path: disk.name, + path: values.device, mountPoint: values.path, chown: values.chown, permanent: values.permanent, @@ -52,11 +64,21 @@ const MountDialogInternal = ({disk, unmount, refresh, open, setOpen }) => {
- You are about to {unmount ? 'unmount' : 'mount'} the disk {disk.name}{disk.mountpoint && (<> mounted at {disk.mountpoint})}. This will make the content {unmount ? 'unavailable' : 'available'} to be viewed in the file explorer. + You are about to {unmount ? 'unmount' : 'mount'} a folder {data && data.mountpoint && (<> mounted at {data && data.mountpoint})}. This will make the content {unmount ? 'unavailable' : 'available'} to be viewed in the file explorer. Permanent {unmount ? 'unmount' : 'mount'} will persist after reboot.
{unmount ? '' : <> + { } -export default MountDialog; \ No newline at end of file +export default MountDialog; +export { MountDialogInternal }; diff --git a/client/src/pages/storage/mountDiskDialog.jsx b/client/src/pages/storage/mountDiskDialog.jsx new file mode 100644 index 0000000..6e13dc7 --- /dev/null +++ b/client/src/pages/storage/mountDiskDialog.jsx @@ -0,0 +1,123 @@ +import { LoadingButton } from "@mui/lab"; +import { Alert, Grid, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormHelperText, Stack, TextField } from "@mui/material"; +import React, {useState} from "react"; +import { CosmosCheckbox } from "../config/users/formShortcuts"; +import { Formik, FormikProvider, useFormik } from "formik"; +import * as yup from "yup"; + +import * as API from '../../api'; + +const MountDiskDialogInternal = ({disk, unmount, refresh, open, setOpen }) => { + const formik = useFormik({ + initialValues: { + path: '/mnt/' + disk.name.replace('/dev/', ''), + permanent: false, + chown: '1000:1000', + }, + validationSchema: yup.object({ + // should start with /mnt/ or /var/mnt + path: yup.string().required('Required').matches(/^\/(mnt|var\/mnt)\/.{1,}$/, 'Path should start with /mnt/ or /var/mnt'), + }), + onSubmit: (values, { setErrors, setStatus, setSubmitting }) => { + setSubmitting(true); + return (unmount ? API.storage.mounts.unmount({ + mountPoint: disk.mountpoint, + chown: values.chown, + permanent: values.permanent, + }) : API.storage.mounts.mount({ + path: disk.name, + mountPoint: values.path, + chown: values.chown, + permanent: values.permanent, + })).then((res) => { + setStatus({ success: true }); + setSubmitting(false); + setOpen(false); + refresh && refresh(); + }).catch((err) => { + setStatus({ success: false }); + setErrors({ submit: err.message }); + setSubmitting(false); + }); + }, + }); + + return <> + setOpen(false)}> + +
+ {unmount ? 'Unmount' : 'Mount'} Disk + + + +
+ + You are about to {unmount ? 'unmount' : 'mount'} the disk {disk.name}{disk.mountpoint && (<> mounted at {disk.mountpoint})}. This will make the content {unmount ? 'unavailable' : 'available'} to be viewed in the file explorer. + Permanent {unmount ? 'unmount' : 'mount'} will persist after reboot. + +
+ {unmount ? '' : <> + + + } +
+ Permanent {unmount ? 'Unmount' : 'Mount'} +
+ {formik.errors.submit && ( + + {formik.errors.submit} + + )} +
+
+
+ + + { + formik.handleSubmit(); + }}>{unmount ? 'Unmount' : 'Mount'} + +
+
+
+ +} + +const MountDiskDialog = ({ disk, unmount, refresh }) => { + const [open, setOpen] = useState(false); + + return <> + {open && } + + + +} + + +export default MountDiskDialog; \ No newline at end of file diff --git a/client/src/pages/storage/mounts.jsx b/client/src/pages/storage/mounts.jsx index d8ddd39..a8b9f5e 100644 --- a/client/src/pages/storage/mounts.jsx +++ b/client/src/pages/storage/mounts.jsx @@ -3,30 +3,36 @@ import { useEffect, useState } from "react"; import * as API from "../../api"; import PrettyTableView from "../../components/tableView/prettyTableView"; import { DeleteButton } from "../../components/delete"; -import { CloudOutlined, CloudServerOutlined, CompassOutlined, DeleteOutlined, DesktopOutlined, EditOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons"; +import { CloudOutlined, CloudServerOutlined, CompassOutlined, DeleteOutlined, DesktopOutlined, EditOutlined, FolderOutlined, LaptopOutlined, MobileOutlined, PlusCircleOutlined, ReloadOutlined, TabletOutlined } from "@ant-design/icons"; import { Alert, Button, CircularProgress, InputLabel, ListItemIcon, ListItemText, MenuItem, Stack } from "@mui/material"; import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts"; import MainCard from "../../components/MainCard"; import { Formik } from "formik"; import { LoadingButton } from "@mui/lab"; import ApiModal from "../../components/apiModal"; -import ConfirmModal from "../../components/confirmModal"; +import ConfirmModal, { ConfirmModalDirect } from "../../components/confirmModal"; import { isDomain } from "../../utils/indexs"; import UploadButtons from "../../components/fileUpload"; import SnapRAIDDialog from "./snapRaidDialog"; import MenuButton from "../../components/MenuButton"; +import MountDialog, { MountDialogInternal } from "./mountDialog"; +import ResponsiveButton from "../../components/responseiveButton"; export const StorageMounts = () => { const [isAdmin, setIsAdmin] = useState(false); const [config, setConfig] = useState(null); const [mounts, setMounts] = useState([]); + const [mountDialog, setMountDialog] = useState(null); + const [loading, setLoading] = useState(false); const refresh = async () => { + setLoading(true); let mountsData = await API.storage.mounts.list(); let configAsync = await API.config.get(); setConfig(configAsync.data); setIsAdmin(configAsync.isAdmin); setMounts(mountsData.data); + setLoading(false); }; useEffect(() => { @@ -34,10 +40,17 @@ export const StorageMounts = () => { }, []); return <> + {mountDialog && } {(config) ? <> `${r.device} - ${refresh.path}`} + buttons={[ + } variant="contained" onClick={() => setMountDialog({data: null, unmount: false})}>New Mount, + } onClick={() => { + refresh(); + }}>Refresh + ]} columns={[ { title: 'Device', @@ -60,17 +73,17 @@ export const StorageMounts = () => { field: (r) => <>
- + setMountDialog({data: r, unmount: false})}> - tryDeleteRaid(r.Name)}>Edit + Edit - + setMountDialog({data: r, unmount: true})}> - tryDeleteRaid(r.Name)}>Delete + unmount
diff --git a/client/src/pages/storage/parity.jsx b/client/src/pages/storage/parity.jsx index 8e348b6..3d6aceb 100644 --- a/client/src/pages/storage/parity.jsx +++ b/client/src/pages/storage/parity.jsx @@ -13,7 +13,7 @@ import ApiModal from "../../components/apiModal"; import ConfirmModal, { ConfirmModalDirect } from "../../components/confirmModal"; import { crontabToText, isDomain } from "../../utils/indexs"; import UploadButtons from "../../components/fileUpload"; -import SnapRAIDDialog from "./snapRaidDialog"; +import SnapRAIDDialog, { SnapRAIDDialogInternal } from "./snapRaidDialog"; import MenuButton from "../../components/MenuButton"; import diskIcon from '../../assets/images/icons/disk.svg'; import ResponsiveButton from "../../components/responseiveButton"; @@ -61,13 +61,16 @@ export const Parity = () => { const [parities, setParities] = useState([]); const [loading, setLoading] = useState(false); const [deleteRaid, setDeleteRaid] = useState(null); + const [editOpened, setEditOpened] = useState(null); const refresh = async () => { + setLoading(true); let paritiesData = await API.storage.snapRAID.list(); let configAsync = await API.config.get(); setConfig(configAsync.data); setIsAdmin(configAsync.isAdmin); setParities(paritiesData.data); + setLoading(false); }; const apiDeleteRaid = async (name) => { @@ -126,8 +129,8 @@ export const Parity = () => { refresh(); }}>Refresh
-
+ {editOpened && } {parities && r.Name} @@ -195,37 +198,35 @@ export const Parity = () => { field: (r) => { return
- + setEditOpened(r)}> - - - + Edit - + sync(r.Name)}> - sync(r.Name)}>Sync + Sync - + scrub(r.Name)}> - scrub(r.Name)}>Scrub + Scrub - + fix(r.Name)}> - fix(r.Name)}>Fix + Fix - + tryDeleteRaid(r.Name)}> - tryDeleteRaid(r.Name)}>Delete + Delete
diff --git a/client/src/pages/storage/snapRaidDialog.jsx b/client/src/pages/storage/snapRaidDialog.jsx index e19a45a..12c6fce 100644 --- a/client/src/pages/storage/snapRaidDialog.jsx +++ b/client/src/pages/storage/snapRaidDialog.jsx @@ -207,4 +207,5 @@ const SnapRAIDDialog = ({ refresh, data }) => { } -export default SnapRAIDDialog; \ No newline at end of file +export default SnapRAIDDialog; +export { SnapRAIDDialog, SnapRAIDDialogInternal }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d7531ef..31202b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "@vitejs/plugin-react": "^3.1.0", + "@xterm/addon-fit": "^0.9.0", + "@xterm/xterm": "^5.4.0", "apexcharts": "^3.35.5", "bcryptjs": "^2.4.3", "browserslist": "^4.21.7", @@ -69,7 +71,6 @@ "web-vitals": "^3.0.2", "whiskers": "^0.4.0", "whiskers.js": "^1.0.0", - "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0", "yup": "^0.32.11" }, @@ -4416,6 +4417,19 @@ "vite": "^4.1.0-beta.0" } }, + "node_modules/@xterm/addon-fit": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.9.0.tgz", + "integrity": "sha512-hDlPPbTVPYyvwXu/asW8HbJkI/2RMi0cMaJnBZYVeJB0SWP2NeESMCNr+I7CvBlyI0sAxpxOg8Wk4OMkxBz9WA==", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.4.0.tgz", + "integrity": "sha512-GlyzcZZ7LJjhFevthHtikhiDIl8lnTSgol6eTM4aoSNLcuXu3OEhnbqdCVIjtIil3jjabf3gDtb1S8FGahsuEw==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -10945,7 +10959,8 @@ "node_modules/xterm": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", - "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==" + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "peer": true }, "node_modules/xterm-addon-fit": { "version": "0.8.0", diff --git a/package.json b/package.json index 3dd2bc9..50bec98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.15.0-unstable19", + "version": "0.15.0-unstable21", "description": "", "main": "test-server.js", "bugs": { @@ -22,6 +22,8 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "@vitejs/plugin-react": "^3.1.0", + "@xterm/addon-fit": "^0.9.0", + "@xterm/xterm": "^5.4.0", "apexcharts": "^3.35.5", "bcryptjs": "^2.4.3", "browserslist": "^4.21.7", @@ -69,7 +71,6 @@ "web-vitals": "^3.0.2", "whiskers": "^0.4.0", "whiskers.js": "^1.0.0", - "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0", "yup": "^0.32.11" }, diff --git a/src/CRON.go b/src/CRON.go index d83c927..d87fbb0 100644 --- a/src/CRON.go +++ b/src/CRON.go @@ -18,9 +18,7 @@ type Version struct { Version string `json:"version"` } -func checkVersion() { - utils.NewVersionAvailable = false - +func GetCosmosVersion() string { ex, err := os.Executable() if err != nil { panic(err) @@ -30,7 +28,7 @@ func checkVersion() { pjs, errPR := os.Open(exPath + "/meta.json") if errPR != nil { utils.Error("checkVersion", errPR) - return + return "" } packageJson, _ := ioutil.ReadAll(pjs) @@ -41,10 +39,21 @@ func checkVersion() { errJ := json.Unmarshal(packageJson, &version) if errJ != nil { utils.Error("checkVersion", errJ) - return + return "" } - myVersion := version.Version + return version.Version + +} + +func checkVersion() { + utils.NewVersionAvailable = false + + myVersion := GetCosmosVersion() + if myVersion == "" { + utils.Error("checkVersion - Could not get version", nil) + return + } response, err := http.Get("https://cosmos-cloud.io/versions/" + myVersion) if err != nil { diff --git a/src/docker/api_terminal.go b/src/docker/api_terminal.go index a45747a..64c65ce 100644 --- a/src/docker/api_terminal.go +++ b/src/docker/api_terminal.go @@ -2,6 +2,8 @@ package docker import ( "context" + "time" + "github.com/gorilla/mux" "github.com/docker/docker/api/types" "github.com/gorilla/websocket" @@ -12,6 +14,8 @@ import ( "github.com/azukaar/cosmos-server/src/utils" ) +const timeoutDuration = 2 * time.Minute + var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -78,6 +82,9 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) { } defer ws.Close() + ws.SetReadDeadline(time.Now().Add(timeoutDuration)) + ws.SetWriteDeadline(time.Now().Add(timeoutDuration)) + ctx := context.Background() errD := Connect() if errD != nil { @@ -107,7 +114,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) { AttachStdin: true, AttachStdout: true, AttachStderr: true, - Cmd: []string{"bash"}, + Cmd: []string{"/bin/sh"}, } execStart := types.ExecStartCheck{ @@ -127,6 +134,9 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) { http.Error(w, "ContainerExecAttach failed: "+err.Error(), http.StatusInternalServerError) return } + + // Start bash if it exists + resp.Conn.Write([]byte("bash\n")) utils.Log("Created new shell and attached to it in container " + containerID) } else { @@ -153,6 +163,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) { var WSChan = make(chan []byte, 1024*1024*4) var DockerChan = make(chan []byte, 1024*1024*4) + // Start a goroutine to read from our websocket and write to the container go func(ctx context.Context) { @@ -162,6 +173,7 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) { case <-ctx.Done(): // Context cancellation return default: + ws.SetReadDeadline(time.Now().Add(timeoutDuration)) _, message, err := ws.ReadMessage() if err != nil { if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { @@ -224,6 +236,8 @@ func TerminalRoute(w http.ResponseWriter, r *http.Request) { utils.Debug("Writing message to websocket " + string(message)) messages := splitIntoChunks(string(message)) + + ws.SetWriteDeadline(time.Now().Add(timeoutDuration)) for _, messageSplit := range messages { err = ws.WriteMessage(websocket.TextMessage, []byte(messageSplit)) diff --git a/src/index.go b/src/index.go index fe3913f..20dc4b0 100644 --- a/src/index.go +++ b/src/index.go @@ -16,7 +16,9 @@ import ( ) func main() { - utils.Log("Starting...") + utils.Log("------------------------------------------") + utils.Log("Starting Cosmos-Server version " + GetCosmosVersion()) + utils.Log("------------------------------------------") // utils.ReBootstrapContainer = docker.BootstrapContainerFromTags utils.PushShieldMetrics = metrics.PushShieldMetrics