Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/components/configuration/JavascriptActionConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onBeforeUnmount, ref } from 'vue'

import {
deleteJavascriptActionConfig,
Expand All @@ -57,8 +57,11 @@ import {
JavascriptActionConfig,
registerJavascriptActionConfig,
} from '@/libs/actions/free-javascript'
import { cockpitTimerManager } from '@/libs/timer-management'
import { useAppInterfaceStore } from '@/stores/appInterface'

const javascriptActionConfigTestRunnerId = 'javascript-action-test-runner'

const emit = defineEmits<{
(e: 'action-saved'): void
(e: 'action-deleted'): void
Expand Down Expand Up @@ -96,6 +99,9 @@ const createActionConfig = (): void => {
}

const saveActionConfig = (): void => {
// Clear all existing managed timers for the test runner
cockpitTimerManager.clearAllTimersForOwner(javascriptActionConfigTestRunnerId)

createActionConfig()
closeActionDialog()
}
Expand All @@ -110,7 +116,13 @@ const resetNewAction = (): void => {
}

const testAction = (): void => {
// Clear all existing managed timers for the test runner
cockpitTimerManager.clearAllTimersForOwner(javascriptActionConfigTestRunnerId)

// Execute the action code with managed timers
cockpitTimerManager.setCurrentOwnerId(javascriptActionConfigTestRunnerId)
executeActionCode(newActionConfig.value.code)
cockpitTimerManager.clearCurrentOwnerId()
}

const exportAction = (id: string): void => {
Expand Down Expand Up @@ -144,6 +156,7 @@ const closeActionDialog = (): void => {

const openEditDialog = (id: string): void => {
const action = getJavascriptActionConfig(id)
cockpitTimerManager.clearAllTimersForOwner(id)
if (action) {
editMode.value = true
newActionConfig.value = JSON.parse(JSON.stringify(action)) // Deep copy
Expand All @@ -156,6 +169,10 @@ const openNewDialog = (): void => {
actionDialog.value.show = true
}

onBeforeUnmount(() => {
cockpitTimerManager.clearAllTimersForOwner(javascriptActionConfigTestRunnerId)
})

defineExpose({
openEditDialog,
openNewDialog,
Expand Down
22 changes: 19 additions & 3 deletions src/components/widgets/DoItYourself.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { computed, onBeforeMount, onBeforeUnmount, onMounted, ref, toRefs } from 'vue'

import { useBlueOsStorage } from '@/composables/settingsSyncer'
import { cockpitTimerManager } from '@/libs/timer-management'
import { useAppInterfaceStore } from '@/stores/appInterface'
import { useWidgetManagerStore } from '@/stores/widgetManager'
import type { Widget } from '@/types/widgets'
Expand Down Expand Up @@ -309,19 +310,32 @@ const applyChanges = (): void => {
executeUserScript()
}

const getDiyWidgetId = (): string => {
return `diy-widget-${widget.value.hash}`
}

const executeUserScript = (): void => {
const js = widget.value.options.js || ''
const scriptElementId = `diy-script-${widget.value.hash}`
const diyWidgetId = getDiyWidgetId()

// Remove existing script element
document.getElementById(scriptElementId)?.remove()
document.getElementById(diyWidgetId)?.remove()

// Clear any existing timers from previous script execution
cockpitTimerManager.clearAllTimersForOwner(diyWidgetId)

// Set the current owner ID so any setInterval/setTimeout calls in the user script are tracked
cockpitTimerManager.setCurrentOwnerId(diyWidgetId)

// Create new script element
const scriptEl = document.createElement('script')
scriptEl.type = 'text/javascript'
scriptEl.textContent = js
scriptEl.id = scriptElementId
scriptEl.id = diyWidgetId
document.body.appendChild(scriptEl)

// Clear the current owner ID
cockpitTimerManager.clearCurrentOwnerId()
}

const resetChanges = (): void => {
Expand Down Expand Up @@ -451,6 +465,8 @@ onMounted(() => {

onBeforeUnmount(() => {
finishEditor()
// Clear any timers created by the user script
cockpitTimerManager.clearAllTimersForOwner(getDiyWidgetId())
})
</script>

Expand Down
26 changes: 26 additions & 0 deletions src/libs/cosmos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
registerNewAction,
unregisterActionCallback,
} from './joystick/protocols/cockpit-actions'
import { type CockpitTimerManager, cockpitTimerManager } from './timer-management'

declare global {
/**
Expand Down Expand Up @@ -187,6 +188,27 @@ declare global {
* @param id - The id of the action to execute the callback for
*/
executeActionCallback: typeof executeActionCallback

// Managed timers
/**
* Managed setInterval that clears previous intervals when action is re-run
* @param {Function} callback - The function to execute
* @param {number} delay - The delay in milliseconds
* @returns {number} The interval ID
*/
setInterval: (callback: () => void, delay?: number) => number
/**
* Managed setTimeout that clears when action is re-run (but not when stopped)
* @param {Function} callback - The function to execute
* @param {number} delay - The delay in milliseconds
* @returns {number} The timeout ID
*/
setTimeout: (callback: () => void, delay?: number) => number

/**
* Internal timer manager (used for action lifecycle integration)
*/
timerManager: CockpitTimerManager
}
/**
* Electron API exposed through preload script
Expand Down Expand Up @@ -461,6 +483,10 @@ window.cockpit = {
registerActionCallback: registerActionCallback,
unregisterActionCallback: unregisterActionCallback,
executeActionCallback: executeActionCallback,
// Timers management:
setInterval: (callback: () => void, delay?: number) => cockpitTimerManager.createManagedSetInterval(callback, delay),
setTimeout: (callback: () => void, delay?: number) => cockpitTimerManager.createManagedSetTimeout(callback, delay),
Comment on lines +487 to +488
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good if we can match the interface for the real ones - returning ID values that allow user code to clear them if they want to (since that could be relevant to complex functionality).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree.

timerManager: cockpitTimerManager,
}

/* c8 ignore start */
Expand Down
8 changes: 8 additions & 0 deletions src/libs/joystick/protocols/cockpit-actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable vue/max-len */
/* eslint-disable prettier/prettier */
/* eslint-disable max-len */
import { cockpitTimerManager } from '@/libs/timer-management'
import { type ProtocolAction,JoystickProtocol } from '@/types/joystick'

/**
Expand Down Expand Up @@ -100,8 +101,15 @@ export class CockpitActionsManager {
}

console.debug(`Executing action callback for action ${id}.`)

// Clear all existing managed timers for the action
cockpitTimerManager.clearAllTimersForOwner(id)

try {
// Execute the action callback with managed timers
cockpitTimerManager.setCurrentOwnerId(id)
callbackEntry.callback()
cockpitTimerManager.clearCurrentOwnerId()
} catch (error) {
console.error(`Error executing action callback for action ${id}.`, error)
}
Expand Down
110 changes: 110 additions & 0 deletions src/libs/timer-management.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Timer management for cockpit
/**
* Information about a timer
*/
export interface TimerInfo {
/**
* The ID of the timer
*/
id: number
/**
* The type of the timer
*/
type: 'interval' | 'timeout'
}

/**
* Manager for Cockpit timers
* Handles the creation and clearing of timers for a specific owner, making sure that only one timer is running at a time for a given owner.
*/
export class CockpitTimerManager {
private currentOwnerId: string | null = null
private ownerTimers: Map<string, TimerInfo[]> = new Map()

/**
* Set the current owner ID
* @param {string} ownerId - The ID of the owner
*/
setCurrentOwnerId(ownerId: string): void {
this.currentOwnerId = ownerId
}

/**
* Clear the current owner ID
*/
clearCurrentOwnerId(): void {
this.currentOwnerId = null
}

/**
* Add a timer to an owner
* @param {string} ownerId - The ID of the owner
* @param {number} timerId - The ID of the timer
* @param {'interval' | 'timeout'} type - The type of the timer
*/
addTimer(ownerId: string, timerId: number, type: 'interval' | 'timeout'): void {
if (!this.ownerTimers.has(ownerId)) {
this.ownerTimers.set(ownerId, [])
}
this.ownerTimers.get(ownerId)!.push({ id: timerId, type })
}

/**
* Clear all timers for an owner
* @param {string} ownerId - The ID of the owner
*/
clearAllTimersForOwner(ownerId: string): void {
console.log(`Clearing ${this.ownerTimers.get(ownerId)?.length ?? 0} timers for ${ownerId}.`)
const timers = this.ownerTimers.get(ownerId)
if (!timers) return

timers.forEach((timer) => {
if (timer.type === 'interval') {
clearInterval(timer.id)
} else {
clearTimeout(timer.id)
}
})

this.ownerTimers.delete(ownerId)
}

/**
* Create a managed interval
* @param {Function} callback - The callback to execute
* @param {number} delay - The delay in milliseconds
* @param {string} ownerId - The ID of the owner
* @returns {number} The interval ID
*/
createManagedSetInterval(callback: () => void, delay?: number, ownerId?: string): number {
console.log(`Creating managed interval with ${delay}ms delay.`)
if (!ownerId && !this.currentOwnerId) {
throw new Error('No owner ID provided and no current owner ID set.')
}
const intervalId = window.setInterval(callback, delay)
this.addTimer((ownerId || this.currentOwnerId)!, intervalId, 'interval')
return intervalId
}

/**
* Create a managed timeout
* @param {Function} callback - The callback to execute
* @param {number} delay - The delay in milliseconds
* @param {string} ownerId - The ID of the owner
* @returns {number} The timeout ID
*/
createManagedSetTimeout(callback: () => void, delay?: number, ownerId?: string): number {
console.log(`Creating managed timeout with ${delay}ms delay.`)
if (!ownerId && !this.currentOwnerId) {
throw new Error('No owner ID provided and no current owner ID set.')
}
const timeoutId = window.setTimeout(callback, delay)
this.addTimer((ownerId || this.currentOwnerId)!, timeoutId, 'timeout')
return timeoutId
}
}

const cockpitTimerManager = new CockpitTimerManager()

// Export the timer manager for use in action execution
export { cockpitTimerManager }
Loading