Skip to content

Commit ba99ef0

Browse files
committed
feat: add shutdown scheduling functionality with IPC integration
1 parent ecb2360 commit ba99ef0

File tree

6 files changed

+350
-11
lines changed

6 files changed

+350
-11
lines changed

src/main/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ import './ipc/setting'
155155
import './ipc/ui'
156156
import './ipc/notif'
157157
import './ipc/dialogs'
158+
import './ipc/shutdown'
158159
import isDev from './shared/isDev'
159160

160161
function createTray() {

src/main/ipc/shutdown.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { exec } from 'node:child_process'
2+
import { ipcMain } from 'electron'
3+
import { EventsKeys } from '../../shared/constants/eventsKeys.constant'
4+
5+
interface ScheduledShutdown {
6+
id: string
7+
scheduledTime: Date
8+
isActive: boolean
9+
description?: string
10+
timeoutId?: NodeJS.Timeout
11+
}
12+
13+
const activeShutdowns: Map<string, ScheduledShutdown> = new Map()
14+
15+
ipcMain.handle(
16+
EventsKeys.SCHEDULE_SHUTDOWN,
17+
async (
18+
_,
19+
data: {
20+
delay: number
21+
scheduledTime: Date
22+
description?: string
23+
},
24+
) => {
25+
try {
26+
const id = Date.now().toString()
27+
28+
await cancelAllScheduledShutdowns()
29+
30+
const shutdownCommand = getShutdownCommand(Math.floor(data.delay / 1000))
31+
32+
return new Promise<string>((resolve, reject) => {
33+
exec(shutdownCommand, (error) => {
34+
if (error) {
35+
console.error('Failed to schedule shutdown:', error)
36+
reject(new Error('Failed to schedule shutdown'))
37+
return
38+
}
39+
40+
const shutdown: ScheduledShutdown = {
41+
id,
42+
scheduledTime: new Date(data.scheduledTime),
43+
isActive: true,
44+
description: data.description,
45+
}
46+
47+
activeShutdowns.set(id, shutdown)
48+
resolve(id)
49+
})
50+
})
51+
} catch (error) {
52+
throw new Error(`Failed to schedule shutdown: ${error}`)
53+
}
54+
},
55+
)
56+
57+
ipcMain.handle(
58+
EventsKeys.CANCEL_SCHEDULED_SHUTDOWN,
59+
async (_, shutdownId: string) => {
60+
try {
61+
const shutdown = activeShutdowns.get(shutdownId)
62+
if (!shutdown) {
63+
throw new Error('Shutdown not found')
64+
}
65+
66+
const cancelCommand = getCancelShutdownCommand()
67+
68+
return new Promise<boolean>((resolve, reject) => {
69+
exec(cancelCommand, (error) => {
70+
if (error) {
71+
console.error('Failed to cancel shutdown:', error)
72+
reject(new Error('Failed to cancel shutdown'))
73+
return
74+
}
75+
76+
activeShutdowns.delete(shutdownId)
77+
resolve(true)
78+
})
79+
})
80+
} catch (error) {
81+
throw new Error(`Failed to cancel shutdown: ${error}`)
82+
}
83+
},
84+
)
85+
86+
ipcMain.handle(EventsKeys.CLEAR_ALL_SHUTDOWNS, async () => {
87+
try {
88+
await cancelAllScheduledShutdowns()
89+
return { success: true }
90+
} catch (error) {
91+
throw new Error(`Failed to clear all shutdowns: ${error}`)
92+
}
93+
})
94+
95+
async function cancelAllScheduledShutdowns(): Promise<void> {
96+
const cancelCommand = getCancelShutdownCommand()
97+
98+
return new Promise((resolve) => {
99+
exec(cancelCommand, (error) => {
100+
if (error) {
101+
console.log('No existing shutdown to cancel or failed to cancel')
102+
}
103+
// Clear our tracking
104+
activeShutdowns.clear()
105+
resolve()
106+
})
107+
})
108+
}
109+
110+
function getShutdownCommand(delayInSeconds: number): string {
111+
const platform = process.platform
112+
113+
switch (platform) {
114+
case 'win32':
115+
return `shutdown /s /t ${delayInSeconds} /c "Scheduled shutdown from DNS Changer"`
116+
case 'darwin': // macOS
117+
return `sudo shutdown -h +${Math.ceil(delayInSeconds / 60)}` // macOS uses minutes
118+
case 'linux':
119+
return `shutdown -h +${Math.ceil(delayInSeconds / 60)}` // Linux uses minutes
120+
default:
121+
throw new Error(`Unsupported platform: ${platform}`)
122+
}
123+
}
124+
125+
function getCancelShutdownCommand(): string {
126+
const platform = process.platform
127+
128+
switch (platform) {
129+
case 'win32':
130+
return 'shutdown /a'
131+
case 'darwin': // macOS
132+
return 'sudo killall shutdown'
133+
case 'linux':
134+
return 'shutdown -c'
135+
default:
136+
throw new Error(`Unsupported platform: ${platform}`)
137+
}
138+
}

src/preload/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
33
import { contextBridge, ipcRenderer } from 'electron'
44

5-
import { Server, ServerStore } from '../shared/interfaces/server.interface'
5+
import os from 'node:os'
6+
import { store } from '../main/store/store'
67
import { EventsKeys } from '../shared/constants/eventsKeys.constant'
8+
import { Server, ServerStore } from '../shared/interfaces/server.interface'
79
import {
810
SettingInStore,
911
StoreKey,
1012
} from '../shared/interfaces/settings.interface'
11-
import os from 'node:os'
12-
import { store } from '../main/store/store'
1313

1414
export const ipcPreload = {
1515
setDns: (server: Server) => ipcRenderer.invoke(EventsKeys.SET_DNS, server),
@@ -43,6 +43,14 @@ export const ipcPreload = {
4343
ipcRenderer.invoke(EventsKeys.TOGGLE_PIN, server),
4444
openLogFile: () => ipcRenderer.send(EventsKeys.OPEN_LOG_FILE),
4545
openDevTools: () => ipcRenderer.send(EventsKeys.OPEN_DEV_TOOLS),
46+
scheduleShutdown: (data: {
47+
delay: number
48+
scheduledTime: Date
49+
description?: string
50+
}) => ipcRenderer.invoke(EventsKeys.SCHEDULE_SHUTDOWN, data),
51+
cancelScheduledShutdown: (shutdownId: string) =>
52+
ipcRenderer.invoke(EventsKeys.CANCEL_SCHEDULED_SHUTDOWN, shutdownId),
53+
clearAllShutdowns: () => ipcRenderer.invoke(EventsKeys.CLEAR_ALL_SHUTDOWNS),
4654
}
4755

4856
export const uiPreload = {

src/renderer/app.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
import React, { useState, useEffect } from 'react'
1+
import React, { useState, useEffect, useMemo } from 'react'
22
import { BottomNavigation, Tooltip } from 'react-daisyui'
33

4-
import { TbSettings, TbSmartHome } from 'react-icons/tb'
4+
import { Toaster } from 'react-hot-toast'
5+
import { IconType } from 'react-icons'
6+
import { BsPower } from 'react-icons/bs'
57
import { MdOutlineExplore } from 'react-icons/md'
6-
import { HomePage } from './pages/home.page'
7-
import { SettingPage } from './pages/setting.page'
8-
import { loadLocaleAsync } from '../i18n/i18n-util.async'
8+
import { TbSettings, TbSmartHome } from 'react-icons/tb'
99
import TypesafeI18n from '../i18n/i18n-react'
10+
import { loadLocaleAsync } from '../i18n/i18n-util.async'
1011
import {
1112
SettingInStore,
1213
Settings,
1314
} from '../shared/interfaces/settings.interface'
1415
import { PageWrapper } from './Wrappers/pages.wrapper'
15-
import { getThemeSystem, themeChanger } from './utils/theme.util'
16-
import { IconType } from 'react-icons'
1716
import { ExplorePage } from './pages/explore.page'
18-
import { Toaster } from 'react-hot-toast'
17+
import { HomePage } from './pages/home.page'
18+
import { SettingPage } from './pages/setting.page'
19+
import { ShutdownPage } from './pages/shutdown.page'
20+
import { getThemeSystem, themeChanger } from './utils/theme.util'
1921
export let settingStore: SettingInStore = window.storePreload.get('settings')
2022
import ReactGA from 'react-ga4'
2123

@@ -37,6 +39,12 @@ export function App() {
3739
icon: MdOutlineExplore,
3840
name: 'Explore',
3941
},
42+
{
43+
key: '/shutdown',
44+
element: <ShutdownPage />,
45+
icon: BsPower,
46+
name: 'Shutdown',
47+
},
4048
{
4149
key: '/setting',
4250
element: <SettingPage />,

0 commit comments

Comments
 (0)