From 08528ec7c1762931a505a7d21a8004c078eeab32 Mon Sep 17 00:00:00 2001 From: LynithDev <61880709+LynithDev@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:03:04 +0200 Subject: [PATCH] feat: Better ClusterGame page + Added multi-cluster process support --- apps/desktop/src/api/commands/cluster.rs | 16 +-- apps/desktop/src/api/commands/mod.rs | 4 + apps/desktop/src/api/commands/processor.rs | 27 ++++- apps/frontend/src/ui/components/Sidebar.tsx | 8 +- .../ui/components/content/FormattedLog.tsx | 2 +- .../src/ui/components/overlay/Modal.tsx | 38 +++++- .../overlay/cluster/ClusterGameSetup.tsx | 20 ++- apps/frontend/src/ui/hooks/useCluster.tsx | 35 +++++- apps/frontend/src/ui/pages/Home.tsx | 8 +- .../src/ui/pages/cluster/ClusterGame.tsx | 102 ++++++++++------ .../src/ui/pages/cluster/ClusterOverview.tsx | 9 +- .../src/ui/pages/cluster/ClusterRoot.tsx | 30 +++-- .../src/ui/pages/cluster/ClusterSettings.tsx | 33 ++++- .../pages/settings/game/SettingsMinecraft.tsx | 59 ++++++++- packages/core/src/api/processor.rs | 84 +++++++++++++ packages/core/src/api/proxy/send.rs | 114 ++++++++---------- packages/core/src/game/mod.rs | 17 +-- packages/core/src/store/processor.rs | 20 ++- packages/core/src/store/settings.rs | 4 + 19 files changed, 455 insertions(+), 175 deletions(-) diff --git a/apps/desktop/src/api/commands/cluster.rs b/apps/desktop/src/api/commands/cluster.rs index 446b2e85..e2cb9bba 100644 --- a/apps/desktop/src/api/commands/cluster.rs +++ b/apps/desktop/src/api/commands/cluster.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use onelauncher::cluster::content::logger; use onelauncher::cluster::{self}; use onelauncher::data::{Loader, PackageData}; +use onelauncher::processor::DetailedProcess; use onelauncher::store::{Cluster, ClusterPath}; use onelauncher::State; use serde::{Deserialize, Serialize}; @@ -132,21 +133,12 @@ pub async fn remove_cluster(uuid: Uuid) -> Result<(), String> { #[specta::specta] #[tauri::command] -pub async fn run_cluster(uuid: Uuid) -> Result<(Uuid, u32), String> { +pub async fn run_cluster(uuid: Uuid) -> Result { let path = ClusterPath::find_by_uuid(uuid).await?; let c_lock = cluster::run_default(&path).await?; + let child = &*c_lock.read().await; - let p_uuid = c_lock.read().await.uuid; - let p_pid = c_lock - .read() - .await - .current_child - .read() - .await - .id() - .unwrap_or(0); - - Ok((p_uuid, p_pid)) + Ok(DetailedProcess::from_processor_child(child).await) } #[specta::specta] diff --git a/apps/desktop/src/api/commands/mod.rs b/apps/desktop/src/api/commands/mod.rs index 692b6e82..04738b46 100644 --- a/apps/desktop/src/api/commands/mod.rs +++ b/apps/desktop/src/api/commands/mod.rs @@ -47,6 +47,10 @@ macro_rules! collect_commands { kill_process, is_cluster_running, get_pid_by_uuid, + get_user_by_process, + get_process_started_at, + get_processes_detailed_by_path, + get_process_detailed_by_id, // Settings get_settings, set_settings, diff --git a/apps/desktop/src/api/commands/processor.rs b/apps/desktop/src/api/commands/processor.rs index f7816308..e1017118 100644 --- a/apps/desktop/src/api/commands/processor.rs +++ b/apps/desktop/src/api/commands/processor.rs @@ -1,4 +1,5 @@ -use onelauncher::processor; +use chrono::{DateTime, Utc}; +use onelauncher::processor::{self, DetailedProcess}; use onelauncher::store::{Cluster, ClusterPath}; use uuid::Uuid; @@ -20,6 +21,30 @@ pub async fn get_processes_by_path(path: ClusterPath) -> Result, Strin Ok(processor::get_uuids_by_cluster_path(path).await?) } +#[specta::specta] +#[tauri::command] +pub async fn get_processes_detailed_by_path(path: ClusterPath) -> Result, String> { + Ok(processor::get_processes_detailed_by_path(path).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_process_detailed_by_id(uuid: Uuid) -> Result { + Ok(processor::get_process_detailed_by_id(uuid).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_user_by_process(uuid: Uuid) -> Result, String> { + Ok(processor::get_user_by_process(uuid).await?) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_process_started_at(uuid: Uuid) -> Result, String> { + Ok(processor::get_process_started_at(uuid).await?) +} + #[specta::specta] #[tauri::command] pub async fn get_pid_by_uuid(uuid: Uuid) -> Result { diff --git a/apps/frontend/src/ui/components/Sidebar.tsx b/apps/frontend/src/ui/components/Sidebar.tsx index 44ed8189..7c986a69 100644 --- a/apps/frontend/src/ui/components/Sidebar.tsx +++ b/apps/frontend/src/ui/components/Sidebar.tsx @@ -26,8 +26,10 @@ function Sidebar(props: SidebarProps) { navigate(url); } - function isActive(link: string) { - return location.pathname === `${props.base}${link}` || `${location.pathname}/` === `${props.base}${link}`; + function isActive(link: string, params: URLSearchParams | undefined) { + const pathnameMatch = location.pathname === `${props.base}${link}` || `${location.pathname}/` === `${props.base}${link}`; + const paramsMatch = params ? location.search.endsWith(params.toString()) : true; + return pathnameMatch && paramsMatch; } return ( @@ -47,7 +49,7 @@ function Sidebar(props: SidebarProps) { goto(link[2], link[3])} class={ - `px-3 py-1 rounded-md text-md hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2]) ? 'bg-component-bg' : ''}` + `px-3 py-1 rounded-md text-md hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg' : ''}` } > diff --git a/apps/frontend/src/ui/components/content/FormattedLog.tsx b/apps/frontend/src/ui/components/content/FormattedLog.tsx index 11ae06f5..13493be4 100644 --- a/apps/frontend/src/ui/components/content/FormattedLog.tsx +++ b/apps/frontend/src/ui/components/content/FormattedLog.tsx @@ -10,7 +10,7 @@ function FormattedLog(props: FormattedLogProps) { const lines = createMemo(() => props.log.split('\n')); return ( -
+
diff --git a/apps/frontend/src/ui/components/overlay/Modal.tsx b/apps/frontend/src/ui/components/overlay/Modal.tsx index a3cc16a0..48a47e62 100644 --- a/apps/frontend/src/ui/components/overlay/Modal.tsx +++ b/apps/frontend/src/ui/components/overlay/Modal.tsx @@ -164,14 +164,48 @@ Modal.Simple = function (props: ModalSimpleProps) { ); }; +type ModalErrorProps = MakeOptional & { + message?: string | undefined; +}; + +Modal.Error = function (props: ModalErrorProps) { + const [split, rest] = splitProps(props, ['message', 'title', 'buttons', 'children']); + + return ( + , + ]} + > +
+ {split.children || ( + Something went wrong. + )} + {split.message && ( + + {split.message} + + )} +
+
+ ); +}; + type ModalDeleteProps = MakeOptional & { onCancel?: () => void; onDelete?: () => void; timeLeft?: number; + deleteBtnText?: string; }; Modal.Delete = function (props: ModalDeleteProps) { - const [split, rest] = splitProps(props, ['title', 'buttons', 'onCancel', 'onDelete', 'timeLeft', 'children']); + const [split, rest] = splitProps(props, ['title', 'buttons', 'onCancel', 'onDelete', 'timeLeft', 'children', 'deleteBtnText']); const [timeLeft, setTimeLeft] = createSignal(1); const [intervalId, setIntervalId] = createSignal(undefined); @@ -235,7 +269,7 @@ Modal.Delete = function (props: ModalDeleteProps) { />,
)} diff --git a/apps/frontend/src/ui/hooks/useCluster.tsx b/apps/frontend/src/ui/hooks/useCluster.tsx index a0e554d5..08717707 100644 --- a/apps/frontend/src/ui/hooks/useCluster.tsx +++ b/apps/frontend/src/ui/hooks/useCluster.tsx @@ -1,7 +1,10 @@ -import { type Context, type ParentProps, type ResourceReturn, Show, createContext, useContext } from 'solid-js'; -import useCommand from './useCommand'; +import { type Accessor, type Context, type ParentProps, type ResourceReturn, Show, createContext, createSignal, useContext } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; +import useCommand, { tryResult } from './useCommand'; import type { Cluster } from '~bindings'; import { bridge } from '~imports'; +import ClusterGame from '~ui/pages/cluster/ClusterGame'; +import Modal, { createModal } from '~ui/components/overlay/Modal'; const ClusterContext = createContext() as Context>; @@ -35,4 +38,32 @@ export function useClusterContext() { return context; } +export function useLaunchCluster(uuid: string | Accessor | (() => string | undefined) | undefined) { + const navigate = useNavigate(); + const [error, setError] = createSignal(undefined); + + const getUuid = () => typeof uuid === 'function' ? uuid() : uuid; + + const modal = createModal(props => ( + + )); + + return () => { + const uuid = getUuid(); + + if (uuid === undefined) + return; + + tryResult(bridge.commands.runCluster, uuid).then((details) => { + navigate(`/clusters/game?${ClusterGame.buildUrl(uuid, details).toString()}`); + }).catch((err) => { + setError(err); + modal.show(); + }); + }; +} + export default useClusterContext; diff --git a/apps/frontend/src/ui/pages/Home.tsx b/apps/frontend/src/ui/pages/Home.tsx index 1f666ffb..c012ecde 100644 --- a/apps/frontend/src/ui/pages/Home.tsx +++ b/apps/frontend/src/ui/pages/Home.tsx @@ -18,6 +18,7 @@ import useCommand from '~ui/hooks/useCommand'; import { bridge } from '~imports'; import { secondsToWords, upperFirst } from '~utils'; import { useClusterCreator } from '~ui/components/overlay/cluster/ClusterCreationModal'; +import { useLaunchCluster } from '~ui/hooks/useCluster'; type GroupedClusters = Record; @@ -183,6 +184,7 @@ function Banner(props: BannerProps) { * If there are no clusters, display a generic background with the button action creating a new cluster. */ const [cluster, setCluster] = createSignal(); + const launch = useLaunchCluster(() => cluster()?.uuid); const navigate = useNavigate(); createEffect(() => { @@ -259,7 +261,7 @@ function Banner(props: BannerProps) { buttonStyle="primary" iconLeft={} children={`Launch ${cluster()!.meta.mc_version}`} - onClick={() => ClusterRoot.launch(navigate, cluster()!.uuid)} + onClick={launch} />
+ ); } -ClusterGame.buildUrl = function (uuid: string, process_uuid: string): URLSearchParams { - return new URLSearchParams({ id: uuid, process_uuid }); +ClusterGame.buildUrl = function (cluster_id: string, detailed: DetailedProcess): URLSearchParams { + return new URLSearchParams({ + id: cluster_id, + process_uuid: detailed.uuid, + started_at: detailed.started_at, + pid: detailed.pid.toString(), + ...(detailed.user ? { user: detailed.user } : {}), + }); }; export default ClusterGame; diff --git a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx index da62c274..97a8de1d 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterOverview.tsx @@ -7,12 +7,11 @@ import { join } from 'pathe'; import ClusterCover from '../../components/game/ClusterCover'; import LoaderIcon from '../../components/game/LoaderIcon'; import Button from '../../components/base/Button'; -import ClusterRoot from './ClusterRoot'; import ScrollableContainer from '~ui/components/ScrollableContainer'; import Sidebar from '~ui/components/Sidebar'; import type { Cluster } from '~bindings'; import { secondsToWords, upperFirst } from '~utils'; -import useClusterContext from '~ui/hooks/useCluster'; +import useClusterContext, { useLaunchCluster } from '~ui/hooks/useCluster'; import { bridge } from '~imports'; import TextField from '~ui/components/base/TextField'; import SettingsRow from '~ui/components/SettingsRow'; @@ -233,11 +232,7 @@ interface BannerProps { } function Banner(props: BannerProps) { - const navigate = useNavigate(); - - async function launch() { - ClusterRoot.launch(navigate, props.cluster.uuid); - } + const launch = useLaunchCluster(() => props.cluster.uuid); async function launchFilePicker() { const selected = await dialog.open({ diff --git a/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx b/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx index 1eff52b5..ef342291 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterRoot.tsx @@ -1,5 +1,5 @@ import { type Navigator, Route, useIsRouting, useSearchParams } from '@solidjs/router'; -import { type ParentProps, createEffect, createResource, createSignal, onCleanup, onMount } from 'solid-js'; +import { type ParentProps, Show, createEffect, createResource, createSignal, onCleanup, onMount } from 'solid-js'; import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-solid'; import type { UnlistenFn } from '@tauri-apps/api/event'; import Sidebar from '../../components/Sidebar'; @@ -27,7 +27,18 @@ function ClusterRoutes() { - + { + const [searchParams] = useSearchParams(); + + return ( + + + + ); + }} + /> ); } @@ -58,12 +69,6 @@ ClusterRoot.open = function (navigate: Navigator, uuid: string) { navigate(`/clusters/?id=${uuid}`); }; -ClusterRoot.launch = function (navigate: Navigator, uuid: string) { - tryResult(bridge.commands.runCluster, uuid).then(([process_uuid]) => { - navigate(`/clusters/game?${ClusterGame.buildUrl(uuid, process_uuid).toString()}`); - }); -}; - function ClusterSidebar() { const [cluster, { refetch: refetchCluster }] = useClusterContext(); const [listener, setListener] = createSignal(); @@ -72,7 +77,7 @@ function ClusterSidebar() { if (!c || typeof c.path !== 'string') return []; - return tryResult(bridge.commands.getProcessesByPath, c.path); + return tryResult(bridge.commands.getProcessesDetailedByPath, c.path); }); const isRouting = useIsRouting(); @@ -106,13 +111,12 @@ function ClusterSidebar() { [, 'Logs', '/logs'], [, 'Game Settings', '/settings'], ], - // TODO: Support multiple running clusters and add used account's avatar as the icon ...(runningProcesses() && runningProcesses()!.length > 0 ? { - Running: runningProcesses()!.map((process_uuid, index) => { - const icon = ; // : ; + Running: runningProcesses()!.map((details, index) => { + const icon = ; ; - return [icon, `Process #${index + 1}`, '/game', ClusterGame.buildUrl(cluster()!.uuid, process_uuid)]; + return [icon, `Process #${index + 1}`, '/game', ClusterGame.buildUrl(cluster()!.uuid, details)]; }), } : {}), diff --git a/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx b/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx index 7a12f8f5..e2259bd4 100644 --- a/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx +++ b/apps/frontend/src/ui/pages/cluster/ClusterSettings.tsx @@ -69,10 +69,35 @@ function PageSettings(cluster: Accessor) { return ( <> - - - - + + + + + + + ); } diff --git a/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx b/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx index 371a8cb8..dc1c7273 100644 --- a/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx +++ b/apps/frontend/src/ui/pages/settings/game/SettingsMinecraft.tsx @@ -1,4 +1,4 @@ -import { CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid'; +import { ActivityIcon, CpuChip01Icon, Database01Icon, EyeIcon, FilePlus02Icon, FileX02Icon, LayoutTopIcon, Maximize01Icon, ParagraphWrapIcon, VariableIcon, XIcon } from '@untitled-theme/icons-solid'; import { type Accessor, type Setter, Show, createSignal, onMount, splitProps, untrack } from 'solid-js'; import type { Memory, Resolution } from '~bindings'; import TextField from '~ui/components/base/TextField'; @@ -192,12 +192,17 @@ export function GameSettings(props: { export function LauncherSettings(props: { hideOnLaunch: CreateSetting | undefined; + allowParallelClusters: CreateSetting | undefined; }) { + const shouldShowHeader = () => Object.values(props).some(setting => setting !== undefined); + return ( <> - + Launcher + + + + + } + isGlobal={props.allowParallelClusters!.isGlobal} + reset={props.allowParallelClusters!.resetToFallback} + > + + + ); } @@ -335,6 +355,7 @@ function PageSettings() { // Launcher const hideOnLaunch = createSetting(settings().hide_on_launch ?? false); + const allowParallelClusters = createSetting(settings().allow_parallel_running_clusters ?? false); // Process const preCommand = createSetting(settings().init_hooks.pre ?? ''); @@ -353,6 +374,7 @@ function PageSettings() { // Launcher hide_on_launch: hideOnLaunch.get(), + allow_parallel_running_clusters: allowParallelClusters.get(), // Process init_hooks: { @@ -368,10 +390,35 @@ function PageSettings() { return ( <> - - - - + + + + + + + ); } diff --git a/packages/core/src/api/processor.rs b/packages/core/src/api/processor.rs index c1380e34..3f731492 100644 --- a/packages/core/src/api/processor.rs +++ b/packages/core/src/api/processor.rs @@ -2,14 +2,98 @@ pub use crate::store::{Cluster, InitHooks, JavaOptions, Memory, Resolution, Settings, State}; use crate::store::{ClusterPath, ProcessorChild}; +use chrono::{DateTime, Utc}; use uuid::Uuid; +#[derive(specta::Type, serde::Serialize)] +pub struct DetailedProcess { + pub uuid: Uuid, + pub user: Option, + pub started_at: DateTime, + pub pid: u32, +} + +impl DetailedProcess { + pub async fn from_processor_child(process: &ProcessorChild) -> Self { + let pid = process.current_child.read().await.id().unwrap_or(0); + Self { + uuid: process.uuid, + user: process.user, + started_at: process.started_at, + pid, + } + } +} + +/// get detailed processes by a [`ClusterPath`]. +#[tracing::instrument] +pub async fn get_process_detailed_by_id(uuid: Uuid) -> crate::Result { + let state = State::get().await?; + let processor = state.processor.read().await; + + let child = processor.get(uuid).ok_or(anyhow::anyhow!("process not found"))?; + let child = child.read().await; + let pid = child.current_child.read().await.id().ok_or(anyhow::anyhow!("process not found"))?; + Ok(DetailedProcess { + uuid, + user: child.user, + started_at: child.started_at, + pid, + }) +} + +/// get detailed processes by a [`ClusterPath`]. +#[tracing::instrument] +pub async fn get_processes_detailed_by_path(path: ClusterPath) -> crate::Result> { + let state = State::get().await?; + let processor = state.processor.read().await; + + let uuids = processor.running_cluster(path).await?; + let mut processes = Vec::new(); + for uuid in uuids { + let child = processor.get(uuid).ok_or(anyhow::anyhow!("process not found"))?; + let child = child.read().await; + let pid = child.current_child.read().await.id().ok_or(anyhow::anyhow!("process not found"))?; + processes.push(DetailedProcess { + uuid, + user: child.user, + started_at: child.started_at, + pid, + }); + } + Ok(processes) +} + /// check whether or not a process has completed by its [`Uuid`]. #[tracing::instrument] pub async fn uuid_is_finished(uuid: Uuid) -> crate::Result { Ok(uuid_exit_status(uuid).await?.is_some()) } +/// get the user from a processor by its [`Uuid`]. +#[tracing::instrument] +pub async fn get_user_by_process(uuid: Uuid) -> crate::Result> { + let state = State::get().await?; + let processor = state.processor.read().await; + if let Some(processor) = processor.get(uuid) { + Ok(processor.read().await.user) + } else { + Err(anyhow::anyhow!("process not found").into()) + } +} + +/// get the user from a processor by its [`Uuid`]. +#[tracing::instrument] +pub async fn get_process_started_at(uuid: Uuid) -> crate::Result> { + let state = State::get().await?; + let processor = state.processor.read().await; + if let Some(processor) = processor.get(uuid) { + Ok(processor.read().await.started_at) + } else { + Err(anyhow::anyhow!("process not found").into()) + } +} + /// check the exit status of a process by its [`Uuid`]. #[tracing::instrument] pub async fn uuid_exit_status(uuid: Uuid) -> crate::Result> { diff --git a/packages/core/src/api/proxy/send.rs b/packages/core/src/api/proxy/send.rs index f704b916..db463901 100644 --- a/packages/core/src/api/proxy/send.rs +++ b/packages/core/src/api/proxy/send.rs @@ -4,6 +4,7 @@ use crate::api::proxy::{ }; use crate::proxy::OfflinePayload; use crate::store::{ClusterPath, IngressProcessType, IngressProcessor}; +use tauri::Emitter; use uuid::Uuid; #[cfg(feature = "cli")] @@ -15,9 +16,6 @@ use tauri_specta::Event; #[cfg(feature = "tauri")] use crate::api::proxy::{ClusterPayload, IngressPayload, MessagePayload, ProcessPayload}; -#[cfg(feature = "tauri")] -use tauri::Manager; - #[onelauncher_macros::memory] pub async fn init_ingress( ingress_type: IngressType, @@ -152,12 +150,6 @@ pub async fn send_ingress( }, ) .map_err(ProxyError::from)?; - // let _ = IngressPayload { - // fraction: display_conv, - // message: message.unwrap_or(&ingress.message).to_string(), - // event: ingress.ingress_type.clone(), - // ingress_uuid: ingress.ingress_uuid, - // }.emit(proxy_state.app.app_handle()); ingress.last_sent = display; } @@ -169,10 +161,15 @@ pub async fn send_message(message: &str) -> crate::Result<()> { #[cfg(feature = "tauri")] { let proxy_state = crate::ProxyState::get().await?; - MessagePayload { - message: message.to_string(), - } - .emit(proxy_state.app.app_handle())?; + proxy_state + .app + .emit( + MessagePayload::NAME, + MessagePayload { + message: message.to_string(), + }, + ) + .map_err(ProxyError::from)?; } tracing::warn!("{}", message); @@ -183,11 +180,15 @@ pub async fn send_offline(offline: bool) -> crate::Result<()> { #[cfg(feature = "tauri")] { let proxy_state = crate::ProxyState::get().await?; - // proxy_state - // .app - // .emit("offline", offline) - // .map_err(ProxyError::from)?; - OfflinePayload { offline }.emit(proxy_state.app.app_handle())?; + proxy_state + .app + .emit( + OfflinePayload::NAME, + OfflinePayload { + offline, + }, + ) + .map_err(ProxyError::from)?; } Ok(()) @@ -198,11 +199,13 @@ pub async fn send_internet(internet: InternetPayload) -> crate::Result<()> { #[cfg(feature = "tauri")] { let proxy_state = crate::ProxyState::get().await?; - // proxy_state - // .app - // .emit("internet", internet) - // .map_err(ProxyError::from)?; - internet.emit(proxy_state.app.app_handle())?; + proxy_state + .app + .emit( + InternetPayload::NAME, + internet + ) + .map_err(ProxyError::from)?; } Ok(()) @@ -217,25 +220,18 @@ pub async fn send_process( #[cfg(feature = "tauri")] { let proxy_state = crate::ProxyState::get().await?; - // proxy_state - // .app - // .emit( - // "process", - // ProcessPayload { - // uuid, - // pid, - // event, - // message: message.to_string(), - // }, - // ) - // .map_err(ProxyError::from)?; - ProcessPayload { - uuid, - pid, - event, - message: message.to_string(), - } - .emit(proxy_state.app.app_handle())?; + proxy_state + .app + .emit( + ProcessPayload::NAME, + ProcessPayload { + uuid, + pid, + event, + message: message.to_string(), + }, + ) + .map_err(ProxyError::from)?; } Ok(()) @@ -251,27 +247,19 @@ pub async fn send_cluster( { let path = cluster_path.full_path().await?; let proxy_state = crate::ProxyState::get().await?; - // proxy_state - // .app - // .emit( - // "cluster", - // ClusterPayload { - // uuid, - // cluster_path: cluster_path.clone(), - // path, - // name: name.to_string(), - // event, - // }, - // ) - // .map_err(ProxyError::from)?; - ClusterPayload { - uuid, - cluster_path: cluster_path.clone(), - path, - name: name.to_string(), - event, - } - .emit(proxy_state.app.app_handle())?; + proxy_state + .app + .emit( + ClusterPayload::NAME, + ClusterPayload { + uuid, + cluster_path: cluster_path.clone(), + path, + name: name.to_string(), + event, + }, + ) + .map_err(ProxyError::from)?; } Ok(()) diff --git a/packages/core/src/game/mod.rs b/packages/core/src/game/mod.rs index 07bb15a4..65d8e3cd 100644 --- a/packages/core/src/game/mod.rs +++ b/packages/core/src/game/mod.rs @@ -317,13 +317,15 @@ pub async fn launch_minecraft( None => Command::new(&java_version.path), }; let env_args = Vec::from(env_args); - let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?; - if let Some(uuid) = existing.first() { - return Err(anyhow::anyhow!( - "cluster {} is already running ({uuid})", - cluster.cluster_path() - ) - .into()); + if !state.settings.read().await.allow_parallel_running_clusters { + let existing = processor::get_uuids_by_cluster_path(cluster.cluster_path()).await?; + if let Some(uuid) = existing.first() { + return Err(anyhow::anyhow!( + "cluster {} is already running ({uuid})", + cluster.cluster_path() + ) + .into()); + } } command @@ -462,6 +464,7 @@ pub async fn launch_minecraft( command, post_hook, censors, + Some(credentials.id) ) .await } diff --git a/packages/core/src/store/processor.rs b/packages/core/src/store/processor.rs index 2aaaa43e..a3c0bb98 100644 --- a/packages/core/src/store/processor.rs +++ b/packages/core/src/store/processor.rs @@ -1,6 +1,6 @@ //! Handles child processes in a unified API -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeZone, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; @@ -119,6 +119,9 @@ pub struct ProcessorCache { pub cluster_path: ClusterPath, /// A post hook to be run once a process has completed. pub post: Option, + /// The [`Uuid`] of the Minecraft account it was started by. + #[serde(skip_serializing_if = "Option::is_none")] + pub user: Option, } impl ChildType { @@ -136,6 +139,7 @@ impl ChildType { uuid: Uuid, cluster_path: ClusterPath, post: Option, + user: Option, ) -> crate::Result<()> { let pid = match self { ChildType::ChildProcess(child) => child.id().unwrap_or(0), @@ -164,6 +168,7 @@ impl ChildType { exe, cluster_path, post, + user }; let path = state.directories.caches_dir().await.join(PROCESSOR_FILE); @@ -264,6 +269,10 @@ pub struct ProcessorChild { pub current_child: Arc>, /// When this process was last updated in [`Utc`]. pub last_updated: DateTime, + /// When this process was last updated in [`Utc`]. + pub started_at: DateTime, + /// What [`Uuid`] this process is running under. + pub user: Option, } impl Processor { @@ -422,6 +431,7 @@ impl Processor { mut command: Command, post: Option, censors: HashMap, + user: Option, ) -> crate::Result>> { let proc = command.spawn().map_err(IOError::from)?; let process = ChildType::ChildProcess(proc); @@ -429,7 +439,7 @@ impl Processor { .id() .ok_or_else(|| anyhow::anyhow!("process failed, couldn't get pid"))?; process - .cache(uuid, cluster_path.clone(), post.clone()) + .cache(uuid, cluster_path.clone(), post.clone(), user) .await?; let current_child = Arc::new(RwLock::new(process)); @@ -449,6 +459,8 @@ impl Processor { current_child, manager, last_updated, + started_at: Utc::now(), + user, }; let child = Arc::new(RwLock::new(child)); self.0.insert(uuid, child.clone()); @@ -511,7 +523,7 @@ impl Processor { .id() .ok_or_else(|| anyhow::anyhow!("process failed, couldnt get pid"))?; process - .cache(cache.uuid, cache.cluster_path.clone(), cache.post.clone()) + .cache(cache.uuid, cache.cluster_path.clone(), cache.post.clone(), cache.user) .await?; let current_child = Arc::new(RwLock::new(process)); let manager = Some(tokio::spawn(Self::manager( @@ -534,9 +546,11 @@ impl Processor { let child = ProcessorChild { uuid: cache.uuid, cluster_path: cache.cluster_path, + started_at: Utc.timestamp_opt(cache.start_time as i64, 0).single().ok_or(anyhow::anyhow!("couldn't convert processor cache timestamp to Utc"))?, // TODO: Look into this current_child, manager, last_updated, + user: cache.user, }; let child = Arc::new(RwLock::new(child)); diff --git a/packages/core/src/store/settings.rs b/packages/core/src/store/settings.rs index 0c72c6da..5bba16f7 100644 --- a/packages/core/src/store/settings.rs +++ b/packages/core/src/store/settings.rs @@ -21,6 +21,9 @@ pub struct Settings { /// A global fullscreen Minecraft state. #[serde(default)] pub force_fullscreen: bool, + /// Whether to allow launching the same cluster under the same account. + #[serde(default)] + pub allow_parallel_running_clusters: bool, /// A global default [`Resolution`] for Minecraft. pub resolution: Resolution, /// A global [`JavaVersions`] list and default version. @@ -95,6 +98,7 @@ impl Settings { hide_close_prompt: true, disable_animations: false, force_fullscreen: false, + allow_parallel_running_clusters: false, resolution: Resolution::default(), java_versions: JavaVersions::new(), memory: Memory::default(),