mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-11 00:05:36 +02:00
⭐ feat(maitred): Update maitred - hookup to the API (#198)
## Description We are attempting to hookup maitred to the API Maitred duties will be: - [ ] Hookup to the API - [ ] Wait for signal (from the API) to start Steam - [ ] Stop signal to stop the gaming session, clean up Steam... and maybe do the backup ## Summary by CodeRabbit - **New Features** - Introduced Docker-based deployment configurations for both the main and relay applications. - Added new API endpoints enabling real-time machine messaging and enhanced IoT operations. - Expanded database schema and actor types to support improved machine tracking. - **Improvements** - Enhanced real-time communication and relay management with streamlined room handling. - Upgraded dependencies, logging, and error handling for greater stability and performance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com> Co-authored-by: Kristian Ollikainen <14197772+DatCaptainHorse@users.noreply.github.com>
This commit is contained in:
12
containers/maitred.Containerfile
Normal file
12
containers/maitred.Containerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM docker.io/golang:1.24-bookworm AS go-build
|
||||
WORKDIR /builder
|
||||
COPY packages/maitred/ /builder/
|
||||
RUN go build
|
||||
|
||||
FROM docker.io/golang:1.24-bookworm
|
||||
COPY --from=go-build /builder/maitred /maitred/maitred
|
||||
WORKDIR /maitred
|
||||
|
||||
RUN apt update && apt install -y --no-install-recommends pciutils
|
||||
|
||||
ENTRYPOINT ["/maitred/maitred"]
|
||||
@@ -13,6 +13,7 @@ WORKDIR /relay
|
||||
ENV VERBOSE=false
|
||||
ENV DEBUG=false
|
||||
ENV ENDPOINT_PORT=8088
|
||||
ENV MESH_PORT=8089
|
||||
ENV WEBRTC_UDP_START=10000
|
||||
ENV WEBRTC_UDP_END=20000
|
||||
ENV STUN_SERVER="stun.l.google.com:19302"
|
||||
@@ -23,6 +24,7 @@ ENV TLS_CERT=""
|
||||
ENV TLS_KEY=""
|
||||
|
||||
EXPOSE $ENDPOINT_PORT
|
||||
EXPOSE $MESH_PORT
|
||||
EXPOSE $WEBRTC_UDP_START-$WEBRTC_UDP_END/udp
|
||||
EXPOSE $WEBRTC_UDP_MUX/udp
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ export const urls = new sst.Linkable("Urls", {
|
||||
export const apiFunction = new sst.aws.Function("ApiFn", {
|
||||
vpc,
|
||||
handler: "packages/functions/src/api/index.handler",
|
||||
permissions: [
|
||||
{
|
||||
actions: ["iot:*"],
|
||||
resources: ["*"],
|
||||
},
|
||||
],
|
||||
link: [
|
||||
bus,
|
||||
urls,
|
||||
|
||||
9
infra/realtime.ts
Normal file
9
infra/realtime.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { urls } from "./api";
|
||||
import { postgres } from "./postgres";
|
||||
|
||||
export const device = new sst.aws.Realtime("Realtime", {
|
||||
authorizer: {
|
||||
link: [urls, postgres],
|
||||
handler: "./packages/functions/src/realtime/authorizer.handler"
|
||||
}
|
||||
})
|
||||
13
packages/core/migrations/0002_tiny_toad_men.sql
Normal file
13
packages/core/migrations/0002_tiny_toad_men.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE "machine" (
|
||||
"id" char(30) PRIMARY KEY NOT NULL,
|
||||
"time_created" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_updated" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"time_deleted" timestamp with time zone,
|
||||
"country" text NOT NULL,
|
||||
"timezone" text NOT NULL,
|
||||
"location" "point" NOT NULL,
|
||||
"fingerprint" varchar(32) NOT NULL,
|
||||
"country_code" varchar(2) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "machine_fingerprint" ON "machine" USING btree ("fingerprint");
|
||||
379
packages/core/migrations/meta/0002_snapshot.json
Normal file
379
packages/core/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"id": "aa60489b-b4e2-4a69-aee7-16e050d02ef9",
|
||||
"prevId": "6f428226-b5d8-4182-a676-d04f842f9ded",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.machine": {
|
||||
"name": "machine",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"timezone": {
|
||||
"name": "timezone",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"location": {
|
||||
"name": "location",
|
||||
"type": "point",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"fingerprint": {
|
||||
"name": "fingerprint",
|
||||
"type": "varchar(32)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"country_code": {
|
||||
"name": "country_code",
|
||||
"type": "varchar(2)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"machine_fingerprint": {
|
||||
"name": "machine_fingerprint",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "fingerprint",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.member": {
|
||||
"name": "member",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"time_seen": {
|
||||
"name": "time_seen",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"email_global": {
|
||||
"name": "email_global",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"member_email": {
|
||||
"name": "member_email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "team_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"member_team_id_id_pk": {
|
||||
"name": "member_team_id_id_pk",
|
||||
"columns": [
|
||||
"team_id",
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.team": {
|
||||
"name": "team",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"plan_type": {
|
||||
"name": "plan_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "char(30)",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"time_created": {
|
||||
"name": "time_created",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_updated": {
|
||||
"name": "time_updated",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"time_deleted": {
|
||||
"name": "time_deleted",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"avatar_url": {
|
||||
"name": "avatar_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"discriminator": {
|
||||
"name": "discriminator",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"polar_customer_id": {
|
||||
"name": "polar_customer_id",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"flags": {
|
||||
"name": "flags",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'{}'::json"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_email": {
|
||||
"name": "user_email",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "email",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_polar_customer_id_unique": {
|
||||
"name": "user_polar_customer_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"polar_customer_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,13 @@
|
||||
"when": 1741955636085,
|
||||
"tag": "0001_nifty_sauron",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1743028682022,
|
||||
"tag": "0002_tiny_toad_men",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
"zod-openapi": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-iot-data-plane": "^3.758.0",
|
||||
"@aws-sdk/client-rds-data": "^3.758.0",
|
||||
"@aws-sdk/client-sesv2": "^3.753.0",
|
||||
"@instantdb/admin": "^0.17.7",
|
||||
|
||||
@@ -37,15 +37,25 @@ export const SystemActor = z.object({
|
||||
});
|
||||
export type SystemActor = z.infer<typeof SystemActor>;
|
||||
|
||||
export const MachineActor = z.object({
|
||||
type: z.literal("machine"),
|
||||
properties: z.object({
|
||||
fingerprint: z.string(),
|
||||
machineID: z.string(),
|
||||
}),
|
||||
});
|
||||
export type MachineActor = z.infer<typeof MachineActor>;
|
||||
|
||||
export const Actor = z.discriminatedUnion("type", [
|
||||
MemberActor,
|
||||
UserActor,
|
||||
PublicActor,
|
||||
SystemActor,
|
||||
MachineActor
|
||||
]);
|
||||
export type Actor = z.infer<typeof Actor>;
|
||||
|
||||
const ActorContext = createContext<Actor>("actor");
|
||||
export const ActorContext = createContext<Actor>("actor");
|
||||
|
||||
export const useActor = ActorContext.use;
|
||||
export const withActor = ActorContext.with;
|
||||
@@ -74,6 +84,12 @@ export function useTeam() {
|
||||
throw new Error(`Expected actor to have teamID`);
|
||||
}
|
||||
|
||||
export function useMachine() {
|
||||
const actor = useActor();
|
||||
if ("machineID" in actor.properties) return actor.properties.fingerprint;
|
||||
throw new Error(`Expected actor to have fingerprint`);
|
||||
}
|
||||
|
||||
export async function assertUserFlag(flag: keyof UserFlags) {
|
||||
return useTransaction((tx) =>
|
||||
tx
|
||||
|
||||
@@ -31,4 +31,13 @@ export module Examples {
|
||||
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
||||
}
|
||||
|
||||
export const Machine = {
|
||||
id: Id("machine"),
|
||||
country: "Kenya",
|
||||
countryCode: "KE",
|
||||
timezone: "Africa/Nairobi",
|
||||
location: { latitude: 36.81550, longitude: -1.28410 },
|
||||
fingerprint: "fc27f428f9ca47d4b41b707ae0c62090",
|
||||
}
|
||||
|
||||
}
|
||||
138
packages/core/src/machine/index.ts
Normal file
138
packages/core/src/machine/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { z } from "zod";
|
||||
import { Common } from "../common";
|
||||
import { createID, fn } from "../utils";
|
||||
import { Examples } from "../examples";
|
||||
import { machineTable } from "./machine.sql";
|
||||
import { getTableColumns, eq, sql, and, isNull } from "../drizzle";
|
||||
import { createTransaction, useTransaction } from "../drizzle/transaction";
|
||||
|
||||
export module Machine {
|
||||
export const Info = z
|
||||
.object({
|
||||
id: z.string().openapi({
|
||||
description: Common.IdDescription,
|
||||
example: Examples.Machine.id,
|
||||
}),
|
||||
country: z.string().openapi({
|
||||
description: "The fullname of the country this machine is running in",
|
||||
example: Examples.Machine.country
|
||||
}),
|
||||
fingerprint: z.string().openapi({
|
||||
description: "The fingerprint of this machine, deduced from the host machine's machine id - /etc/machine-id",
|
||||
example: Examples.Machine.fingerprint
|
||||
}),
|
||||
location: z.object({ longitude: z.number(), latitude: z.number() }).openapi({
|
||||
description: "This is the 2d location of this machine, they might not be accurate",
|
||||
example: Examples.Machine.location
|
||||
}),
|
||||
countryCode: z.string().openapi({
|
||||
description: "This is the 2 character country code of the country this machine [ISO 3166-1 alpha-2] ",
|
||||
example: Examples.Machine.countryCode
|
||||
}),
|
||||
timezone: z.string().openapi({
|
||||
description: "The IANA timezone formatted string of the timezone of the location where the machine is running",
|
||||
example: Examples.Machine.timezone
|
||||
})
|
||||
})
|
||||
.openapi({
|
||||
ref: "Machine",
|
||||
description: "Represents a hosted or BYOG machine connected to Nestri",
|
||||
example: Examples.Machine,
|
||||
});
|
||||
|
||||
export type Info = z.infer<typeof Info>;
|
||||
|
||||
export const create = fn(Info.partial({ id: true }), async (input) =>
|
||||
createTransaction(async (tx) => {
|
||||
const id = input.id ?? createID("machine");
|
||||
await tx.insert(machineTable).values({
|
||||
id,
|
||||
country: input.country,
|
||||
timezone: input.timezone,
|
||||
fingerprint: input.fingerprint,
|
||||
countryCode: input.countryCode,
|
||||
location: { x: input.location.longitude, y: input.location.latitude },
|
||||
})
|
||||
|
||||
// await afterTx(() =>
|
||||
// bus.publish(Resource.Bus, Events.Created, {
|
||||
// teamID: id,
|
||||
// }),
|
||||
// );
|
||||
return id;
|
||||
})
|
||||
)
|
||||
|
||||
export const list = fn(z.void(), async () =>
|
||||
useTransaction(async (tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(machineTable)
|
||||
.where(isNull(machineTable.timeDeleted))
|
||||
.then((rows) => rows.map(serialize))
|
||||
)
|
||||
)
|
||||
|
||||
export const fromID = fn(Info.shape.id, async (id) =>
|
||||
useTransaction(async (tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(machineTable)
|
||||
.where(and(eq(machineTable.id, id), isNull(machineTable.timeDeleted)))
|
||||
.then((rows) => rows.map(serialize).at(0))
|
||||
)
|
||||
)
|
||||
|
||||
export const fromFingerprint = fn(Info.shape.fingerprint, async (fingerprint) =>
|
||||
useTransaction(async (tx) =>
|
||||
tx
|
||||
.select()
|
||||
.from(machineTable)
|
||||
.where(and(eq(machineTable.fingerprint, fingerprint), isNull(machineTable.timeDeleted)))
|
||||
.execute()
|
||||
.then((rows) => rows.map(serialize).at(0))
|
||||
)
|
||||
)
|
||||
|
||||
export const remove = fn(Info.shape.id, (id) =>
|
||||
useTransaction(async (tx) => {
|
||||
await tx
|
||||
.update(machineTable)
|
||||
.set({
|
||||
timeDeleted: sql`now()`,
|
||||
})
|
||||
.where(and(eq(machineTable.id, id)))
|
||||
.execute();
|
||||
return id;
|
||||
}),
|
||||
);
|
||||
|
||||
export const fromLocation = fn(Info.shape.location, async (location) =>
|
||||
useTransaction(async (tx) => {
|
||||
const sqlDistance = sql`location <-> point(${location.longitude}, ${location.latitude})`;
|
||||
return tx
|
||||
.select({
|
||||
...getTableColumns(machineTable),
|
||||
distance: sql`round((${sqlDistance})::numeric, 2)`
|
||||
})
|
||||
.from(machineTable)
|
||||
.where(isNull(machineTable.timeDeleted)) //Should have a status update
|
||||
.orderBy(sqlDistance)
|
||||
.limit(3)
|
||||
.then((rows) => rows.map(serialize))
|
||||
})
|
||||
)
|
||||
|
||||
export function serialize(
|
||||
input: typeof machineTable.$inferSelect,
|
||||
): z.infer<typeof Info> {
|
||||
return {
|
||||
id: input.id,
|
||||
country: input.country,
|
||||
timezone: input.timezone,
|
||||
fingerprint: input.fingerprint,
|
||||
countryCode: input.countryCode,
|
||||
location: { latitude: input.location.y, longitude: input.location.x },
|
||||
};
|
||||
}
|
||||
}
|
||||
37
packages/core/src/machine/machine.sql.ts
Normal file
37
packages/core/src/machine/machine.sql.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { } from "drizzle-orm/postgres-js";
|
||||
import { timestamps, id } from "../drizzle/types";
|
||||
import {
|
||||
text,
|
||||
varchar,
|
||||
pgTable,
|
||||
uniqueIndex,
|
||||
point,
|
||||
} from "drizzle-orm/pg-core";
|
||||
|
||||
export const machineTable = pgTable(
|
||||
"machine",
|
||||
{
|
||||
...id,
|
||||
...timestamps,
|
||||
country: text('country').notNull(),
|
||||
timezone: text('timezone').notNull(),
|
||||
location: point('location', { mode: 'xy' }).notNull(),
|
||||
fingerprint: varchar('fingerprint', { length: 32 }).notNull(),
|
||||
countryCode: varchar('country_code', { length: 2 }).notNull(),
|
||||
// provider: text("provider").notNull(),
|
||||
// gpuType: text("gpu_type").notNull(),
|
||||
// storage: numeric("storage").notNull(),
|
||||
// ipaddress: text("ipaddress").notNull(),
|
||||
// gpuNumber: integer("gpu_number").notNull(),
|
||||
// computePrice: numeric("compute_price").notNull(),
|
||||
// driverVersion: integer("driver_version").notNull(),
|
||||
// operatingSystem: text("operating_system").notNull(),
|
||||
// fingerprint: varchar("fingerprint", { length: 32 }).notNull(),
|
||||
// externalID: varchar("external_id", { length: 255 }).notNull(),
|
||||
// cudaVersion: numeric("cuda_version", { precision: 4, scale: 2 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
// uniqueIndex("external_id").on(table.externalID),
|
||||
uniqueIndex("machine_fingerprint").on(table.fingerprint)
|
||||
],
|
||||
);
|
||||
@@ -78,16 +78,16 @@ export module Member {
|
||||
}),
|
||||
);
|
||||
|
||||
export const remove = fn(Info.shape.id, (input) =>
|
||||
export const remove = fn(Info.shape.id, (id) =>
|
||||
useTransaction(async (tx) => {
|
||||
await tx
|
||||
.update(memberTable)
|
||||
.set({
|
||||
timeDeleted: sql`now()`,
|
||||
})
|
||||
.where(and(eq(memberTable.id, input), eq(memberTable.teamID, useTeam())))
|
||||
.where(and(eq(memberTable.id, id), eq(memberTable.teamID, useTeam())))
|
||||
.execute();
|
||||
return input;
|
||||
return id;
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -98,11 +98,10 @@ export module Member {
|
||||
.from(memberTable)
|
||||
.where(and(eq(memberTable.email, email), isNull(memberTable.timeDeleted)))
|
||||
.orderBy(asc(memberTable.timeCreated))
|
||||
.then((rows) => rows.map(serialize))
|
||||
.then((rows) => rows.at(0))
|
||||
),
|
||||
.then((rows) => rows.map(serialize).at(0))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
export const fromID = fn(z.string(), async (id) =>
|
||||
useTransaction(async (tx) =>
|
||||
tx
|
||||
@@ -110,8 +109,7 @@ export module Member {
|
||||
.from(memberTable)
|
||||
.where(and(eq(memberTable.id, id), isNull(memberTable.timeDeleted)))
|
||||
.orderBy(asc(memberTable.timeCreated))
|
||||
.then((rows) => rows.map(serialize))
|
||||
.then((rows) => rows.at(0))
|
||||
.then((rows) => rows.map(serialize).at(0))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
24
packages/core/src/realtime/index.ts
Normal file
24
packages/core/src/realtime/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
IoTDataPlaneClient,
|
||||
PublishCommand,
|
||||
} from "@aws-sdk/client-iot-data-plane";
|
||||
import {useMachine} from "../actor";
|
||||
import {Resource} from "sst";
|
||||
|
||||
export module Realtime {
|
||||
const client = new IoTDataPlaneClient({});
|
||||
|
||||
export async function publish(message: any, subTopic?: string) {
|
||||
const fingerprint = useMachine();
|
||||
let topic = `${Resource.App.name}/${Resource.App.stage}/${fingerprint}/`;
|
||||
if (subTopic)
|
||||
topic = `${topic}${subTopic}`;
|
||||
|
||||
await client.send(
|
||||
new PublishCommand({
|
||||
payload: Buffer.from(JSON.stringify(message)),
|
||||
topic: topic,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
20
packages/core/src/task/task.sql.todo
Normal file
20
packages/core/src/task/task.sql.todo
Normal file
@@ -0,0 +1,20 @@
|
||||
import { id, timestamps } from "../drizzle/types";
|
||||
import { pgTable, uniqueIndex, varchar } from "drizzle-orm/pg-core";
|
||||
|
||||
//This represents a task created on a machine for running a game
|
||||
//Add billing info here?
|
||||
//Add who owns the task here
|
||||
// Add the session ID here
|
||||
//Add which machine owns this task
|
||||
|
||||
export const taskTable = pgTable(
|
||||
"task",
|
||||
{
|
||||
...id,
|
||||
...timestamps,
|
||||
fingerprint: varchar('fingerprint', { length: 32 }).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("task_fingerprint").on(table.fingerprint),
|
||||
],
|
||||
);
|
||||
@@ -5,7 +5,7 @@ import { Common } from "../common";
|
||||
import { Examples } from "../examples";
|
||||
import { createEvent } from "../event";
|
||||
import { createID, fn } from "../utils";
|
||||
import { and, eq, sql } from "../drizzle";
|
||||
import { and, eq, sql, isNull } from "../drizzle";
|
||||
import { PlanType, teamTable } from "./team.sql";
|
||||
import { assertActor, withActor } from "../actor";
|
||||
import { memberTable } from "../member/member.sql";
|
||||
@@ -111,6 +111,7 @@ export module Team {
|
||||
tx
|
||||
.select()
|
||||
.from(teamTable)
|
||||
.where(isNull(teamTable.timeDeleted))
|
||||
.execute()
|
||||
.then((rows) => rows.map(serialize)),
|
||||
),
|
||||
@@ -121,10 +122,9 @@ export module Team {
|
||||
return tx
|
||||
.select()
|
||||
.from(teamTable)
|
||||
.where(eq(teamTable.id, id))
|
||||
.where(and(eq(teamTable.id, id), isNull(teamTable.timeDeleted)))
|
||||
.execute()
|
||||
.then((rows) => rows.map(serialize))
|
||||
.then((rows) => rows.at(0));
|
||||
.then((rows) => rows.map(serialize).at(0))
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -133,10 +133,9 @@ export module Team {
|
||||
return tx
|
||||
.select()
|
||||
.from(teamTable)
|
||||
.where(eq(teamTable.slug, input))
|
||||
.where(and(eq(teamTable.slug, input), isNull(teamTable.timeDeleted)))
|
||||
.execute()
|
||||
.then((rows) => rows.map(serialize))
|
||||
.then((rows) => rows.at(0));
|
||||
.then((rows) => rows.map(serialize).at(0))
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -183,16 +183,16 @@ export module User {
|
||||
};
|
||||
}
|
||||
|
||||
export const remove = fn(Info.shape.id, (input) =>
|
||||
export const remove = fn(Info.shape.id, (id) =>
|
||||
useTransaction(async (tx) => {
|
||||
await tx
|
||||
.update(userTable)
|
||||
.set({
|
||||
timeDeleted: sql`now()`,
|
||||
})
|
||||
.where(and(eq(userTable.id, input)))
|
||||
.where(and(eq(userTable.id, id)))
|
||||
.execute();
|
||||
return input;
|
||||
return id;
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ import { ulid } from "ulid";
|
||||
export const prefixes = {
|
||||
user: "usr",
|
||||
team: "tea",
|
||||
member: "mbr"
|
||||
task: "tsk",
|
||||
machine: "mch",
|
||||
member: "mbr",
|
||||
} as const;
|
||||
|
||||
export function createID(prefix: keyof typeof prefixes): string {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { Resource } from "sst";
|
||||
import { subjects } from "../subjects";
|
||||
import { type MiddlewareHandler } from "hono";
|
||||
import { VisibleError } from "@nestri/core/error";
|
||||
import { ActorContext } from "@nestri/core/actor";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { useActor, withActor } from "@nestri/core/actor";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
import { ErrorCodes, VisibleError } from "@nestri/core/error";
|
||||
|
||||
const client = createClient({
|
||||
issuer: Resource.Urls.auth,
|
||||
clientID: "api",
|
||||
issuer: Resource.Urls.auth
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const notPublic: MiddlewareHandler = async (c, next) => {
|
||||
const actor = useActor();
|
||||
if (actor.type === "public")
|
||||
@@ -42,16 +47,22 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
||||
"Invalid bearer token",
|
||||
);
|
||||
}
|
||||
|
||||
if (result.subject.type === "machine") {
|
||||
console.log("machine detected")
|
||||
return withActor(result.subject, next);
|
||||
}
|
||||
|
||||
if (result.subject.type === "user") {
|
||||
const teamID = c.req.header("x-nestri-team") //|| c.req.query("teamID");
|
||||
if (!teamID) return withActor(result.subject, next);
|
||||
// const email = result.subject.properties.email;
|
||||
return withActor(
|
||||
{
|
||||
type: "system",
|
||||
properties: {
|
||||
teamID,
|
||||
if (result.subject.type === "user") {
|
||||
const teamID = c.req.header("x-nestri-team") //|| c.req.query("teamID");
|
||||
if (!teamID) return withActor(result.subject, next);
|
||||
// const email = result.subject.properties.email;
|
||||
return withActor(
|
||||
{
|
||||
type: "system",
|
||||
properties: {
|
||||
teamID,
|
||||
},
|
||||
},
|
||||
},
|
||||
next
|
||||
@@ -71,4 +82,5 @@ export const auth: MiddlewareHandler = async (c, next) => {
|
||||
// },
|
||||
);
|
||||
}
|
||||
return ActorContext.with({ type: "public", properties: {} }, next);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { auth } from "./auth";
|
||||
import { TeamApi } from "./team";
|
||||
import { logger } from "hono/logger";
|
||||
import { AccountApi } from "./account";
|
||||
import { MachineApi } from "./machine";
|
||||
import { openAPISpecs } from "hono-openapi";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { handle, streamHandle } from "hono/aws-lambda";
|
||||
@@ -22,6 +23,7 @@ const routes = app
|
||||
.get("/", (c) => c.text("Hello World!"))
|
||||
.route("/team", TeamApi.route)
|
||||
.route("/account", AccountApi.route)
|
||||
.route("/machine", MachineApi.route)
|
||||
.onError((error, c) => {
|
||||
console.warn(error);
|
||||
if (error instanceof VisibleError) {
|
||||
@@ -38,7 +40,7 @@ const routes = app
|
||||
code: ErrorCodes.Validation.INVALID_PARAMETER,
|
||||
message: "Invalid request",
|
||||
},
|
||||
400,
|
||||
error.status,
|
||||
);
|
||||
}
|
||||
console.error("unhandled error:", error);
|
||||
|
||||
224
packages/functions/src/api/machine.ts
Normal file
224
packages/functions/src/api/machine.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import {z} from "zod"
|
||||
import {Hono} from "hono";
|
||||
import {notPublic} from "./auth";
|
||||
import {Result} from "../common";
|
||||
import {describeRoute} from "hono-openapi";
|
||||
import {assertActor} from "@nestri/core/actor";
|
||||
import {Realtime} from "@nestri/core/realtime/index";
|
||||
import {validator} from "hono-openapi/zod";
|
||||
import {CreateMessageSchema, StartMessageSchema, StopMessageSchema} from "./messages.ts";
|
||||
|
||||
export module MachineApi {
|
||||
export const route = new Hono()
|
||||
.use(notPublic)
|
||||
.post("/",
|
||||
describeRoute({
|
||||
tags: ["Machine"],
|
||||
summary: "Send messages to the machine",
|
||||
description: "Send messages directly to the machine",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.literal("ok")
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Successfully sent the message to Maitred"
|
||||
},
|
||||
// 404: {
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: resolver(z.object({ error: z.string() })),
|
||||
// },
|
||||
// },
|
||||
// description: "This account does not exist",
|
||||
// },
|
||||
}
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.any()
|
||||
),
|
||||
async (c) => {
|
||||
const actor = assertActor("machine");
|
||||
console.log("actor.id", actor.properties.machineID)
|
||||
|
||||
await Realtime.publish(c.req.valid("json"))
|
||||
|
||||
return c.json({
|
||||
data: "ok"
|
||||
}, 200);
|
||||
},
|
||||
)
|
||||
.post("/:machineID/create",
|
||||
describeRoute({
|
||||
tags: ["Machine"],
|
||||
summary: "Request to create a container for a specific machine",
|
||||
description: "Publishes a message to create a container via MQTT for the given machine ID",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({
|
||||
message: z.literal("create request sent"),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Create request successfully sent to MQTT",
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({error: z.string()})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Failed to publish create request",
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("json", CreateMessageSchema.shape.payload.optional()), // No payload required for create
|
||||
async (c) => {
|
||||
const actor = assertActor("machine");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const message = {
|
||||
type: "create" as const,
|
||||
payload: body || {}, // Empty payload if none provided
|
||||
};
|
||||
|
||||
try {
|
||||
await Realtime.publish(message, "create");
|
||||
console.log("Published create request to");
|
||||
} catch (error) {
|
||||
console.error("Failed to publish to MQTT:", error);
|
||||
return c.json({error: "Failed to send create request"}, 400);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
message: "create request sent",
|
||||
},
|
||||
}, 200);
|
||||
}
|
||||
)
|
||||
.post("/:machineID/start",
|
||||
describeRoute({
|
||||
tags: ["Machine"],
|
||||
summary: "Request to start a container for a specific machine",
|
||||
description: "Publishes a message to start a container via MQTT for the given machine ID",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({
|
||||
message: z.literal("start request sent"),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Start request successfully sent to MQTT",
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({error: z.string()})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Failed to publish start request",
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("json", StartMessageSchema.shape.payload), // Use the payload schema
|
||||
async (c) => {
|
||||
const actor = assertActor("machine");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const message = {
|
||||
type: "start" as const,
|
||||
payload: {
|
||||
container_id: body.container_id,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await Realtime.publish(message, "start");
|
||||
console.log("Published start request");
|
||||
} catch (error) {
|
||||
console.error("Failed to publish to MQTT:", error);
|
||||
return c.json({error: "Failed to send start request"}, 400);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
message: "start request sent",
|
||||
},
|
||||
}, 200);
|
||||
}
|
||||
)
|
||||
.post("/:machineID/stop",
|
||||
describeRoute({
|
||||
tags: ["Machine"],
|
||||
summary: "Request to stop a container for a specific machine",
|
||||
description: "Publishes a message to stop a container via MQTT for the given machine ID",
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({
|
||||
message: z.literal("stop request sent"),
|
||||
})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Stop request successfully sent to MQTT",
|
||||
},
|
||||
400: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Result(
|
||||
z.object({error: z.string()})
|
||||
),
|
||||
},
|
||||
},
|
||||
description: "Failed to publish start request",
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator("json", StopMessageSchema.shape.payload), // Use the payload schema
|
||||
async (c) => {
|
||||
const actor = assertActor("machine");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const message = {
|
||||
type: "stop" as const,
|
||||
payload: {
|
||||
container_id: body.container_id,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await Realtime.publish(message, "stop");
|
||||
console.log("Published stop request");
|
||||
} catch (error) {
|
||||
console.error("Failed to publish to MQTT:", error);
|
||||
return c.json({error: "Failed to send stop request"}, 400);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
message: "stop request sent",
|
||||
},
|
||||
}, 200);
|
||||
}
|
||||
)
|
||||
}
|
||||
54
packages/functions/src/api/messages.ts
Normal file
54
packages/functions/src/api/messages.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { z } from "zod"
|
||||
|
||||
// Base message interface
|
||||
export interface BaseMessage {
|
||||
type: string; // e.g., "start", "stop", "status"
|
||||
payload: Record<string, any>; // Generic payload, refined by specific types
|
||||
}
|
||||
|
||||
// Specific message types
|
||||
export interface StartMessage extends BaseMessage {
|
||||
type: "start";
|
||||
payload: {
|
||||
container_id: string;
|
||||
[key: string]: any; // Allow additional fields for future expansion
|
||||
};
|
||||
}
|
||||
|
||||
// Example future message type
|
||||
export interface StopMessage extends BaseMessage {
|
||||
type: "stop";
|
||||
payload: {
|
||||
container_id: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
// Union type for all possible messages (expandable)
|
||||
export type MachineMessage = StartMessage | StopMessage; // Add more types as needed
|
||||
|
||||
// Zod schema for validation
|
||||
export const BaseMessageSchema = z.object({
|
||||
type: z.string(),
|
||||
payload: z.record(z.any()),
|
||||
});
|
||||
|
||||
export const CreateMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal("create"),
|
||||
});
|
||||
|
||||
export const StartMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal("start"),
|
||||
payload: z.object({
|
||||
container_id: z.string(),
|
||||
}).passthrough(),
|
||||
});
|
||||
|
||||
export const StopMessageSchema = BaseMessageSchema.extend({
|
||||
type: z.literal("stop"),
|
||||
payload: z.object({
|
||||
container_id: z.string(),
|
||||
}).passthrough(),
|
||||
});
|
||||
|
||||
export const MachineMessageSchema = z.union([StartMessageSchema, StopMessageSchema]);
|
||||
@@ -9,6 +9,7 @@ import { User } from "@nestri/core/user/index"
|
||||
import { Email } from "@nestri/core/email/index";
|
||||
import { handleDiscord, handleGithub } from "./utils";
|
||||
import { GithubAdapter } from "./ui/adapters/github";
|
||||
import { Machine } from "@nestri/core/machine/index"
|
||||
import { DiscordAdapter } from "./ui/adapters/discord";
|
||||
import { PasswordAdapter } from "./ui/adapters/password"
|
||||
import { type Provider } from "@openauthjs/openauth/provider/provider"
|
||||
@@ -22,10 +23,11 @@ type OauthUser = {
|
||||
avatar: any;
|
||||
username: any;
|
||||
}
|
||||
|
||||
const app = issuer({
|
||||
select: Select({
|
||||
providers: {
|
||||
device: {
|
||||
machine: {
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
@@ -73,29 +75,24 @@ const app = issuer({
|
||||
},
|
||||
}),
|
||||
),
|
||||
device: {
|
||||
type: "device",
|
||||
machine: {
|
||||
type: "machine",
|
||||
async client(input) {
|
||||
if (input.clientSecret !== Resource.AuthFingerprintKey.value) {
|
||||
throw new Error("Invalid authorization token");
|
||||
}
|
||||
const teamSlug = input.params.team;
|
||||
if (!teamSlug) {
|
||||
throw new Error("Team slug is required");
|
||||
}
|
||||
|
||||
const hostname = input.params.hostname;
|
||||
if (!hostname) {
|
||||
const fingerprint = input.params.fingerprint;
|
||||
if (!fingerprint) {
|
||||
throw new Error("Hostname is required");
|
||||
}
|
||||
|
||||
return {
|
||||
hostname,
|
||||
teamSlug
|
||||
fingerprint,
|
||||
};
|
||||
},
|
||||
init() { }
|
||||
} as Provider<{ teamSlug: string; hostname: string; }>,
|
||||
} as Provider<{ fingerprint: string; }>,
|
||||
},
|
||||
allow: async (input) => {
|
||||
const url = new URL(input.redirectURI);
|
||||
@@ -104,20 +101,45 @@ const app = issuer({
|
||||
if (hostname === "localhost") return true;
|
||||
return false;
|
||||
},
|
||||
success: async (ctx, value) => {
|
||||
// if (value.provider === "device") {
|
||||
// const team = await Teams.fromSlug(value.teamSlug)
|
||||
// console.log("team", team)
|
||||
// console.log("teamSlug", value.teamSlug)
|
||||
// if (team) {
|
||||
// await Instances.create({ hostname: value.hostname, teamID: team.id })
|
||||
success: async (ctx, value, req) => {
|
||||
if (value.provider === "machine") {
|
||||
const countryCode = req.headers.get('CloudFront-Viewer-Country') || 'Unknown'
|
||||
const country = req.headers.get('CloudFront-Viewer-Country-Name') || 'Unknown'
|
||||
const latitude = Number(req.headers.get('CloudFront-Viewer-Latitude')) || 0
|
||||
const longitude = Number(req.headers.get('CloudFront-Viewer-Longitude')) || 0
|
||||
const timezone = req.headers.get('CloudFront-Viewer-Time-Zone') || 'Unknown'
|
||||
const fingerprint = value.fingerprint
|
||||
|
||||
// return await ctx.subject("device", {
|
||||
// teamSlug: value.teamSlug,
|
||||
// hostname: value.hostname,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
const existing = await Machine.fromFingerprint(fingerprint)
|
||||
if (!existing) {
|
||||
const machineID = await Machine.create({
|
||||
countryCode,
|
||||
country,
|
||||
fingerprint,
|
||||
timezone,
|
||||
location: {
|
||||
latitude,
|
||||
longitude
|
||||
}
|
||||
})
|
||||
return ctx.subject("machine", {
|
||||
machineID,
|
||||
fingerprint
|
||||
});
|
||||
}
|
||||
|
||||
return ctx.subject("machine", {
|
||||
machineID: existing.id,
|
||||
fingerprint
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: This works, so use this while registering the task
|
||||
// console.log("country_code", req.headers.get('CloudFront-Viewer-Country'))
|
||||
// console.log("country_name", req.headers.get('CloudFront-Viewer-Country-Name'))
|
||||
// console.log("latitude", req.headers.get('CloudFront-Viewer-Latitude'))
|
||||
// console.log("longitude", req.headers.get('CloudFront-Viewer-Longitude'))
|
||||
// console.log("timezone", req.headers.get('CloudFront-Viewer-Time-Zone'))
|
||||
|
||||
if (value.provider === "password") {
|
||||
const email = value.email
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Resource } from "sst";
|
||||
import { subjects } from "../subjects";
|
||||
import { realtime } from "sst/aws/realtime";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
|
||||
export const handler = realtime.authorizer(async (token) => {
|
||||
//TODO: Use the following criteria for a topic - team-slug/container-id (container ids are not unique globally)
|
||||
//TODO: Allow the authorizer to subscriber/publisher to listen on - team-slug topics only (as the container will listen on the team-slug/container-id topic to be specific)
|
||||
// Return the topics to subscribe and publish
|
||||
|
||||
const client = createClient({
|
||||
clientID: "api",
|
||||
issuer: Resource.Urls.auth
|
||||
});
|
||||
|
||||
const result = await client.verify(subjects, token);
|
||||
|
||||
if (result.err) {
|
||||
console.log("error", result.err)
|
||||
return {
|
||||
subscribe: [],
|
||||
publish: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (result.subject.type != "device") {
|
||||
return {
|
||||
subscribe: [],
|
||||
publish: [],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
//It can publish and listen to other instances under this team
|
||||
subscribe: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.teamSlug}/*`],
|
||||
publish: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.teamSlug}/*`],
|
||||
};
|
||||
});
|
||||
40
packages/functions/src/realtime/authorizer.ts
Normal file
40
packages/functions/src/realtime/authorizer.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Resource } from "sst";
|
||||
import { subjects } from "../subjects";
|
||||
import { realtime } from "sst/aws/realtime";
|
||||
import { createClient } from "@openauthjs/openauth/client";
|
||||
|
||||
const client = createClient({
|
||||
clientID: "realtime",
|
||||
issuer: Resource.Urls.auth
|
||||
});
|
||||
|
||||
export const handler = realtime.authorizer(async (token) => {
|
||||
|
||||
console.log("token", token)
|
||||
|
||||
const result = await client.verify(subjects, token);
|
||||
|
||||
if (result.err) {
|
||||
console.log("error", result.err)
|
||||
return {
|
||||
subscribe: [],
|
||||
publish: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (result.subject.type == "machine") {
|
||||
console.log("machineID", result.subject.properties.machineID)
|
||||
console.log("fingerprint", result.subject.properties.fingerprint)
|
||||
|
||||
return {
|
||||
//It can publish and listen to other instances under this machineID
|
||||
publish: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.fingerprint}/*`],
|
||||
subscribe: [`${Resource.App.name}/${Resource.App.stage}/${result.subject.properties.fingerprint}/*`],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
publish: [],
|
||||
subscribe: [],
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as v from "valibot"
|
||||
import { Subscription } from "./type"
|
||||
import { createSubjects } from "@openauthjs/openauth/subject"
|
||||
|
||||
export const subjects = createSubjects({
|
||||
@@ -7,8 +6,8 @@ export const subjects = createSubjects({
|
||||
email: v.string(),
|
||||
userID: v.string(),
|
||||
}),
|
||||
// device: v.object({
|
||||
// teamSlug: v.string(),
|
||||
// hostname: v.string(),
|
||||
// })
|
||||
machine: v.object({
|
||||
fingerprint: v.string(),
|
||||
machineID: v.string(),
|
||||
})
|
||||
})
|
||||
5
packages/functions/sst-env.d.ts
vendored
5
packages/functions/sst-env.d.ts
vendored
@@ -68,6 +68,11 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Realtime": {
|
||||
"authorizer": string
|
||||
"endpoint": string
|
||||
"type": "sst.aws.Realtime"
|
||||
}
|
||||
"Steam": {
|
||||
"service": string
|
||||
"type": "sst.aws.Service"
|
||||
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
export class WebRTCStream {
|
||||
private _ws: WebSocket | undefined = undefined;
|
||||
private _pc: RTCPeerConnection | undefined = undefined;
|
||||
private _mediaStream: MediaStream | undefined = undefined;
|
||||
private _audioTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _videoTrack: MediaStreamTrack | undefined = undefined;
|
||||
private _dataChannel: RTCDataChannel | undefined = undefined;
|
||||
private _onConnected: ((stream: MediaStream | null) => void) | undefined = undefined;
|
||||
private _connectionTimeout: number = 7000;
|
||||
private _connectionTimer: NodeJS.Timeout | NodeJS.Timer | undefined = undefined;
|
||||
private _serverURL: string | undefined = undefined;
|
||||
private _roomName: string | undefined = undefined;
|
||||
@@ -163,12 +163,13 @@ export class WebRTCStream {
|
||||
],
|
||||
});
|
||||
|
||||
// Start connection timeout
|
||||
this._startConnectionTimer();
|
||||
|
||||
this._pc.ontrack = (e) => {
|
||||
console.log("Track received: ", e.track);
|
||||
this._mediaStream = e.streams[e.streams.length - 1];
|
||||
if (e.track.kind === "audio")
|
||||
this._audioTrack = e.track;
|
||||
else if (e.track.kind === "video")
|
||||
this._videoTrack = e.track;
|
||||
|
||||
this._checkConnectionState();
|
||||
};
|
||||
|
||||
@@ -184,6 +185,7 @@ export class WebRTCStream {
|
||||
|
||||
this._pc.onicegatheringstatechange = () => {
|
||||
console.log("ICE gathering state changed to: ", this._pc!.iceGatheringState);
|
||||
this._checkConnectionState();
|
||||
};
|
||||
|
||||
this._pc.onicecandidate = (e) => {
|
||||
@@ -208,16 +210,31 @@ export class WebRTCStream {
|
||||
console.log("Checking connection state:", {
|
||||
connectionState: this._pc.connectionState,
|
||||
iceConnectionState: this._pc.iceConnectionState,
|
||||
hasMediaStream: !!this._mediaStream,
|
||||
hasAudioTrack: !!this._audioTrack,
|
||||
hasVideoTrack: !!this._videoTrack,
|
||||
isConnected: this._isConnected
|
||||
});
|
||||
|
||||
if (this._pc.connectionState === "connected" && this._mediaStream) {
|
||||
if (this._pc.connectionState === "connected" && this._audioTrack !== undefined && this._videoTrack !== undefined) {
|
||||
this._clearConnectionTimer();
|
||||
if (!this._isConnected) { // Only trigger callback if not already connected
|
||||
if (!this._isConnected) {
|
||||
// Only trigger callback if not already connected
|
||||
this._isConnected = true;
|
||||
if (this._onConnected) {
|
||||
this._onConnected(this._mediaStream);
|
||||
if (this._onConnected !== undefined) {
|
||||
this._onConnected(new MediaStream([this._audioTrack, this._videoTrack]));
|
||||
|
||||
// Continuously set low-latency target
|
||||
this._pc.getReceivers().forEach((receiver: RTCRtpReceiver) => {
|
||||
let intervalLoop = setInterval(async () => {
|
||||
if (receiver.track.readyState !== "live" || receiver.transport.state !== "connected") {
|
||||
clearInterval(intervalLoop);
|
||||
return;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
receiver.jitterBufferTarget = receiver.jitterBufferDelayHint = receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}, 15);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,14 +264,6 @@ export class WebRTCStream {
|
||||
}
|
||||
}
|
||||
|
||||
private _startConnectionTimer() {
|
||||
this._clearConnectionTimer();
|
||||
this._connectionTimer = setTimeout(() => {
|
||||
console.log("Connection timeout reached");
|
||||
this._handleConnectionFailure();
|
||||
}, this._connectionTimeout);
|
||||
}
|
||||
|
||||
private _cleanupPeerConnection() {
|
||||
if (this._pc) {
|
||||
try {
|
||||
@@ -265,13 +274,17 @@ export class WebRTCStream {
|
||||
this._pc = undefined;
|
||||
}
|
||||
|
||||
if (this._mediaStream) {
|
||||
if (this._audioTrack || this._videoTrack) {
|
||||
try {
|
||||
this._mediaStream.getTracks().forEach(track => track.stop());
|
||||
if (this._audioTrack)
|
||||
this._audioTrack.stop();
|
||||
if (this._videoTrack)
|
||||
this._videoTrack.stop();
|
||||
} catch (err) {
|
||||
console.error("Error stopping media tracks:", err);
|
||||
}
|
||||
this._mediaStream = undefined;
|
||||
this._audioTrack = undefined;
|
||||
this._videoTrack = undefined;
|
||||
}
|
||||
|
||||
if (this._dataChannel) {
|
||||
@@ -301,11 +314,10 @@ export class WebRTCStream {
|
||||
}
|
||||
|
||||
private _gatherFrameRate() {
|
||||
if (this._pc === undefined || this._mediaStream === undefined)
|
||||
if (this._pc === undefined || this._videoTrack === undefined)
|
||||
return;
|
||||
|
||||
const videoInfoPromise = new Promise<{ fps: number}>((resolve) => {
|
||||
const track = this._mediaStream!.getVideoTracks()[0];
|
||||
// Keep trying to get fps until it's found
|
||||
const interval = setInterval(async () => {
|
||||
if (this._pc === undefined) {
|
||||
@@ -313,7 +325,7 @@ export class WebRTCStream {
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = await this._pc!.getStats(track);
|
||||
const stats = await this._pc!.getStats(this._videoTrack);
|
||||
stats.forEach((report) => {
|
||||
if (report.type === "inbound-rtp") {
|
||||
clearInterval(interval);
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
module nestri/maitred
|
||||
|
||||
go 1.23.3
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/docker/docker v28.0.1+incompatible
|
||||
github.com/eclipse/paho.golang v0.22.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.10.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
|
||||
go.opentelemetry.io/otel v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/time v0.10.0 // indirect
|
||||
gotest.tools/v3 v3.5.2 // indirect
|
||||
)
|
||||
|
||||
@@ -1,49 +1,134 @@
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s=
|
||||
github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/eclipse/paho.golang v0.22.0 h1:JhhUngr8TBlyUZDZw/L6WVayPi9qmSmdWeki48i5AVE=
|
||||
github.com/eclipse/paho.golang v0.22.0/go.mod h1:9ZiYJ93iEfGRJri8tErNeStPKLXIGBHiqbHV74t5pqI=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
|
||||
google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
|
||||
45
packages/maitred/internal/auth/auth.go
Normal file
45
packages/maitred/internal/auth/auth.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"nestri/maitred/internal/resource"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type UserCredentials struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func FetchUserToken(machineID string, resource *resource.Resource) (*UserCredentials, error) {
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "client_credentials")
|
||||
data.Set("client_id", "maitred")
|
||||
data.Set("client_secret", resource.AuthFingerprintKey.Value)
|
||||
data.Set("fingerprint", machineID)
|
||||
data.Set("provider", "machine")
|
||||
resp, err := http.PostForm(resource.Auth.Url+"/token", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err = Body.Close()
|
||||
if err != nil {
|
||||
slog.Error("Error closing body", "err", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("failed to auth: " + string(body))
|
||||
}
|
||||
credentials := UserCredentials{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &credentials, nil
|
||||
}
|
||||
38
packages/maitred/internal/containers/containers.go
Normal file
38
packages/maitred/internal/containers/containers.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Container represents a container instance
|
||||
type Container struct {
|
||||
ID string
|
||||
Name string
|
||||
State string
|
||||
Image string
|
||||
}
|
||||
|
||||
// ContainerEngine defines the common interface for differing container engines
|
||||
type ContainerEngine interface {
|
||||
Close() error
|
||||
ListContainers(ctx context.Context) ([]Container, error)
|
||||
ListContainersByImage(ctx context.Context, img string) ([]Container, error)
|
||||
NewContainer(ctx context.Context, img string, envs []string) (string, error)
|
||||
StartContainer(ctx context.Context, id string) error
|
||||
StopContainer(ctx context.Context, id string) error
|
||||
RemoveContainer(ctx context.Context, id string) error
|
||||
InspectContainer(ctx context.Context, id string) (*Container, error)
|
||||
PullImage(ctx context.Context, img string) error
|
||||
Info(ctx context.Context) (string, error)
|
||||
LogsContainer(ctx context.Context, id string) (string, error)
|
||||
}
|
||||
|
||||
func NewContainerEngine() (ContainerEngine, error) {
|
||||
dockerEngine, err := NewDockerEngine()
|
||||
if err == nil {
|
||||
return dockerEngine, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to create container engine: %w", err)
|
||||
}
|
||||
299
packages/maitred/internal/containers/docker.go
Normal file
299
packages/maitred/internal/containers/docker.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DockerEngine implements the ContainerEngine interface for Docker / Docker compatible engines
|
||||
type DockerEngine struct {
|
||||
cli *client.Client
|
||||
}
|
||||
|
||||
func NewDockerEngine() (*DockerEngine, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create Docker client: %w", err)
|
||||
}
|
||||
return &DockerEngine{cli: cli}, nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) Close() error {
|
||||
return d.cli.Close()
|
||||
}
|
||||
|
||||
func (d *DockerEngine) ListContainers(ctx context.Context) ([]Container, error) {
|
||||
containerList, err := d.cli.ContainerList(ctx, container.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list containers: %w", err)
|
||||
}
|
||||
|
||||
var result []Container
|
||||
for _, c := range containerList {
|
||||
result = append(result, Container{
|
||||
ID: c.ID,
|
||||
Name: strings.TrimPrefix(strings.Join(c.Names, ","), "/"),
|
||||
State: c.State,
|
||||
Image: c.Image,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) ListContainersByImage(ctx context.Context, img string) ([]Container, error) {
|
||||
if len(img) <= 0 {
|
||||
return nil, fmt.Errorf("image name cannot be empty")
|
||||
}
|
||||
|
||||
containerList, err := d.cli.ContainerList(ctx, container.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list containers: %w", err)
|
||||
}
|
||||
|
||||
var result []Container
|
||||
for _, c := range containerList {
|
||||
if c.Image == img {
|
||||
result = append(result, Container{
|
||||
ID: c.ID,
|
||||
Name: strings.TrimPrefix(strings.Join(c.Names, ","), "/"),
|
||||
State: c.State,
|
||||
Image: c.Image,
|
||||
})
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) NewContainer(ctx context.Context, img string, envs []string) (string, error) {
|
||||
// Create a new container with the given image and environment variables
|
||||
resp, err := d.cli.ContainerCreate(ctx, &container.Config{
|
||||
Image: img,
|
||||
Env: envs,
|
||||
}, &container.HostConfig{
|
||||
NetworkMode: "host",
|
||||
}, nil, nil, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create container: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.ID) <= 0 {
|
||||
return "", fmt.Errorf("failed to create container, no ID returned")
|
||||
}
|
||||
|
||||
return resp.ID, nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) StartContainer(ctx context.Context, id string) error {
|
||||
err := d.cli.ContainerStart(ctx, id, container.StartOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start container: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the container to start
|
||||
if err = d.waitForContainer(ctx, id, "running"); err != nil {
|
||||
return fmt.Errorf("container failed to reach running state: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) StopContainer(ctx context.Context, id string) error {
|
||||
// Waiter for the container to stop
|
||||
respChan, errChan := d.cli.ContainerWait(ctx, id, container.WaitConditionNotRunning)
|
||||
|
||||
// Stop the container
|
||||
err := d.cli.ContainerStop(ctx, id, container.StopOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop container: %w", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-respChan:
|
||||
// Container stopped successfully
|
||||
break
|
||||
case err = <-errChan:
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for container to stop: %w", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled while waiting for container to stop")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) RemoveContainer(ctx context.Context, id string) error {
|
||||
// Waiter for the container to be removed
|
||||
respChan, errChan := d.cli.ContainerWait(ctx, id, container.WaitConditionRemoved)
|
||||
|
||||
err := d.cli.ContainerRemove(ctx, id, container.RemoveOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove container: %w", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-respChan:
|
||||
// Container removed successfully
|
||||
break
|
||||
case err = <-errChan:
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for container to be removed: %w", err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("context canceled while waiting for container to stop")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) InspectContainer(ctx context.Context, id string) (*Container, error) {
|
||||
info, err := d.cli.ContainerInspect(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to inspect container: %w", err)
|
||||
}
|
||||
|
||||
return &Container{
|
||||
ID: info.ID,
|
||||
Name: info.Name,
|
||||
State: info.State.Status,
|
||||
Image: info.Config.Image,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) PullImage(ctx context.Context, img string) error {
|
||||
if len(img) <= 0 {
|
||||
return fmt.Errorf("image name cannot be empty")
|
||||
}
|
||||
|
||||
slog.Info("Starting image pull", "image", img)
|
||||
|
||||
reader, err := d.cli.ImagePull(ctx, img, image.PullOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start image pull for %s: %w", img, err)
|
||||
}
|
||||
defer func(reader io.ReadCloser) {
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to close reader", "err", err)
|
||||
}
|
||||
}(reader)
|
||||
|
||||
// Parse the JSON stream for progress
|
||||
decoder := json.NewDecoder(reader)
|
||||
lastDownloadPercent := 0
|
||||
downloadTotals := make(map[string]int64)
|
||||
downloadCurrents := make(map[string]int64)
|
||||
|
||||
var msg struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
ProgressDetail struct {
|
||||
Current int64 `json:"current"`
|
||||
Total int64 `json:"total"`
|
||||
} `json:"progressDetail"`
|
||||
}
|
||||
|
||||
for {
|
||||
err = decoder.Decode(&msg)
|
||||
if err == io.EOF {
|
||||
break // Pull completed
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding pull response for %s: %w", img, err)
|
||||
}
|
||||
|
||||
// Skip if no progress details or ID
|
||||
if msg.ID == "" || msg.ProgressDetail.Total == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(msg.Status), "downloading") {
|
||||
downloadTotals[msg.ID] = msg.ProgressDetail.Total
|
||||
downloadCurrents[msg.ID] = msg.ProgressDetail.Current
|
||||
var total, current int64
|
||||
for _, t := range downloadTotals {
|
||||
total += t
|
||||
}
|
||||
for _, c := range downloadCurrents {
|
||||
current += c
|
||||
}
|
||||
percent := int((float64(current) / float64(total)) * 100)
|
||||
if percent >= lastDownloadPercent+10 && percent <= 100 {
|
||||
slog.Info("Download progress", "image", img, "percent", percent)
|
||||
lastDownloadPercent = percent - (percent % 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info("Pulled image", "image", img)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) Info(ctx context.Context) (string, error) {
|
||||
info, err := d.cli.Info(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get Docker info: %w", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Docker Engine Version: %s", info.ServerVersion), nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) LogsContainer(ctx context.Context, id string) (string, error) {
|
||||
reader, err := d.cli.ContainerLogs(ctx, id, container.LogsOptions{ShowStdout: true, ShowStderr: true})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get container logs: %w", err)
|
||||
}
|
||||
defer func(reader io.ReadCloser) {
|
||||
err = reader.Close()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to close reader", "err", err)
|
||||
}
|
||||
}(reader)
|
||||
|
||||
logs, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read container logs: %w", err)
|
||||
}
|
||||
|
||||
return string(logs), nil
|
||||
}
|
||||
|
||||
func (d *DockerEngine) waitForContainer(ctx context.Context, id, desiredState string) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
// Inspect the container to get its current state
|
||||
inspection, err := d.cli.ContainerInspect(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect container: %w", err)
|
||||
}
|
||||
|
||||
// Check the container's state
|
||||
currentState := strings.ToLower(inspection.State.Status)
|
||||
switch currentState {
|
||||
case desiredState:
|
||||
// Container is in the desired state (e.g., "running")
|
||||
return nil
|
||||
case "exited", "dead", "removing":
|
||||
// Container failed or stopped unexpectedly, get logs and return error
|
||||
logs, _ := d.LogsContainer(ctx, id)
|
||||
return fmt.Errorf("container failed to reach %s state, logs: %s", desiredState, logs)
|
||||
}
|
||||
|
||||
// Wait before polling again
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("timed out after 10s waiting for container to reach %s state", desiredState)
|
||||
case <-time.After(1 * time.Second):
|
||||
// Continue polling
|
||||
}
|
||||
}
|
||||
}
|
||||
70
packages/maitred/internal/flags.go
Normal file
70
packages/maitred/internal/flags.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var globalFlags *Flags
|
||||
|
||||
type Flags struct {
|
||||
Verbose bool // Log everything to console
|
||||
Debug bool // Enable debug mode, implies Verbose - disables SST and MQTT connections
|
||||
NoMonitor bool // Disable system monitoring
|
||||
}
|
||||
|
||||
func (flags *Flags) DebugLog() {
|
||||
slog.Info("Maitred flags",
|
||||
"verbose", flags.Verbose,
|
||||
"debug", flags.Debug,
|
||||
"no-monitor", flags.NoMonitor,
|
||||
)
|
||||
}
|
||||
|
||||
func getEnvAsInt(name string, defaultVal int) int {
|
||||
valueStr := os.Getenv(name)
|
||||
if value, err := strconv.Atoi(valueStr); err != nil {
|
||||
return defaultVal
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
func getEnvAsBool(name string, defaultVal bool) bool {
|
||||
valueStr := os.Getenv(name)
|
||||
val, err := strconv.ParseBool(valueStr)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func getEnvAsString(name string, defaultVal string) string {
|
||||
valueStr := os.Getenv(name)
|
||||
if len(valueStr) == 0 {
|
||||
return defaultVal
|
||||
}
|
||||
return valueStr
|
||||
}
|
||||
|
||||
func InitFlags() {
|
||||
// Create Flags struct
|
||||
globalFlags = &Flags{}
|
||||
// Get flags
|
||||
flag.BoolVar(&globalFlags.Verbose, "verbose", getEnvAsBool("VERBOSE", false), "Verbose mode")
|
||||
flag.BoolVar(&globalFlags.Debug, "debug", getEnvAsBool("DEBUG", false), "Debug mode")
|
||||
flag.BoolVar(&globalFlags.NoMonitor, "no-monitor", getEnvAsBool("NO_MONITOR", false), "Disable system monitoring")
|
||||
// Parse flags
|
||||
flag.Parse()
|
||||
|
||||
// If debug is enabled, verbose is also enabled
|
||||
if globalFlags.Debug {
|
||||
globalFlags.Verbose = true
|
||||
}
|
||||
}
|
||||
|
||||
func GetFlags() *Flags {
|
||||
return globalFlags
|
||||
}
|
||||
48
packages/maitred/internal/handler.go
Normal file
48
packages/maitred/internal/handler.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CustomHandler struct {
|
||||
Handler slog.Handler
|
||||
}
|
||||
|
||||
func (h *CustomHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return h.Handler.Enabled(nil, level)
|
||||
}
|
||||
|
||||
func (h *CustomHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
// Format the timestamp as "2006/01/02 15:04:05"
|
||||
timestamp := r.Time.Format("2006/01/02 15:04:05")
|
||||
// Convert level to uppercase string (e.g., "INFO")
|
||||
level := strings.ToUpper(r.Level.String())
|
||||
// Build the message
|
||||
msg := fmt.Sprintf("%s %s %s", timestamp, level, r.Message)
|
||||
|
||||
// Handle additional attributes if they exist
|
||||
var attrs []string
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
attrs = append(attrs, fmt.Sprintf("%s=%v", a.Key, a.Value))
|
||||
return true
|
||||
})
|
||||
if len(attrs) > 0 {
|
||||
msg += " " + strings.Join(attrs, " ")
|
||||
}
|
||||
|
||||
// Write the formatted message to stdout
|
||||
_, err := fmt.Fprintln(os.Stdout, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CustomHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &CustomHandler{Handler: h.Handler.WithAttrs(attrs)}
|
||||
}
|
||||
|
||||
func (h *CustomHandler) WithGroup(name string) slog.Handler {
|
||||
return &CustomHandler{Handler: h.Handler.WithGroup(name)}
|
||||
}
|
||||
366
packages/maitred/internal/realtime/managed.go
Normal file
366
packages/maitred/internal/realtime/managed.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package realtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"nestri/maitred/internal"
|
||||
"nestri/maitred/internal/containers"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
nestriRunnerImage = "ghcr.io/nestrilabs/nestri/runner:nightly"
|
||||
nestriRelayImage = "ghcr.io/nestrilabs/nestri/relay:nightly"
|
||||
)
|
||||
|
||||
type ManagedContainerType int
|
||||
|
||||
const (
|
||||
// Runner is the nestri runner container
|
||||
Runner ManagedContainerType = iota
|
||||
// Relay is the nestri relay container
|
||||
Relay
|
||||
)
|
||||
|
||||
// ManagedContainer type with extra information fields
|
||||
type ManagedContainer struct {
|
||||
containers.Container
|
||||
Type ManagedContainerType
|
||||
}
|
||||
|
||||
// managedContainers is a map of containers that are managed by us (maitred)
|
||||
var (
|
||||
managedContainers = make(map[string]ManagedContainer)
|
||||
managedContainersMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// InitializeManager handles the initialization of the managed containers and pulls their latest images
|
||||
func InitializeManager(ctx context.Context, ctrEngine containers.ContainerEngine) error {
|
||||
// If debug, override the images
|
||||
if internal.GetFlags().Debug {
|
||||
nestriRunnerImage = "ghcr.io/datcaptainhorse/nestri-cachyos:latest-v3"
|
||||
nestriRelayImage = "ghcr.io/datcaptainhorse/nestri-relay:latest"
|
||||
}
|
||||
|
||||
// Look for existing stopped runner containers and remove them
|
||||
slog.Info("Checking and removing old runner containers")
|
||||
oldRunners, err := ctrEngine.ListContainersByImage(ctx, nestriRunnerImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range oldRunners {
|
||||
// If running, stop first
|
||||
if strings.Contains(strings.ToLower(c.State), "running") {
|
||||
slog.Info("Stopping old runner container", "id", c.ID)
|
||||
if err = ctrEngine.StopContainer(ctx, c.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
slog.Info("Removing old runner container", "id", c.ID)
|
||||
if err = ctrEngine.RemoveContainer(ctx, c.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Pull the runner image if not in debug mode
|
||||
if !internal.GetFlags().Debug {
|
||||
slog.Info("Pulling runner image", "image", nestriRunnerImage)
|
||||
if err := ctrEngine.PullImage(ctx, nestriRunnerImage); err != nil {
|
||||
return fmt.Errorf("failed to pull runner image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Look for existing stopped relay containers and remove them
|
||||
slog.Info("Checking and removing old relay containers")
|
||||
oldRelays, err := ctrEngine.ListContainersByImage(ctx, nestriRelayImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range oldRelays {
|
||||
// If running, stop first
|
||||
if strings.Contains(strings.ToLower(c.State), "running") {
|
||||
slog.Info("Stopping old relay container", "id", c.ID)
|
||||
if err = ctrEngine.StopContainer(ctx, c.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
slog.Info("Removing old relay container", "id", c.ID)
|
||||
if err = ctrEngine.RemoveContainer(ctx, c.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Pull the relay image if not in debug mode
|
||||
if !internal.GetFlags().Debug {
|
||||
slog.Info("Pulling relay image", "image", nestriRelayImage)
|
||||
if err := ctrEngine.PullImage(ctx, nestriRelayImage); err != nil {
|
||||
return fmt.Errorf("failed to pull relay image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateRunner creates a new runner image container
|
||||
func CreateRunner(ctx context.Context, ctrEngine containers.ContainerEngine) (string, error) {
|
||||
// For safety, limit to 4 runners
|
||||
if CountRunners() >= 4 {
|
||||
return "", fmt.Errorf("maximum number of runners reached")
|
||||
}
|
||||
|
||||
// Create the container
|
||||
containerID, err := ctrEngine.NewContainer(ctx, nestriRunnerImage, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add the container to the managed list
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
managedContainers[containerID] = ManagedContainer{
|
||||
Container: containers.Container{
|
||||
ID: containerID,
|
||||
},
|
||||
Type: Runner,
|
||||
}
|
||||
|
||||
return containerID, nil
|
||||
}
|
||||
|
||||
// StartRunner starts a runner container, keeping track of it's state
|
||||
func StartRunner(ctx context.Context, ctrEngine containers.ContainerEngine, id string) error {
|
||||
// Verify the container is part of the managed list
|
||||
managedContainersMutex.RLock()
|
||||
if _, ok := managedContainers[id]; !ok {
|
||||
managedContainersMutex.RUnlock()
|
||||
return fmt.Errorf("container %s is not managed", id)
|
||||
}
|
||||
managedContainersMutex.RUnlock()
|
||||
|
||||
// Start the container
|
||||
if err := ctrEngine.StartContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check container status in background at 10 second intervals, if it exits print it's logs
|
||||
go func() {
|
||||
err := monitorContainer(ctx, ctrEngine, id)
|
||||
if err != nil {
|
||||
slog.Error("failure while monitoring runner container", "id", id, "err", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveRunner removes a runner container
|
||||
func RemoveRunner(ctx context.Context, ctrEngine containers.ContainerEngine, id string) error {
|
||||
// Stop the container if it's running
|
||||
if strings.Contains(strings.ToLower(managedContainers[id].State), "running") {
|
||||
if err := ctrEngine.StopContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the container
|
||||
if err := ctrEngine.RemoveContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the container from the managed list
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
delete(managedContainers, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRunners returns a list of all runner containers
|
||||
func ListRunners() []ManagedContainer {
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
var runners []ManagedContainer
|
||||
for _, v := range managedContainers {
|
||||
if v.Type == Runner {
|
||||
runners = append(runners, v)
|
||||
}
|
||||
}
|
||||
return runners
|
||||
}
|
||||
|
||||
// CountRunners returns the number of runner containers
|
||||
func CountRunners() int {
|
||||
return len(ListRunners())
|
||||
}
|
||||
|
||||
// CreateRelay creates a new relay image container
|
||||
func CreateRelay(ctx context.Context, ctrEngine containers.ContainerEngine) (string, error) {
|
||||
// Limit to 1 relay
|
||||
if CountRelays() >= 1 {
|
||||
return "", fmt.Errorf("maximum number of relays reached")
|
||||
}
|
||||
|
||||
// TODO: Placeholder for control secret, should be generated at runtime
|
||||
secretEnv := fmt.Sprintf("CONTROL_SECRET=%s", "1234")
|
||||
|
||||
// Create the container
|
||||
containerID, err := ctrEngine.NewContainer(ctx, nestriRelayImage, []string{secretEnv})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Add the container to the managed list
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
managedContainers[containerID] = ManagedContainer{
|
||||
Container: containers.Container{
|
||||
ID: containerID,
|
||||
},
|
||||
Type: Relay,
|
||||
}
|
||||
|
||||
return containerID, nil
|
||||
}
|
||||
|
||||
// StartRelay starts a relay container, keeping track of it's state
|
||||
func StartRelay(ctx context.Context, ctrEngine containers.ContainerEngine, id string) error {
|
||||
// Verify the container is part of the managed list
|
||||
managedContainersMutex.RLock()
|
||||
if _, ok := managedContainers[id]; !ok {
|
||||
managedContainersMutex.RUnlock()
|
||||
return fmt.Errorf("container %s is not managed", id)
|
||||
}
|
||||
managedContainersMutex.RUnlock()
|
||||
|
||||
// Start the container
|
||||
if err := ctrEngine.StartContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check container status in background at 10 second intervals, if it exits print it's logs
|
||||
go func() {
|
||||
err := monitorContainer(ctx, ctrEngine, id)
|
||||
if err != nil {
|
||||
slog.Error("failure while monitoring relay container", "id", id, "err", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveRelay removes a relay container
|
||||
func RemoveRelay(ctx context.Context, ctrEngine containers.ContainerEngine, id string) error {
|
||||
// Stop the container if it's running
|
||||
if strings.Contains(strings.ToLower(managedContainers[id].State), "running") {
|
||||
if err := ctrEngine.StopContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the container
|
||||
if err := ctrEngine.RemoveContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the container from the managed list
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
delete(managedContainers, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRelays returns a list of all relay containers
|
||||
func ListRelays() []ManagedContainer {
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
var relays []ManagedContainer
|
||||
for _, v := range managedContainers {
|
||||
if v.Type == Relay {
|
||||
relays = append(relays, v)
|
||||
}
|
||||
}
|
||||
return relays
|
||||
}
|
||||
|
||||
// CountRelays returns the number of relay containers
|
||||
func CountRelays() int {
|
||||
return len(ListRelays())
|
||||
}
|
||||
|
||||
// CleanupManaged stops and removes all managed containers
|
||||
func CleanupManaged(ctx context.Context, ctrEngine containers.ContainerEngine) error {
|
||||
if len(managedContainers) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
slog.Info("Cleaning up managed containers")
|
||||
managedContainersMutex.Lock()
|
||||
defer managedContainersMutex.Unlock()
|
||||
for id := range managedContainers {
|
||||
// If running, stop first
|
||||
if strings.Contains(strings.ToLower(managedContainers[id].State), "running") {
|
||||
slog.Info("Stopping managed container", "id", id)
|
||||
if err := ctrEngine.StopContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the container
|
||||
slog.Info("Removing managed container", "id", id)
|
||||
if err := ctrEngine.RemoveContainer(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove from the managed list
|
||||
delete(managedContainers, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func monitorContainer(ctx context.Context, ctrEngine containers.ContainerEngine, id string) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
// Check the container status
|
||||
ctr, err := ctrEngine.InspectContainer(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect container: %w", err)
|
||||
}
|
||||
|
||||
// Update the container state in the managed list
|
||||
managedContainersMutex.Lock()
|
||||
managedContainers[id] = ManagedContainer{
|
||||
Container: containers.Container{
|
||||
ID: ctr.ID,
|
||||
Name: ctr.Name,
|
||||
State: ctr.State,
|
||||
Image: ctr.Image,
|
||||
},
|
||||
Type: Relay,
|
||||
}
|
||||
managedContainersMutex.Unlock()
|
||||
|
||||
if !strings.Contains(strings.ToLower(ctr.State), "running") {
|
||||
// Container is not running, print logs
|
||||
logs, err := ctrEngine.LogsContainer(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get container logs: %w", err)
|
||||
}
|
||||
return fmt.Errorf("container %s stopped running: %s", id, logs)
|
||||
}
|
||||
}
|
||||
// Sleep for 10 seconds
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-time.After(10 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
52
packages/maitred/internal/realtime/messages.go
Normal file
52
packages/maitred/internal/realtime/messages.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package realtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// BaseMessage is the generic top-level message structure
|
||||
type BaseMessage struct {
|
||||
Type string `json:"type"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
}
|
||||
|
||||
type CreatePayload struct{}
|
||||
|
||||
type StartPayload struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
}
|
||||
|
||||
type StopPayload struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
}
|
||||
|
||||
// ParseMessage parses a BaseMessage and returns the specific payload
|
||||
func ParseMessage(data []byte) (BaseMessage, interface{}, error) {
|
||||
var base BaseMessage
|
||||
if err := json.Unmarshal(data, &base); err != nil {
|
||||
return base, nil, err
|
||||
}
|
||||
|
||||
switch base.Type {
|
||||
case "create":
|
||||
var payload CreatePayload
|
||||
if err := json.Unmarshal(base.Payload, &payload); err != nil {
|
||||
return base, nil, err
|
||||
}
|
||||
return base, payload, nil
|
||||
case "start":
|
||||
var payload StartPayload
|
||||
if err := json.Unmarshal(base.Payload, &payload); err != nil {
|
||||
return base, nil, err
|
||||
}
|
||||
return base, payload, nil
|
||||
case "stop":
|
||||
var payload StopPayload
|
||||
if err := json.Unmarshal(base.Payload, &payload); err != nil {
|
||||
return base, nil, err
|
||||
}
|
||||
return base, payload, nil
|
||||
default:
|
||||
return base, base.Payload, nil
|
||||
}
|
||||
}
|
||||
182
packages/maitred/internal/realtime/realtime.go
Normal file
182
packages/maitred/internal/realtime/realtime.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package realtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/eclipse/paho.golang/autopaho"
|
||||
"github.com/eclipse/paho.golang/paho"
|
||||
"log/slog"
|
||||
"nestri/maitred/internal/auth"
|
||||
"nestri/maitred/internal/containers"
|
||||
"nestri/maitred/internal/resource"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, machineID string, containerEngine containers.ContainerEngine, resource *resource.Resource) error {
|
||||
var clientID = generateClientID()
|
||||
var topic = fmt.Sprintf("%s/%s/%s", resource.App.Name, resource.App.Stage, machineID)
|
||||
var serverURL = fmt.Sprintf("wss://%s/mqtt?x-amz-customauthorizer-name=%s", resource.Realtime.Endpoint, resource.Realtime.Authorizer)
|
||||
|
||||
slog.Info("Realtime", "topic", topic)
|
||||
|
||||
userTokens, err := auth.FetchUserToken(machineID, resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info("Realtime", "token", userTokens.AccessToken)
|
||||
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
router := paho.NewStandardRouter()
|
||||
router.DefaultHandler(func(p *paho.Publish) {
|
||||
slog.Debug("DefaultHandler", "topic", p.Topic, "message", fmt.Sprintf("default handler received message: %s - with topic: %s", p.Payload, p.Topic))
|
||||
})
|
||||
|
||||
createTopic := fmt.Sprintf("%s/create", topic)
|
||||
slog.Debug("Registering handler", "topic", createTopic)
|
||||
router.RegisterHandler(createTopic, func(p *paho.Publish) {
|
||||
slog.Debug("Router", "message", "received create message with payload", fmt.Sprintf("%s", p.Payload))
|
||||
|
||||
base, _, err := ParseMessage(p.Payload)
|
||||
if err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprintf("failed to parse message: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if base.Type != "create" {
|
||||
slog.Error("Router", "err", "unexpected message type")
|
||||
return
|
||||
}
|
||||
|
||||
// Create runner container
|
||||
containerID, err := CreateRunner(ctx, containerEngine)
|
||||
if err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprintf("failed to create runner container: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Router", "info", fmt.Sprintf("created runner container: %s", containerID))
|
||||
})
|
||||
|
||||
startTopic := fmt.Sprintf("%s/start", topic)
|
||||
slog.Debug("Registering handler", "topic", startTopic)
|
||||
router.RegisterHandler(startTopic, func(p *paho.Publish) {
|
||||
slog.Debug("Router", "message", "received start message with payload", fmt.Sprintf("%s", p.Payload))
|
||||
|
||||
base, payload, err := ParseMessage(p.Payload)
|
||||
if err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprintf("failed to parse message: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if base.Type != "start" {
|
||||
slog.Error("Router", "err", "unexpected message type")
|
||||
return
|
||||
}
|
||||
|
||||
// Get container ID
|
||||
startPayload, ok := payload.(StartPayload)
|
||||
if !ok {
|
||||
slog.Error("Router", "err", "failed to get payload")
|
||||
return
|
||||
}
|
||||
|
||||
// Start runner container
|
||||
if err = containerEngine.StartContainer(ctx, startPayload.ContainerID); err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprintf("failed to start runner container: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Router", "info", fmt.Sprintf("started runner container: %s", startPayload.ContainerID))
|
||||
})
|
||||
|
||||
stopTopic := fmt.Sprintf("%s/stop", topic)
|
||||
slog.Debug("Registering handler", "topic", stopTopic)
|
||||
router.RegisterHandler(stopTopic, func(p *paho.Publish) {
|
||||
slog.Debug("Router", "message", "received stop message with payload", fmt.Sprintf("%s", p.Payload))
|
||||
|
||||
base, payload, err := ParseMessage(p.Payload)
|
||||
if err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprintf("failed to parse message: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
if base.Type != "stop" {
|
||||
slog.Error("Router", "err", "unexpected message type")
|
||||
return
|
||||
}
|
||||
|
||||
// Get container ID
|
||||
stopPayload, ok := payload.(StopPayload)
|
||||
if !ok {
|
||||
slog.Error("Router", "err", "failed to get payload")
|
||||
return
|
||||
}
|
||||
|
||||
// Stop runner container
|
||||
if err = containerEngine.StopContainer(ctx, stopPayload.ContainerID); err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprintf("failed to stop runner container: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
slog.Info("Router", "info", fmt.Sprintf("stopped runner container: %s", stopPayload.ContainerID))
|
||||
})
|
||||
|
||||
legacyLogger := slog.NewLogLogger(slog.NewTextHandler(os.Stdout, nil), slog.LevelError)
|
||||
cliCfg := autopaho.ClientConfig{
|
||||
ServerUrls: []*url.URL{u},
|
||||
ConnectUsername: "",
|
||||
ConnectPassword: []byte(userTokens.AccessToken),
|
||||
KeepAlive: 20,
|
||||
CleanStartOnInitialConnection: true,
|
||||
SessionExpiryInterval: 60,
|
||||
ReconnectBackoff: autopaho.NewConstantBackoff(time.Second),
|
||||
OnConnectionUp: func(cm *autopaho.ConnectionManager, connAck *paho.Connack) {
|
||||
slog.Info("Router", "info", "MQTT connection is up and running")
|
||||
if _, err = cm.Subscribe(context.Background(), &paho.Subscribe{
|
||||
Subscriptions: []paho.SubscribeOptions{
|
||||
{Topic: fmt.Sprintf("%s/#", topic), QoS: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
slog.Error("Router", "err", fmt.Sprint("failed to subscribe, likely no messages will be received: ", err))
|
||||
}
|
||||
},
|
||||
Errors: legacyLogger,
|
||||
OnConnectError: func(err error) {
|
||||
slog.Error("Router", "err", fmt.Sprintf("error whilst attempting connection: %s", err))
|
||||
},
|
||||
ClientConfig: paho.ClientConfig{
|
||||
ClientID: clientID,
|
||||
OnPublishReceived: []func(paho.PublishReceived) (bool, error){
|
||||
func(pr paho.PublishReceived) (bool, error) {
|
||||
router.Route(pr.Packet.Packet())
|
||||
return true, nil
|
||||
}},
|
||||
OnClientError: func(err error) { slog.Error("Router", "err", fmt.Sprintf("client error: %s", err)) },
|
||||
OnServerDisconnect: func(d *paho.Disconnect) {
|
||||
if d.Properties != nil {
|
||||
slog.Info("Router", "info", fmt.Sprintf("server requested disconnect: %s", d.Properties.ReasonString))
|
||||
} else {
|
||||
slog.Info("Router", "info", fmt.Sprintf("server requested disconnect; reason code: %d", d.ReasonCode))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c, err := autopaho.NewConnection(ctx, cliCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.AwaitConnection(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
17
packages/maitred/internal/realtime/utils.go
Normal file
17
packages/maitred/internal/realtime/utils.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package realtime
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
func generateClientID() string {
|
||||
// Create a source of entropy (cryptographically secure)
|
||||
entropy := ulid.Monotonic(rand.Reader, 0)
|
||||
// Generate a new ULID
|
||||
id := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
|
||||
// Create the client ID string
|
||||
return fmt.Sprintf("mch_%s", id.String())
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type resource struct {
|
||||
type Resource struct {
|
||||
Api struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -17,7 +17,7 @@ type resource struct {
|
||||
AuthFingerprintKey struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
Party struct {
|
||||
Realtime struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Authorizer string `json:"authorizer"`
|
||||
}
|
||||
@@ -27,20 +27,20 @@ type resource struct {
|
||||
}
|
||||
}
|
||||
|
||||
var Resource resource
|
||||
|
||||
func init() {
|
||||
val := reflect.ValueOf(&Resource).Elem()
|
||||
func NewResource() (*Resource, error) {
|
||||
resource := Resource{}
|
||||
val := reflect.ValueOf(&resource).Elem()
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
typeField := val.Type().Field(i)
|
||||
envVarName := fmt.Sprintf("SST_RESOURCE_%s", typeField.Name)
|
||||
envValue, exists := os.LookupEnv(envVarName)
|
||||
if !exists {
|
||||
panic(fmt.Sprintf("Environment variable %s is required", envVarName))
|
||||
return nil, fmt.Errorf("missing environment variable %s", envVarName)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(envValue), field.Addr().Interface()); err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("error unmarshalling %s: %w", envVarName, err)
|
||||
}
|
||||
}
|
||||
return &resource, nil
|
||||
}
|
||||
184
packages/maitred/internal/system/gpu.go
Normal file
184
packages/maitred/internal/system/gpu.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
pciClassVGA = 0x0300 // VGA compatible controller
|
||||
pciClass3D = 0x0302 // 3D controller
|
||||
pciClassDisplay = 0x0380 // Display controller
|
||||
pciClassCoProcessor = 0x0b40 // Co-processor (e.g., NVIDIA Tesla)
|
||||
)
|
||||
|
||||
type infoPair struct {
|
||||
Name string
|
||||
ID int
|
||||
}
|
||||
|
||||
type PCIInfo struct {
|
||||
Slot string
|
||||
Class infoPair
|
||||
Vendor infoPair
|
||||
Device infoPair
|
||||
SVendor infoPair
|
||||
SDevice infoPair
|
||||
Rev string
|
||||
ProgIf string
|
||||
Driver string
|
||||
Modules []string
|
||||
IOMMUGroup string
|
||||
}
|
||||
|
||||
const (
|
||||
VendorIntel = 0x8086
|
||||
VendorNVIDIA = 0x10de
|
||||
VendorAMD = 0x1002
|
||||
)
|
||||
|
||||
func GetAllGPUInfo() ([]PCIInfo, error) {
|
||||
var gpus []PCIInfo
|
||||
|
||||
cmd := exec.Command("lspci", "-mmvvvnnkD")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sections := bytes.Split(output, []byte("\n\n"))
|
||||
for _, section := range sections {
|
||||
var info PCIInfo
|
||||
|
||||
lines := bytes.Split(section, []byte("\n"))
|
||||
for _, line := range lines {
|
||||
parts := bytes.SplitN(line, []byte(":"), 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(string(parts[0]))
|
||||
value := strings.TrimSpace(string(parts[1]))
|
||||
|
||||
switch key {
|
||||
case "Slot":
|
||||
info.Slot = value
|
||||
case "Class":
|
||||
info.Class, err = parseInfoPair(value)
|
||||
case "Vendor":
|
||||
info.Vendor, err = parseInfoPair(value)
|
||||
case "Device":
|
||||
info.Device, err = parseInfoPair(value)
|
||||
case "SVendor":
|
||||
info.SVendor, err = parseInfoPair(value)
|
||||
case "SDevice":
|
||||
info.SDevice, err = parseInfoPair(value)
|
||||
case "Rev":
|
||||
info.Rev = value
|
||||
case "ProgIf":
|
||||
info.ProgIf = value
|
||||
case "Driver":
|
||||
info.Driver = value
|
||||
case "Module":
|
||||
info.Modules = append(info.Modules, value)
|
||||
case "IOMMUGroup":
|
||||
info.IOMMUGroup = value
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a GPU device
|
||||
if isGPUClass(info.Class.ID) {
|
||||
gpus = append(gpus, info)
|
||||
}
|
||||
}
|
||||
|
||||
return gpus, nil
|
||||
}
|
||||
|
||||
// gets infoPair from "SomeName [SomeID]"
|
||||
// example: "DG2 [Arc A770] [56a0]" -> Name: "DG2 [Arc A770]", ID: "56a0"
|
||||
func parseInfoPair(pair string) (infoPair, error) {
|
||||
parts := strings.Split(pair, "[")
|
||||
if len(parts) < 2 {
|
||||
return infoPair{}, errors.New("invalid info pair")
|
||||
}
|
||||
|
||||
id := strings.TrimSuffix(parts[len(parts)-1], "]")
|
||||
name := strings.TrimSuffix(pair, "["+id)
|
||||
name = strings.TrimSpace(name)
|
||||
id = strings.TrimSpace(id)
|
||||
|
||||
// Remove ID including square brackets from name
|
||||
name = strings.ReplaceAll(name, "["+id+"]", "")
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
idHex, err := parseHexID(id)
|
||||
if err != nil {
|
||||
return infoPair{}, err
|
||||
}
|
||||
|
||||
return infoPair{
|
||||
Name: name,
|
||||
ID: idHex,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseHexID(id string) (int, error) {
|
||||
if strings.HasPrefix(id, "0x") {
|
||||
id = id[2:]
|
||||
}
|
||||
parsed, err := strconv.ParseInt(id, 16, 32)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(parsed), nil
|
||||
}
|
||||
|
||||
func isGPUClass(class int) bool {
|
||||
return class == pciClassVGA || class == pciClass3D || class == pciClassDisplay || class == pciClassCoProcessor
|
||||
}
|
||||
|
||||
// GetCardDevices returns the /dev/dri/cardX and /dev/dri/renderDXXX device
|
||||
func (info PCIInfo) GetCardDevices() (cardPath, renderPath string, err error) {
|
||||
busID := strings.ToLower(info.Slot)
|
||||
if !strings.HasPrefix(busID, "0000:") || len(busID) != 12 || busID[4] != ':' || busID[7] != ':' || busID[10] != '.' {
|
||||
return "", "", fmt.Errorf("invalid PCI Bus ID format: %s (expected 0000:XX:YY.Z)", busID)
|
||||
}
|
||||
|
||||
byPathDir := "/dev/dri/by-path/"
|
||||
entries, err := os.ReadDir(byPathDir)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to read %s: %v", byPathDir, err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if strings.HasPrefix(name, "pci-"+busID+"-card") {
|
||||
cardPath, err = filepath.EvalSymlinks(filepath.Join(byPathDir, name))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to resolve card symlink %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(name, "pci-"+busID+"-render") {
|
||||
renderPath, err = filepath.EvalSymlinks(filepath.Join(byPathDir, name))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to resolve render symlink %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cardPath == "" && renderPath == "" {
|
||||
return "", "", fmt.Errorf("no DRM devices found for PCI Bus ID: %s", busID)
|
||||
}
|
||||
return cardPath, renderPath, nil
|
||||
}
|
||||
290
packages/maitred/internal/system/gpu_intel.go
Normal file
290
packages/maitred/internal/system/gpu_intel.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// FDInfo holds parsed fdinfo data
|
||||
type FDInfo struct {
|
||||
ClientID string
|
||||
EngineTime uint64 // i915: "drm-engine-render" in ns
|
||||
Cycles uint64 // Xe: "drm-cycles-rcs"
|
||||
TotalCycles uint64 // Xe: "drm-total-cycles-rcs"
|
||||
MemoryVRAM uint64 // i915: "drm-memory-vram", Xe: "drm-total-vram0" in bytes
|
||||
}
|
||||
|
||||
// findCardX maps PCI slot to /dev/dri/cardX
|
||||
func findCardX(pciSlot string) (string, error) {
|
||||
driPath := "/sys/class/drm"
|
||||
entries, err := os.ReadDir(driPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read /sys/class/drm: %v", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), "card") {
|
||||
deviceLink := filepath.Join(driPath, entry.Name(), "device")
|
||||
target, err := os.Readlink(deviceLink)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(target, pciSlot) {
|
||||
return entry.Name(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no cardX found for PCI slot %s", pciSlot)
|
||||
}
|
||||
|
||||
// getDriver retrieves the driver name
|
||||
func getDriver(cardX string) (string, error) {
|
||||
driverLink := filepath.Join("/sys/class/drm", cardX, "device", "driver")
|
||||
target, err := os.Readlink(driverLink)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read driver link for %s: %v", cardX, err)
|
||||
}
|
||||
return filepath.Base(target), nil
|
||||
}
|
||||
|
||||
// collectFDInfo gathers fdinfo data
|
||||
func collectFDInfo(cardX string) ([]FDInfo, error) {
|
||||
var fdInfos []FDInfo
|
||||
clientIDs := make(map[string]struct{})
|
||||
|
||||
procDirs, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read /proc: %v", err)
|
||||
}
|
||||
|
||||
for _, procDir := range procDirs {
|
||||
if !procDir.IsDir() {
|
||||
continue
|
||||
}
|
||||
pid := procDir.Name()
|
||||
if _, err := strconv.Atoi(pid); err != nil {
|
||||
continue
|
||||
}
|
||||
fdDir := filepath.Join("/proc", pid, "fd")
|
||||
fdEntries, err := os.ReadDir(fdDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, fdEntry := range fdEntries {
|
||||
fdPath := filepath.Join(fdDir, fdEntry.Name())
|
||||
target, err := os.Readlink(fdPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if target == "/dev/dri/"+cardX {
|
||||
fdinfoPath := filepath.Join("/proc", pid, "fdinfo", fdEntry.Name())
|
||||
file, err := os.Open(fdinfoPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var clientID, engineTime, cycles, totalCycles, memoryVRAM string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
switch key {
|
||||
case "drm-client-id":
|
||||
clientID = value
|
||||
case "drm-engine-render":
|
||||
engineTime = value
|
||||
case "drm-cycles-rcs":
|
||||
cycles = value
|
||||
case "drm-total-cycles-rcs":
|
||||
totalCycles = value
|
||||
case "drm-memory-vram", "drm-total-vram0": // i915 and Xe keys
|
||||
memoryVRAM = value
|
||||
}
|
||||
}
|
||||
if clientID == "" || clientID == "0" {
|
||||
continue
|
||||
}
|
||||
if _, exists := clientIDs[clientID]; exists {
|
||||
continue
|
||||
}
|
||||
clientIDs[clientID] = struct{}{}
|
||||
|
||||
fdInfo := FDInfo{ClientID: clientID}
|
||||
if engineTime != "" {
|
||||
fdInfo.EngineTime, _ = strconv.ParseUint(engineTime, 10, 64)
|
||||
}
|
||||
if cycles != "" {
|
||||
fdInfo.Cycles, _ = strconv.ParseUint(cycles, 10, 64)
|
||||
}
|
||||
if totalCycles != "" {
|
||||
fdInfo.TotalCycles, _ = strconv.ParseUint(totalCycles, 10, 64)
|
||||
}
|
||||
if memoryVRAM != "" {
|
||||
if strings.HasSuffix(memoryVRAM, " kB") || strings.HasSuffix(memoryVRAM, " KiB") {
|
||||
memKB := strings.TrimSuffix(strings.TrimSuffix(memoryVRAM, " kB"), " KiB")
|
||||
if mem, err := strconv.ParseUint(memKB, 10, 64); err == nil {
|
||||
fdInfo.MemoryVRAM = mem * 1024 // Convert kB to bytes
|
||||
}
|
||||
} else {
|
||||
fdInfo.MemoryVRAM, _ = strconv.ParseUint(memoryVRAM, 10, 64) // Assume bytes if no unit
|
||||
}
|
||||
}
|
||||
fdInfos = append(fdInfos, fdInfo)
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
return fdInfos, nil
|
||||
}
|
||||
|
||||
// drmIoctl wraps the syscall.Syscall for ioctl
|
||||
func drmIoctl(fd int, request uintptr, data unsafe.Pointer) error {
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), request, uintptr(data))
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("ioctl failed: %v", errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func monitorIntelGPU(device PCIInfo) GPUUsage {
|
||||
// Map PCI slot to cardX
|
||||
cardX, err := findCardX(device.Slot)
|
||||
if err != nil {
|
||||
slog.Warn("failed to find cardX for Intel GPU", "slot", device.Slot, "error", err)
|
||||
return GPUUsage{}
|
||||
}
|
||||
|
||||
// Determine driver
|
||||
driver, err := getDriver(cardX)
|
||||
if err != nil {
|
||||
slog.Warn("failed to get driver", "card", cardX, "error", err)
|
||||
return GPUUsage{}
|
||||
}
|
||||
if driver != "i915" && driver != "xe" {
|
||||
slog.Warn("unsupported Intel driver", "driver", driver, "card", cardX)
|
||||
return GPUUsage{}
|
||||
}
|
||||
|
||||
// PCIInfo also has the driver, let's warn if they don't match
|
||||
if device.Driver != driver {
|
||||
slog.Warn("driver mismatch", "card", cardX, "lspci driver", device.Driver, "sysfs driver", driver)
|
||||
}
|
||||
|
||||
// Open DRM device
|
||||
cardPath := "/dev/dri/" + cardX
|
||||
fd, err := syscall.Open(cardPath, syscall.O_RDWR, 0)
|
||||
if err != nil {
|
||||
slog.Error("failed to open DRM device", "path", cardPath, "error", err)
|
||||
return GPUUsage{}
|
||||
}
|
||||
defer func(fd int) {
|
||||
_ = syscall.Close(fd)
|
||||
}(fd)
|
||||
|
||||
// Get total and used VRAM via ioctl
|
||||
var totalVRAM, usedVRAMFromIOCTL uint64
|
||||
if driver == "i915" {
|
||||
totalVRAM, usedVRAMFromIOCTL, err = getMemoryRegionsI915(fd)
|
||||
} else { // xe
|
||||
totalVRAM, usedVRAMFromIOCTL, err = queryMemoryRegionsXE(fd)
|
||||
}
|
||||
if err != nil {
|
||||
//slog.Debug("failed to get memory regions", "card", cardX, "error", err)
|
||||
// Proceed with totalVRAM = 0 if ioctl fails
|
||||
}
|
||||
|
||||
// Collect samples for usage percentage
|
||||
firstFDInfos, err := collectFDInfo(cardX)
|
||||
if err != nil {
|
||||
slog.Warn("failed to collect first FDInfo", "card", cardX, "error", err)
|
||||
return GPUUsage{}
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
secondFDInfos, err := collectFDInfo(cardX)
|
||||
if err != nil {
|
||||
slog.Warn("failed to collect second FDInfo", "card", cardX, "error", err)
|
||||
return GPUUsage{}
|
||||
}
|
||||
|
||||
// Calculate usage percentage
|
||||
var usagePercent float64
|
||||
if driver == "i915" {
|
||||
var totalDeltaTime uint64
|
||||
for _, second := range secondFDInfos {
|
||||
for _, first := range firstFDInfos {
|
||||
if second.ClientID == first.ClientID {
|
||||
totalDeltaTime += second.EngineTime - first.EngineTime
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if totalDeltaTime > 0 {
|
||||
usagePercent = float64(totalDeltaTime) / 1e9 * 100 // ns to percent
|
||||
}
|
||||
} else { // xe
|
||||
var totalDeltaCycles, deltaTotalCycles uint64
|
||||
for i, second := range secondFDInfos {
|
||||
for _, first := range firstFDInfos {
|
||||
if second.ClientID == first.ClientID {
|
||||
deltaCycles := second.Cycles - first.Cycles
|
||||
totalDeltaCycles += deltaCycles
|
||||
if i == 0 {
|
||||
deltaTotalCycles = second.TotalCycles - first.TotalCycles
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if deltaTotalCycles > 0 {
|
||||
usagePercent = float64(totalDeltaCycles) / float64(deltaTotalCycles) * 100
|
||||
}
|
||||
}
|
||||
if usagePercent > 100 {
|
||||
usagePercent = 100
|
||||
}
|
||||
|
||||
// Sum per-process VRAM usage as fallback
|
||||
var usedVRAM uint64
|
||||
for _, fdInfo := range secondFDInfos {
|
||||
usedVRAM += fdInfo.MemoryVRAM
|
||||
}
|
||||
|
||||
// Prefer ioctl used VRAM if available and non-zero
|
||||
if usedVRAMFromIOCTL != 0 {
|
||||
usedVRAM = usedVRAMFromIOCTL
|
||||
}
|
||||
|
||||
// Compute VRAM metrics
|
||||
var freeVRAM uint64
|
||||
var usedPercent float64
|
||||
if totalVRAM > 0 {
|
||||
if usedVRAM > totalVRAM {
|
||||
usedVRAM = totalVRAM
|
||||
}
|
||||
freeVRAM = totalVRAM - usedVRAM
|
||||
usedPercent = float64(usedVRAM) / float64(totalVRAM) * 100
|
||||
}
|
||||
|
||||
return GPUUsage{
|
||||
Info: device,
|
||||
UsagePercent: usagePercent,
|
||||
VRAM: VRAMUsage{
|
||||
Total: totalVRAM,
|
||||
Used: usedVRAM,
|
||||
Free: freeVRAM,
|
||||
UsedPercent: usedPercent,
|
||||
},
|
||||
}
|
||||
}
|
||||
86
packages/maitred/internal/system/gpu_intel_i915.go
Normal file
86
packages/maitred/internal/system/gpu_intel_i915.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Constants for i915
|
||||
const (
|
||||
DRM_COMMAND_BASE = 0x40
|
||||
DRM_I915_QUERY = 0x39
|
||||
DRM_IOCTL_I915_QUERY = 0x80106479 // _IOWR('d', 0x79, 16)
|
||||
DRM_I915_QUERY_MEMORY_REGIONS = 4
|
||||
I915_MEMORY_CLASS_DEVICE = 1
|
||||
)
|
||||
|
||||
// drmI915QueryItem mirrors struct drm_i915_query_item
|
||||
type drmI915QueryItem struct {
|
||||
QueryID uintptr
|
||||
Length int32
|
||||
Flags uint32
|
||||
DataPtr uintptr
|
||||
}
|
||||
|
||||
// drmI915Query mirrors struct drm_i915_query
|
||||
type drmI915Query struct {
|
||||
NumItems uint32
|
||||
Flags uint32
|
||||
ItemsPtr uintptr
|
||||
}
|
||||
|
||||
// drmI915MemoryRegionInfo mirrors struct drm_i915_memory_region_info
|
||||
type drmI915MemoryRegionInfo struct {
|
||||
Region struct {
|
||||
MemoryClass uint16
|
||||
MemoryInstance uint16
|
||||
}
|
||||
Rsvd0 uint32
|
||||
ProbedSize uint64
|
||||
UnallocatedSize uint64
|
||||
Rsvd1 [8]uint64
|
||||
}
|
||||
|
||||
func getMemoryRegionsI915(fd int) (totalVRAM, usedVRAM uint64, err error) {
|
||||
// Step 1: Get the required buffer size
|
||||
item := drmI915QueryItem{
|
||||
QueryID: DRM_I915_QUERY_MEMORY_REGIONS,
|
||||
Length: 0,
|
||||
}
|
||||
query := drmI915Query{
|
||||
NumItems: 1,
|
||||
ItemsPtr: uintptr(unsafe.Pointer(&item)),
|
||||
}
|
||||
if err = drmIoctl(fd, DRM_IOCTL_I915_QUERY, unsafe.Pointer(&query)); err != nil {
|
||||
return 0, 0, fmt.Errorf("initial i915 query failed: %v", err)
|
||||
}
|
||||
if item.Length <= 0 {
|
||||
return 0, 0, fmt.Errorf("i915 query returned invalid length: %d", item.Length)
|
||||
}
|
||||
|
||||
// Step 2: Allocate buffer and perform the query
|
||||
data := make([]byte, item.Length)
|
||||
item.DataPtr = uintptr(unsafe.Pointer(&data[0]))
|
||||
if err = drmIoctl(fd, DRM_IOCTL_I915_QUERY, unsafe.Pointer(&query)); err != nil {
|
||||
return 0, 0, fmt.Errorf("second i915 query failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Parse the memory regions
|
||||
numRegions := *(*uint32)(unsafe.Pointer(&data[0]))
|
||||
headerSize := uint32(16) // num_regions (4) + rsvd[3] (12) = 16 bytes
|
||||
regionSize := uint32(88) // Size of drm_i915_memory_region_info (calculated: 4+4+8+8+64)
|
||||
|
||||
for i := uint32(0); i < numRegions; i++ {
|
||||
offset := headerSize + i*regionSize
|
||||
if offset+regionSize > uint32(len(data)) {
|
||||
return 0, 0, fmt.Errorf("data buffer too small for i915 region %d", i)
|
||||
}
|
||||
mr := (*drmI915MemoryRegionInfo)(unsafe.Pointer(&data[offset]))
|
||||
if mr.Region.MemoryClass == I915_MEMORY_CLASS_DEVICE {
|
||||
totalVRAM += mr.ProbedSize
|
||||
usedVRAM += mr.ProbedSize - mr.UnallocatedSize
|
||||
}
|
||||
}
|
||||
|
||||
return totalVRAM, usedVRAM, nil
|
||||
}
|
||||
84
packages/maitred/internal/system/gpu_intel_xe.go
Normal file
84
packages/maitred/internal/system/gpu_intel_xe.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Constants from xe_drm.h
|
||||
const (
|
||||
DRM_XE_DEVICE_QUERY_MEM_REGIONS = 1
|
||||
DRM_XE_MEM_REGION_CLASS_VRAM = 1
|
||||
DRM_XE_DEVICE_QUERY = 0x00
|
||||
DRM_IOCTL_XE_DEVICE_QUERY uintptr = 0xC0286440 // Precomputed as above
|
||||
)
|
||||
|
||||
// drmXEDeviceQuery mirrors struct drm_xe_device_query
|
||||
type drmXEDeviceQuery struct {
|
||||
Extensions uint64
|
||||
Query uint32
|
||||
Size uint32
|
||||
Data uint64
|
||||
Reserved [2]uint64
|
||||
}
|
||||
|
||||
// drmXEQueryMemRegions mirrors struct drm_xe_query_mem_regions header
|
||||
type drmXEQueryMemRegions struct {
|
||||
NumMemRegions uint32
|
||||
Pad uint32
|
||||
// mem_regions[] follows
|
||||
}
|
||||
|
||||
// drmXEMemRegion mirrors struct drm_xe_mem_region
|
||||
type drmXEMemRegion struct {
|
||||
MemClass uint16
|
||||
Instance uint16
|
||||
MinPageSize uint32
|
||||
TotalSize uint64
|
||||
Used uint64
|
||||
CPUVisibleSize uint64
|
||||
CPUVisibleUsed uint64
|
||||
Reserved [6]uint64
|
||||
}
|
||||
|
||||
func queryMemoryRegionsXE(fd int) (totalVRAM, usedVRAM uint64, err error) {
|
||||
// Step 1: Get the required size
|
||||
query := drmXEDeviceQuery{
|
||||
Query: DRM_XE_DEVICE_QUERY_MEM_REGIONS,
|
||||
Size: 0,
|
||||
}
|
||||
if err = drmIoctl(fd, DRM_IOCTL_XE_DEVICE_QUERY, unsafe.Pointer(&query)); err != nil {
|
||||
return 0, 0, fmt.Errorf("initial xe query failed: %v", err)
|
||||
}
|
||||
if query.Size == 0 {
|
||||
return 0, 0, fmt.Errorf("xe query returned zero size")
|
||||
}
|
||||
|
||||
// Step 2: Allocate buffer and perform the query
|
||||
data := make([]byte, query.Size)
|
||||
query.Data = uint64(uintptr(unsafe.Pointer(&data[0])))
|
||||
query.Size = uint32(len(data))
|
||||
if err = drmIoctl(fd, DRM_IOCTL_XE_DEVICE_QUERY, unsafe.Pointer(&query)); err != nil {
|
||||
return 0, 0, fmt.Errorf("second xe query failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Parse the memory regions
|
||||
header := (*drmXEQueryMemRegions)(unsafe.Pointer(&data[0]))
|
||||
numRegions := header.NumMemRegions
|
||||
headerSize := unsafe.Sizeof(drmXEQueryMemRegions{})
|
||||
regionSize := unsafe.Sizeof(drmXEMemRegion{})
|
||||
|
||||
for i := uint32(0); i < numRegions; i++ {
|
||||
offset := headerSize + uintptr(i)*regionSize
|
||||
if offset+regionSize > uintptr(len(data)) {
|
||||
return 0, 0, fmt.Errorf("data buffer too small for xe region %d", i)
|
||||
}
|
||||
mr := (*drmXEMemRegion)(unsafe.Pointer(&data[offset]))
|
||||
if mr.MemClass == DRM_XE_MEM_REGION_CLASS_VRAM {
|
||||
totalVRAM += mr.TotalSize
|
||||
usedVRAM += mr.Used
|
||||
}
|
||||
}
|
||||
|
||||
return totalVRAM, usedVRAM, nil
|
||||
}
|
||||
57
packages/maitred/internal/system/gpu_nvidia.go
Normal file
57
packages/maitred/internal/system/gpu_nvidia.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// monitorNVIDIAGPU monitors an NVIDIA GPU using nvidia-smi
|
||||
func monitorNVIDIAGPU(device PCIInfo) GPUUsage {
|
||||
// Query nvidia-smi for GPU metrics
|
||||
cmd := exec.Command("nvidia-smi", "--query-gpu=pci.bus_id,utilization.gpu,memory.total,memory.used,memory.free", "--format=csv,noheader,nounits")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
slog.Warn("failed to run nvidia-smi", "error", err)
|
||||
return GPUUsage{}
|
||||
}
|
||||
|
||||
// Parse output and find matching GPU
|
||||
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Split(line, ", ")
|
||||
if len(fields) != 5 {
|
||||
continue
|
||||
}
|
||||
busID := fields[0] // e.g., "0000:01:00.0"
|
||||
if strings.Contains(busID, device.Slot) || strings.Contains(device.Slot, busID) {
|
||||
usagePercent, _ := strconv.ParseFloat(fields[1], 64)
|
||||
totalMiB, _ := strconv.ParseUint(fields[2], 10, 64)
|
||||
usedMiB, _ := strconv.ParseUint(fields[3], 10, 64)
|
||||
freeMiB, _ := strconv.ParseUint(fields[4], 10, 64)
|
||||
|
||||
// Convert MiB to bytes
|
||||
total := totalMiB * 1024 * 1024
|
||||
used := usedMiB * 1024 * 1024
|
||||
free := freeMiB * 1024 * 1024
|
||||
usedPercent := float64(0)
|
||||
if total > 0 {
|
||||
usedPercent = float64(used) / float64(total) * 100
|
||||
}
|
||||
|
||||
return GPUUsage{
|
||||
Info: device,
|
||||
UsagePercent: usagePercent,
|
||||
VRAM: VRAMUsage{
|
||||
Total: total,
|
||||
Used: used,
|
||||
Free: free,
|
||||
UsedPercent: usedPercent,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
slog.Warn("No NVIDIA GPU found matching PCI slot", "slot", device.Slot)
|
||||
return GPUUsage{}
|
||||
}
|
||||
24
packages/maitred/internal/system/id.go
Normal file
24
packages/maitred/internal/system/id.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusPath = "/var/lib/dbus/machine-id"
|
||||
dbusPathEtc = "/etc/machine-id"
|
||||
)
|
||||
|
||||
// GetID returns the machine ID specified at `/var/lib/dbus/machine-id` or `/etc/machine-id`.
|
||||
// If there is an error reading the files an empty string is returned.
|
||||
func GetID() (string, error) {
|
||||
id, err := os.ReadFile(dbusPath)
|
||||
if err != nil {
|
||||
id, err = os.ReadFile(dbusPathEtc)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Trim(string(id), " \n"), nil
|
||||
}
|
||||
405
packages/maitred/internal/system/resources.go
Normal file
405
packages/maitred/internal/system/resources.go
Normal file
@@ -0,0 +1,405 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CPUInfo contains CPU model information
|
||||
type CPUInfo struct {
|
||||
Vendor string `json:"vendor"` // CPU vendor (e.g., "AMD", "Intel")
|
||||
Model string `json:"model"` // CPU model name
|
||||
}
|
||||
|
||||
// CPUUsage contains CPU usage metrics
|
||||
type CPUUsage struct {
|
||||
Info CPUInfo `json:"info"` // CPU vendor and model information
|
||||
Total float64 `json:"total"` // Total CPU usage in percentage (0-100)
|
||||
PerCore []float64 `json:"per_core"` // CPU usage per core in percentage (0-100)
|
||||
}
|
||||
|
||||
// MemoryUsage contains memory usage metrics
|
||||
type MemoryUsage struct {
|
||||
Total uint64 `json:"total"` // Total memory in bytes
|
||||
Used uint64 `json:"used"` // Used memory in bytes
|
||||
Available uint64 `json:"available"` // Available memory in bytes
|
||||
Free uint64 `json:"free"` // Free memory in bytes
|
||||
UsedPercent float64 `json:"used_percent"` // Used memory in percentage (0-100)
|
||||
}
|
||||
|
||||
// FilesystemUsage contains usage metrics for a filesystem path
|
||||
type FilesystemUsage struct {
|
||||
Path string `json:"path"` // Filesystem path
|
||||
Total uint64 `json:"total"` // Total disk space in bytes
|
||||
Used uint64 `json:"used"` // Used disk space in bytes
|
||||
Free uint64 `json:"free"` // Free disk space in bytes
|
||||
UsedPercent float64 `json:"used_percent"` // Used disk space in percentage (0-100)
|
||||
}
|
||||
|
||||
// GPUUsage contains GPU usage metrics
|
||||
type GPUUsage struct {
|
||||
Info PCIInfo `json:"pci_info"` // GPU PCI information
|
||||
UsagePercent float64 `json:"usage_percent"` // GPU usage in percentage (0-100)
|
||||
VRAM VRAMUsage `json:"vram"` // GPU memory usage metrics
|
||||
}
|
||||
|
||||
// VRAMUsage contains GPU memory usage metrics
|
||||
type VRAMUsage struct {
|
||||
Total uint64 `json:"total"` // Total VRAM in bytes
|
||||
Used uint64 `json:"used"` // Used VRAM in bytes
|
||||
Free uint64 `json:"free"` // Free VRAM in bytes
|
||||
UsedPercent float64 `json:"used_percent"` // Used VRAM in percentage (0-100)
|
||||
}
|
||||
|
||||
// ResourceUsage contains resource usage metrics
|
||||
type ResourceUsage struct {
|
||||
CPU CPUUsage `json:"cpu"` // CPU usage metrics
|
||||
Memory MemoryUsage `json:"memory"` // Memory usage metrics
|
||||
Disk FilesystemUsage `json:"disk"` // Disk usage metrics
|
||||
GPUs []GPUUsage `json:"gpus"` // Per-GPU usage metrics
|
||||
}
|
||||
|
||||
var (
|
||||
lastUsage ResourceUsage
|
||||
lastUsageMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// GetSystemUsage returns last known system resource usage metrics
|
||||
func GetSystemUsage() ResourceUsage {
|
||||
lastUsageMutex.RLock()
|
||||
defer lastUsageMutex.RUnlock()
|
||||
return lastUsage
|
||||
}
|
||||
|
||||
// StartMonitoring begins periodic system usage monitoring with the given interval
|
||||
func StartMonitoring(ctx context.Context, interval time.Duration) {
|
||||
slog.Info("Starting system monitoring")
|
||||
go func() {
|
||||
// Initial sample immediately
|
||||
updateUsage()
|
||||
|
||||
// Ticker for periodic updates
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
slog.Info("Stopping system monitoring")
|
||||
return
|
||||
case <-ticker.C:
|
||||
updateUsage()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// updateUsage collects and updates the lastUsage variable
|
||||
func updateUsage() {
|
||||
// Collect CPU usage
|
||||
cpu := GetCPUUsage()
|
||||
|
||||
// Collect memory usage
|
||||
memory := GetMemoryUsage()
|
||||
|
||||
// Collect root filesystem usage
|
||||
rootfs, err := GetFilesystemUsage("/")
|
||||
if err != nil {
|
||||
slog.Warn("Failed to get root filesystem usage", "error", err)
|
||||
}
|
||||
|
||||
// Collect GPU usage
|
||||
gpus := GetGPUUsage()
|
||||
|
||||
// Update shared variable safely
|
||||
lastUsageMutex.Lock()
|
||||
lastUsage = ResourceUsage{
|
||||
CPU: cpu,
|
||||
Memory: memory,
|
||||
Disk: rootfs,
|
||||
GPUs: gpus,
|
||||
}
|
||||
lastUsageMutex.Unlock()
|
||||
}
|
||||
|
||||
// PrettyString returns resource usage metrics in a human-readable format string
|
||||
func (r ResourceUsage) PrettyString() string {
|
||||
res := "Resource Usage:\n"
|
||||
res += fmt.Sprintf(" CPU:\n")
|
||||
res += fmt.Sprintf(" Vendor: %s\n", r.CPU.Info.Vendor)
|
||||
res += fmt.Sprintf(" Model: %s\n", r.CPU.Info.Model)
|
||||
res += fmt.Sprintf(" Total Usage: %.2f%%\n", r.CPU.Total)
|
||||
res += fmt.Sprintf(" Per-Core Usage:\n")
|
||||
res += fmt.Sprintf(" [")
|
||||
for i, coreUsage := range r.CPU.PerCore {
|
||||
res += fmt.Sprintf("%.2f%%", coreUsage)
|
||||
if i < len(r.CPU.PerCore)-1 {
|
||||
res += ", "
|
||||
}
|
||||
}
|
||||
res += "]\n"
|
||||
|
||||
res += fmt.Sprintf(" Memory:\n")
|
||||
res += fmt.Sprintf(" Total: %d bytes\n", r.Memory.Total)
|
||||
res += fmt.Sprintf(" Used: %d bytes\n", r.Memory.Used)
|
||||
res += fmt.Sprintf(" Available: %d bytes\n", r.Memory.Available)
|
||||
res += fmt.Sprintf(" Free: %d bytes\n", r.Memory.Free)
|
||||
res += fmt.Sprintf(" Used Percent: %.2f%%\n", r.Memory.UsedPercent)
|
||||
|
||||
res += fmt.Sprintf(" Filesystem:\n")
|
||||
res += fmt.Sprintf(" Path: %s\n", r.Disk.Path)
|
||||
res += fmt.Sprintf(" Total: %d bytes\n", r.Disk.Total)
|
||||
res += fmt.Sprintf(" Used: %d bytes\n", r.Disk.Used)
|
||||
res += fmt.Sprintf(" Free: %d bytes\n", r.Disk.Free)
|
||||
res += fmt.Sprintf(" Used Percent: %.2f%%\n", r.Disk.UsedPercent)
|
||||
|
||||
res += fmt.Sprintf(" GPUs:\n")
|
||||
for i, gpu := range r.GPUs {
|
||||
cardDev, renderDev, err := gpu.Info.GetCardDevices()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to get card and render devices", "error", err)
|
||||
}
|
||||
|
||||
res += fmt.Sprintf(" GPU %d:\n", i)
|
||||
res += fmt.Sprintf(" Vendor: %s\n", gpu.Info.Vendor.Name)
|
||||
res += fmt.Sprintf(" Model: %s\n", gpu.Info.Device.Name)
|
||||
res += fmt.Sprintf(" Driver: %s\n", gpu.Info.Driver)
|
||||
res += fmt.Sprintf(" Card Device: %s\n", cardDev)
|
||||
res += fmt.Sprintf(" Render Device: %s\n", renderDev)
|
||||
res += fmt.Sprintf(" Usage Percent: %.2f%%\n", gpu.UsagePercent)
|
||||
res += fmt.Sprintf(" VRAM:\n")
|
||||
res += fmt.Sprintf(" Total: %d bytes\n", gpu.VRAM.Total)
|
||||
res += fmt.Sprintf(" Used: %d bytes\n", gpu.VRAM.Used)
|
||||
res += fmt.Sprintf(" Free: %d bytes\n", gpu.VRAM.Free)
|
||||
res += fmt.Sprintf(" Used Percent: %.2f%%\n", gpu.VRAM.UsedPercent)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// GetCPUUsage gathers CPU usage
|
||||
func GetCPUUsage() CPUUsage {
|
||||
// Helper to read /proc/stat
|
||||
readStat := func() (uint64, uint64, []uint64, []uint64) {
|
||||
statBytes, err := os.ReadFile("/proc/stat")
|
||||
if err != nil {
|
||||
slog.Warn("Failed to read /proc/stat", "error", err)
|
||||
return 0, 0, nil, nil
|
||||
}
|
||||
statScanner := bufio.NewScanner(bytes.NewReader(statBytes))
|
||||
statScanner.Scan() // Total CPU line
|
||||
fields := strings.Fields(statScanner.Text())[1:]
|
||||
var total, idle uint64
|
||||
for i, field := range fields {
|
||||
val, _ := strconv.ParseUint(field, 10, 64)
|
||||
total += val
|
||||
if i == 3 { // Idle time
|
||||
idle = val
|
||||
}
|
||||
}
|
||||
|
||||
var perCoreTotals, perCoreIdles []uint64
|
||||
for statScanner.Scan() {
|
||||
line := statScanner.Text()
|
||||
if !strings.HasPrefix(line, "cpu") {
|
||||
break
|
||||
}
|
||||
coreFields := strings.Fields(line)[1:]
|
||||
var coreTotal, coreIdle uint64
|
||||
for i, field := range coreFields {
|
||||
val, _ := strconv.ParseUint(field, 10, 64)
|
||||
coreTotal += val
|
||||
if i == 3 { // Idle time
|
||||
coreIdle = val
|
||||
}
|
||||
}
|
||||
perCoreTotals = append(perCoreTotals, coreTotal)
|
||||
perCoreIdles = append(perCoreIdles, coreIdle)
|
||||
}
|
||||
return total, idle, perCoreTotals, perCoreIdles
|
||||
}
|
||||
|
||||
// First sample
|
||||
prevTotal, prevIdle, prevPerCoreTotals, prevPerCoreIdles := readStat()
|
||||
time.Sleep(1 * time.Second) // Delay for accurate delta
|
||||
// Second sample
|
||||
currTotal, currIdle, currPerCoreTotals, currPerCoreIdles := readStat()
|
||||
|
||||
// Calculate total CPU usage
|
||||
totalDiff := float64(currTotal - prevTotal)
|
||||
idleDiff := float64(currIdle - prevIdle)
|
||||
var totalUsage float64
|
||||
if totalDiff > 0 {
|
||||
totalUsage = ((totalDiff - idleDiff) / totalDiff) * 100
|
||||
}
|
||||
|
||||
// Calculate per-core usage
|
||||
var perCore []float64
|
||||
for i := range currPerCoreTotals {
|
||||
coreTotalDiff := float64(currPerCoreTotals[i] - prevPerCoreTotals[i])
|
||||
coreIdleDiff := float64(currPerCoreIdles[i] - prevPerCoreIdles[i])
|
||||
if coreTotalDiff > 0 {
|
||||
perCoreUsage := ((coreTotalDiff - coreIdleDiff) / coreTotalDiff) * 100
|
||||
perCore = append(perCore, perCoreUsage)
|
||||
} else {
|
||||
perCore = append(perCore, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Get CPU info
|
||||
cpuInfoBytes, err := os.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
slog.Warn("Failed to read /proc/cpuinfo", "error", err)
|
||||
return CPUUsage{}
|
||||
}
|
||||
cpuInfo := string(cpuInfoBytes)
|
||||
scanner := bufio.NewScanner(strings.NewReader(cpuInfo))
|
||||
var vendor, model string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "vendor_id") {
|
||||
vendor = strings.TrimSpace(strings.Split(line, ":")[1])
|
||||
} else if strings.HasPrefix(line, "model name") {
|
||||
model = strings.TrimSpace(strings.Split(line, ":")[1])
|
||||
}
|
||||
if vendor != "" && model != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return CPUUsage{
|
||||
Info: CPUInfo{
|
||||
Vendor: vendor,
|
||||
Model: model,
|
||||
},
|
||||
Total: totalUsage,
|
||||
PerCore: perCore,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMemoryUsage gathers memory usage from /proc/meminfo
|
||||
func GetMemoryUsage() MemoryUsage {
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
var total, free, available uint64
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
total = parseMemInfoLine(line)
|
||||
} else if strings.HasPrefix(line, "MemFree:") {
|
||||
free = parseMemInfoLine(line)
|
||||
} else if strings.HasPrefix(line, "MemAvailable:") {
|
||||
available = parseMemInfoLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
used := total - available
|
||||
usedPercent := (float64(used) / float64(total)) * 100
|
||||
|
||||
return MemoryUsage{
|
||||
Total: total * 1024, // Convert from KB to bytes
|
||||
Used: used * 1024,
|
||||
Available: available * 1024,
|
||||
Free: free * 1024,
|
||||
UsedPercent: usedPercent,
|
||||
}
|
||||
}
|
||||
|
||||
// parseMemInfoLine parses a line from /proc/meminfo
|
||||
func parseMemInfoLine(line string) uint64 {
|
||||
fields := strings.Fields(line)
|
||||
val, _ := strconv.ParseUint(fields[1], 10, 64)
|
||||
return val
|
||||
}
|
||||
|
||||
// GetFilesystemUsage gathers usage statistics for the specified path
|
||||
func GetFilesystemUsage(path string) (FilesystemUsage, error) {
|
||||
cmd := exec.Command("df", path)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return FilesystemUsage{}, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
if len(lines) < 2 {
|
||||
return FilesystemUsage{}, fmt.Errorf("unexpected `df` output format for path: %s", path)
|
||||
}
|
||||
|
||||
fields := strings.Fields(lines[1])
|
||||
if len(fields) < 5 {
|
||||
return FilesystemUsage{}, fmt.Errorf("insufficient fields in `df` output for path: %s", path)
|
||||
}
|
||||
|
||||
total, err := strconv.ParseUint(fields[1], 10, 64)
|
||||
if err != nil {
|
||||
return FilesystemUsage{}, fmt.Errorf("failed to parse total space: %v", err)
|
||||
}
|
||||
|
||||
used, err := strconv.ParseUint(fields[2], 10, 64)
|
||||
if err != nil {
|
||||
return FilesystemUsage{}, fmt.Errorf("failed to parse used space: %v", err)
|
||||
}
|
||||
|
||||
free, err := strconv.ParseUint(fields[3], 10, 64)
|
||||
if err != nil {
|
||||
return FilesystemUsage{}, fmt.Errorf("failed to parse free space: %v", err)
|
||||
}
|
||||
|
||||
usedPercent, err := strconv.ParseFloat(strings.TrimSuffix(fields[4], "%"), 64)
|
||||
if err != nil {
|
||||
return FilesystemUsage{}, fmt.Errorf("failed to parse used percentage: %v", err)
|
||||
}
|
||||
|
||||
return FilesystemUsage{
|
||||
Path: path,
|
||||
Total: total * 1024,
|
||||
Used: used * 1024,
|
||||
Free: free * 1024,
|
||||
UsedPercent: usedPercent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetGPUUsage gathers GPU usage for all detected GPUs
|
||||
func GetGPUUsage() []GPUUsage {
|
||||
var gpus []GPUUsage
|
||||
|
||||
// Detect all GPUs
|
||||
pciInfos, err := GetAllGPUInfo()
|
||||
if err != nil {
|
||||
slog.Warn("Failed to get GPU info", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Monitor each GPU
|
||||
for _, gpu := range pciInfos {
|
||||
var gpuUsage GPUUsage
|
||||
switch gpu.Vendor.ID {
|
||||
case VendorIntel:
|
||||
gpuUsage = monitorIntelGPU(gpu)
|
||||
case VendorNVIDIA:
|
||||
gpuUsage = monitorNVIDIAGPU(gpu)
|
||||
case VendorAMD:
|
||||
// TODO: Implement if needed
|
||||
continue
|
||||
default:
|
||||
continue
|
||||
}
|
||||
gpus = append(gpus, gpuUsage)
|
||||
}
|
||||
|
||||
return gpus
|
||||
}
|
||||
@@ -1,21 +1,126 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"nestri/maitred/pkg/party"
|
||||
"context"
|
||||
"log/slog"
|
||||
"nestri/maitred/internal"
|
||||
"nestri/maitred/internal/containers"
|
||||
"nestri/maitred/internal/realtime"
|
||||
"nestri/maitred/internal/resource"
|
||||
"nestri/maitred/internal/system"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var teamSlug string //FIXME: Switch to team-slug as they are more memorable but still unique
|
||||
// Setup main context and stopper
|
||||
mainCtx, mainStop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
teamSlug = os.Args[1]
|
||||
} else {
|
||||
log.Fatal("Nestri needs a team slug to register this container to")
|
||||
// Get flags and log them
|
||||
internal.InitFlags()
|
||||
internal.GetFlags().DebugLog()
|
||||
|
||||
logLevel := slog.LevelInfo
|
||||
if internal.GetFlags().Verbose {
|
||||
logLevel = slog.LevelDebug
|
||||
}
|
||||
party.Run(teamSlug)
|
||||
|
||||
//TODO: On stop here, set the API as the instance is not running (stopped)
|
||||
// Create the base handler with debug level
|
||||
baseHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: logLevel,
|
||||
})
|
||||
customHandler := &internal.CustomHandler{Handler: baseHandler}
|
||||
logger := slog.New(customHandler)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
if !internal.GetFlags().NoMonitor {
|
||||
// Start system monitoring, fetch every 5 seconds
|
||||
system.StartMonitoring(mainCtx, 5*time.Second)
|
||||
}
|
||||
|
||||
// Get machine ID
|
||||
machineID, err := system.GetID()
|
||||
if err != nil {
|
||||
slog.Error("failed getting machine id", "err", machineID)
|
||||
}
|
||||
|
||||
slog.Info("Machine ID", "id", machineID)
|
||||
|
||||
// Initialize container engine
|
||||
ctrEngine, err := containers.NewContainerEngine()
|
||||
if err != nil {
|
||||
slog.Error("failed initializing container engine", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
defer func(ctrEngine containers.ContainerEngine) {
|
||||
// Stop our managed containers first, with a 30 second timeout
|
||||
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cleanupCancel()
|
||||
err = realtime.CleanupManaged(cleanupCtx, ctrEngine)
|
||||
if err != nil {
|
||||
slog.Error("failed cleaning up managed containers", "err", err)
|
||||
}
|
||||
|
||||
err = ctrEngine.Close()
|
||||
if err != nil {
|
||||
slog.Error("failed closing container engine", "err", err)
|
||||
}
|
||||
}(ctrEngine)
|
||||
|
||||
// Print engine info
|
||||
info, err := ctrEngine.Info(mainCtx)
|
||||
if err != nil {
|
||||
slog.Error("failed getting engine info", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
slog.Info("Container engine", "info", info)
|
||||
|
||||
if err = realtime.InitializeManager(mainCtx, ctrEngine); err != nil {
|
||||
slog.Error("failed initializing container manager", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
|
||||
// If in debug mode, skip running SST - MQTT connections
|
||||
if !internal.GetFlags().Debug {
|
||||
// Initialize SST resource
|
||||
res, err := resource.NewResource()
|
||||
if err != nil {
|
||||
slog.Error("failed getting resource", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
|
||||
// Run realtime
|
||||
err = realtime.Run(mainCtx, machineID, ctrEngine, res)
|
||||
if err != nil {
|
||||
slog.Error("failed running realtime", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create relay container
|
||||
slog.Info("Creating default relay container")
|
||||
relayID, err := realtime.CreateRelay(mainCtx, ctrEngine)
|
||||
if err != nil {
|
||||
slog.Error("failed creating relay container", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
// Start relay container
|
||||
slog.Info("Starting default relay container", "id", relayID)
|
||||
if err = realtime.StartRelay(mainCtx, ctrEngine, relayID); err != nil {
|
||||
slog.Error("failed starting relay container", "err", err)
|
||||
mainStop()
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for signal
|
||||
<-mainCtx.Done()
|
||||
slog.Info("Shutting down gracefully by signal..")
|
||||
}
|
||||
|
||||
11
packages/maitred/package.json
Normal file
11
packages/maitred/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@nestri/maitred",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"dev": "sst shell go run main.go"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"nestri/maitred/pkg/resource"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
type UserCredentials struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
func FetchUserToken(teamSlug string) (*UserCredentials, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatal("Could not get the hostname")
|
||||
}
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "client_credentials")
|
||||
data.Set("client_id", "device")
|
||||
data.Set("client_secret", resource.Resource.AuthFingerprintKey.Value)
|
||||
data.Set("team", teamSlug)
|
||||
data.Set("hostname", hostname)
|
||||
data.Set("provider", "device")
|
||||
resp, err := http.PostForm(resource.Resource.Auth.Url+"/token", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Println(string(body))
|
||||
return nil, fmt.Errorf("failed to auth: " + string(body))
|
||||
}
|
||||
credentials := UserCredentials{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&credentials)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &credentials, nil
|
||||
}
|
||||
|
||||
func GetHostname() string {
|
||||
cmd, err := exec.Command("cat", "/etc/hostname").Output()
|
||||
if err != nil {
|
||||
log.Error("error getting container hostname", "err", err)
|
||||
}
|
||||
output := string(cmd)
|
||||
return output
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// logger implements the paho.Logger interface
|
||||
type logger struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
// Println is the library provided NOOPLogger's
|
||||
// implementation of the required interface function()
|
||||
func (l logger) Println(v ...interface{}) {
|
||||
// fmt.Println(append([]interface{}{l.prefix + ":"}, v...)...)
|
||||
log.Info(l.prefix, "info", v)
|
||||
}
|
||||
|
||||
// Printf is the library provided NOOPLogger's
|
||||
// implementation of the required interface function(){}
|
||||
func (l logger) Printf(format string, v ...interface{}) {
|
||||
// if len(format) > 0 && format[len(format)-1] != '\n' {
|
||||
// format = format + "\n" // some log calls in paho do not add \n
|
||||
// }
|
||||
// fmt.Printf(l.prefix+":"+format, v...)
|
||||
log.Info(l.prefix, "info", v)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"nestri/maitred/pkg/auth"
|
||||
"nestri/maitred/pkg/resource"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/eclipse/paho.golang/autopaho"
|
||||
"github.com/eclipse/paho.golang/paho"
|
||||
)
|
||||
|
||||
func Run(teamSlug string) {
|
||||
var topic = fmt.Sprintf("%s/%s/%s", resource.Resource.App.Name, resource.Resource.App.Stage, teamSlug)
|
||||
var serverURL = fmt.Sprintf("wss://%s/mqtt?x-amz-customauthorizer-name=%s", resource.Resource.Party.Endpoint, resource.Resource.Party.Authorizer)
|
||||
var clientID = generateClientID()
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatal(" Could not get the hostname")
|
||||
}
|
||||
|
||||
// App will run until cancelled by user (e.g. ctrl-c)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
userTokens, err := auth.FetchUserToken(teamSlug)
|
||||
if err != nil {
|
||||
log.Error("Error trying to request for credentials", "err", err)
|
||||
stop()
|
||||
}
|
||||
|
||||
// We will connect to the Eclipse test server (note that you may see messages that other users publish)
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
router := paho.NewStandardRouter()
|
||||
router.DefaultHandler(func(p *paho.Publish) {
|
||||
infoLogger.Info("Router", "info", fmt.Sprintf("default handler received message with topic: %s\n", p.Topic))
|
||||
})
|
||||
|
||||
cliCfg := autopaho.ClientConfig{
|
||||
ServerUrls: []*url.URL{u},
|
||||
ConnectUsername: "", // Must be empty for the authorizer
|
||||
ConnectPassword: []byte(userTokens.AccessToken),
|
||||
KeepAlive: 20, // Keepalive message should be sent every 20 seconds
|
||||
// We don't want the broker to delete any session info when we disconnect
|
||||
CleanStartOnInitialConnection: true,
|
||||
SessionExpiryInterval: 60, // Session remains live 60 seconds after disconnect
|
||||
ReconnectBackoff: autopaho.NewConstantBackoff(time.Second),
|
||||
OnConnectionUp: func(cm *autopaho.ConnectionManager, connAck *paho.Connack) {
|
||||
infoLogger.Info("Router", "info", "MQTT connection is up and running")
|
||||
if _, err := cm.Subscribe(context.Background(), &paho.Subscribe{
|
||||
Subscriptions: []paho.SubscribeOptions{
|
||||
{Topic: fmt.Sprintf("%s/#", topic), QoS: 1}, //Listen to all messages from this team
|
||||
},
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("failed to subscribe (%s). This is likely to mean no messages will be received.", err))
|
||||
}
|
||||
},
|
||||
Errors: logger{prefix: "subscribe"},
|
||||
OnConnectError: func(err error) {
|
||||
infoLogger.Error("Router", "err", fmt.Sprintf("error whilst attempting connection: %s\n", err))
|
||||
},
|
||||
// eclipse/paho.golang/paho provides base mqtt functionality, the below config will be passed in for each connection
|
||||
ClientConfig: paho.ClientConfig{
|
||||
// If you are using QOS 1/2, then it's important to specify a client id (which must be unique)
|
||||
ClientID: clientID,
|
||||
// OnPublishReceived is a slice of functions that will be called when a message is received.
|
||||
// You can write the function(s) yourself or use the supplied Router
|
||||
OnPublishReceived: []func(paho.PublishReceived) (bool, error){
|
||||
func(pr paho.PublishReceived) (bool, error) {
|
||||
router.Route(pr.Packet.Packet())
|
||||
return true, nil // we assume that the router handles all messages (todo: amend router API)
|
||||
}},
|
||||
OnClientError: func(err error) { infoLogger.Error("Router", "err", fmt.Sprintf("client error: %s\n", err)) },
|
||||
OnServerDisconnect: func(d *paho.Disconnect) {
|
||||
if d.Properties != nil {
|
||||
infoLogger.Info("Router", "info", fmt.Sprintf("server requested disconnect: %s\n", d.Properties.ReasonString))
|
||||
} else {
|
||||
infoLogger.Info("Router", "info", fmt.Sprintf("server requested disconnect; reason code: %d\n", d.ReasonCode))
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c, err := autopaho.NewConnection(ctx, cliCfg) // starts process; will reconnect until context cancelled
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = c.AwaitConnection(ctx); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Handlers can be registered/deregistered at any time. It's important to note that you need to subscribe AND create
|
||||
// a handler
|
||||
//TODO: Have different routes for different things, like starting a session, stopping a session, and stopping the container altogether
|
||||
//TODO: Listen on team-slug/container-hostname topic only
|
||||
router.RegisterHandler(fmt.Sprintf("%s/%s/start", topic, hostname), func(p *paho.Publish) {
|
||||
infoLogger.Info("Router", "info", fmt.Sprintf("start a game: %s\n", p.Topic))
|
||||
})
|
||||
router.RegisterHandler(fmt.Sprintf("%s/%s/stop", topic, hostname), func(p *paho.Publish) { fmt.Printf("stop the game that is running: %s\n", p.Topic) })
|
||||
router.RegisterHandler(fmt.Sprintf("%s/%s/download", topic, hostname), func(p *paho.Publish) { fmt.Printf("download a game: %s\n", p.Topic) })
|
||||
router.RegisterHandler(fmt.Sprintf("%s/%s/quit", topic, hostname), func(p *paho.Publish) { stop() }) // Stop and quit this running container
|
||||
|
||||
// We publish three messages to test out the various route handlers
|
||||
// topics := []string{"test/test", "test/test/foo", "test/xxNoMatch", "test/quit"}
|
||||
// for _, t := range topics {
|
||||
// if _, err := c.Publish(ctx, &paho.Publish{
|
||||
// QoS: 1,
|
||||
// Topic: fmt.Sprintf("%s/%s", topic, t),
|
||||
// Payload: []byte("TestMessage on topic: " + t),
|
||||
// }); err != nil {
|
||||
// if ctx.Err() == nil {
|
||||
// panic(err) // Publish will exit when context cancelled or if something went wrong
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
<-c.Done() // Wait for clean shutdown (cancelling the context triggered the shutdown)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package party
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"math/rand"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/oklog/ulid/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
infoLogger = log.NewWithOptions(os.Stderr, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
// Prefix: "Realtime",
|
||||
})
|
||||
)
|
||||
|
||||
func generateClientID() string {
|
||||
// Create a source of entropy (use cryptographically secure randomness in production)
|
||||
entropy := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Generate a new ULID
|
||||
id := ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
|
||||
|
||||
// Create the client ID string
|
||||
return fmt.Sprintf("client_%s", id.String())
|
||||
}
|
||||
9
packages/maitred/sst-env.d.ts
vendored
Normal file
9
packages/maitred/sst-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/* This file is auto-generated by SST. Do not edit. */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/* deno-fmt-ignore-file */
|
||||
|
||||
/// <reference path="../../sst-env.d.ts" />
|
||||
|
||||
import "sst"
|
||||
export {}
|
||||
@@ -3,30 +3,110 @@ module relay
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/pion/ice/v4 v4.0.7
|
||||
github.com/libp2p/go-libp2p v0.41.1
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1
|
||||
github.com/libp2p/go-reuseport v0.4.0
|
||||
github.com/multiformats/go-multiaddr v0.15.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/pion/ice/v4 v4.0.9
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/pion/webrtc/v4 v4.0.12
|
||||
google.golang.org/protobuf v1.36.5
|
||||
github.com/pion/rtp v1.8.13
|
||||
github.com/pion/webrtc/v4 v4.0.14
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/elastic/gosigar v0.14.3 // indirect
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huin/goupnp v1.3.0 // indirect
|
||||
github.com/ipfs/go-cid v0.5.0 // indirect
|
||||
github.com/ipfs/go-log/v2 v2.5.1 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/koron/go-ssdp v0.0.5 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.2.0 // indirect
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect
|
||||
github.com/libp2p/go-msgio v0.3.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.2 // indirect
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/miekg/dns v1.1.64 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/multiformats/go-base32 v0.1.0 // indirect
|
||||
github.com/multiformats/go-base36 v0.2.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect
|
||||
github.com/multiformats/go-multibase v0.2.0 // indirect
|
||||
github.com/multiformats/go-multicodec v0.9.0 // indirect
|
||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||
github.com/multiformats/go-multistream v0.6.0 // indirect
|
||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/logging v0.2.3 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.15 // indirect
|
||||
github.com/pion/rtp v1.8.12 // indirect
|
||||
github.com/pion/sctp v1.8.36 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.10 // indirect
|
||||
github.com/pion/sctp v1.8.37 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.11 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.50.1 // indirect
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
go.uber.org/dig v1.18.1 // indirect
|
||||
go.uber.org/fx v1.23.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
lukechampine.com/blake3 v1.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,19 +1,231 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo=
|
||||
dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
|
||||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU=
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo=
|
||||
github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg=
|
||||
github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk=
|
||||
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
|
||||
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk=
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/koron/go-ssdp v0.0.5 h1:E1iSMxIs4WqxTbIBLtmNBeOOC+1sCIXQeqTWVnpmwhk=
|
||||
github.com/koron/go-ssdp v0.0.5/go.mod h1:Qm59B7hpKpDqfyRNWRNr00jGwLdXjDyZh6y7rH6VS0w=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8=
|
||||
github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg=
|
||||
github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw=
|
||||
github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc=
|
||||
github.com/libp2p/go-libp2p v0.41.1 h1:8ecNQVT5ev/jqALTvisSJeVNvXYJyK4NhQx1nNRXQZE=
|
||||
github.com/libp2p/go-libp2p v0.41.1/go.mod h1:DcGTovJzQl/I7HMrby5ZRjeD0kQkGiy+9w6aEkSZpRI=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94=
|
||||
github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1 h1:tV3ttzzZSCk0EtEXnxVmWIXgjVxXx+20Jwjbs/Ctzjo=
|
||||
github.com/libp2p/go-libp2p-pubsub v0.13.1/go.mod h1:MKPU5vMI8RRFyTP0HfdsF9cLmL1nHAeJm44AxJGJx44=
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=
|
||||
github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg=
|
||||
github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0=
|
||||
github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM=
|
||||
github.com/libp2p/go-netroute v0.2.2 h1:Dejd8cQ47Qx2kRABg6lPwknU7+nBnFRpko45/fFPuZ8=
|
||||
github.com/libp2p/go-netroute v0.2.2/go.mod h1:Rntq6jUAH0l9Gg17w5bFGhcC9a+vk4KNXs6s7IljKYE=
|
||||
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
|
||||
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0 h1:2djUh96d3Jiac/JpGkKs4TO49YhsfLopAoryfPmf+Po=
|
||||
github.com/libp2p/go-yamux/v5 v5.0.0/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk=
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ=
|
||||
github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8=
|
||||
github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc=
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU=
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc=
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s=
|
||||
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
|
||||
github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
|
||||
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
|
||||
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
|
||||
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
|
||||
github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
|
||||
github.com/multiformats/go-multiaddr v0.15.0 h1:zB/HeaI/apcZiTDwhY5YqMvNVl/oQYvs3XySU+qeAVo=
|
||||
github.com/multiformats/go-multiaddr v0.15.0/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0=
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M=
|
||||
github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
|
||||
github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
|
||||
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
|
||||
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
|
||||
github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg=
|
||||
github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k=
|
||||
github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
|
||||
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
|
||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||
github.com/multiformats/go-multistream v0.6.0 h1:ZaHKbsL404720283o4c/IHQXiS6gb8qAN5EIJ4PN5EA=
|
||||
github.com/multiformats/go-multistream v0.6.0/go.mod h1:MOyoG5otO24cHIg8kf9QW2/NozURlkP/rvi2FQJyCPg=
|
||||
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
|
||||
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU8lpJfSlR0xww=
|
||||
github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
|
||||
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
|
||||
github.com/pion/ice/v4 v4.0.7 h1:mnwuT3n3RE/9va41/9QJqN5+Bhc0H/x/ZyiVlWMw35M=
|
||||
github.com/pion/ice/v4 v4.0.7/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E=
|
||||
github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU=
|
||||
github.com/pion/ice/v4 v4.0.9 h1:VKgU4MwA2LUDVLq+WBkpEHTcAb8c5iCvFMECeuPOZNk=
|
||||
github.com/pion/ice/v4 v4.0.9/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM=
|
||||
@@ -22,37 +234,302 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=
|
||||
github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=
|
||||
github.com/pion/rtp v1.8.12 h1:nsKs8Wi0jQyBFHU3qmn/OvtZrhktVfJY0vRxwACsL5U=
|
||||
github.com/pion/rtp v1.8.12/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.36 h1:owNudmnz1xmhfYje5L/FCav3V9wpPRePHle3Zi+P+M0=
|
||||
github.com/pion/sctp v1.8.36/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA=
|
||||
github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg=
|
||||
github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4=
|
||||
github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs=
|
||||
github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=
|
||||
github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI=
|
||||
github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=
|
||||
github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M=
|
||||
github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pion/webrtc/v4 v4.0.12 h1:/omInB15DdJDlA3WoAQAAhIQQvFCWNHdJ2t5e2+ozx4=
|
||||
github.com/pion/webrtc/v4 v4.0.12/go.mod h1:sMOtH6DSNVu6tfndczTMvJkKnyFVVeq+/G3dval418g=
|
||||
github.com/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg=
|
||||
github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
|
||||
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
|
||||
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
||||
github.com/raulk/go-watchdog v1.3.0 h1:oUmdlHxdkXRJlwfG0O9omj8ukerm8MEQavSiDTEtBsk=
|
||||
github.com/raulk/go-watchdog v1.3.0/go.mod h1:fIvOnLbF0b0ZwkB9YU4mOW9Did//4vPZtDqv66NfsMU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
|
||||
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
|
||||
github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag=
|
||||
github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg=
|
||||
github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw=
|
||||
github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y=
|
||||
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
|
||||
github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q=
|
||||
github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ=
|
||||
github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I=
|
||||
github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0=
|
||||
github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ=
|
||||
github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk=
|
||||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/dig v1.18.1 h1:rLww6NuajVjeQn+49u5NcezUJEGwd5uXmyoCKW2g5Es=
|
||||
go.uber.org/dig v1.18.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
|
||||
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
|
||||
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
lukechampine.com/blake3 v1.4.0 h1:xDbKOZCVbnZsfzM6mHSYcGRHZ3YrLDzqz8XnV4uaD5w=
|
||||
lukechampine.com/blake3 v1.4.0/go.mod h1:MQJNQCTnR+kwOP/JEZSxj3MaQjp80FOFSNMMHXcSeX0=
|
||||
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
|
||||
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package relay
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/libp2p/go-reuseport"
|
||||
"github.com/pion/ice/v4"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"log"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var globalWebRTCAPI *webrtc.API
|
||||
@@ -21,6 +24,19 @@ func InitWebRTCAPI() error {
|
||||
// Media engine
|
||||
mediaEngine := &webrtc.MediaEngine{}
|
||||
|
||||
// Register additional header extensions to reduce latency
|
||||
// Playout Delay
|
||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||
URI: ExtensionPlayoutDelay,
|
||||
}, webrtc.RTPCodecTypeVideo); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mediaEngine.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{
|
||||
URI: ExtensionPlayoutDelay,
|
||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Default codecs cover most of our needs
|
||||
err = mediaEngine.RegisterDefaultCodecs()
|
||||
if err != nil {
|
||||
@@ -66,17 +82,23 @@ func InitWebRTCAPI() error {
|
||||
|
||||
muxPort := GetFlags().UDPMuxPort
|
||||
if muxPort > 0 {
|
||||
mux, err := ice.NewMultiUDPMuxFromPort(muxPort)
|
||||
// Use reuseport to allow multiple listeners on the same port
|
||||
pktListener, err := reuseport.ListenPacket("udp", ":"+strconv.Itoa(muxPort))
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create UDP listener: %w", err)
|
||||
}
|
||||
|
||||
mux := ice.NewMultiUDPMuxDefault(ice.NewUDPMuxDefault(ice.UDPMuxParams{
|
||||
UDPConn: pktListener,
|
||||
}))
|
||||
slog.Info("Using UDP Mux for WebRTC", "port", muxPort)
|
||||
settingEngine.SetICEUDPMux(mux)
|
||||
} else {
|
||||
// Set the UDP port range used by WebRTC
|
||||
err = settingEngine.SetEphemeralUDPPortRange(uint16(flags.WebRTCUDPStart), uint16(flags.WebRTCUDPEnd))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Set the UDP port range used by WebRTC
|
||||
err = settingEngine.SetEphemeralUDPPortRange(uint16(flags.WebRTCUDPStart), uint16(flags.WebRTCUDPEnd))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settingEngine.SetIncludeLoopbackCandidate(true) // Just in case
|
||||
@@ -107,7 +129,7 @@ func CreatePeerConnection(onClose func()) (*webrtc.PeerConnection, error) {
|
||||
connectionState == webrtc.PeerConnectionStateClosed {
|
||||
err = pc.Close()
|
||||
if err != nil {
|
||||
log.Printf("Error closing PeerConnection: %s\n", err.Error())
|
||||
slog.Error("Failed to close PeerConnection", "err", err)
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
19
packages/relay/internal/common/crypto.go
Normal file
19
packages/relay/internal/common/crypto.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewULID() (ulid.ULID, error) {
|
||||
return ulid.New(ulid.Timestamp(time.Now()), ulid.Monotonic(rand.Reader, 0))
|
||||
}
|
||||
|
||||
// Helper function to generate PSK from token
|
||||
func GeneratePSKFromToken(token string) ([]byte, error) {
|
||||
// Simple hash-based PSK generation (32 bytes for libp2p)
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
return hash[:], nil
|
||||
}
|
||||
11
packages/relay/internal/common/extensions.go
Normal file
11
packages/relay/internal/common/extensions.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package common
|
||||
|
||||
const (
|
||||
ExtensionPlayoutDelay string = "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay"
|
||||
)
|
||||
|
||||
// ExtensionMap maps URIs to their IDs based on registration order
|
||||
// IMPORTANT: This must match the order in which extensions are registered in common.go!
|
||||
var ExtensionMap = map[string]uint8{
|
||||
ExtensionPlayoutDelay: 1,
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package relay
|
||||
package common
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -13,9 +13,10 @@ import (
|
||||
var globalFlags *Flags
|
||||
|
||||
type Flags struct {
|
||||
Verbose bool // Verbose mode - log more information to console
|
||||
Debug bool // Debug mode - log deeper debug information to console
|
||||
Verbose bool // Log everything to console
|
||||
Debug bool // Enable debug mode, implies Verbose
|
||||
EndpointPort int // Port for HTTP/S and WS/S endpoint (TCP)
|
||||
MeshPort int // Port for Mesh connections (TCP)
|
||||
WebRTCUDPStart int // WebRTC UDP port range start - ignored if UDPMuxPort is set
|
||||
WebRTCUDPEnd int // WebRTC UDP port range end - ignored if UDPMuxPort is set
|
||||
STUNServer string // WebRTC STUN server
|
||||
@@ -24,23 +25,25 @@ type Flags struct {
|
||||
NAT11IPs []string // WebRTC NAT 1 to 1 IP(s) - allows specifying host IP(s) if behind NAT
|
||||
TLSCert string // Path to TLS certificate
|
||||
TLSKey string // Path to TLS key
|
||||
ControlSecret string // Shared secret for this relay's control endpoint
|
||||
}
|
||||
|
||||
func (flags *Flags) DebugLog() {
|
||||
log.Println("Relay Flags:")
|
||||
log.Println("> Verbose: ", flags.Verbose)
|
||||
log.Println("> Debug: ", flags.Debug)
|
||||
log.Println("> Endpoint Port: ", flags.EndpointPort)
|
||||
log.Println("> WebRTC UDP Range Start: ", flags.WebRTCUDPStart)
|
||||
log.Println("> WebRTC UDP Range End: ", flags.WebRTCUDPEnd)
|
||||
log.Println("> WebRTC STUN Server: ", flags.STUNServer)
|
||||
log.Println("> WebRTC UDP Mux Port: ", flags.UDPMuxPort)
|
||||
log.Println("> Auto Add Local IP: ", flags.AutoAddLocalIP)
|
||||
for i, ip := range flags.NAT11IPs {
|
||||
log.Printf("> WebRTC NAT 1 to 1 IP (%d): %s\n", i, ip)
|
||||
}
|
||||
log.Println("> Path to TLS Cert: ", flags.TLSCert)
|
||||
log.Println("> Path to TLS Key: ", flags.TLSKey)
|
||||
slog.Info("Relay flags",
|
||||
"verbose", flags.Verbose,
|
||||
"debug", flags.Debug,
|
||||
"endpointPort", flags.EndpointPort,
|
||||
"meshPort", flags.MeshPort,
|
||||
"webrtcUDPStart", flags.WebRTCUDPStart,
|
||||
"webrtcUDPEnd", flags.WebRTCUDPEnd,
|
||||
"stunServer", flags.STUNServer,
|
||||
"webrtcUDPMux", flags.UDPMuxPort,
|
||||
"autoAddLocalIP", flags.AutoAddLocalIP,
|
||||
"webrtcNAT11IPs", strings.Join(flags.NAT11IPs, ","),
|
||||
"tlsCert", flags.TLSCert,
|
||||
"tlsKey", flags.TLSKey,
|
||||
"controlSecret", flags.ControlSecret,
|
||||
)
|
||||
}
|
||||
|
||||
func getEnvAsInt(name string, defaultVal int) int {
|
||||
@@ -76,6 +79,7 @@ func InitFlags() {
|
||||
flag.BoolVar(&globalFlags.Verbose, "verbose", getEnvAsBool("VERBOSE", false), "Verbose mode")
|
||||
flag.BoolVar(&globalFlags.Debug, "debug", getEnvAsBool("DEBUG", false), "Debug mode")
|
||||
flag.IntVar(&globalFlags.EndpointPort, "endpointPort", getEnvAsInt("ENDPOINT_PORT", 8088), "HTTP endpoint port")
|
||||
flag.IntVar(&globalFlags.MeshPort, "meshPort", getEnvAsInt("MESH_PORT", 8089), "Mesh connections TCP port")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPStart, "webrtcUDPStart", getEnvAsInt("WEBRTC_UDP_START", 10000), "WebRTC UDP port range start")
|
||||
flag.IntVar(&globalFlags.WebRTCUDPEnd, "webrtcUDPEnd", getEnvAsInt("WEBRTC_UDP_END", 20000), "WebRTC UDP port range end")
|
||||
flag.StringVar(&globalFlags.STUNServer, "stunServer", getEnvAsString("STUN_SERVER", "stun.l.google.com:19302"), "WebRTC STUN server")
|
||||
@@ -83,12 +87,20 @@ func InitFlags() {
|
||||
flag.BoolVar(&globalFlags.AutoAddLocalIP, "autoAddLocalIP", getEnvAsBool("AUTO_ADD_LOCAL_IP", true), "Automatically add local IP to NAT 1 to 1 IPs")
|
||||
// String with comma separated IPs
|
||||
nat11IPs := ""
|
||||
flag.StringVar(&nat11IPs, "webrtcNAT11IPs", getEnvAsString("WEBRTC_NAT_IPS", ""), "WebRTC NAT 1 to 1 IP(s)")
|
||||
flag.StringVar(&nat11IPs, "webrtcNAT11IPs", getEnvAsString("WEBRTC_NAT_IPS", ""), "WebRTC NAT 1 to 1 IP(s), comma delimited")
|
||||
flag.StringVar(&globalFlags.TLSCert, "tlsCert", getEnvAsString("TLS_CERT", ""), "Path to TLS certificate")
|
||||
flag.StringVar(&globalFlags.TLSKey, "tlsKey", getEnvAsString("TLS_KEY", ""), "Path to TLS key")
|
||||
flag.StringVar(&globalFlags.ControlSecret, "controlSecret", getEnvAsString("CONTROL_SECRET", ""), "Shared secret for control endpoint")
|
||||
// Parse flags
|
||||
flag.Parse()
|
||||
|
||||
// If debug is enabled, verbose is also enabled
|
||||
if globalFlags.Debug {
|
||||
globalFlags.Verbose = true
|
||||
// If Debug is enabled, set ControlSecret to 1234
|
||||
globalFlags.ControlSecret = "1234"
|
||||
}
|
||||
|
||||
// ICE STUN servers
|
||||
globalWebRTCConfig.ICEServers = []webrtc.ICEServer{
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
package relay
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
48
packages/relay/internal/common/loghandler.go
Normal file
48
packages/relay/internal/common/loghandler.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CustomHandler struct {
|
||||
Handler slog.Handler
|
||||
}
|
||||
|
||||
func (h *CustomHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return h.Handler.Enabled(nil, level)
|
||||
}
|
||||
|
||||
func (h *CustomHandler) Handle(_ context.Context, r slog.Record) error {
|
||||
// Format the timestamp as "2006/01/02 15:04:05"
|
||||
timestamp := r.Time.Format("2006/01/02 15:04:05")
|
||||
// Convert level to uppercase string (e.g., "INFO")
|
||||
level := strings.ToUpper(r.Level.String())
|
||||
// Build the message
|
||||
msg := fmt.Sprintf("%s %s %s", timestamp, level, r.Message)
|
||||
|
||||
// Handle additional attributes if they exist
|
||||
var attrs []string
|
||||
r.Attrs(func(a slog.Attr) bool {
|
||||
attrs = append(attrs, fmt.Sprintf("%s=%v", a.Key, a.Value))
|
||||
return true
|
||||
})
|
||||
if len(attrs) > 0 {
|
||||
msg += " " + strings.Join(attrs, " ")
|
||||
}
|
||||
|
||||
// Write the formatted message to stdout
|
||||
_, err := fmt.Fprintln(os.Stdout, msg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *CustomHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &CustomHandler{Handler: h.Handler.WithAttrs(attrs)}
|
||||
}
|
||||
|
||||
func (h *CustomHandler) WithGroup(name string) slog.Handler {
|
||||
return &CustomHandler{Handler: h.Handler.WithGroup(name)}
|
||||
}
|
||||
101
packages/relay/internal/common/map.go
Normal file
101
packages/relay/internal/common/map.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyNotFound = errors.New("key not found")
|
||||
ErrValueNotPointer = errors.New("value is not a pointer")
|
||||
ErrFieldNotFound = errors.New("field not found")
|
||||
ErrTypeMismatch = errors.New("type mismatch")
|
||||
)
|
||||
|
||||
// SafeMap is a generic thread-safe map with its own mutex
|
||||
type SafeMap[K comparable, V any] struct {
|
||||
mu sync.RWMutex
|
||||
m map[K]V
|
||||
}
|
||||
|
||||
// NewSafeMap creates a new SafeMap instance
|
||||
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
|
||||
return &SafeMap[K, V]{
|
||||
m: make(map[K]V),
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves a value from the map
|
||||
func (sm *SafeMap[K, V]) Get(key K) (V, bool) {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
v, ok := sm.m[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// Set adds or updates a value in the map
|
||||
func (sm *SafeMap[K, V]) Set(key K, value V) {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
sm.m[key] = value
|
||||
}
|
||||
|
||||
// Delete removes a key from the map
|
||||
func (sm *SafeMap[K, V]) Delete(key K) {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
delete(sm.m, key)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the map
|
||||
func (sm *SafeMap[K, V]) Len() int {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
return len(sm.m)
|
||||
}
|
||||
|
||||
// Copy creates a shallow copy of the map and returns it
|
||||
func (sm *SafeMap[K, V]) Copy() map[K]V {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
copied := make(map[K]V, len(sm.m))
|
||||
for k, v := range sm.m {
|
||||
copied[k] = v
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
// Update updates a specific field in the value data
|
||||
func (sm *SafeMap[K, V]) Update(key K, fieldName string, newValue any) error {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
v, ok := sm.m[key]
|
||||
if !ok {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
|
||||
// Use reflect to update the field
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
return ErrValueNotPointer
|
||||
}
|
||||
|
||||
rv = rv.Elem()
|
||||
// Check if the field exists
|
||||
field := rv.FieldByName(fieldName)
|
||||
if !field.IsValid() || !field.CanSet() {
|
||||
return ErrFieldNotFound
|
||||
}
|
||||
|
||||
newRV := reflect.ValueOf(newValue)
|
||||
if newRV.Type() != field.Type() {
|
||||
return ErrTypeMismatch
|
||||
}
|
||||
|
||||
field.Set(newRV)
|
||||
sm.m[key] = v
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package relay
|
||||
package connections
|
||||
|
||||
import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"log"
|
||||
"log/slog"
|
||||
gen "relay/internal/proto"
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ func NewNestriDataChannel(dc *webrtc.DataChannel) *NestriDataChannel {
|
||||
// Decode message
|
||||
var base gen.ProtoMessageInput
|
||||
if err := proto.Unmarshal(msg.Data, &base); err != nil {
|
||||
log.Printf("Failed to decode binary DataChannel message, reason: %s\n", err)
|
||||
slog.Error("failed to decode binary DataChannel message", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
package relay
|
||||
package connections
|
||||
|
||||
import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"relay/internal/common"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OnMessageCallback is a callback for messages of given type
|
||||
type OnMessageCallback func(data []byte)
|
||||
|
||||
// MessageBase is the base type for WS/DC messages.
|
||||
type MessageBase struct {
|
||||
PayloadType string `json:"payload_type"`
|
||||
Latency *LatencyTracker `json:"latency,omitempty"`
|
||||
PayloadType string `json:"payload_type"`
|
||||
Latency *common.LatencyTracker `json:"latency,omitempty"`
|
||||
}
|
||||
|
||||
// MessageLog represents a log message.
|
||||
119
packages/relay/internal/connections/messages_mesh.go
Normal file
119
packages/relay/internal/connections/messages_mesh.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package connections
|
||||
|
||||
import (
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
gen "relay/internal/proto"
|
||||
)
|
||||
|
||||
// SendMeshHandshake sends a handshake message to another relay.
|
||||
func (ws *SafeWebSocket) SendMeshHandshake(relayID, publicKey string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_Handshake{
|
||||
Handshake: &gen.Handshake{
|
||||
RelayId: relayID,
|
||||
DhPublicKey: publicKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
// SendMeshHandshakeResponse sends a handshake response to a relay.
|
||||
func (ws *SafeWebSocket) SendMeshHandshakeResponse(relayID, dhPublicKey string, approvals map[string]string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_HandshakeResponse{
|
||||
HandshakeResponse: &gen.HandshakeResponse{
|
||||
RelayId: relayID,
|
||||
DhPublicKey: dhPublicKey,
|
||||
Approvals: approvals,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
// SendMeshForwardSDP sends a forwarded SDP message to another relay
|
||||
func (ws *SafeWebSocket) SendMeshForwardSDP(roomName, participantID string, sdp webrtc.SessionDescription) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_ForwardSdp{
|
||||
ForwardSdp: &gen.ForwardSDP{
|
||||
RoomName: roomName,
|
||||
ParticipantId: participantID,
|
||||
Sdp: sdp.SDP,
|
||||
Type: sdp.Type.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
// SendMeshForwardICE sends a forwarded ICE candidate to another relay
|
||||
func (ws *SafeWebSocket) SendMeshForwardICE(roomName, participantID string, candidate webrtc.ICECandidateInit) error {
|
||||
var sdpMLineIndex uint32
|
||||
if candidate.SDPMLineIndex != nil {
|
||||
sdpMLineIndex = uint32(*candidate.SDPMLineIndex)
|
||||
}
|
||||
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_ForwardIce{
|
||||
ForwardIce: &gen.ForwardICE{
|
||||
RoomName: roomName,
|
||||
ParticipantId: participantID,
|
||||
Candidate: &gen.ICECandidateInit{
|
||||
Candidate: candidate.Candidate,
|
||||
SdpMid: candidate.SDPMid,
|
||||
SdpMLineIndex: &sdpMLineIndex,
|
||||
UsernameFragment: candidate.UsernameFragment,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
func (ws *SafeWebSocket) SendMeshForwardIngest(roomName string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_ForwardIngest{
|
||||
ForwardIngest: &gen.ForwardIngest{
|
||||
RoomName: roomName,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
|
||||
func (ws *SafeWebSocket) SendMeshStreamRequest(roomName string) error {
|
||||
msg := &gen.MeshMessage{
|
||||
Type: &gen.MeshMessage_StreamRequest{
|
||||
StreamRequest: &gen.StreamRequest{
|
||||
RoomName: roomName,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ws.SendBinary(data)
|
||||
}
|
||||
@@ -1,26 +1,37 @@
|
||||
package relay
|
||||
package connections
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"log/slog"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// OnMessageCallback is a callback for messages of given type
|
||||
type OnMessageCallback func(data []byte)
|
||||
|
||||
// SafeWebSocket is a websocket with a mutex
|
||||
type SafeWebSocket struct {
|
||||
*websocket.Conn
|
||||
sync.Mutex
|
||||
closeCallback func() // OnClose callback
|
||||
callbacks map[string]OnMessageCallback // MessageBase type -> callback
|
||||
closed bool
|
||||
closeCallback func() // Callback to call on close
|
||||
closeChan chan struct{} // Channel to signal closure
|
||||
callbacks map[string]OnMessageCallback // MessageBase type -> callback
|
||||
binaryCallback OnMessageCallback // Binary message callback
|
||||
sharedSecret []byte
|
||||
}
|
||||
|
||||
// NewSafeWebSocket creates a new SafeWebSocket from *websocket.Conn
|
||||
func NewSafeWebSocket(conn *websocket.Conn) *SafeWebSocket {
|
||||
ws := &SafeWebSocket{
|
||||
Conn: conn,
|
||||
closeCallback: nil,
|
||||
callbacks: make(map[string]OnMessageCallback),
|
||||
Conn: conn,
|
||||
closed: false,
|
||||
closeCallback: nil,
|
||||
closeChan: make(chan struct{}),
|
||||
callbacks: make(map[string]OnMessageCallback),
|
||||
binaryCallback: nil,
|
||||
sharedSecret: nil,
|
||||
}
|
||||
|
||||
// Launch a goroutine to handle messages
|
||||
@@ -30,14 +41,12 @@ func NewSafeWebSocket(conn *websocket.Conn) *SafeWebSocket {
|
||||
kind, data, err := ws.Conn.ReadMessage()
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) {
|
||||
// If unexpected close error, break
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Unexpected WebSocket close error, reason: %s\n", err)
|
||||
}
|
||||
slog.Debug("WebSocket closed unexpectedly", "err", err)
|
||||
break
|
||||
} else if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway, websocket.CloseAbnormalClosure, websocket.CloseNoStatusReceived) {
|
||||
break
|
||||
} else if err != nil {
|
||||
log.Printf("Failed to read WebSocket message, reason: %s\n", err)
|
||||
slog.Error("Failed reading WebSocket message", "err", err)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -46,32 +55,48 @@ func NewSafeWebSocket(conn *websocket.Conn) *SafeWebSocket {
|
||||
// Decode message
|
||||
var msg MessageBase
|
||||
if err = json.Unmarshal(data, &msg); err != nil {
|
||||
log.Printf("Failed to decode text WebSocket message, reason: %s\n", err)
|
||||
slog.Error("Failed decoding WebSocket message", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle message type callback
|
||||
if callback, ok := ws.callbacks[msg.PayloadType]; ok {
|
||||
callback(data)
|
||||
} // TODO: Log unknown message type?
|
||||
} // TODO: Log unknown message payload type?
|
||||
break
|
||||
case websocket.BinaryMessage:
|
||||
// Handle binary message callback
|
||||
if ws.binaryCallback != nil {
|
||||
ws.binaryCallback(data)
|
||||
}
|
||||
break
|
||||
default:
|
||||
log.Printf("Unknown WebSocket message type: %d\n", kind)
|
||||
slog.Warn("Unknown WebSocket message type", "type", kind)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Call close callback
|
||||
// Signal closure to callback first
|
||||
if ws.closeCallback != nil {
|
||||
ws.closeCallback()
|
||||
}
|
||||
close(ws.closeChan)
|
||||
ws.closed = true
|
||||
}()
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
// SetSharedSecret sets the shared secret for the websocket
|
||||
func (ws *SafeWebSocket) SetSharedSecret(secret []byte) {
|
||||
ws.sharedSecret = secret
|
||||
}
|
||||
|
||||
// GetSharedSecret returns the shared secret for the websocket
|
||||
func (ws *SafeWebSocket) GetSharedSecret() []byte {
|
||||
return ws.sharedSecret
|
||||
}
|
||||
|
||||
// SendJSON writes JSON to a websocket with a mutex
|
||||
func (ws *SafeWebSocket) SendJSON(v interface{}) error {
|
||||
ws.Lock()
|
||||
@@ -88,31 +113,46 @@ func (ws *SafeWebSocket) SendBinary(data []byte) error {
|
||||
|
||||
// RegisterMessageCallback sets the callback for binary message of given type
|
||||
func (ws *SafeWebSocket) RegisterMessageCallback(msgType string, callback OnMessageCallback) {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
if ws.callbacks == nil {
|
||||
ws.callbacks = make(map[string]OnMessageCallback)
|
||||
}
|
||||
ws.callbacks[msgType] = callback
|
||||
}
|
||||
|
||||
// RegisterBinaryMessageCallback sets the callback for all binary messages
|
||||
func (ws *SafeWebSocket) RegisterBinaryMessageCallback(callback OnMessageCallback) {
|
||||
ws.binaryCallback = callback
|
||||
}
|
||||
|
||||
// UnregisterMessageCallback removes the callback for binary message of given type
|
||||
func (ws *SafeWebSocket) UnregisterMessageCallback(msgType string) {
|
||||
ws.Lock()
|
||||
defer ws.Unlock()
|
||||
if ws.callbacks != nil {
|
||||
delete(ws.callbacks, msgType)
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterBinaryMessageCallback removes the callback for all binary messages
|
||||
func (ws *SafeWebSocket) UnregisterBinaryMessageCallback() {
|
||||
ws.binaryCallback = nil
|
||||
}
|
||||
|
||||
// RegisterOnClose sets the callback for websocket closing
|
||||
func (ws *SafeWebSocket) RegisterOnClose(callback func()) {
|
||||
ws.closeCallback = func() {
|
||||
// Clear our callbacks
|
||||
ws.Lock()
|
||||
ws.callbacks = nil
|
||||
ws.Unlock()
|
||||
ws.binaryCallback = nil
|
||||
// Call the callback
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// Closed returns a channel that closes when the WebSocket connection is terminated
|
||||
func (ws *SafeWebSocket) Closed() <-chan struct{} {
|
||||
return ws.closeChan
|
||||
}
|
||||
|
||||
// IsClosed returns true if the WebSocket connection is closed
|
||||
func (ws *SafeWebSocket) IsClosed() bool {
|
||||
return ws.closed
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
package relay
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"log"
|
||||
"log/slog"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
gen "relay/internal/proto"
|
||||
)
|
||||
|
||||
func participantHandler(participant *Participant, room *Room) {
|
||||
// Callback for closing PeerConnection
|
||||
func ParticipantHandler(participant *Participant, room *Room, relay *Relay) {
|
||||
onPCClose := func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Closed PeerConnection for participant: '%s'\n", participant.ID)
|
||||
}
|
||||
slog.Debug("Participant PeerConnection closed", "participant", participant.ID, "room", room.Name)
|
||||
room.removeParticipantByID(participant.ID)
|
||||
}
|
||||
|
||||
var err error
|
||||
participant.PeerConnection, err = CreatePeerConnection(onPCClose)
|
||||
participant.PeerConnection, err = common.CreatePeerConnection(onPCClose)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create PeerConnection for participant: '%s' - reason: %s\n", participant.ID, err)
|
||||
slog.Error("Failed to create participant PeerConnection", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,67 +32,32 @@ func participantHandler(participant *Participant, room *Room) {
|
||||
MaxRetransmits: &settingMaxRetransmits,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to create data channel for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
slog.Error("Failed to create data channel for participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
participant.DataChannel = NewNestriDataChannel(dc)
|
||||
participant.DataChannel = connections.NewNestriDataChannel(dc)
|
||||
|
||||
// Register channel opening handling
|
||||
participant.DataChannel.RegisterOnOpen(func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("DataChannel open for participant: %s\n", participant.ID)
|
||||
}
|
||||
slog.Debug("DataChannel opened for participant", "participant", participant.ID, "room", room.Name)
|
||||
})
|
||||
|
||||
// Register channel closing handling
|
||||
participant.DataChannel.RegisterOnClose(func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("DataChannel closed for participant: %s\n", participant.ID)
|
||||
}
|
||||
slog.Debug("DataChannel closed for participant", "participant", participant.ID, "room", room.Name)
|
||||
})
|
||||
|
||||
// Register text message handling
|
||||
participant.DataChannel.RegisterMessageCallback("input", func(data []byte) {
|
||||
// Send to room if it has a DataChannel
|
||||
if room.DataChannel != nil {
|
||||
// If debug mode, decode and add our timestamp, otherwise just send to room
|
||||
if GetFlags().Debug {
|
||||
var inputMsg gen.ProtoMessageInput
|
||||
if err = proto.Unmarshal(data, &inputMsg); err != nil {
|
||||
log.Printf("Failed to decode input message from participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
protoLat := inputMsg.GetMessageBase().GetLatency()
|
||||
if protoLat != nil {
|
||||
lat := LatencyTrackerFromProto(protoLat)
|
||||
lat.AddTimestamp("relay_to_node")
|
||||
protoLat = lat.ToProto()
|
||||
}
|
||||
|
||||
// Marshal and send
|
||||
if data, err = proto.Marshal(&inputMsg); err != nil {
|
||||
log.Printf("Failed to marshal input message for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = room.DataChannel.SendBinary(data); err != nil {
|
||||
log.Printf("Failed to send input message to room: '%s' - reason: %s\n", room.Name, err)
|
||||
}
|
||||
}
|
||||
ForwardParticipantDataChannelMessage(participant, room, data)
|
||||
})
|
||||
|
||||
participant.PeerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("ICE candidate for participant: '%s' in room: '%s'\n", participant.ID, room.Name)
|
||||
}
|
||||
err = participant.WebSocket.SendICECandidateMessageWS(candidate.ToJSON())
|
||||
if err != nil {
|
||||
log.Printf("Failed to send ICE candidate for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
if err := participant.WebSocket.SendICECandidateMessageWS(candidate.ToJSON()); err != nil {
|
||||
slog.Error("Failed to send ICE candidate to participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -100,35 +65,32 @@ func participantHandler(participant *Participant, room *Room) {
|
||||
|
||||
// ICE callback
|
||||
participant.WebSocket.RegisterMessageCallback("ice", func(data []byte) {
|
||||
var iceMsg MessageICECandidate
|
||||
var iceMsg connections.MessageICECandidate
|
||||
if err = json.Unmarshal(data, &iceMsg); err != nil {
|
||||
log.Printf("Failed to decode ICE message from participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
slog.Error("Failed to decode ICE candidate message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
candidate := webrtc.ICECandidateInit{
|
||||
Candidate: iceMsg.Candidate.Candidate,
|
||||
}
|
||||
if participant.PeerConnection.RemoteDescription() != nil {
|
||||
if err = participant.PeerConnection.AddICECandidate(candidate); err != nil {
|
||||
log.Printf("Failed to add ICE candidate from participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
if err = participant.PeerConnection.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||
slog.Error("Failed to add ICE candidate for participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
}
|
||||
// Add held ICE candidates
|
||||
for _, heldCandidate := range iceHolder {
|
||||
if err = participant.PeerConnection.AddICECandidate(heldCandidate); err != nil {
|
||||
log.Printf("Failed to add held ICE candidate from participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
slog.Error("Failed to add held ICE candidate for participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
}
|
||||
}
|
||||
iceHolder = nil
|
||||
} else {
|
||||
iceHolder = append(iceHolder, candidate)
|
||||
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||
}
|
||||
})
|
||||
|
||||
// SDP answer callback
|
||||
participant.WebSocket.RegisterMessageCallback("sdp", func(data []byte) {
|
||||
var sdpMsg MessageSDP
|
||||
var sdpMsg connections.MessageSDP
|
||||
if err = json.Unmarshal(data, &sdpMsg); err != nil {
|
||||
log.Printf("Failed to decode SDP message from participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
slog.Error("Failed to decode SDP message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
handleParticipantSDP(participant, sdpMsg)
|
||||
@@ -136,9 +98,9 @@ func participantHandler(participant *Participant, room *Room) {
|
||||
|
||||
// Log callback
|
||||
participant.WebSocket.RegisterMessageCallback("log", func(data []byte) {
|
||||
var logMsg MessageLog
|
||||
var logMsg connections.MessageLog
|
||||
if err = json.Unmarshal(data, &logMsg); err != nil {
|
||||
log.Printf("Failed to decode log message from participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
slog.Error("Failed to decode log message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
// TODO: Handle log message sending to metrics server
|
||||
@@ -150,38 +112,36 @@ func participantHandler(participant *Participant, room *Room) {
|
||||
})
|
||||
|
||||
participant.WebSocket.RegisterOnClose(func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("WebSocket closed for participant: '%s' in room: '%s'\n", participant.ID, room.Name)
|
||||
}
|
||||
slog.Debug("WebSocket closed for participant", "participant", participant.ID, "room", room.Name)
|
||||
// Remove from Room
|
||||
room.removeParticipantByID(participant.ID)
|
||||
})
|
||||
|
||||
log.Printf("Participant: '%s' in room: '%s' is now ready, sending an OK\n", participant.ID, room.Name)
|
||||
if err = participant.WebSocket.SendAnswerMessageWS(AnswerOK); err != nil {
|
||||
log.Printf("Failed to send OK answer for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
slog.Info("Participant ready, sending OK answer", "participant", participant.ID, "room", room.Name)
|
||||
if err := participant.WebSocket.SendAnswerMessageWS(connections.AnswerOK); err != nil {
|
||||
slog.Error("Failed to send OK answer", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
}
|
||||
|
||||
// If room is already online, send also offer
|
||||
// If room is online, also send offer
|
||||
if room.Online {
|
||||
if room.AudioTrack != nil {
|
||||
if err = participant.addTrack(&room.AudioTrack); err != nil {
|
||||
log.Printf("Failed to add audio track for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
}
|
||||
if err = room.signalParticipantWithTracks(participant); err != nil {
|
||||
slog.Error("Failed to signal participant with tracks", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
}
|
||||
if room.VideoTrack != nil {
|
||||
if err = participant.addTrack(&room.VideoTrack); err != nil {
|
||||
log.Printf("Failed to add video track for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
} else {
|
||||
active, provider := relay.IsRoomActive(room.ID)
|
||||
if active {
|
||||
slog.Debug("Room active remotely, requesting stream", "room", room.Name, "provider", provider)
|
||||
if _, err := relay.requestStream(context.Background(), room.Name, room.ID, provider); err != nil {
|
||||
slog.Error("Failed to request stream", "room", room.Name, "err", err)
|
||||
} else {
|
||||
slog.Debug("Stream requested successfully", "room", room.Name, "provider", provider)
|
||||
}
|
||||
}
|
||||
if err = participant.signalOffer(); err != nil {
|
||||
log.Printf("Failed to signal offer for participant: '%s' in room: '%s' - reason: %s\n", participant.ID, room.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SDP answer handler for participants
|
||||
func handleParticipantSDP(participant *Participant, answerMsg MessageSDP) {
|
||||
func handleParticipantSDP(participant *Participant, answerMsg connections.MessageSDP) {
|
||||
// Get SDP offer
|
||||
sdpAnswer := answerMsg.SDP.SDP
|
||||
|
||||
@@ -191,6 +151,37 @@ func handleParticipantSDP(participant *Participant, answerMsg MessageSDP) {
|
||||
SDP: sdpAnswer,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to set remote description for participant: '%s' - reason: %s\n", participant.ID, err)
|
||||
slog.Error("Failed to set remote SDP answer for participant", "participant", participant.ID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ForwardParticipantDataChannelMessage(participant *Participant, room *Room, data []byte) {
|
||||
// Debug mode: Add latency timestamp
|
||||
if common.GetFlags().Debug {
|
||||
var inputMsg gen.ProtoMessageInput
|
||||
if err := proto.Unmarshal(data, &inputMsg); err != nil {
|
||||
slog.Error("Failed to decode input message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
protoLat := inputMsg.GetMessageBase().GetLatency()
|
||||
if protoLat != nil {
|
||||
lat := common.LatencyTrackerFromProto(protoLat)
|
||||
lat.AddTimestamp("relay_to_node")
|
||||
protoLat = lat.ToProto()
|
||||
}
|
||||
if newData, err := proto.Marshal(&inputMsg); err != nil {
|
||||
slog.Error("Failed to marshal input message from participant", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
return
|
||||
} else {
|
||||
// Update data with the modified message
|
||||
data = newData
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to local room DataChannel if it exists (e.g., local ingest)
|
||||
if room.DataChannel != nil {
|
||||
if err := room.DataChannel.SendBinary(data); err != nil {
|
||||
slog.Error("Failed to send input message to room", "participant", participant.ID, "room", room.Name, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,61 @@
|
||||
package relay
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"log"
|
||||
"github.com/libp2p/go-reuseport"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var httpMux *http.ServeMux
|
||||
|
||||
func InitHTTPEndpoint() error {
|
||||
func InitHTTPEndpoint(_ context.Context, ctxCancel context.CancelFunc) error {
|
||||
// Create HTTP mux which serves our WS endpoint
|
||||
httpMux = http.NewServeMux()
|
||||
|
||||
// Endpoints themselves
|
||||
httpMux.Handle("/", http.NotFoundHandler())
|
||||
// If control endpoint secret is set, enable the control endpoint
|
||||
if len(common.GetFlags().ControlSecret) > 0 {
|
||||
httpMux.HandleFunc("/api/control", corsAnyHandler(controlHandler))
|
||||
}
|
||||
// WS endpoint
|
||||
httpMux.HandleFunc("/api/ws/{roomName}", corsAnyHandler(wsHandler))
|
||||
|
||||
// Get our serving port
|
||||
port := GetFlags().EndpointPort
|
||||
tlsCert := GetFlags().TLSCert
|
||||
tlsKey := GetFlags().TLSKey
|
||||
port := common.GetFlags().EndpointPort
|
||||
tlsCert := common.GetFlags().TLSCert
|
||||
tlsKey := common.GetFlags().TLSKey
|
||||
|
||||
// Create re-usable listener port
|
||||
httpListener, err := reuseport.Listen("tcp", ":"+strconv.Itoa(port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create TCP listener: %w", err)
|
||||
}
|
||||
|
||||
// Log and start the endpoint server
|
||||
if len(tlsCert) <= 0 && len(tlsKey) <= 0 {
|
||||
log.Println("Starting HTTP endpoint server on :", strconv.Itoa(port))
|
||||
slog.Info("Starting HTTP endpoint server", "port", port)
|
||||
go func() {
|
||||
log.Fatal((&http.Server{
|
||||
Handler: httpMux,
|
||||
Addr: ":" + strconv.Itoa(port),
|
||||
}).ListenAndServe())
|
||||
if err := http.Serve(httpListener, httpMux); err != nil {
|
||||
slog.Error("Failed to start HTTP server", "err", err)
|
||||
ctxCancel()
|
||||
}
|
||||
}()
|
||||
} else if len(tlsCert) > 0 && len(tlsKey) > 0 {
|
||||
log.Println("Starting HTTPS endpoint server on :", strconv.Itoa(port))
|
||||
slog.Info("Starting HTTPS endpoint server", "port", port)
|
||||
go func() {
|
||||
log.Fatal((&http.Server{
|
||||
Handler: httpMux,
|
||||
Addr: ":" + strconv.Itoa(port),
|
||||
}).ListenAndServeTLS(tlsCert, tlsKey))
|
||||
if err := http.ServeTLS(httpListener, httpMux, tlsCert, tlsKey); err != nil {
|
||||
slog.Error("Failed to start HTTPS server", "err", err)
|
||||
ctxCancel()
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
return errors.New("no TLS certificate or TLS key provided")
|
||||
@@ -49,8 +65,8 @@ func InitHTTPEndpoint() error {
|
||||
|
||||
// logHTTPError logs (if verbose) and sends an error code to requester
|
||||
func logHTTPError(w http.ResponseWriter, err string, code int) {
|
||||
if GetFlags().Verbose {
|
||||
log.Println(err)
|
||||
if common.GetFlags().Verbose {
|
||||
slog.Error("HTTP error", "code", code, "message", err)
|
||||
}
|
||||
http.Error(w, err, code)
|
||||
}
|
||||
@@ -78,8 +94,9 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
rel := GetRelay()
|
||||
// Get or create room in any case
|
||||
room := GetOrCreateRoom(roomName)
|
||||
room := rel.GetOrCreateRoom(roomName)
|
||||
|
||||
// Upgrade to WebSocket
|
||||
upgrader := websocket.Upgrader{
|
||||
@@ -94,47 +111,92 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Create SafeWebSocket
|
||||
ws := NewSafeWebSocket(wsConn)
|
||||
ws := connections.NewSafeWebSocket(wsConn)
|
||||
// Assign message handler for join request
|
||||
ws.RegisterMessageCallback("join", func(data []byte) {
|
||||
var joinMsg MessageJoin
|
||||
var joinMsg connections.MessageJoin
|
||||
if err = json.Unmarshal(data, &joinMsg); err != nil {
|
||||
log.Printf("Failed to decode join message: %s\n", err)
|
||||
slog.Error("Failed to unmarshal join message", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Join request for room: '%s' from: '%s'\n", room.Name, joinMsg.JoinerType.String())
|
||||
}
|
||||
slog.Debug("Join message", "room", room.Name, "joinerType", joinMsg.JoinerType)
|
||||
|
||||
// Handle join request, depending if it's from ingest/node or participant/client
|
||||
switch joinMsg.JoinerType {
|
||||
case JoinerNode:
|
||||
case connections.JoinerNode:
|
||||
// If room already online, send InUse answer
|
||||
if room.Online {
|
||||
if err = ws.SendAnswerMessageWS(AnswerInUse); err != nil {
|
||||
log.Printf("Failed to send InUse answer for Room: '%s' - reason: %s\n", room.Name, err)
|
||||
if err = ws.SendAnswerMessageWS(connections.AnswerInUse); err != nil {
|
||||
slog.Error("Failed to send InUse answer to node", "room", room.Name, "err", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
room.assignWebSocket(ws)
|
||||
go ingestHandler(room)
|
||||
case JoinerClient:
|
||||
room.AssignWebSocket(ws)
|
||||
go IngestHandler(room)
|
||||
case connections.JoinerClient:
|
||||
// Create participant and add to room regardless of online status
|
||||
participant := NewParticipant(ws)
|
||||
room.addParticipant(participant)
|
||||
room.AddParticipant(participant)
|
||||
// If room not online, send Offline answer
|
||||
if !room.Online {
|
||||
if err = ws.SendAnswerMessageWS(AnswerOffline); err != nil {
|
||||
log.Printf("Failed to send Offline answer for Room: '%s' - reason: %s\n", room.Name, err)
|
||||
if err = ws.SendAnswerMessageWS(connections.AnswerOffline); err != nil {
|
||||
slog.Error("Failed to send offline answer to participant", "room", room.Name, "err", err)
|
||||
}
|
||||
}
|
||||
go participantHandler(participant, room)
|
||||
go ParticipantHandler(participant, room, rel)
|
||||
default:
|
||||
log.Printf("Unknown joiner type: %d\n", joinMsg.JoinerType)
|
||||
slog.Error("Unknown joiner type", "joinerType", joinMsg.JoinerType)
|
||||
}
|
||||
|
||||
// Unregister ourselves, if something happens on the other side they should just reconnect?
|
||||
ws.UnregisterMessageCallback("join")
|
||||
})
|
||||
}
|
||||
|
||||
// controlMessage is the JSON struct for the control messages
|
||||
type controlMessage struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// controlHandler is the handler for the /api/control endpoint, for controlling this relay
|
||||
func controlHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Check for control secret in Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if len(authHeader) <= 0 || authHeader != common.GetFlags().ControlSecret {
|
||||
logHTTPError(w, "missing or invalid Authorization header", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle CORS preflight request
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Decode the control message
|
||||
var msg controlMessage
|
||||
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||||
logHTTPError(w, "failed to decode control message", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
//relay := GetRelay()
|
||||
switch msg.Type {
|
||||
case "join_mesh":
|
||||
// Join the mesh network, get relay address from msg.Value
|
||||
if len(msg.Value) <= 0 {
|
||||
logHTTPError(w, "missing relay address", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
if err := GetRelay().ConnectToRelay(ctx, msg.Value); err != nil {
|
||||
http.Error(w, fmt.Sprintf("Failed to connect: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("Successfully connected to relay"))
|
||||
default:
|
||||
logHTTPError(w, "unknown control message type", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +1,96 @@
|
||||
package relay
|
||||
package internal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/pion/rtp"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ingestHandler(room *Room) {
|
||||
func IngestHandler(room *Room) {
|
||||
relay := GetRelay()
|
||||
|
||||
// Callback for closing PeerConnection
|
||||
onPCClose := func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Closed PeerConnection for room: '%s'\n", room.Name)
|
||||
}
|
||||
slog.Debug("ingest PeerConnection closed", "room", room.Name)
|
||||
room.Online = false
|
||||
DeleteRoomIfEmpty(room)
|
||||
room.signalParticipantsOffline()
|
||||
relay.DeleteRoomIfEmpty(room)
|
||||
}
|
||||
|
||||
var err error
|
||||
room.PeerConnection, err = CreatePeerConnection(onPCClose)
|
||||
room.PeerConnection, err = common.CreatePeerConnection(onPCClose)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create PeerConnection for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to create ingest PeerConnection", "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
room.PeerConnection.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
var localTrack *webrtc.TrackLocalStaticRTP
|
||||
if remoteTrack.Kind() == webrtc.RTPCodecTypeVideo {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Received video track for room: '%s'\n", room.Name)
|
||||
}
|
||||
localTrack, err = webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, "video", fmt.Sprint("nestri-", room.Name))
|
||||
if err != nil {
|
||||
log.Printf("Failed to create local video track for room: '%s' - reason: %s\n", room.Name, err)
|
||||
return
|
||||
}
|
||||
room.VideoTrack = localTrack
|
||||
} else if remoteTrack.Kind() == webrtc.RTPCodecTypeAudio {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Received audio track for room: '%s'\n", room.Name)
|
||||
}
|
||||
localTrack, err = webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, "audio", fmt.Sprint("nestri-", room.Name))
|
||||
if err != nil {
|
||||
log.Printf("Failed to create local audio track for room: '%s' - reason: %s\n", room.Name, err)
|
||||
return
|
||||
}
|
||||
room.AudioTrack = localTrack
|
||||
localTrack, err := webrtc.NewTrackLocalStaticRTP(remoteTrack.Codec().RTPCodecCapability, remoteTrack.Kind().String(), fmt.Sprintf("nestri-%s-%s", room.Name, remoteTrack.Kind().String()))
|
||||
if err != nil {
|
||||
slog.Error("Failed to create local track for room", "room", room.Name, "kind", remoteTrack.Kind(), "err", err)
|
||||
return
|
||||
}
|
||||
slog.Debug("Received track for room", "room", room.Name, "kind", remoteTrack.Kind())
|
||||
|
||||
// Set track and let Room handle state
|
||||
room.SetTrack(remoteTrack.Kind(), localTrack)
|
||||
|
||||
// Prepare PlayoutDelayExtension so we don't need to recreate it for each packet
|
||||
playoutExt := &rtp.PlayoutDelayExtension{
|
||||
MinDelay: 0,
|
||||
MaxDelay: 0,
|
||||
}
|
||||
playoutPayload, err := playoutExt.Marshal()
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal PlayoutDelayExtension for room", "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If both audio and video tracks are set, set online state
|
||||
if room.AudioTrack != nil && room.VideoTrack != nil {
|
||||
room.Online = true
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Room online and receiving: '%s' - signaling participants\n", room.Name)
|
||||
}
|
||||
room.signalParticipantsWithTracks()
|
||||
}
|
||||
|
||||
rtpBuffer := make([]byte, 1400)
|
||||
for {
|
||||
read, _, err := remoteTrack.Read(rtpBuffer)
|
||||
rtpPacket, _, err := remoteTrack.ReadRTP()
|
||||
if err != nil {
|
||||
// EOF is expected when stopping room
|
||||
if !errors.Is(err, io.EOF) {
|
||||
log.Printf("RTP read error from room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to read RTP from remote track for room", "room", room.Name, "err", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
_, err = localTrack.Write(rtpBuffer[:read])
|
||||
// Use PlayoutDelayExtension for low latency, only for video tracks
|
||||
if err := rtpPacket.SetExtension(common.ExtensionMap[common.ExtensionPlayoutDelay], playoutPayload); err != nil {
|
||||
slog.Error("Failed to set PlayoutDelayExtension for room", "room", room.Name, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = localTrack.WriteRTP(rtpPacket)
|
||||
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.Printf("Failed to write RTP to local track for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to write RTP to local track for room", "room", room.Name, "err", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if remoteTrack.Kind() == webrtc.RTPCodecTypeVideo {
|
||||
room.VideoTrack = nil
|
||||
} else if remoteTrack.Kind() == webrtc.RTPCodecTypeAudio {
|
||||
room.AudioTrack = nil
|
||||
}
|
||||
slog.Debug("Track closed for room", "room", room.Name, "kind", remoteTrack.Kind())
|
||||
|
||||
if room.VideoTrack == nil && room.AudioTrack == nil {
|
||||
room.Online = false
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("Room offline and not receiving: '%s'\n", room.Name)
|
||||
}
|
||||
// Signal participants of room offline
|
||||
room.signalParticipantsOffline()
|
||||
DeleteRoomIfEmpty(room)
|
||||
}
|
||||
// Clear track when done
|
||||
room.SetTrack(remoteTrack.Kind(), nil)
|
||||
})
|
||||
|
||||
room.PeerConnection.OnDataChannel(func(dc *webrtc.DataChannel) {
|
||||
room.DataChannel = NewNestriDataChannel(dc)
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("New DataChannel for room: '%s' - '%s'\n", room.Name, room.DataChannel.Label())
|
||||
}
|
||||
room.DataChannel = connections.NewNestriDataChannel(dc)
|
||||
slog.Debug("Ingest received DataChannel for room", "room", room.Name)
|
||||
|
||||
// Register channel opening handling
|
||||
room.DataChannel.RegisterOnOpen(func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("DataChannel for room: '%s' - '%s' open\n", room.Name, room.DataChannel.Label())
|
||||
}
|
||||
slog.Debug("ingest DataChannel opened for room", "room", room.Name)
|
||||
})
|
||||
|
||||
room.DataChannel.OnClose(func() {
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("DataChannel for room: '%s' - '%s' closed\n", room.Name, room.DataChannel.Label())
|
||||
}
|
||||
slog.Debug("ingest DataChannel closed for room", "room", room.Name)
|
||||
})
|
||||
|
||||
// We do not handle any messages from ingest via DataChannel yet
|
||||
@@ -121,12 +100,10 @@ func ingestHandler(room *Room) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("ICE candidate for room: '%s'\n", room.Name)
|
||||
}
|
||||
slog.Debug("ingest received ICECandidate for room", "room", room.Name)
|
||||
err = room.WebSocket.SendICECandidateMessageWS(candidate.ToJSON())
|
||||
if err != nil {
|
||||
log.Printf("Failed to send ICE candidate for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to send ICE candidate message to ingest for room", "room", room.Name, "err", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -134,57 +111,52 @@ func ingestHandler(room *Room) {
|
||||
|
||||
// ICE callback
|
||||
room.WebSocket.RegisterMessageCallback("ice", func(data []byte) {
|
||||
var iceMsg MessageICECandidate
|
||||
var iceMsg connections.MessageICECandidate
|
||||
if err = json.Unmarshal(data, &iceMsg); err != nil {
|
||||
log.Printf("Failed to decode ICE candidate message from ingest for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to decode ICE candidate message from ingest for room", "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
candidate := webrtc.ICECandidateInit{
|
||||
Candidate: iceMsg.Candidate.Candidate,
|
||||
}
|
||||
if room.PeerConnection != nil {
|
||||
// If remote isn't set yet, store ICE candidates
|
||||
if room.PeerConnection.RemoteDescription() != nil {
|
||||
if err = room.PeerConnection.AddICECandidate(candidate); err != nil {
|
||||
log.Printf("Failed to add ICE candidate for room: '%s' - reason: %s\n", room.Name, err)
|
||||
if err = room.PeerConnection.AddICECandidate(iceMsg.Candidate); err != nil {
|
||||
slog.Error("Failed to add ICE candidate for room", "room", room.Name, "err", err)
|
||||
}
|
||||
// Add any held ICE candidates
|
||||
for _, heldCandidate := range iceHolder {
|
||||
if err = room.PeerConnection.AddICECandidate(heldCandidate); err != nil {
|
||||
log.Printf("Failed to add held ICE candidate for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to add held ICE candidate for room", "room", room.Name, "err", err)
|
||||
}
|
||||
}
|
||||
iceHolder = nil
|
||||
iceHolder = make([]webrtc.ICECandidateInit, 0)
|
||||
} else {
|
||||
iceHolder = append(iceHolder, candidate)
|
||||
iceHolder = append(iceHolder, iceMsg.Candidate)
|
||||
}
|
||||
} else {
|
||||
log.Printf("ICE candidate received before PeerConnection for room: '%s'\n", room.Name)
|
||||
slog.Error("ICE candidate received but PeerConnection is nil for room", "room", room.Name)
|
||||
}
|
||||
})
|
||||
|
||||
// SDP offer callback
|
||||
room.WebSocket.RegisterMessageCallback("sdp", func(data []byte) {
|
||||
var sdpMsg MessageSDP
|
||||
var sdpMsg connections.MessageSDP
|
||||
if err = json.Unmarshal(data, &sdpMsg); err != nil {
|
||||
log.Printf("Failed to decode SDP message from ingest for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to decode SDP message from ingest for room", "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
answer := handleIngestSDP(room, sdpMsg)
|
||||
if answer != nil {
|
||||
if err = room.WebSocket.SendSDPMessageWS(*answer); err != nil {
|
||||
log.Printf("Failed to send SDP answer to ingest for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to send SDP answer message to ingest for room", "room", room.Name, "err", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Failed to handle SDP message from ingest for room: '%s'\n", room.Name)
|
||||
slog.Error("Failed to handle ingest SDP message for room", "room", room.Name)
|
||||
}
|
||||
})
|
||||
|
||||
// Log callback
|
||||
room.WebSocket.RegisterMessageCallback("log", func(data []byte) {
|
||||
var logMsg MessageLog
|
||||
var logMsg connections.MessageLog
|
||||
if err = json.Unmarshal(data, &logMsg); err != nil {
|
||||
log.Printf("Failed to decode log message from ingest for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to decode log message from ingest for room", "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
// TODO: Handle log message sending to metrics server
|
||||
@@ -192,63 +164,52 @@ func ingestHandler(room *Room) {
|
||||
|
||||
// Metrics callback
|
||||
room.WebSocket.RegisterMessageCallback("metrics", func(data []byte) {
|
||||
var metricsMsg MessageMetrics
|
||||
var metricsMsg connections.MessageMetrics
|
||||
if err = json.Unmarshal(data, &metricsMsg); err != nil {
|
||||
log.Printf("Failed to decode metrics message from ingest for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to decode metrics message from ingest for room", "room", room.Name, "err", err)
|
||||
return
|
||||
}
|
||||
// TODO: Handle metrics message sending to metrics server
|
||||
})
|
||||
|
||||
room.WebSocket.RegisterOnClose(func() {
|
||||
// If PeerConnection is still open, close it
|
||||
if room.PeerConnection != nil {
|
||||
if err = room.PeerConnection.Close(); err != nil {
|
||||
log.Printf("Failed to close PeerConnection for room: '%s' - reason: %s\n", room.Name, err)
|
||||
}
|
||||
room.PeerConnection = nil
|
||||
}
|
||||
slog.Debug("ingest WebSocket closed for room", "room", room.Name)
|
||||
room.Online = false
|
||||
DeleteRoomIfEmpty(room)
|
||||
room.signalParticipantsOffline()
|
||||
relay.DeleteRoomIfEmpty(room)
|
||||
})
|
||||
|
||||
log.Printf("Room: '%s' is ready, sending an OK\n", room.Name)
|
||||
if err = room.WebSocket.SendAnswerMessageWS(AnswerOK); err != nil {
|
||||
log.Printf("Failed to send OK answer for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Info("Room is ready, sending OK answer to ingest", "room", room.Name)
|
||||
if err = room.WebSocket.SendAnswerMessageWS(connections.AnswerOK); err != nil {
|
||||
slog.Error("Failed to send OK answer message to ingest for room", "room", room.Name, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SDP offer handler, returns SDP answer
|
||||
func handleIngestSDP(room *Room, offerMsg MessageSDP) *webrtc.SessionDescription {
|
||||
func handleIngestSDP(room *Room, offerMsg connections.MessageSDP) *webrtc.SessionDescription {
|
||||
var err error
|
||||
|
||||
// Get SDP offer
|
||||
sdpOffer := offerMsg.SDP.SDP
|
||||
|
||||
// Modify SDP offer to remove opus "sprop-maxcapturerate=24000" (fixes opus bad quality issue, present in GStreamer)
|
||||
sdpOffer = strings.Replace(sdpOffer, ";sprop-maxcapturerate=24000", "", -1)
|
||||
|
||||
// Set new remote description
|
||||
err = room.PeerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
SDP: sdpOffer,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to set remote description for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to set remote description for room", "room", room.Name, "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create SDP answer
|
||||
answer, err := room.PeerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create SDP answer for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to create SDP answer for room", "room", room.Name, "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set local description
|
||||
err = room.PeerConnection.SetLocalDescription(answer)
|
||||
if err != nil {
|
||||
log.Printf("Failed to set local description for room: '%s' - reason: %s\n", room.Name, err)
|
||||
slog.Error("Failed to set local description for room", "room", room.Name, "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
package relay
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
)
|
||||
|
||||
type Participant struct {
|
||||
ID uuid.UUID //< Internal IDs are useful to keeping unique internal track and not have conflicts later
|
||||
ID ulid.ULID //< Internal IDs are useful to keeping unique internal track and not have conflicts later
|
||||
Name string
|
||||
WebSocket *SafeWebSocket
|
||||
WebSocket *connections.SafeWebSocket
|
||||
PeerConnection *webrtc.PeerConnection
|
||||
DataChannel *NestriDataChannel
|
||||
DataChannel *connections.NestriDataChannel
|
||||
}
|
||||
|
||||
func NewParticipant(ws *SafeWebSocket) *Participant {
|
||||
func NewParticipant(ws *connections.SafeWebSocket) *Participant {
|
||||
id, err := common.NewULID()
|
||||
if err != nil {
|
||||
slog.Error("Failed to create ULID for Participant", "err", err)
|
||||
return nil
|
||||
}
|
||||
return &Participant{
|
||||
ID: uuid.New(),
|
||||
ID: id,
|
||||
Name: createRandomName(),
|
||||
WebSocket: ws,
|
||||
}
|
||||
}
|
||||
|
||||
func (vw *Participant) addTrack(trackLocal *webrtc.TrackLocal) error {
|
||||
rtpSender, err := vw.PeerConnection.AddTrack(*trackLocal)
|
||||
func (p *Participant) addTrack(trackLocal *webrtc.TrackLocalStaticRTP) error {
|
||||
rtpSender, err := p.PeerConnection.AddTrack(trackLocal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -41,22 +49,22 @@ func (vw *Participant) addTrack(trackLocal *webrtc.TrackLocal) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vw *Participant) signalOffer() error {
|
||||
if vw.PeerConnection == nil {
|
||||
return fmt.Errorf("peer connection is nil for participant: '%s' - cannot signal offer", vw.ID)
|
||||
func (p *Participant) signalOffer() error {
|
||||
if p.PeerConnection == nil {
|
||||
return fmt.Errorf("peer connection is nil for participant: '%s' - cannot signal offer", p.ID)
|
||||
}
|
||||
|
||||
offer, err := vw.PeerConnection.CreateOffer(nil)
|
||||
offer, err := p.PeerConnection.CreateOffer(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = vw.PeerConnection.SetLocalDescription(offer)
|
||||
err = p.PeerConnection.SetLocalDescription(offer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return vw.WebSocket.SendSDPMessageWS(offer)
|
||||
return p.WebSocket.SendSDPMessageWS(offer)
|
||||
}
|
||||
|
||||
var namesFirst = []string{"Happy", "Sad", "Angry", "Calm", "Excited", "Bored", "Confused", "Confident", "Curious", "Depressed", "Disappointed", "Embarrassed", "Energetic", "Fearful", "Frustrated", "Glad", "Guilty", "Hopeful", "Impatient", "Jealous", "Lonely", "Motivated", "Nervous", "Optimistic", "Pessimistic", "Proud", "Relaxed", "Shy", "Stressed", "Surprised", "Tired", "Worried"}
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
// "github.com/gorilla/mux"
|
||||
"github.com/hashicorp/memberlist"
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// PeerInfo represents information about an SFU peer
|
||||
type PeerInfo struct {
|
||||
NodeID string `json:"nodeId"`
|
||||
Zone string `json:"zone"`
|
||||
PublicIP string `json:"publicIp"`
|
||||
PrivateIP string `json:"privateIp,omitempty"`
|
||||
Streams map[string]bool `json:"streams"` // streamID -> isOrigin
|
||||
}
|
||||
|
||||
// StreamInfo tracks a stream's origin and local subscribers
|
||||
type StreamInfo struct {
|
||||
ID string
|
||||
OriginPeerID string
|
||||
IsLocal bool
|
||||
Publisher *webrtc.PeerConnection
|
||||
Subscribers map[string]*webrtc.PeerConnection
|
||||
InterPeerConn map[string]*webrtc.PeerConnection // connections to other SFU peers
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// DistributedSFU manages streams and peer communication
|
||||
type DistributedSFU struct {
|
||||
nodeID string
|
||||
zone string
|
||||
publicIP string
|
||||
privateIP string
|
||||
streams map[string]*StreamInfo
|
||||
peers map[string]*PeerInfo
|
||||
memberlist *memberlist.Memberlist
|
||||
mu sync.RWMutex
|
||||
config webrtc.Configuration
|
||||
}
|
||||
|
||||
// NewDistributedSFU creates a new distributed SFU instance
|
||||
func NewDistributedSFU(nodeID, zone, publicIP, privateIP string, seeds []string) (*DistributedSFU, error) {
|
||||
sfu := &DistributedSFU{
|
||||
nodeID: nodeID,
|
||||
zone: zone,
|
||||
publicIP: publicIP,
|
||||
privateIP: privateIP,
|
||||
streams: make(map[string]*StreamInfo),
|
||||
peers: make(map[string]*PeerInfo),
|
||||
config: webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Configure memberlist for peer discovery
|
||||
config := memberlist.DefaultLANConfig()
|
||||
config.Name = nodeID
|
||||
config.BindAddr = privateIP
|
||||
config.AdvertiseAddr = publicIP
|
||||
|
||||
// Add delegate for handling peer updates
|
||||
config.Delegate = &peerDelegate{sfu: sfu}
|
||||
|
||||
// Initialize memberlist
|
||||
list, err := memberlist.Create(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Join the cluster if seeds are provided
|
||||
if len(seeds) > 0 {
|
||||
_, err = list.Join(seeds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sfu.memberlist = list
|
||||
return sfu, nil
|
||||
}
|
||||
|
||||
// peerDelegate implements memberlist.Delegate
|
||||
type peerDelegate struct {
|
||||
sfu *DistributedSFU
|
||||
}
|
||||
|
||||
// NodeMeta returns metadata about the current node
|
||||
func (d *peerDelegate) NodeMeta(limit int) []byte {
|
||||
meta := PeerInfo{
|
||||
NodeID: d.sfu.nodeID,
|
||||
Zone: d.sfu.zone,
|
||||
PublicIP: d.sfu.publicIP,
|
||||
PrivateIP: d.sfu.privateIP,
|
||||
Streams: make(map[string]bool),
|
||||
}
|
||||
|
||||
d.sfu.mu.RLock()
|
||||
for id, info := range d.sfu.streams {
|
||||
meta.Streams[id] = info.IsLocal
|
||||
}
|
||||
d.sfu.mu.RUnlock()
|
||||
|
||||
data, _ := json.Marshal(meta)
|
||||
return data
|
||||
}
|
||||
|
||||
// NotifyMsg handles peer updates
|
||||
func (d *peerDelegate) NotifyMsg(msg []byte) {
|
||||
var peer PeerInfo
|
||||
if err := json.Unmarshal(msg, &peer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d.sfu.mu.Lock()
|
||||
d.sfu.peers[peer.NodeID] = &peer
|
||||
|
||||
// Check for new streams we don't have locally
|
||||
for streamID, isOrigin := range peer.Streams {
|
||||
if isOrigin {
|
||||
if _, exists := d.sfu.streams[streamID]; !exists {
|
||||
// Initialize inter-peer connection for this stream
|
||||
d.sfu.initInterPeerStream(streamID, peer.NodeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
d.sfu.mu.Unlock()
|
||||
}
|
||||
|
||||
// initInterPeerStream sets up connection to another SFU for a stream
|
||||
func (sfu *DistributedSFU) initInterPeerStream(streamID, peerID string) {
|
||||
stream := &StreamInfo{
|
||||
ID: streamID,
|
||||
OriginPeerID: peerID,
|
||||
IsLocal: false,
|
||||
Subscribers: make(map[string]*webrtc.PeerConnection),
|
||||
InterPeerConn: make(map[string]*webrtc.PeerConnection),
|
||||
}
|
||||
|
||||
// Create peer connection to the origin SFU
|
||||
pc, err := webrtc.NewPeerConnection(sfu.config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stream.InterPeerConn[peerID] = pc
|
||||
sfu.streams[streamID] = stream
|
||||
|
||||
// Setup inter-peer WebRTC connection
|
||||
go sfu.establishInterPeerConnection(streamID, peerID, pc)
|
||||
}
|
||||
|
||||
// establishInterPeerConnection handles WebRTC signaling between SFU peers
|
||||
func (sfu *DistributedSFU) establishInterPeerConnection(streamID, peerID string, pc *webrtc.PeerConnection) {
|
||||
// This would typically involve making an HTTP request to the peer's control endpoint
|
||||
// to exchange SDP offers/answers and ICE candidates
|
||||
peerInfo := sfu.peers[peerID]
|
||||
|
||||
// Example endpoint URL construction
|
||||
peerURL := fmt.Sprintf("http://%s:8080/peer/%s/stream/%s",
|
||||
peerInfo.PublicIP, sfu.nodeID, streamID)
|
||||
|
||||
// Handle incoming tracks from peer
|
||||
pc.OnTrack(func(remoteTrack *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
sfu.mu.RLock()
|
||||
stream := sfu.streams[streamID]
|
||||
sfu.mu.RUnlock()
|
||||
|
||||
// Forward the track to local subscribers
|
||||
stream.mu.RLock()
|
||||
for _, subscriber := range stream.Subscribers {
|
||||
localTrack, err := webrtc.NewTrackLocalStaticRTP(
|
||||
remoteTrack.Codec().RTPCodecCapability,
|
||||
remoteTrack.ID(),
|
||||
remoteTrack.StreamID(),
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := subscriber.AddTrack(localTrack); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
packet, _, err := remoteTrack.ReadRTP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := localTrack.WriteRTP(packet); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
stream.mu.RUnlock()
|
||||
})
|
||||
|
||||
// Implement SDP exchange with peer
|
||||
// ... (signaling implementation)
|
||||
}
|
||||
|
||||
// HandleWHIPPublish now includes peer notification
|
||||
func (sfu *DistributedSFU) HandleWHIPPublish(w http.ResponseWriter, r *http.Request) {
|
||||
streamID := mux.Vars(r)["streamID"]
|
||||
|
||||
// Create stream info
|
||||
stream := &StreamInfo{
|
||||
ID: streamID,
|
||||
IsLocal: true,
|
||||
Subscribers: make(map[string]*webrtc.PeerConnection),
|
||||
InterPeerConn: make(map[string]*webrtc.PeerConnection),
|
||||
}
|
||||
|
||||
// ... (rest of WHIP publish logic)
|
||||
|
||||
// Notify other peers about the new stream
|
||||
sfu.broadcastStreamUpdate(streamID, true)
|
||||
}
|
||||
|
||||
// HandleWHEPSubscribe now checks both local and remote streams
|
||||
func (sfu *DistributedSFU) HandleWHEPSubscribe(w http.ResponseWriter, r *http.Request) {
|
||||
streamID := mux.Vars(r)["streamID"]
|
||||
|
||||
sfu.mu.RLock()
|
||||
stream, exists := sfu.streams[streamID]
|
||||
sfu.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
// Check if any peer has this stream
|
||||
if peer := sfu.findStreamPeer(streamID); peer != nil {
|
||||
// Initialize inter-peer connection if needed
|
||||
sfu.initInterPeerStream(streamID, peer.NodeID)
|
||||
} else {
|
||||
http.Error(w, "Stream not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// ... (rest of WHEP subscribe logic)
|
||||
}
|
||||
|
||||
// findStreamPeer finds the peer that has the origin of a stream
|
||||
func (sfu *DistributedSFU) findStreamPeer(streamID string) *PeerInfo {
|
||||
sfu.mu.RLock()
|
||||
defer sfu.mu.RUnlock()
|
||||
|
||||
for _, peer := range sfu.peers {
|
||||
if isOrigin, exists := peer.Streams[streamID]; exists && isOrigin {
|
||||
return peer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Initialize the distributed SFU
|
||||
sfu, err := NewDistributedSFU(
|
||||
"sfu-1",
|
||||
"us-east",
|
||||
"203.0.113.1",
|
||||
"10.0.0.1",
|
||||
[]string{"203.0.113.2:7946", "203.0.113.3:7946"},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
||||
// Regular WHIP/WHEP endpoints
|
||||
router.HandleFunc("/whip/{streamID}", sfu.HandleWHIPPublish).Methods("POST")
|
||||
router.HandleFunc("/whep/{streamID}/{subscriberID}", sfu.HandleWHEPSubscribe).Methods("POST")
|
||||
|
||||
// Inter-peer communication endpoint
|
||||
router.HandleFunc("/peer/{peerID}/stream/{streamID}", sfu.HandlePeerSignaling).Methods("POST")
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: router,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
server.ListenAndServe()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.4
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc (unknown)
|
||||
// source: latency_tracker.proto
|
||||
|
||||
@@ -128,27 +128,18 @@ func (x *ProtoLatencyTracker) GetTimestamps() []*ProtoTimestampEntry {
|
||||
|
||||
var File_latency_tracker_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_latency_tracker_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x15, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x65,
|
||||
0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f,
|
||||
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
|
||||
0x5b, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x04,
|
||||
0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x72, 0x0a, 0x13,
|
||||
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x54, 0x72, 0x61, 0x63,
|
||||
0x6b, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e,
|
||||
0x63, 0x65, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73,
|
||||
0x42, 0x16, 0x5a, 0x14, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||
0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
const file_latency_tracker_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x15latency_tracker.proto\x12\x05proto\x1a\x1fgoogle/protobuf/timestamp.proto\"[\n" +
|
||||
"\x13ProtoTimestampEntry\x12\x14\n" +
|
||||
"\x05stage\x18\x01 \x01(\tR\x05stage\x12.\n" +
|
||||
"\x04time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x04time\"r\n" +
|
||||
"\x13ProtoLatencyTracker\x12\x1f\n" +
|
||||
"\vsequence_id\x18\x01 \x01(\tR\n" +
|
||||
"sequenceId\x12:\n" +
|
||||
"\n" +
|
||||
"timestamps\x18\x02 \x03(\v2\x1a.proto.ProtoTimestampEntryR\n" +
|
||||
"timestampsB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_latency_tracker_proto_rawDescOnce sync.Once
|
||||
|
||||
1170
packages/relay/internal/proto/mesh.pb.go
Normal file
1170
packages/relay/internal/proto/mesh.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.4
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc (unknown)
|
||||
// source: messages.proto
|
||||
|
||||
@@ -127,28 +127,15 @@ func (x *ProtoMessageInput) GetData() *ProtoInput {
|
||||
|
||||
var File_messages_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_messages_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x0e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x15, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x74, 0x72,
|
||||
0x61, 0x63, 0x6b, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x6b, 0x0a, 0x10, 0x50,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x12,
|
||||
0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x79,
|
||||
0x70, 0x65, 0x12, 0x34, 0x0a, 0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x4c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x65, 0x72, 0x52,
|
||||
0x07, 0x6c, 0x61, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x22, 0x76, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x3a, 0x0a,
|
||||
0x0c, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x52, 0x0b, 0x6d, 0x65,
|
||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x61, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x04, 0x64, 0x61, 0x74,
|
||||
0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e,
|
||||
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61,
|
||||
0x42, 0x16, 0x5a, 0x14, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||
0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
const file_messages_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0emessages.proto\x12\x05proto\x1a\vtypes.proto\x1a\x15latency_tracker.proto\"k\n" +
|
||||
"\x10ProtoMessageBase\x12!\n" +
|
||||
"\fpayload_type\x18\x01 \x01(\tR\vpayloadType\x124\n" +
|
||||
"\alatency\x18\x02 \x01(\v2\x1a.proto.ProtoLatencyTrackerR\alatency\"v\n" +
|
||||
"\x11ProtoMessageInput\x12:\n" +
|
||||
"\fmessage_base\x18\x01 \x01(\v2\x17.proto.ProtoMessageBaseR\vmessageBase\x12%\n" +
|
||||
"\x04data\x18\x02 \x01(\v2\x11.proto.ProtoInputR\x04dataB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_messages_proto_rawDescOnce sync.Once
|
||||
|
||||
151
packages/relay/internal/proto/state.pb.go
Normal file
151
packages/relay/internal/proto/state.pb.go
Normal file
@@ -0,0 +1,151 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc (unknown)
|
||||
// source: state.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// EntityState represents the state of an entity in the mesh (e.g., a room).
|
||||
type EntityState struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EntityType string `protobuf:"bytes,1,opt,name=entity_type,json=entityType,proto3" json:"entity_type,omitempty"` // Type of entity (e.g., "room")
|
||||
EntityId string `protobuf:"bytes,2,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` // Unique identifier (e.g., room name)
|
||||
Active bool `protobuf:"varint,3,opt,name=active,proto3" json:"active,omitempty"` // Whether the entity is active
|
||||
OwnerRelayId string `protobuf:"bytes,4,opt,name=owner_relay_id,json=ownerRelayId,proto3" json:"owner_relay_id,omitempty"` // Relay ID that owns this entity
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EntityState) Reset() {
|
||||
*x = EntityState{}
|
||||
mi := &file_state_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EntityState) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EntityState) ProtoMessage() {}
|
||||
|
||||
func (x *EntityState) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_state_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EntityState.ProtoReflect.Descriptor instead.
|
||||
func (*EntityState) Descriptor() ([]byte, []int) {
|
||||
return file_state_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *EntityState) GetEntityType() string {
|
||||
if x != nil {
|
||||
return x.EntityType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EntityState) GetEntityId() string {
|
||||
if x != nil {
|
||||
return x.EntityId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *EntityState) GetActive() bool {
|
||||
if x != nil {
|
||||
return x.Active
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *EntityState) GetOwnerRelayId() string {
|
||||
if x != nil {
|
||||
return x.OwnerRelayId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_state_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_state_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\vstate.proto\x12\x05proto\"\x89\x01\n" +
|
||||
"\vEntityState\x12\x1f\n" +
|
||||
"\ventity_type\x18\x01 \x01(\tR\n" +
|
||||
"entityType\x12\x1b\n" +
|
||||
"\tentity_id\x18\x02 \x01(\tR\bentityId\x12\x16\n" +
|
||||
"\x06active\x18\x03 \x01(\bR\x06active\x12$\n" +
|
||||
"\x0eowner_relay_id\x18\x04 \x01(\tR\fownerRelayIdB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_state_proto_rawDescOnce sync.Once
|
||||
file_state_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_state_proto_rawDescGZIP() []byte {
|
||||
file_state_proto_rawDescOnce.Do(func() {
|
||||
file_state_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_state_proto_rawDesc), len(file_state_proto_rawDesc)))
|
||||
})
|
||||
return file_state_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_state_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_state_proto_goTypes = []any{
|
||||
(*EntityState)(nil), // 0: proto.EntityState
|
||||
}
|
||||
var file_state_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_state_proto_init() }
|
||||
func file_state_proto_init() {
|
||||
if File_state_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_state_proto_rawDesc), len(file_state_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_state_proto_goTypes,
|
||||
DependencyIndexes: file_state_proto_depIdxs,
|
||||
MessageInfos: file_state_proto_msgTypes,
|
||||
}.Build()
|
||||
File_state_proto = out.File
|
||||
file_state_proto_goTypes = nil
|
||||
file_state_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.4
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc (unknown)
|
||||
// source: types.proto
|
||||
|
||||
@@ -581,65 +581,48 @@ func (*ProtoInput_KeyUp) isProtoInput_InputType() {}
|
||||
|
||||
var File_types_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_types_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x22, 0x40, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75,
|
||||
0x73, 0x65, 0x4d, 0x6f, 0x76, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x22, 0x43, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d,
|
||||
0x6f, 0x75, 0x73, 0x65, 0x4d, 0x6f, 0x76, 0x65, 0x41, 0x62, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74,
|
||||
0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12,
|
||||
0x0c, 0x0a, 0x01, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a,
|
||||
0x01, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x22, 0x41, 0x0a, 0x0f, 0x50,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x57, 0x68, 0x65, 0x65, 0x6c, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79,
|
||||
0x70, 0x65, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78,
|
||||
0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x22, 0x39,
|
||||
0x0a, 0x11, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x44,
|
||||
0x6f, 0x77, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x37, 0x0a, 0x0f, 0x50, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x55, 0x70, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6b,
|
||||
0x65, 0x79, 0x22, 0x34, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4b, 0x65, 0x79, 0x44, 0x6f,
|
||||
0x77, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x32, 0x0a, 0x0a, 0x50, 0x72, 0x6f, 0x74,
|
||||
0x6f, 0x4b, 0x65, 0x79, 0x55, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65,
|
||||
0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xab, 0x03, 0x0a,
|
||||
0x0a, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x36, 0x0a, 0x0a, 0x6d,
|
||||
0x6f, 0x75, 0x73, 0x65, 0x5f, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x15, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75,
|
||||
0x73, 0x65, 0x4d, 0x6f, 0x76, 0x65, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x4d,
|
||||
0x6f, 0x76, 0x65, 0x12, 0x40, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x5f, 0x6d, 0x6f, 0x76,
|
||||
0x65, 0x5f, 0x61, 0x62, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x4d, 0x6f,
|
||||
0x76, 0x65, 0x41, 0x62, 0x73, 0x48, 0x00, 0x52, 0x0c, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x4d, 0x6f,
|
||||
0x76, 0x65, 0x41, 0x62, 0x73, 0x12, 0x39, 0x0a, 0x0b, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x5f, 0x77,
|
||||
0x68, 0x65, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x57, 0x68, 0x65,
|
||||
0x65, 0x6c, 0x48, 0x00, 0x52, 0x0a, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x57, 0x68, 0x65, 0x65, 0x6c,
|
||||
0x12, 0x40, 0x0a, 0x0e, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x6f,
|
||||
0x77, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x44, 0x6f,
|
||||
0x77, 0x6e, 0x48, 0x00, 0x52, 0x0c, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x44, 0x6f,
|
||||
0x77, 0x6e, 0x12, 0x3a, 0x0a, 0x0c, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f,
|
||||
0x75, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4d, 0x6f, 0x75, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x55, 0x70,
|
||||
0x48, 0x00, 0x52, 0x0a, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x55, 0x70, 0x12, 0x30,
|
||||
0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4b, 0x65,
|
||||
0x79, 0x44, 0x6f, 0x77, 0x6e, 0x48, 0x00, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x44, 0x6f, 0x77, 0x6e,
|
||||
0x12, 0x2a, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x75, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x4b, 0x65,
|
||||
0x79, 0x55, 0x70, 0x48, 0x00, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x55, 0x70, 0x42, 0x0c, 0x0a, 0x0a,
|
||||
0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x16, 0x5a, 0x14, 0x72, 0x65,
|
||||
0x6c, 0x61, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
const file_types_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\vtypes.proto\x12\x05proto\"@\n" +
|
||||
"\x0eProtoMouseMove\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\f\n" +
|
||||
"\x01x\x18\x02 \x01(\x05R\x01x\x12\f\n" +
|
||||
"\x01y\x18\x03 \x01(\x05R\x01y\"C\n" +
|
||||
"\x11ProtoMouseMoveAbs\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\f\n" +
|
||||
"\x01x\x18\x02 \x01(\x05R\x01x\x12\f\n" +
|
||||
"\x01y\x18\x03 \x01(\x05R\x01y\"A\n" +
|
||||
"\x0fProtoMouseWheel\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\f\n" +
|
||||
"\x01x\x18\x02 \x01(\x05R\x01x\x12\f\n" +
|
||||
"\x01y\x18\x03 \x01(\x05R\x01y\"9\n" +
|
||||
"\x11ProtoMouseKeyDown\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03key\x18\x02 \x01(\x05R\x03key\"7\n" +
|
||||
"\x0fProtoMouseKeyUp\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03key\x18\x02 \x01(\x05R\x03key\"4\n" +
|
||||
"\fProtoKeyDown\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03key\x18\x02 \x01(\x05R\x03key\"2\n" +
|
||||
"\n" +
|
||||
"ProtoKeyUp\x12\x12\n" +
|
||||
"\x04type\x18\x01 \x01(\tR\x04type\x12\x10\n" +
|
||||
"\x03key\x18\x02 \x01(\x05R\x03key\"\xab\x03\n" +
|
||||
"\n" +
|
||||
"ProtoInput\x126\n" +
|
||||
"\n" +
|
||||
"mouse_move\x18\x01 \x01(\v2\x15.proto.ProtoMouseMoveH\x00R\tmouseMove\x12@\n" +
|
||||
"\x0emouse_move_abs\x18\x02 \x01(\v2\x18.proto.ProtoMouseMoveAbsH\x00R\fmouseMoveAbs\x129\n" +
|
||||
"\vmouse_wheel\x18\x03 \x01(\v2\x16.proto.ProtoMouseWheelH\x00R\n" +
|
||||
"mouseWheel\x12@\n" +
|
||||
"\x0emouse_key_down\x18\x04 \x01(\v2\x18.proto.ProtoMouseKeyDownH\x00R\fmouseKeyDown\x12:\n" +
|
||||
"\fmouse_key_up\x18\x05 \x01(\v2\x16.proto.ProtoMouseKeyUpH\x00R\n" +
|
||||
"mouseKeyUp\x120\n" +
|
||||
"\bkey_down\x18\x06 \x01(\v2\x13.proto.ProtoKeyDownH\x00R\akeyDown\x12*\n" +
|
||||
"\x06key_up\x18\a \x01(\v2\x11.proto.ProtoKeyUpH\x00R\x05keyUpB\f\n" +
|
||||
"\n" +
|
||||
"input_typeB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_types_proto_rawDescOnce sync.Once
|
||||
|
||||
154
packages/relay/internal/proto/webrtc.pb.go
Normal file
154
packages/relay/internal/proto/webrtc.pb.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc (unknown)
|
||||
// source: webrtc.proto
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type ICECandidateInit struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Candidate string `protobuf:"bytes,1,opt,name=candidate,proto3" json:"candidate,omitempty"`
|
||||
SdpMid *string `protobuf:"bytes,2,opt,name=sdp_mid,json=sdpMid,proto3,oneof" json:"sdp_mid,omitempty"`
|
||||
SdpMLineIndex *uint32 `protobuf:"varint,3,opt,name=sdp_m_line_index,json=sdpMLineIndex,proto3,oneof" json:"sdp_m_line_index,omitempty"`
|
||||
UsernameFragment *string `protobuf:"bytes,4,opt,name=username_fragment,json=usernameFragment,proto3,oneof" json:"username_fragment,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ICECandidateInit) Reset() {
|
||||
*x = ICECandidateInit{}
|
||||
mi := &file_webrtc_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ICECandidateInit) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ICECandidateInit) ProtoMessage() {}
|
||||
|
||||
func (x *ICECandidateInit) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_webrtc_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ICECandidateInit.ProtoReflect.Descriptor instead.
|
||||
func (*ICECandidateInit) Descriptor() ([]byte, []int) {
|
||||
return file_webrtc_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ICECandidateInit) GetCandidate() string {
|
||||
if x != nil {
|
||||
return x.Candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ICECandidateInit) GetSdpMid() string {
|
||||
if x != nil && x.SdpMid != nil {
|
||||
return *x.SdpMid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ICECandidateInit) GetSdpMLineIndex() uint32 {
|
||||
if x != nil && x.SdpMLineIndex != nil {
|
||||
return *x.SdpMLineIndex
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ICECandidateInit) GetUsernameFragment() string {
|
||||
if x != nil && x.UsernameFragment != nil {
|
||||
return *x.UsernameFragment
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_webrtc_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_webrtc_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\fwebrtc.proto\x12\x05proto\"\xe5\x01\n" +
|
||||
"\x10ICECandidateInit\x12\x1c\n" +
|
||||
"\tcandidate\x18\x01 \x01(\tR\tcandidate\x12\x1c\n" +
|
||||
"\asdp_mid\x18\x02 \x01(\tH\x00R\x06sdpMid\x88\x01\x01\x12,\n" +
|
||||
"\x10sdp_m_line_index\x18\x03 \x01(\rH\x01R\rsdpMLineIndex\x88\x01\x01\x120\n" +
|
||||
"\x11username_fragment\x18\x04 \x01(\tH\x02R\x10usernameFragment\x88\x01\x01B\n" +
|
||||
"\n" +
|
||||
"\b_sdp_midB\x13\n" +
|
||||
"\x11_sdp_m_line_indexB\x14\n" +
|
||||
"\x12_username_fragmentB\x16Z\x14relay/internal/protob\x06proto3"
|
||||
|
||||
var (
|
||||
file_webrtc_proto_rawDescOnce sync.Once
|
||||
file_webrtc_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_webrtc_proto_rawDescGZIP() []byte {
|
||||
file_webrtc_proto_rawDescOnce.Do(func() {
|
||||
file_webrtc_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_webrtc_proto_rawDesc), len(file_webrtc_proto_rawDesc)))
|
||||
})
|
||||
return file_webrtc_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_webrtc_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_webrtc_proto_goTypes = []any{
|
||||
(*ICECandidateInit)(nil), // 0: proto.ICECandidateInit
|
||||
}
|
||||
var file_webrtc_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_webrtc_proto_init() }
|
||||
func file_webrtc_proto_init() {
|
||||
if File_webrtc_proto != nil {
|
||||
return
|
||||
}
|
||||
file_webrtc_proto_msgTypes[0].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_webrtc_proto_rawDesc), len(file_webrtc_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_webrtc_proto_goTypes,
|
||||
DependencyIndexes: file_webrtc_proto_depIdxs,
|
||||
MessageInfos: file_webrtc_proto_msgTypes,
|
||||
}.Build()
|
||||
File_webrtc_proto = out.File
|
||||
file_webrtc_proto_goTypes = nil
|
||||
file_webrtc_proto_depIdxs = nil
|
||||
}
|
||||
702
packages/relay/internal/relay.go
Normal file
702
packages/relay/internal/relay.go
Normal file
@@ -0,0 +1,702 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/libp2p/go-libp2p"
|
||||
"github.com/libp2p/go-libp2p-pubsub"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/libp2p/go-libp2p/core/network"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/libp2p/go-libp2p/core/pnet"
|
||||
"github.com/libp2p/go-libp2p/p2p/security/noise"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"io"
|
||||
"log/slog"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
)
|
||||
|
||||
var globalRelay *Relay
|
||||
|
||||
// networkNotifier logs connection events
|
||||
type networkNotifier struct{}
|
||||
|
||||
func (n *networkNotifier) Connected(net network.Network, conn network.Conn) {
|
||||
slog.Info("Peer connected", "local", conn.LocalPeer(), "remote", conn.RemotePeer())
|
||||
}
|
||||
func (n *networkNotifier) Disconnected(net network.Network, conn network.Conn) {
|
||||
slog.Info("Peer disconnected", "local", conn.LocalPeer(), "remote", conn.RemotePeer())
|
||||
}
|
||||
func (n *networkNotifier) Listen(net network.Network, addr multiaddr.Multiaddr) {}
|
||||
func (n *networkNotifier) ListenClose(net network.Network, addr multiaddr.Multiaddr) {}
|
||||
|
||||
type ICEMessage struct {
|
||||
PeerID string
|
||||
TargetID string
|
||||
RoomID ulid.ULID
|
||||
Candidate []byte
|
||||
}
|
||||
|
||||
type Relay struct {
|
||||
ID peer.ID
|
||||
Rooms *common.SafeMap[ulid.ULID, *Room]
|
||||
Host host.Host // libp2p host for peer-to-peer networking
|
||||
PubSub *pubsub.PubSub // PubSub for state synchronization
|
||||
MeshState *common.SafeMap[ulid.ULID, RoomInfo] // room ID -> state
|
||||
RelayPCs *common.SafeMap[ulid.ULID, *webrtc.PeerConnection] // room ID -> relay PeerConnection
|
||||
pubTopicState *pubsub.Topic // topic for room states
|
||||
pubTopicICECandidate *pubsub.Topic // topic for ICE candidates aimed to this relay
|
||||
}
|
||||
|
||||
func NewRelay(ctx context.Context, port int) (*Relay, error) {
|
||||
listenAddrs := []string{
|
||||
fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), // IPv4
|
||||
fmt.Sprintf("/ip6/::/tcp/%d", port), // IPv6
|
||||
}
|
||||
|
||||
// Use "testToken" as the pre-shared token for authentication
|
||||
// TODO: Give via flags, before PR commit
|
||||
token := "testToken"
|
||||
// Generate 32-byte PSK from the token using SHA-256
|
||||
shaToken := sha256.Sum256([]byte(token))
|
||||
tokenPSK := pnet.PSK(shaToken[:])
|
||||
|
||||
// Initialize libp2p host
|
||||
p2pHost, err := libp2p.New(
|
||||
libp2p.ListenAddrStrings(listenAddrs...),
|
||||
libp2p.Security(noise.ID, noise.New),
|
||||
libp2p.EnableRelay(),
|
||||
libp2p.EnableHolePunching(),
|
||||
libp2p.PrivateNetwork(tokenPSK),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create libp2p host for relay: %w", err)
|
||||
}
|
||||
|
||||
// Set up pubsub
|
||||
p2pPubsub, err := pubsub.NewGossipSub(ctx, p2pHost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create pubsub: %w", err)
|
||||
}
|
||||
|
||||
// Add network notifier to log connections
|
||||
p2pHost.Network().Notify(&networkNotifier{})
|
||||
|
||||
r := &Relay{
|
||||
ID: p2pHost.ID(),
|
||||
Host: p2pHost,
|
||||
PubSub: p2pPubsub,
|
||||
Rooms: common.NewSafeMap[ulid.ULID, *Room](),
|
||||
MeshState: common.NewSafeMap[ulid.ULID, RoomInfo](),
|
||||
RelayPCs: common.NewSafeMap[ulid.ULID, *webrtc.PeerConnection](),
|
||||
}
|
||||
|
||||
// Set up state synchronization and stream handling
|
||||
r.setupStateSync(ctx)
|
||||
r.setupStreamHandler()
|
||||
|
||||
slog.Info("Relay initialized", "id", r.ID, "addrs", p2pHost.Addrs())
|
||||
|
||||
peerInfo := peer.AddrInfo{
|
||||
ID: p2pHost.ID(),
|
||||
Addrs: p2pHost.Addrs(),
|
||||
}
|
||||
addrs, err := peer.AddrInfoToP2pAddrs(&peerInfo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert peer info to addresses: %w", err)
|
||||
}
|
||||
|
||||
slog.Debug("Connect with one of the following URLs below:")
|
||||
for _, addr := range addrs {
|
||||
slog.Debug(fmt.Sprintf("- %s", addr.String()))
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func InitRelay(ctx context.Context, ctxCancel context.CancelFunc, port int) error {
|
||||
var err error
|
||||
globalRelay, err = NewRelay(ctx, port)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create relay: %w", err)
|
||||
}
|
||||
|
||||
if err := common.InitWebRTCAPI(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := InitHTTPEndpoint(ctx, ctxCancel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slog.Info("Relay initialized", "id", globalRelay.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRelay() *Relay {
|
||||
return globalRelay
|
||||
}
|
||||
|
||||
func (r *Relay) GetRoomByID(id ulid.ULID) *Room {
|
||||
if room, ok := r.Rooms.Get(id); ok {
|
||||
return room
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Relay) GetOrCreateRoom(name string) *Room {
|
||||
if room := r.GetRoomByName(name); room != nil {
|
||||
return room
|
||||
}
|
||||
|
||||
id, err := common.NewULID()
|
||||
if err != nil {
|
||||
slog.Error("Failed to generate new ULID for room", "err", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
room := NewRoom(name, id, r.ID)
|
||||
room.Relay = r
|
||||
r.Rooms.Set(room.ID, room)
|
||||
|
||||
slog.Debug("Created new room", "name", name, "id", room.ID)
|
||||
return room
|
||||
}
|
||||
|
||||
func (r *Relay) DeleteRoomIfEmpty(room *Room) {
|
||||
participantCount := room.Participants.Len()
|
||||
if participantCount > 0 {
|
||||
slog.Debug("Room not empty, not deleting", "name", room.Name, "id", room.ID, "participants", participantCount)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a "tombstone" state for the room, this allows propagation of the room deletion
|
||||
tombstoneState := RoomInfo{
|
||||
ID: room.ID,
|
||||
Name: room.Name,
|
||||
Online: false,
|
||||
OwnerID: room.OwnerID,
|
||||
}
|
||||
|
||||
// Publish updated state to mesh
|
||||
if err := r.publishRoomState(context.Background(), tombstoneState); err != nil {
|
||||
slog.Error("Failed to publish room states on change", "room", room.Name, "err", err)
|
||||
}
|
||||
|
||||
slog.Info("Deleting room since empty and offline", "name", room.Name, "id", room.ID)
|
||||
r.Rooms.Delete(room.ID)
|
||||
}
|
||||
|
||||
func (r *Relay) setupStateSync(ctx context.Context) {
|
||||
var err error
|
||||
r.pubTopicState, err = r.PubSub.Join("room-states")
|
||||
if err != nil {
|
||||
slog.Error("Failed to join pubsub topic", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
sub, err := r.pubTopicState.Subscribe()
|
||||
if err != nil {
|
||||
slog.Error("Failed to subscribe to topic", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.pubTopicICECandidate, err = r.PubSub.Join("ice-candidates")
|
||||
if err != nil {
|
||||
slog.Error("Failed to join ICE candidates topic", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
iceCandidateSub, err := r.pubTopicICECandidate.Subscribe()
|
||||
if err != nil {
|
||||
slog.Error("Failed to subscribe to ICE candidates topic", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle state updates only from authenticated peers
|
||||
go func() {
|
||||
for {
|
||||
msg, err := sub.Next(ctx)
|
||||
if err != nil {
|
||||
slog.Error("Error receiving pubsub message", "err", err)
|
||||
return
|
||||
}
|
||||
if msg.GetFrom() == r.Host.ID() {
|
||||
continue // Ignore own messages
|
||||
}
|
||||
var states []RoomInfo
|
||||
if err := json.Unmarshal(msg.Data, &states); err != nil {
|
||||
slog.Error("Failed to unmarshal room states", "err", err)
|
||||
continue
|
||||
}
|
||||
r.updateMeshState(states)
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle incoming ICE candidates for given room
|
||||
go func() {
|
||||
// Map of ICE candidate slices per room ID
|
||||
iceHolder := make(map[ulid.ULID][]webrtc.ICECandidateInit)
|
||||
|
||||
for {
|
||||
msg, err := iceCandidateSub.Next(ctx)
|
||||
if err != nil {
|
||||
slog.Error("Error receiving ICE candidate message", "err", err)
|
||||
return
|
||||
}
|
||||
if msg.GetFrom() == r.Host.ID() {
|
||||
continue // Ignore own messages
|
||||
}
|
||||
|
||||
var iceMsg ICEMessage
|
||||
if err := json.Unmarshal(msg.Data, &iceMsg); err != nil {
|
||||
slog.Error("Failed to unmarshal ICE candidate message", "err", err)
|
||||
continue
|
||||
}
|
||||
if iceMsg.TargetID != r.ID.String() {
|
||||
continue // Ignore messages not meant for this relay
|
||||
}
|
||||
|
||||
if iceHolder[iceMsg.RoomID] == nil {
|
||||
iceHolder[iceMsg.RoomID] = make([]webrtc.ICECandidateInit, 0)
|
||||
}
|
||||
|
||||
if pc, ok := r.RelayPCs.Get(iceMsg.RoomID); ok {
|
||||
// Unmarshal ice candidate
|
||||
var candidate webrtc.ICECandidateInit
|
||||
if err := json.Unmarshal(iceMsg.Candidate, &candidate); err != nil {
|
||||
slog.Error("Failed to unmarshal ICE candidate", "err", err)
|
||||
continue
|
||||
}
|
||||
if pc.RemoteDescription() != nil {
|
||||
if err := pc.AddICECandidate(candidate); err != nil {
|
||||
slog.Error("Failed to add ICE candidate", "err", err)
|
||||
}
|
||||
// Add any held candidates
|
||||
for _, heldCandidate := range iceHolder[iceMsg.RoomID] {
|
||||
if err := pc.AddICECandidate(heldCandidate); err != nil {
|
||||
slog.Error("Failed to add held ICE candidate", "err", err)
|
||||
}
|
||||
}
|
||||
iceHolder[iceMsg.RoomID] = make([]webrtc.ICECandidateInit, 0)
|
||||
} else {
|
||||
iceHolder[iceMsg.RoomID] = append(iceHolder[iceMsg.RoomID], candidate)
|
||||
}
|
||||
} else {
|
||||
slog.Error("PeerConnection for room not found when adding ICE candidate", "roomID", iceMsg.RoomID)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *Relay) publishRoomState(ctx context.Context, state RoomInfo) error {
|
||||
data, err := json.Marshal([]RoomInfo{state})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.pubTopicState.Publish(ctx, data)
|
||||
}
|
||||
|
||||
func (r *Relay) publishRoomStates(ctx context.Context) error {
|
||||
var states []RoomInfo
|
||||
for _, room := range r.Rooms.Copy() {
|
||||
states = append(states, RoomInfo{
|
||||
ID: room.ID,
|
||||
Name: room.Name,
|
||||
Online: room.Online,
|
||||
OwnerID: r.ID,
|
||||
})
|
||||
}
|
||||
data, err := json.Marshal(states)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.pubTopicState.Publish(ctx, data)
|
||||
}
|
||||
|
||||
func (r *Relay) updateMeshState(states []RoomInfo) {
|
||||
for _, state := range states {
|
||||
if state.OwnerID == r.ID {
|
||||
continue // Skip own state
|
||||
}
|
||||
existing, exists := r.MeshState.Get(state.ID)
|
||||
r.MeshState.Set(state.ID, state)
|
||||
slog.Debug("Updated mesh state", "room", state.Name, "online", state.Online, "owner", state.OwnerID)
|
||||
|
||||
// React to state changes
|
||||
if !exists || existing.Online != state.Online {
|
||||
room := r.GetRoomByName(state.Name)
|
||||
if state.Online {
|
||||
if room == nil || !room.Online {
|
||||
slog.Info("Room became active remotely, requesting stream", "room", state.Name, "owner", state.OwnerID)
|
||||
go func() {
|
||||
if _, err := r.requestStream(context.Background(), state.Name, state.ID, state.OwnerID); err != nil {
|
||||
slog.Error("Failed to request stream", "room", state.Name, "err", err)
|
||||
} else {
|
||||
slog.Info("Successfully requested stream", "room", state.Name, "owner", state.OwnerID)
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else if room != nil && room.Online {
|
||||
slog.Info("Room became inactive remotely, stopping local stream", "room", state.Name)
|
||||
if pc, ok := r.RelayPCs.Get(state.ID); ok {
|
||||
_ = pc.Close()
|
||||
r.RelayPCs.Delete(state.ID)
|
||||
}
|
||||
room.Online = false
|
||||
room.signalParticipantsOffline()
|
||||
} else if room == nil && !exists {
|
||||
slog.Info("Received tombstone state for room", "name", state.Name, "id", state.ID)
|
||||
if pc, ok := r.RelayPCs.Get(state.ID); ok {
|
||||
_ = pc.Close()
|
||||
r.RelayPCs.Delete(state.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Relay) IsRoomActive(roomID ulid.ULID) (bool, peer.ID) {
|
||||
if state, exists := r.MeshState.Get(roomID); exists && state.Online {
|
||||
return true, state.OwnerID
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
|
||||
func (r *Relay) GetRoomByName(name string) *Room {
|
||||
for _, room := range r.Rooms.Copy() {
|
||||
if room.Name == name {
|
||||
return room
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeMessage(stream network.Stream, data []byte) error {
|
||||
length := uint32(len(data))
|
||||
if err := binary.Write(stream, binary.BigEndian, length); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := stream.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func readMessage(stream network.Stream) ([]byte, error) {
|
||||
var length uint32
|
||||
if err := binary.Read(stream, binary.BigEndian, &length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make([]byte, length)
|
||||
_, err := io.ReadFull(stream, data)
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (r *Relay) setupStreamHandler() {
|
||||
r.Host.SetStreamHandler("/nestri-relay/stream/1.0.0", func(stream network.Stream) {
|
||||
defer func(stream network.Stream) {
|
||||
err := stream.Close()
|
||||
if err != nil {
|
||||
slog.Error("Failed to close stream", "err", err)
|
||||
}
|
||||
}(stream)
|
||||
remotePeer := stream.Conn().RemotePeer()
|
||||
|
||||
roomNameData, err := readMessage(stream)
|
||||
if err != nil && err != io.EOF {
|
||||
slog.Error("Failed to read room name", "peer", remotePeer, "err", err)
|
||||
return
|
||||
}
|
||||
roomName := string(roomNameData)
|
||||
|
||||
slog.Info("Stream request from peer", "peer", remotePeer, "room", roomName)
|
||||
|
||||
room := r.GetRoomByName(roomName)
|
||||
if room == nil || !room.Online {
|
||||
slog.Error("Cannot provide stream for inactive room", "room", roomName)
|
||||
return
|
||||
}
|
||||
|
||||
pc, err := common.CreatePeerConnection(func() {
|
||||
r.RelayPCs.Delete(room.ID)
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to create relay PeerConnection", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
r.RelayPCs.Set(room.ID, pc)
|
||||
|
||||
if room.AudioTrack != nil {
|
||||
_, err := pc.AddTrack(room.AudioTrack)
|
||||
if err != nil {
|
||||
slog.Error("Failed to add audio track", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if room.VideoTrack != nil {
|
||||
_, err := pc.AddTrack(room.VideoTrack)
|
||||
if err != nil {
|
||||
slog.Error("Failed to add video track", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
settingOrdered := true
|
||||
settingMaxRetransmits := uint16(0)
|
||||
dc, err := pc.CreateDataChannel("relay-data", &webrtc.DataChannelInit{
|
||||
Ordered: &settingOrdered,
|
||||
MaxRetransmits: &settingMaxRetransmits,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to create relay DataChannel", "err", err)
|
||||
return
|
||||
}
|
||||
relayDC := connections.NewNestriDataChannel(dc)
|
||||
|
||||
relayDC.RegisterOnOpen(func() {
|
||||
slog.Debug("Relay DataChannel opened", "room", roomName)
|
||||
})
|
||||
|
||||
relayDC.RegisterOnClose(func() {
|
||||
slog.Debug("Relay DataChannel closed", "room", roomName)
|
||||
})
|
||||
|
||||
relayDC.RegisterMessageCallback("input", func(data []byte) {
|
||||
if room.DataChannel != nil {
|
||||
// Forward message to the room's data channel
|
||||
if err := room.DataChannel.SendBinary(data); err != nil {
|
||||
slog.Error("Failed to send DataChannel message", "room", roomName, "err", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
offer, err := pc.CreateOffer(nil)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create offer", "err", err)
|
||||
return
|
||||
}
|
||||
if err := pc.SetLocalDescription(offer); err != nil {
|
||||
slog.Error("Failed to set local description", "err", err)
|
||||
return
|
||||
}
|
||||
offerData, err := json.Marshal(offer)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal offer", "err", err)
|
||||
return
|
||||
}
|
||||
if err := writeMessage(stream, offerData); err != nil {
|
||||
slog.Error("Failed to send offer", "peer", remotePeer, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle our generated ICE candidates
|
||||
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
candidateData, err := json.Marshal(candidate.ToJSON())
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal ICE candidate", "err", err)
|
||||
return
|
||||
}
|
||||
iceMsg := ICEMessage{
|
||||
PeerID: r.Host.ID().String(),
|
||||
TargetID: remotePeer.String(),
|
||||
RoomID: room.ID,
|
||||
Candidate: candidateData,
|
||||
}
|
||||
data, err := json.Marshal(iceMsg)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal ICE message", "err", err)
|
||||
return
|
||||
}
|
||||
if err := r.pubTopicICECandidate.Publish(context.Background(), data); err != nil {
|
||||
slog.Error("Failed to publish ICE candidate message", "err", err)
|
||||
}
|
||||
})
|
||||
|
||||
answerData, err := readMessage(stream)
|
||||
if err != nil && err != io.EOF {
|
||||
slog.Error("Failed to read answer", "peer", remotePeer, "err", err)
|
||||
return
|
||||
}
|
||||
var answer webrtc.SessionDescription
|
||||
if err := json.Unmarshal(answerData, &answer); err != nil {
|
||||
slog.Error("Failed to unmarshal answer", "err", err)
|
||||
return
|
||||
}
|
||||
if err := pc.SetRemoteDescription(answer); err != nil {
|
||||
slog.Error("Failed to set remote description", "err", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Relay) requestStream(ctx context.Context, roomName string, roomID ulid.ULID, providerPeer peer.ID) (*webrtc.PeerConnection, error) {
|
||||
stream, err := r.Host.NewStream(ctx, providerPeer, "/nestri-relay/stream/1.0.0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stream: %w", err)
|
||||
}
|
||||
defer func(stream network.Stream) {
|
||||
err := stream.Close()
|
||||
if err != nil {
|
||||
slog.Error("Failed to close stream", "err", err)
|
||||
}
|
||||
}(stream)
|
||||
|
||||
if err := writeMessage(stream, []byte(roomName)); err != nil {
|
||||
return nil, fmt.Errorf("failed to send room name: %w", err)
|
||||
}
|
||||
|
||||
room := r.GetRoomByName(roomName)
|
||||
if room == nil {
|
||||
room = NewRoom(roomName, roomID, providerPeer)
|
||||
r.Rooms.Set(roomID, room)
|
||||
} else if room.ID != roomID {
|
||||
// Mismatch, prefer the one from the provider
|
||||
// TODO: When mesh is created, if there are mismatches, we should have relays negotiate common room IDs
|
||||
room.ID = roomID
|
||||
room.OwnerID = providerPeer
|
||||
r.Rooms.Set(roomID, room)
|
||||
}
|
||||
|
||||
pc, err := common.CreatePeerConnection(func() {
|
||||
r.RelayPCs.Delete(roomID)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PeerConnection: %w", err)
|
||||
}
|
||||
|
||||
r.RelayPCs.Set(roomID, pc)
|
||||
|
||||
offerData, err := readMessage(stream)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, fmt.Errorf("failed to read offer: %w", err)
|
||||
}
|
||||
var offer webrtc.SessionDescription
|
||||
if err := json.Unmarshal(offerData, &offer); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal offer: %w", err)
|
||||
}
|
||||
if err := pc.SetRemoteDescription(offer); err != nil {
|
||||
return nil, fmt.Errorf("failed to set remote description: %w", err)
|
||||
}
|
||||
|
||||
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||
localTrack, _ := webrtc.NewTrackLocalStaticRTP(track.Codec().RTPCodecCapability, track.ID(), "relay-"+roomName+"-"+track.Kind().String())
|
||||
slog.Debug("Received track for mesh relay room", "room", roomName, "kind", track.Kind())
|
||||
|
||||
room.SetTrack(track.Kind(), localTrack)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
rtpPacket, _, err := track.ReadRTP()
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
slog.Error("Failed to read RTP packet from remote track for room", "room", roomName, "err", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
err = localTrack.WriteRTP(rtpPacket)
|
||||
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
slog.Error("Failed to write RTP to local track for room", "room", room.Name, "err", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// ICE candidate handling
|
||||
pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
candidateData, err := json.Marshal(candidate.ToJSON())
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal ICE candidate", "err", err)
|
||||
return
|
||||
}
|
||||
iceMsg := ICEMessage{
|
||||
PeerID: r.Host.ID().String(),
|
||||
TargetID: providerPeer.String(),
|
||||
RoomID: roomID,
|
||||
Candidate: candidateData,
|
||||
}
|
||||
data, err := json.Marshal(iceMsg)
|
||||
if err != nil {
|
||||
slog.Error("Failed to marshal ICE message", "err", err)
|
||||
return
|
||||
}
|
||||
if err := r.pubTopicICECandidate.Publish(ctx, data); err != nil {
|
||||
slog.Error("Failed to publish ICE candidate message", "err", err)
|
||||
}
|
||||
})
|
||||
|
||||
pc.OnDataChannel(func(dc *webrtc.DataChannel) {
|
||||
relayDC := connections.NewNestriDataChannel(dc)
|
||||
slog.Debug("Received DataChannel from peer", "room", roomName)
|
||||
|
||||
relayDC.RegisterOnOpen(func() {
|
||||
slog.Debug("Relay DataChannel opened", "room", roomName)
|
||||
})
|
||||
|
||||
relayDC.OnClose(func() {
|
||||
slog.Debug("Relay DataChannel closed", "room", roomName)
|
||||
})
|
||||
|
||||
// Override room DataChannel with the mesh-relay one to forward messages
|
||||
room.DataChannel = relayDC
|
||||
})
|
||||
|
||||
answer, err := pc.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create answer: %w", err)
|
||||
}
|
||||
if err := pc.SetLocalDescription(answer); err != nil {
|
||||
return nil, fmt.Errorf("failed to set local description: %w", err)
|
||||
}
|
||||
answerData, err := json.Marshal(answer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal answer: %w", err)
|
||||
}
|
||||
if err := writeMessage(stream, answerData); err != nil {
|
||||
return nil, fmt.Errorf("failed to send answer: %w", err)
|
||||
}
|
||||
|
||||
return pc, nil
|
||||
}
|
||||
|
||||
// ConnectToRelay manually connects to another relay by its multiaddress
|
||||
func (r *Relay) ConnectToRelay(ctx context.Context, addr string) error {
|
||||
// Parse the multiaddress
|
||||
ma, err := multiaddr.NewMultiaddr(addr)
|
||||
if err != nil {
|
||||
slog.Error("Invalid multiaddress", "addr", addr, "err", err)
|
||||
return fmt.Errorf("invalid multiaddress: %w", err)
|
||||
}
|
||||
|
||||
// Extract peer ID from multiaddress
|
||||
peerInfo, err := peer.AddrInfoFromP2pAddr(ma)
|
||||
if err != nil {
|
||||
slog.Error("Failed to extract peer info", "addr", addr, "err", err)
|
||||
return fmt.Errorf("failed to extract peer info: %w", err)
|
||||
}
|
||||
|
||||
// Connect to the peer
|
||||
if err := r.Host.Connect(ctx, *peerInfo); err != nil {
|
||||
slog.Error("Failed to connect to peer", "peer", peerInfo.ID, "addr", addr, "err", err)
|
||||
return fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
// Publish challenge on join
|
||||
//go r.sendAuthChallenge(ctx)
|
||||
|
||||
slog.Info("Successfully connected to peer", "peer", peerInfo.ID, "addr", addr)
|
||||
return nil
|
||||
}
|
||||
@@ -1,179 +1,164 @@
|
||||
package relay
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"log"
|
||||
"sync"
|
||||
"log/slog"
|
||||
"relay/internal/common"
|
||||
"relay/internal/connections"
|
||||
)
|
||||
|
||||
var Rooms = make(map[uuid.UUID]*Room) //< Room ID -> Room
|
||||
var RoomsMutex = sync.RWMutex{}
|
||||
|
||||
func GetRoomByID(id uuid.UUID) *Room {
|
||||
RoomsMutex.RLock()
|
||||
defer RoomsMutex.RUnlock()
|
||||
if room, ok := Rooms[id]; ok {
|
||||
return room
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetRoomByName(name string) *Room {
|
||||
RoomsMutex.RLock()
|
||||
defer RoomsMutex.RUnlock()
|
||||
for _, room := range Rooms {
|
||||
if room.Name == name {
|
||||
return room
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetOrCreateRoom(name string) *Room {
|
||||
if room := GetRoomByName(name); room != nil {
|
||||
return room
|
||||
}
|
||||
RoomsMutex.Lock()
|
||||
room := NewRoom(name)
|
||||
Rooms[room.ID] = room
|
||||
if GetFlags().Verbose {
|
||||
log.Printf("New room: '%s'\n", room.Name)
|
||||
}
|
||||
RoomsMutex.Unlock()
|
||||
return room
|
||||
}
|
||||
|
||||
func DeleteRoomIfEmpty(room *Room) {
|
||||
room.ParticipantsMutex.RLock()
|
||||
defer room.ParticipantsMutex.RUnlock()
|
||||
if !room.Online && len(room.Participants) <= 0 {
|
||||
RoomsMutex.Lock()
|
||||
delete(Rooms, room.ID)
|
||||
RoomsMutex.Unlock()
|
||||
}
|
||||
type RoomInfo struct {
|
||||
ID ulid.ULID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Online bool `json:"online"`
|
||||
OwnerID peer.ID `json:"owner_id"`
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
ID uuid.UUID //< Internal IDs are useful to keeping unique internal track
|
||||
Name string
|
||||
Online bool //< Whether the room is currently online, i.e. receiving data from a nestri-server
|
||||
WebSocket *SafeWebSocket
|
||||
PeerConnection *webrtc.PeerConnection
|
||||
AudioTrack webrtc.TrackLocal
|
||||
VideoTrack webrtc.TrackLocal
|
||||
DataChannel *NestriDataChannel
|
||||
Participants map[uuid.UUID]*Participant
|
||||
ParticipantsMutex sync.RWMutex
|
||||
RoomInfo
|
||||
WebSocket *connections.SafeWebSocket
|
||||
PeerConnection *webrtc.PeerConnection
|
||||
AudioTrack *webrtc.TrackLocalStaticRTP
|
||||
VideoTrack *webrtc.TrackLocalStaticRTP
|
||||
DataChannel *connections.NestriDataChannel
|
||||
Participants *common.SafeMap[ulid.ULID, *Participant]
|
||||
Relay *Relay
|
||||
}
|
||||
|
||||
func NewRoom(name string) *Room {
|
||||
func NewRoom(name string, roomID ulid.ULID, ownerID peer.ID) *Room {
|
||||
return &Room{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
Online: false,
|
||||
Participants: make(map[uuid.UUID]*Participant),
|
||||
RoomInfo: RoomInfo{
|
||||
ID: roomID,
|
||||
Name: name,
|
||||
Online: false,
|
||||
OwnerID: ownerID,
|
||||
},
|
||||
Participants: common.NewSafeMap[ulid.ULID, *Participant](),
|
||||
}
|
||||
}
|
||||
|
||||
// Assigns a WebSocket connection to a Room
|
||||
func (r *Room) assignWebSocket(ws *SafeWebSocket) {
|
||||
// If WS already assigned, warn
|
||||
// AssignWebSocket assigns a WebSocket connection to a Room
|
||||
func (r *Room) AssignWebSocket(ws *connections.SafeWebSocket) {
|
||||
if r.WebSocket != nil {
|
||||
log.Printf("Warning: Room '%s' already has a WebSocket assigned\n", r.Name)
|
||||
slog.Warn("WebSocket already assigned to room", "room", r.Name)
|
||||
}
|
||||
r.WebSocket = ws
|
||||
}
|
||||
|
||||
// Adds a Participant to a Room
|
||||
func (r *Room) addParticipant(participant *Participant) {
|
||||
r.ParticipantsMutex.Lock()
|
||||
r.Participants[participant.ID] = participant
|
||||
r.ParticipantsMutex.Unlock()
|
||||
// AddParticipant adds a Participant to a Room
|
||||
func (r *Room) AddParticipant(participant *Participant) {
|
||||
slog.Debug("Adding participant to room", "participant", participant.ID, "room", r.Name)
|
||||
r.Participants.Set(participant.ID, participant)
|
||||
}
|
||||
|
||||
// Removes a Participant from a Room by participant's ID.
|
||||
// If Room is offline and this is the last participant, the room is deleted
|
||||
func (r *Room) removeParticipantByID(pID uuid.UUID) {
|
||||
r.ParticipantsMutex.Lock()
|
||||
delete(r.Participants, pID)
|
||||
r.ParticipantsMutex.Unlock()
|
||||
DeleteRoomIfEmpty(r)
|
||||
// Removes a Participant from a Room by participant's ID
|
||||
func (r *Room) removeParticipantByID(pID ulid.ULID) {
|
||||
if _, ok := r.Participants.Get(pID); ok {
|
||||
r.Participants.Delete(pID)
|
||||
}
|
||||
}
|
||||
|
||||
// Removes a Participant from a Room by participant's name.
|
||||
// If Room is offline and this is the last participant, the room is deleted
|
||||
// Removes a Participant from a Room by participant's name
|
||||
func (r *Room) removeParticipantByName(pName string) {
|
||||
r.ParticipantsMutex.Lock()
|
||||
for id, p := range r.Participants {
|
||||
if p.Name == pName {
|
||||
delete(r.Participants, id)
|
||||
for id, participant := range r.Participants.Copy() {
|
||||
if participant.Name == pName {
|
||||
if err := r.signalParticipantOffline(participant); err != nil {
|
||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
r.Participants.Delete(id)
|
||||
break
|
||||
}
|
||||
}
|
||||
r.ParticipantsMutex.Unlock()
|
||||
DeleteRoomIfEmpty(r)
|
||||
}
|
||||
|
||||
// Signals all participants with offer and add tracks to their PeerConnections
|
||||
// Removes all participants from a Room
|
||||
func (r *Room) removeAllParticipants() {
|
||||
for id, participant := range r.Participants.Copy() {
|
||||
if err := r.signalParticipantOffline(participant); err != nil {
|
||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
r.Participants.Delete(id)
|
||||
slog.Debug("Removed participant from room", "participant", id, "room", r.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) SetTrack(trackType webrtc.RTPCodecType, track *webrtc.TrackLocalStaticRTP) {
|
||||
switch trackType {
|
||||
case webrtc.RTPCodecTypeAudio:
|
||||
r.AudioTrack = track
|
||||
slog.Debug("Audio track set", "room", r.Name, "track", track != nil)
|
||||
case webrtc.RTPCodecTypeVideo:
|
||||
r.VideoTrack = track
|
||||
slog.Debug("Video track set", "room", r.Name, "track", track != nil)
|
||||
default:
|
||||
slog.Warn("Unknown track type", "room", r.Name, "trackType", trackType)
|
||||
}
|
||||
|
||||
newOnline := r.AudioTrack != nil && r.VideoTrack != nil
|
||||
if r.Online != newOnline {
|
||||
r.Online = newOnline
|
||||
if r.Online {
|
||||
slog.Debug("Room online, participants will be signaled", "room", r.Name)
|
||||
r.signalParticipantsWithTracks()
|
||||
} else {
|
||||
slog.Debug("Room offline, signaling participants", "room", r.Name)
|
||||
r.signalParticipantsOffline()
|
||||
}
|
||||
|
||||
// Publish updated state to mesh
|
||||
go func() {
|
||||
if err := r.Relay.publishRoomStates(context.Background()); err != nil {
|
||||
slog.Error("Failed to publish room states on change", "room", r.Name, "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) signalParticipantsWithTracks() {
|
||||
r.ParticipantsMutex.RLock()
|
||||
for _, participant := range r.Participants {
|
||||
// Add tracks to participant's PeerConnection
|
||||
if r.AudioTrack != nil {
|
||||
if err := participant.addTrack(&r.AudioTrack); err != nil {
|
||||
log.Printf("Failed to add audio track to participant: '%s' - reason: %s\n", participant.ID, err)
|
||||
}
|
||||
}
|
||||
if r.VideoTrack != nil {
|
||||
if err := participant.addTrack(&r.VideoTrack); err != nil {
|
||||
log.Printf("Failed to add video track to participant: '%s' - reason: %s\n", participant.ID, err)
|
||||
}
|
||||
}
|
||||
// Signal participant with offer
|
||||
if err := participant.signalOffer(); err != nil {
|
||||
log.Printf("Error signaling participant: %v\n", err)
|
||||
for _, participant := range r.Participants.Copy() {
|
||||
if err := r.signalParticipantWithTracks(participant); err != nil {
|
||||
slog.Error("Failed to signal participant with tracks", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
}
|
||||
r.ParticipantsMutex.RUnlock()
|
||||
}
|
||||
|
||||
// Signals all participants that the Room is offline
|
||||
func (r *Room) signalParticipantWithTracks(participant *Participant) error {
|
||||
if r.AudioTrack != nil {
|
||||
if err := participant.addTrack(r.AudioTrack); err != nil {
|
||||
return fmt.Errorf("failed to add audio track: %w", err)
|
||||
}
|
||||
}
|
||||
if r.VideoTrack != nil {
|
||||
if err := participant.addTrack(r.VideoTrack); err != nil {
|
||||
return fmt.Errorf("failed to add video track: %w", err)
|
||||
}
|
||||
}
|
||||
if err := participant.signalOffer(); err != nil {
|
||||
return fmt.Errorf("failed to signal offer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Room) signalParticipantsOffline() {
|
||||
r.ParticipantsMutex.RLock()
|
||||
for _, participant := range r.Participants {
|
||||
if err := participant.WebSocket.SendAnswerMessageWS(AnswerOffline); err != nil {
|
||||
log.Printf("Failed to send Offline answer for participant: '%s' - reason: %s\n", participant.ID, err)
|
||||
for _, participant := range r.Participants.Copy() {
|
||||
if err := r.signalParticipantOffline(participant); err != nil {
|
||||
slog.Error("Failed to signal participant offline", "participant", participant.ID, "room", r.Name, "err", err)
|
||||
}
|
||||
}
|
||||
r.ParticipantsMutex.RUnlock()
|
||||
}
|
||||
|
||||
// Broadcasts a message to Room's Participant's - excluding one given ID of
|
||||
func (r *Room) broadcastMessage(msg webrtc.DataChannelMessage, excludeID uuid.UUID) {
|
||||
r.ParticipantsMutex.RLock()
|
||||
for d, participant := range r.Participants {
|
||||
if participant.DataChannel != nil {
|
||||
if d != excludeID { // Don't send back to the sender
|
||||
if err := participant.DataChannel.SendText(string(msg.Data)); err != nil {
|
||||
log.Printf("Error broadcasting to %s: %v\n", participant.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// signalParticipantOffline signals a single participant offline
|
||||
func (r *Room) signalParticipantOffline(participant *Participant) error {
|
||||
// Skip if websocket is nil or closed
|
||||
if participant.WebSocket == nil || participant.WebSocket.IsClosed() {
|
||||
return nil
|
||||
}
|
||||
if r.DataChannel != nil {
|
||||
if err := r.DataChannel.SendText(string(msg.Data)); err != nil {
|
||||
log.Printf("Error broadcasting to Room: %v\n", err)
|
||||
}
|
||||
}
|
||||
r.ParticipantsMutex.RUnlock()
|
||||
}
|
||||
|
||||
// Sends message to Room (nestri-server)
|
||||
func (r *Room) sendToRoom(msg webrtc.DataChannelMessage) {
|
||||
if r.DataChannel != nil {
|
||||
if err := r.DataChannel.SendText(string(msg.Data)); err != nil {
|
||||
log.Printf("Error broadcasting to Room: %v\n", err)
|
||||
}
|
||||
if err := participant.WebSocket.SendAnswerMessageWS(connections.AnswerOffline); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,32 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
relay "relay/internal"
|
||||
"relay/internal"
|
||||
"relay/internal/common"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
stopCh := make(chan os.Signal, 1)
|
||||
signal.Notify(stopCh, os.Interrupt, syscall.SIGTERM)
|
||||
// Setup main context and stopper
|
||||
mainCtx, mainStopper := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Get flags and log them
|
||||
relay.InitFlags()
|
||||
relay.GetFlags().DebugLog()
|
||||
common.InitFlags()
|
||||
common.GetFlags().DebugLog()
|
||||
|
||||
// Init WebRTC API
|
||||
err = relay.InitWebRTCAPI()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to initialize WebRTC API: ", err)
|
||||
logLevel := slog.LevelInfo
|
||||
if common.GetFlags().Verbose {
|
||||
logLevel = slog.LevelDebug
|
||||
}
|
||||
|
||||
// Start our HTTP endpoints
|
||||
relay.InitHTTPEndpoint()
|
||||
// Create the base handler with debug level
|
||||
baseHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: logLevel,
|
||||
})
|
||||
customHandler := &common.CustomHandler{Handler: baseHandler}
|
||||
logger := slog.New(customHandler)
|
||||
slog.SetDefault(logger)
|
||||
|
||||
// Start relay
|
||||
err := internal.InitRelay(mainCtx, mainStopper, common.GetFlags().MeshPort)
|
||||
if err != nil {
|
||||
slog.Error("Failed to initialize relay", "err", err)
|
||||
mainStopper()
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for exit signal
|
||||
<-stopCh
|
||||
<-mainCtx.Done()
|
||||
log.Println("Shutting down gracefully by signal...")
|
||||
}
|
||||
|
||||
5
sst-env.d.ts
vendored
5
sst-env.d.ts
vendored
@@ -68,6 +68,11 @@ declare module "sst" {
|
||||
"type": "sst.sst.Secret"
|
||||
"value": string
|
||||
}
|
||||
"Realtime": {
|
||||
"authorizer": string
|
||||
"endpoint": string
|
||||
"type": "sst.aws.Realtime"
|
||||
}
|
||||
"Steam": {
|
||||
"service": string
|
||||
"type": "sst.aws.Service"
|
||||
|
||||
Reference in New Issue
Block a user