269 lines
8.2 KiB
TypeScript
269 lines
8.2 KiB
TypeScript
// services/drone.ts
|
|
// Copyright (C) 2026 Robert Colbert <rob.colbert@openplatform.us>
|
|
// All Rights Reserved
|
|
|
|
import { PopulateOptions } from "mongoose";
|
|
|
|
import { IUser, DroneStatus, IDroneRegistration, GadgetId } from "@gadget/api";
|
|
import DroneRegistration from "../models/drone-registration.ts";
|
|
|
|
import { DtpService } from "../lib/service.ts";
|
|
import SocketService from "./socket.ts";
|
|
|
|
export interface IDroneDefinition {
|
|
hostname: string;
|
|
workspaceDir: string;
|
|
}
|
|
|
|
class DroneService extends DtpService {
|
|
private populateDroneRegistration: PopulateOptions[];
|
|
|
|
constructor() {
|
|
super();
|
|
this.populateDroneRegistration = [
|
|
{
|
|
path: "user",
|
|
select: "-passwordSalt -password",
|
|
},
|
|
];
|
|
}
|
|
|
|
get name(): string {
|
|
return "DroneService";
|
|
}
|
|
|
|
get slug(): string {
|
|
return "svc:drone";
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
this.log.info("service started");
|
|
}
|
|
|
|
async stop(): Promise<void> {
|
|
this.log.info("service stopped");
|
|
}
|
|
|
|
/**
|
|
* The drone calls this service to register itself into the platform as
|
|
* belonging to a specific User. Users can only ever access their own drones.
|
|
* @param user The user for which a drone is being registered
|
|
* @param definition The definition of the drone being registered
|
|
* @returns the new drone registration
|
|
*/
|
|
async register(
|
|
user: IUser,
|
|
definition: IDroneDefinition,
|
|
): Promise<IDroneRegistration> {
|
|
const NOW = new Date();
|
|
let registration = new DroneRegistration();
|
|
|
|
registration.createdAt = NOW;
|
|
registration.updatedAt = NOW;
|
|
registration.user = user._id;
|
|
registration.hostname = definition.hostname;
|
|
registration.workspaceDir = definition.workspaceDir;
|
|
registration.status = DroneStatus.Starting;
|
|
|
|
this.log.info("registering drone", { hostname: registration.hostname });
|
|
await registration.save();
|
|
|
|
return registration.populate(this.populateDroneRegistration);
|
|
}
|
|
|
|
/**
|
|
* The drone calls this service to unregister from the platform when shutting
|
|
* down. This will set the status to offline and remove any associated
|
|
* tasks.
|
|
* @param registration The drone registration being unregistered (offline)
|
|
* @returns the updated drone registration
|
|
*/
|
|
async unregister(
|
|
registration: IDroneRegistration,
|
|
): Promise<IDroneRegistration> {
|
|
this.log.info("unregistering drone", { hostname: registration.hostname });
|
|
return this.setStatus(registration, DroneStatus.Offline);
|
|
}
|
|
|
|
/**
|
|
* Retrieve a drone registration by _id. Throws if the registration doesn't
|
|
* exist.
|
|
* @param registrationId The _id of the drone registration to be fetched
|
|
* @returns the drone registration document
|
|
*/
|
|
async getById(registrationId: GadgetId): Promise<IDroneRegistration> {
|
|
const registration = await DroneRegistration.findById(
|
|
registrationId,
|
|
).populate(this.populateDroneRegistration);
|
|
if (!registration) {
|
|
const error = new Error("drone registration not found");
|
|
error.statusCode = 404;
|
|
throw error;
|
|
}
|
|
return registration;
|
|
}
|
|
|
|
/**
|
|
* Request a User's list of registered drones.
|
|
* @param user The user for which a list of registered drones is being requested
|
|
* @param statusFilter Optional array of statuses to exclude from the results
|
|
* @returns The list of drone registrations for the user.
|
|
*/
|
|
async getForUser(
|
|
user: IUser,
|
|
statusFilter?: DroneStatus[],
|
|
): Promise<IDroneRegistration[]> {
|
|
const query: Record<string, unknown> = { user: user._id };
|
|
|
|
if (statusFilter && statusFilter.length > 0) {
|
|
query.status = { $nin: statusFilter };
|
|
}
|
|
|
|
const registrations = await DroneRegistration.find(query)
|
|
.sort({ hostname: 1, workspaceDir: 1 })
|
|
.populate(this.populateDroneRegistration);
|
|
return registrations;
|
|
}
|
|
|
|
/**
|
|
* Request a User's complete list of drone registrations for the Drone Manager.
|
|
* Returns all drones (online and offline) without filtering.
|
|
* @param user The user for which a list of drone registrations is being requested
|
|
* @returns The complete list of drone registrations for the user.
|
|
*/
|
|
async getForManager(user: IUser): Promise<IDroneRegistration[]> {
|
|
const registrations = await DroneRegistration.find({ user: user._id })
|
|
.sort({ status: 1, hostname: 1, workspaceDir: 1 })
|
|
.populate(this.populateDroneRegistration);
|
|
return registrations;
|
|
}
|
|
|
|
/**
|
|
* Request a drone to terminate itself. Sets a 10-second timeout to wait for
|
|
* the drone to mark itself as offline. If the timeout expires, forces the
|
|
* status to offline.
|
|
* @param registrationId The _id of the drone registration to terminate
|
|
* @returns Promise that resolves when termination is complete (success or timeout)
|
|
*/
|
|
async requestTermination(
|
|
registrationId: GadgetId,
|
|
): Promise<{ success: boolean; message?: string }> {
|
|
const registration = await this.getById(registrationId);
|
|
|
|
if (registration.status === DroneStatus.Offline) {
|
|
return { success: false, message: "Drone is already offline" };
|
|
}
|
|
|
|
let droneSession: any;
|
|
try {
|
|
droneSession = SocketService.getDroneSession(registration);
|
|
} catch (error) {
|
|
// Drone is not connected - mark as offline immediately
|
|
this.log.warn("drone not connected, marking offline", {
|
|
registrationId: registrationId,
|
|
hostname: registration.hostname,
|
|
});
|
|
await this.setStatus(registration, DroneStatus.Offline);
|
|
return {
|
|
success: true,
|
|
message: "Drone was not connected. Marked offline.",
|
|
};
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
let resolved = false;
|
|
let timeoutHandle: NodeJS.Timeout;
|
|
|
|
const cleanup = () => {
|
|
clearTimeout(timeoutHandle);
|
|
resolved = true;
|
|
};
|
|
|
|
timeoutHandle = setTimeout(async () => {
|
|
if (resolved) return;
|
|
cleanup();
|
|
|
|
this.log.warn("drone termination timed out, forcing offline", {
|
|
registrationId: registrationId,
|
|
hostname: registration.hostname,
|
|
});
|
|
|
|
await this.setStatus(registration, DroneStatus.Offline);
|
|
resolve({
|
|
success: true,
|
|
message: "Drone termination timed out. Marked offline.",
|
|
});
|
|
}, 10000);
|
|
|
|
droneSession.socket.emit(
|
|
"requestTermination",
|
|
async (success: boolean) => {
|
|
if (resolved) return;
|
|
|
|
if (!success) {
|
|
cleanup();
|
|
resolve({
|
|
success: false,
|
|
message: "Drone rejected termination request",
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.log.info("drone accepted termination request", {
|
|
registrationId: registrationId,
|
|
hostname: registration.hostname,
|
|
});
|
|
|
|
const checkInterval = setInterval(async () => {
|
|
try {
|
|
const updated = await this.getById(registrationId);
|
|
if (updated.status === DroneStatus.Offline) {
|
|
clearInterval(checkInterval);
|
|
cleanup();
|
|
resolve({ success: true });
|
|
}
|
|
} catch (error) {
|
|
clearInterval(checkInterval);
|
|
cleanup();
|
|
resolve({
|
|
success: true,
|
|
message: "Drone termination timed out. Marked offline.",
|
|
});
|
|
}
|
|
}, 500);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The drone calls this to update it's status in the database.
|
|
* @param registration The registration for which a status is being updated.
|
|
* @param status The new status of the drone.
|
|
* @returns the updated drone registration
|
|
*/
|
|
async setStatus(
|
|
registration: IDroneRegistration,
|
|
status: DroneStatus,
|
|
): Promise<IDroneRegistration> {
|
|
this.log.info("setting drone status", {
|
|
hostname: registration.hostname,
|
|
old: registration.status,
|
|
new: status,
|
|
});
|
|
const newRegistration = await DroneRegistration.findOneAndUpdate(
|
|
{ _id: registration._id },
|
|
{ $set: { status } },
|
|
{ new: true, populate: this.populateDroneRegistration },
|
|
);
|
|
if (!newRegistration) {
|
|
const error = new Error("drone registration has been removed");
|
|
error.statusCode = 404;
|
|
throw error;
|
|
}
|
|
return newRegistration;
|
|
}
|
|
}
|
|
|
|
export default new DroneService();
|