Skip to content

Commit f0997f8

Browse files
wip
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
1 parent 3a74a30 commit f0997f8

File tree

4 files changed

+230
-11
lines changed

4 files changed

+230
-11
lines changed

core/frontend/src/views/ServiceManagerView.vue

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,23 @@
232232
Command
233233
</td>
234234
<td class="font-mono text-body-2">
235-
{{ commandString }}
235+
{{ selected_service.command?.join(' ') ?? '' }}
236+
</td>
237+
</tr>
238+
<tr>
239+
<td class="font-weight-bold">
240+
Custom Command
241+
</td>
242+
<td>
243+
<v-text-field
244+
v-model="config_form.command"
245+
dense
246+
outlined
247+
hide-details
248+
placeholder="Override command (space-separated)"
249+
class="font-mono text-body-2"
250+
style="max-width: 500px"
251+
/>
236252
</td>
237253
</tr>
238254
<tr v-if="selected_service.cwd">
@@ -261,6 +277,18 @@
261277
<div class="d-flex align-center mb-2">
262278
<h4>Behavior Settings</h4>
263279
<v-spacer />
280+
<v-btn
281+
v-tooltip="'Reset to defaults'"
282+
small
283+
text
284+
class="mr-2"
285+
@click="resetServiceConfig"
286+
>
287+
<v-icon left small>
288+
mdi-restore
289+
</v-icon>
290+
Reset
291+
</v-btn>
264292
<v-btn
265293
v-tooltip="'Save configuration changes'"
266294
small
@@ -276,6 +304,15 @@
276304
</v-btn>
277305
</div>
278306
<v-row dense>
307+
<v-col cols="12" sm="6" md="4">
308+
<v-switch
309+
v-model="config_form.enabled"
310+
label="Service enabled"
311+
dense
312+
hide-details
313+
class="mt-0"
314+
/>
315+
</v-col>
279316
<v-col cols="12" sm="6" md="4">
280317
<v-switch
281318
v-model="config_form.restart"
@@ -680,6 +717,8 @@ export default Vue.extend({
680717
action_loading: false,
681718
config_saving: false,
682719
config_form: {
720+
enabled: true,
721+
command: '' as string,
683722
restart: false,
684723
restart_delay_sec: 1.0,
685724
stop_timeout_sec: 10.0,
@@ -713,9 +752,6 @@ export default Vue.extend({
713752
running_count(): number {
714753
return this.services.filter((s) => s.status === 'running').length
715754
},
716-
commandString(): string {
717-
return this.selected_service?.command?.join(' ') ?? ''
718-
},
719755
hasEnvironmentVariables(): boolean {
720756
const env = this.selected_service?.env
721757
return !!env && Object.keys(env).length > 0
@@ -724,7 +760,10 @@ export default Vue.extend({
724760
if (!this.selected_service) return false
725761
const s = this.selected_service
726762
const f = this.config_form
727-
return f.restart !== s.restart
763+
const originalCommand = s.command?.join(' ') ?? ''
764+
return f.enabled !== s.enabled
765+
|| f.command !== '' && f.command !== originalCommand
766+
|| f.restart !== s.restart
728767
|| f.restart_delay_sec !== s.restart_delay_sec
729768
|| f.stop_timeout_sec !== s.stop_timeout_sec
730769
|| (f.limits.cpu_cores ?? 0) !== (s.limits?.cpu_cores ?? 0)
@@ -917,6 +956,8 @@ export default Vue.extend({
917956
918957
populateConfigForm(service: ServiceState): void {
919958
this.config_form = {
959+
enabled: service.enabled,
960+
command: service.command?.join(' ') ?? '',
920961
restart: service.restart,
921962
restart_delay_sec: service.restart_delay_sec,
922963
stop_timeout_sec: service.stop_timeout_sec,
@@ -1043,7 +1084,9 @@ export default Vue.extend({
10431084
async saveConfig(): Promise<void> {
10441085
if (!this.selected_service) return
10451086
this.config_saving = true
1046-
const body = {
1087+
const originalCommand = this.selected_service.command?.join(' ') ?? ''
1088+
const body: Record<string, unknown> = {
1089+
enabled: this.config_form.enabled,
10471090
restart: this.config_form.restart,
10481091
restart_delay_sec: this.config_form.restart_delay_sec,
10491092
stop_timeout_sec: this.config_form.stop_timeout_sec,
@@ -1053,6 +1096,10 @@ export default Vue.extend({
10531096
max_pids: this.config_form.limits.max_pids ?? 0,
10541097
},
10551098
}
1099+
// Only send command if it's been changed
1100+
if (this.config_form.command && this.config_form.command !== originalCommand) {
1101+
body.command = this.config_form.command.split(/\s+/).filter((s: string) => s.length > 0)
1102+
}
10561103
await back_axios({
10571104
method: 'patch',
10581105
url: `${API_URL}/services/${this.selected_service.name}/config`,
@@ -1073,6 +1120,24 @@ export default Vue.extend({
10731120
})
10741121
},
10751122
1123+
async resetServiceConfig(): Promise<void> {
1124+
if (!this.selected_service) return
1125+
await back_axios({
1126+
method: 'post',
1127+
url: `${API_URL}/services/${this.selected_service.name}/reset`,
1128+
timeout: 10000,
1129+
})
1130+
.then((response) => {
1131+
const data = response.data as MessageResponse
1132+
message_manager.emitMessage(MessageLevel.Success, data.message)
1133+
this.fetchServices()
1134+
})
1135+
.catch((error) => {
1136+
if (isBackendOffline(error)) return
1137+
message_manager.emitMessage(MessageLevel.Error, `Failed to reset config: ${error}`)
1138+
})
1139+
},
1140+
10761141
formatTimestamp(timestamp: string): string {
10771142
try {
10781143
return format(new Date(timestamp), 'HH:mm:ss.SSS')

core/services/service_manager/service_manager/config.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,91 @@ def get_service(self, name: str) -> ServiceSpec | None:
167167
if svc.name == name:
168168
return svc
169169
return None
170+
171+
172+
def _default_overrides_path() -> Path:
173+
"""Get default path for user configuration overrides."""
174+
import os
175+
176+
if os.geteuid() == 0:
177+
return Path("/etc/service-manager/overrides.json")
178+
return Path.home() / ".config/service-manager/overrides.json"
179+
180+
181+
class ConfigPersistence:
182+
"""Handles saving and loading user configuration overrides."""
183+
184+
def __init__(self, overrides_path: Path | None = None):
185+
self.overrides_path = overrides_path or _default_overrides_path()
186+
self._overrides: dict[str, Any] = {}
187+
self._load_overrides()
188+
189+
def _load_overrides(self) -> None:
190+
"""Load overrides from file if it exists."""
191+
import json
192+
193+
if self.overrides_path.exists():
194+
try:
195+
self._overrides = json.loads(self.overrides_path.read_text(encoding="utf-8"))
196+
except (json.JSONDecodeError, OSError):
197+
self._overrides = {}
198+
else:
199+
self._overrides = {}
200+
201+
def _save_overrides(self) -> None:
202+
"""Save overrides to file."""
203+
import json
204+
205+
self.overrides_path.parent.mkdir(parents=True, exist_ok=True)
206+
self.overrides_path.write_text(json.dumps(self._overrides, indent=2), encoding="utf-8")
207+
208+
def get_service_overrides(self, name: str) -> dict[str, Any]:
209+
"""Get overrides for a specific service."""
210+
services: dict[str, dict[str, Any]] = self._overrides.get("services", {})
211+
return services.get(name, {})
212+
213+
def set_service_override(self, name: str, key: str, value: Any) -> None:
214+
"""Set an override for a service."""
215+
if "services" not in self._overrides:
216+
self._overrides["services"] = {}
217+
if name not in self._overrides["services"]:
218+
self._overrides["services"][name] = {}
219+
self._overrides["services"][name][key] = value
220+
self._save_overrides()
221+
222+
def clear_service_overrides(self, name: str) -> None:
223+
"""Clear all overrides for a service."""
224+
services = self._overrides.get("services", {})
225+
if name in services:
226+
del services[name]
227+
self._save_overrides()
228+
229+
def clear_all_overrides(self) -> None:
230+
"""Clear all service overrides."""
231+
self._overrides = {}
232+
self._save_overrides()
233+
234+
def apply_overrides(self, spec: ServiceSpec) -> None:
235+
"""Apply saved overrides to a service spec."""
236+
overrides = self.get_service_overrides(spec.name)
237+
if not overrides:
238+
return
239+
240+
if "enabled" in overrides:
241+
spec.enabled = overrides["enabled"]
242+
if "command" in overrides:
243+
spec.command = overrides["command"]
244+
if "restart" in overrides:
245+
spec.restart = overrides["restart"]
246+
if "restart_delay_sec" in overrides:
247+
spec.restart_delay_sec = overrides["restart_delay_sec"]
248+
if "stop_timeout_sec" in overrides:
249+
spec.stop_timeout_sec = overrides["stop_timeout_sec"]
250+
if "limits" in overrides:
251+
limits_data = overrides["limits"]
252+
if "cpu_cores" in limits_data:
253+
spec.limits.cpu_cores = limits_data["cpu_cores"]
254+
if "memory_mb" in limits_data:
255+
spec.limits.memory_mb = limits_data["memory_mb"]
256+
if "max_pids" in limits_data:
257+
spec.limits.max_pids = limits_data["max_pids"]

core/services/service_manager/service_manager/daemon.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
import uvicorn
1616
from service_manager.cgroup import CgroupController
17-
from service_manager.config import AgentConfig
17+
from service_manager.config import AgentConfig, ConfigPersistence
1818
from service_manager.metrics import MetricsSampler
1919
from service_manager.output import OutputCapture
2020
from service_manager.registry import ServiceRegistry
@@ -129,6 +129,7 @@ def __init__(self, config_path: Path | None = None, foreground: bool = False):
129129
self._output: OutputCapture | None = None
130130
self._supervisor: ProcessSupervisor | None = None
131131
self._sampler: MetricsSampler | None = None
132+
self._persistence: ConfigPersistence | None = None
132133

133134
def _setup_logging(self) -> None:
134135
"""Configure logging."""
@@ -220,15 +221,17 @@ async def _start_components(self) -> None:
220221
self._cgroup,
221222
self.config.metrics_interval_sec,
222223
)
224+
self._persistence = ConfigPersistence()
223225

224226
# Ensure cgroup root exists
225227
await self._cgroup.ensure_root()
226228

227229
# Reconcile with any orphan cgroups from previous run
228230
await self._reconcile_orphans()
229231

230-
# Register services from config
232+
# Register services from config, applying user overrides
231233
for spec in self.config.services:
234+
self._persistence.apply_overrides(spec)
232235
self._registry.register(spec)
233236

234237
# Start enabled services
@@ -256,6 +259,7 @@ def _start_uvicorn_server(self) -> None:
256259
self._output,
257260
self._sampler,
258261
self._cgroup,
262+
self._persistence,
259263
)
260264

261265
config = self.config

0 commit comments

Comments
 (0)