161 lines
5.7 KiB
TypeScript
161 lines
5.7 KiB
TypeScript
import * as vscode from 'vscode';
|
|
import * as https from 'https';
|
|
import * as http from 'http';
|
|
import { URL } from 'url';
|
|
|
|
/** Ensure URL has http(s):// prefix and no trailing slash */
|
|
function normalizeUrl(raw: string): string {
|
|
let url = raw.trim().replace(/\/+$/, '');
|
|
if (!/^https?:\/\//i.test(url)) {
|
|
url = 'http://' + url;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
export class ADClient {
|
|
private productionMode = false;
|
|
private _onProductionModeChanged = new vscode.EventEmitter<boolean>();
|
|
readonly onProductionModeChanged = this._onProductionModeChanged.event;
|
|
private log: vscode.OutputChannel;
|
|
|
|
constructor(log: vscode.OutputChannel) {
|
|
this.log = log;
|
|
}
|
|
|
|
private getConfig() {
|
|
const config = vscode.workspace.getConfiguration('appdaemon');
|
|
return {
|
|
url: normalizeUrl(config.get<string>('adUrl', 'http://localhost:5050'))
|
|
};
|
|
}
|
|
|
|
private async request<T = unknown>(method: string, path: string, body?: unknown): Promise<T> {
|
|
const { url } = this.getConfig();
|
|
const fullUrl = new URL(path, url);
|
|
const isHttps = fullUrl.protocol === 'https:';
|
|
const lib = isHttps ? https : http;
|
|
|
|
this.log.appendLine(`[AD] ${method} ${fullUrl.href}`);
|
|
|
|
return new Promise<T>((resolve, reject) => {
|
|
const options: http.RequestOptions = {
|
|
hostname: fullUrl.hostname,
|
|
port: parseInt(fullUrl.port, 10) || (isHttps ? 443 : 5050),
|
|
path: fullUrl.pathname + fullUrl.search,
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
};
|
|
|
|
const req = lib.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', (chunk: Buffer) => { data += chunk; });
|
|
res.on('end', () => {
|
|
this.log.appendLine(`[AD] ${res.statusCode} (${data.length} bytes)`);
|
|
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
try {
|
|
resolve(JSON.parse(data) as T);
|
|
} catch {
|
|
resolve(data as unknown as T);
|
|
}
|
|
} else {
|
|
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', (err) => {
|
|
this.log.appendLine(`[AD] ERROR: ${err.message}`);
|
|
reject(err);
|
|
});
|
|
req.setTimeout(10000, () => {
|
|
req.destroy();
|
|
reject(new Error('Request timed out'));
|
|
});
|
|
|
|
if (body) {
|
|
req.write(JSON.stringify(body));
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Restart a specific list of AppDaemon apps by name.
|
|
*/
|
|
async restartApps(appNames: string[]): Promise<void> {
|
|
this.log.appendLine(`[AD] Restarting specific apps: ${appNames.join(', ')}`);
|
|
await Promise.all(appNames.map(app =>
|
|
this.request('POST', '/api/appdaemon/service/admin/app/restart', { app })
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Reload all AppDaemon apps by fetching the app list then restarting each one.
|
|
*/
|
|
async reloadApps(): Promise<void> {
|
|
// Fetch app names from admin state
|
|
type AdminState = { state: Record<string, unknown> };
|
|
const resp = await this.request<AdminState>('GET', '/api/appdaemon/state/admin');
|
|
const stateMap = resp?.state ?? (resp as unknown as Record<string, unknown>);
|
|
const appNames = Object.keys(stateMap)
|
|
.filter(k => k.startsWith('app.'))
|
|
.map(k => k.slice(4)); // strip "app." prefix
|
|
|
|
if (appNames.length === 0) {
|
|
throw new Error('No apps found in AppDaemon state');
|
|
}
|
|
|
|
this.log.appendLine(`[AD] Restarting ${appNames.length} apps: ${appNames.join(', ')}`);
|
|
|
|
await Promise.all(appNames.map(app =>
|
|
this.request('POST', '/api/appdaemon/service/admin/app/restart', { app })
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Set AppDaemon production mode on/off via the admin API.
|
|
*/
|
|
async setProductionMode(mode: boolean): Promise<void> {
|
|
await this.request('POST', '/api/appdaemon/service/admin/production_mode/set', { mode });
|
|
this.productionMode = mode;
|
|
this._onProductionModeChanged.fire(mode);
|
|
}
|
|
|
|
/**
|
|
* Try to read the current production mode from AppDaemon state.
|
|
*/
|
|
async fetchProductionMode(): Promise<boolean> {
|
|
try {
|
|
const resp = await this.request<{ state: Record<string, { state?: unknown }> }>('GET', '/api/appdaemon/state/admin');
|
|
const state = resp?.state ?? resp as unknown as Record<string, { state?: unknown }>;
|
|
if (state && typeof state === 'object') {
|
|
for (const key of Object.keys(state)) {
|
|
if (key.includes('production_mode')) {
|
|
const val = (state[key] as { state?: unknown })?.state;
|
|
this.productionMode = val === true || val === 'True' || val === 'true';
|
|
this._onProductionModeChanged.fire(this.productionMode);
|
|
return this.productionMode;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Fallback to cached value
|
|
}
|
|
return this.productionMode;
|
|
}
|
|
|
|
isProductionMode(): boolean {
|
|
return this.productionMode;
|
|
}
|
|
|
|
async toggleProductionMode(): Promise<void> {
|
|
await this.setProductionMode(!this.productionMode);
|
|
}
|
|
|
|
dispose() {
|
|
this._onProductionModeChanged.dispose();
|
|
}
|
|
}
|