mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
426 lines
11 KiB
JavaScript
Executable File
426 lines
11 KiB
JavaScript
Executable File
require("dotenv").config()
|
|
|
|
import fs from "node:fs"
|
|
import path from "node:path"
|
|
import repl from "node:repl"
|
|
import { Transform } from "node:stream"
|
|
import ChildProcess from "node:child_process"
|
|
import { Observable } from "@gullerya/object-observer"
|
|
import chalk from "chalk"
|
|
import Spinnies from "spinnies"
|
|
import chokidar from "chokidar"
|
|
|
|
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
|
|
|
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
|
|
import getInternalIp from "./lib/getInternalIp"
|
|
import comtyAscii from "./ascii"
|
|
import pkg from "./package.json"
|
|
|
|
const bootloaderBin = path.resolve(__dirname, "boot")
|
|
const servicesPath = path.resolve(__dirname, "services")
|
|
|
|
async function scanServices() {
|
|
const finalServices = []
|
|
|
|
let services = fs.readdirSync(servicesPath)
|
|
|
|
for await (let _path of services) {
|
|
_path = path.resolve(servicesPath, _path)
|
|
|
|
if (fs.lstatSync(_path).isDirectory()) {
|
|
// search main file "*.service.*" (using regex) on the root of the service path
|
|
const mainFile = fs.readdirSync(_path).find((filename) => {
|
|
const regex = new RegExp(`^.*\.service\..*$`)
|
|
|
|
return regex.test(filename)
|
|
})
|
|
|
|
if (mainFile) {
|
|
finalServices.push(path.resolve(_path, mainFile))
|
|
}
|
|
}
|
|
}
|
|
|
|
return finalServices
|
|
}
|
|
|
|
let allReady = false
|
|
let selectedProcessInstance = null
|
|
let internalIp = null
|
|
let services = null
|
|
|
|
const spinnies = new Spinnies()
|
|
|
|
const ipcRouter = global.ipcRouter = new IPCRouter()
|
|
const instancePool = global.instancePool = []
|
|
const serviceFileReference = {}
|
|
const serviceRegistry = global.serviceRegistry = Observable.from({})
|
|
|
|
Observable.observe(serviceRegistry, (changes) => {
|
|
const { type, path, value } = changes[0]
|
|
|
|
switch (type) {
|
|
case "update": {
|
|
//console.log(`Updated service | ${path} > ${value}`)
|
|
|
|
//check if all services all ready
|
|
if (Object.values(serviceRegistry).every((service) => service.ready)) {
|
|
handleAllReady()
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
})
|
|
|
|
function detachInstanceStd(instance) {
|
|
if (instance.logs) {
|
|
instance.logs.stdout.unpipe(process.stdout)
|
|
instance.logs.stderr.unpipe(process.stderr)
|
|
}
|
|
}
|
|
|
|
function attachInstanceStd(instance, { afterMsg } = {}) {
|
|
if (instance.logs) {
|
|
console.clear()
|
|
|
|
if (afterMsg) {
|
|
console.log(afterMsg)
|
|
}
|
|
|
|
instance.logs.stdout.pipe(process.stdout)
|
|
instance.logs.stderr.pipe(process.stderr)
|
|
}
|
|
}
|
|
|
|
const relp_commands = [
|
|
{
|
|
cmd: "select",
|
|
aliases: ["s", "sel"],
|
|
fn: (cb, service) => {
|
|
if (!isNaN(parseInt(service))) {
|
|
service = serviceRegistry[Object.keys(serviceRegistry)[service]]
|
|
} else {
|
|
service = serviceRegistry[service]
|
|
}
|
|
|
|
if (!service) {
|
|
console.error(`Service [${service}] not found`)
|
|
return false
|
|
}
|
|
|
|
if (selectedProcessInstance) {
|
|
detachInstanceStd(selectedProcessInstance.instance)
|
|
selectedProcessInstance = null
|
|
}
|
|
|
|
selectedProcessInstance = instancePool.find((instance) => instance.id === service.id)
|
|
|
|
if (!selectedProcessInstance) {
|
|
selectedProcessInstance = null
|
|
|
|
console.error(`Cannot find service [${service.id}] in the instances pool`)
|
|
|
|
return false
|
|
}
|
|
|
|
attachInstanceStd(selectedProcessInstance.instance)
|
|
|
|
return true
|
|
}
|
|
}
|
|
]
|
|
|
|
async function getIgnoredFiles(cwd) {
|
|
// git check-ignore -- *
|
|
let output = await new Promise((resolve, reject) => {
|
|
ChildProcess.exec("git check-ignore -- *", {
|
|
cwd: cwd
|
|
}, (err, stdout) => {
|
|
if (err) {
|
|
resolve(``)
|
|
}
|
|
|
|
resolve(stdout)
|
|
})
|
|
})
|
|
|
|
output = output.split("\n").map((file) => {
|
|
return `**/${file.trim()}`
|
|
})
|
|
|
|
output = output.filter((file) => {
|
|
return file
|
|
})
|
|
|
|
return output
|
|
}
|
|
|
|
async function handleAllReady() {
|
|
console.clear()
|
|
|
|
allReady = true
|
|
|
|
console.log(comtyAscii)
|
|
console.log(`🎉 All services[${services.length}] ready!\n`)
|
|
console.log(`USE: select <service>, reboot, exit`)
|
|
}
|
|
|
|
// SERVICE WATCHER FUNCTIONS
|
|
async function handleNewServiceStarting(id) {
|
|
if (serviceRegistry[id].ready === false) {
|
|
spinnies.add(id, {
|
|
text: `📦 [${id}] Loading service...`,
|
|
spinner: DefaultSpinner
|
|
})
|
|
}
|
|
}
|
|
|
|
async function handleServiceStarted(id) {
|
|
if (serviceRegistry[id].ready === false) {
|
|
if (spinnies.pick(id)) {
|
|
spinnies.succeed(id, { text: `[${id}][${serviceRegistry[id].index}] Ready` })
|
|
}
|
|
}
|
|
|
|
serviceRegistry[id].ready = true
|
|
}
|
|
|
|
async function handleServiceExit(id, code, err) {
|
|
//console.log(`🛑 Service ${id} exited with code ${code}`, err)
|
|
|
|
if (serviceRegistry[id].ready === false) {
|
|
if (spinnies.pick(id)) {
|
|
spinnies.fail(id, { text: `[${id}][${serviceRegistry[id].index}] Failed with code ${code}` })
|
|
}
|
|
}
|
|
|
|
serviceRegistry[id].ready = false
|
|
}
|
|
|
|
// PROCESS HANDLERS
|
|
async function handleProcessExit(error, code) {
|
|
if (error) {
|
|
console.error(error)
|
|
}
|
|
|
|
console.log(`\nPreparing to exit...`)
|
|
|
|
for await (let instance of instancePool) {
|
|
console.log(`🛑 Killing ${instance.id} [${instance.instance.pid}]`)
|
|
await instance.instance.kill()
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
async function handleIPCData(id, data) {
|
|
if (data.type === "log") {
|
|
console.log(`[${id}] ${data.message}`)
|
|
}
|
|
|
|
if (data.status === "ready") {
|
|
await handleServiceStarted(id)
|
|
}
|
|
}
|
|
|
|
function spawnService({ id, service, cwd }) {
|
|
handleNewServiceStarting(id)
|
|
|
|
const instanceEnv = {
|
|
...process.env,
|
|
lb_service: {
|
|
id: service.id,
|
|
index: service.index,
|
|
},
|
|
}
|
|
|
|
let instance = ChildProcess.fork(bootloaderBin, [service], {
|
|
detached: false,
|
|
silent: true,
|
|
cwd: cwd,
|
|
env: instanceEnv,
|
|
})
|
|
|
|
instance.reload = () => {
|
|
ipcRouter.unregister({ id, instance })
|
|
|
|
instance.kill()
|
|
|
|
instance = spawnService({ id, service, cwd })
|
|
|
|
const instanceIndex = instancePool.findIndex((_instance) => _instance.id === id)
|
|
|
|
if (instanceIndex !== -1) {
|
|
instancePool[instanceIndex].instance = instance
|
|
}
|
|
|
|
// check if selectedProcessInstance
|
|
if (selectedProcessInstance) {
|
|
detachInstanceStd(selectedProcessInstance.instance)
|
|
|
|
//if the selected process is this service, reattach std
|
|
if (selectedProcessInstance.id === id) {
|
|
attachInstanceStd(instance, {
|
|
afterMsg: "Reloading service...",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
instance.logs = {
|
|
stdout: createServiceLogTransformer({ id }),
|
|
stderr: createServiceLogTransformer({ id, color: "bgRed" }),
|
|
}
|
|
|
|
instance.logs.stdout.history = []
|
|
instance.logs.stderr.history = []
|
|
|
|
// push to buffer history
|
|
instance.stdout.pipe(instance.logs.stdout)
|
|
instance.stderr.pipe(instance.logs.stderr)
|
|
|
|
instance.on("message", (data) => {
|
|
return handleIPCData(id, data)
|
|
})
|
|
|
|
instance.on("close", (code, err) => {
|
|
return handleServiceExit(id, code, err)
|
|
})
|
|
|
|
ipcRouter.register({ id, instance })
|
|
|
|
return instance
|
|
}
|
|
|
|
function createServiceLogTransformer({ id, color = "bgCyan" }) {
|
|
return new Transform({
|
|
transform(data, encoding, callback) {
|
|
callback(null, `${chalk[color](`[${id}]`)} > ${data.toString()}`)
|
|
}
|
|
})
|
|
}
|
|
|
|
async function main() {
|
|
internalIp = await getInternalIp()
|
|
|
|
console.clear()
|
|
console.log(comtyAscii)
|
|
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
|
|
|
|
services = await scanServices()
|
|
|
|
if (services.length === 0) {
|
|
console.error("❌ No service found")
|
|
return process.exit(1)
|
|
}
|
|
|
|
console.log(`📦 Found ${services.length} service(s)`)
|
|
|
|
// create watchers
|
|
for await (let service of services) {
|
|
const instanceFile = path.basename(service)
|
|
const instanceBasePath = path.dirname(service)
|
|
|
|
const { name: id, version, proxy } = require(path.resolve(instanceBasePath, "package.json"))
|
|
|
|
serviceFileReference[instanceFile] = id
|
|
|
|
serviceRegistry[id] = {
|
|
index: services.indexOf(service),
|
|
id: id,
|
|
version: version,
|
|
file: instanceFile,
|
|
cwd: instanceBasePath,
|
|
proxy: proxy,
|
|
buffer: [],
|
|
ready: false,
|
|
}
|
|
}
|
|
|
|
// create a new process of node for each service
|
|
for await (let service of services) {
|
|
const { id, version, cwd } = serviceRegistry[serviceFileReference[path.basename(service)]]
|
|
|
|
const instance = spawnService({ id, service, cwd })
|
|
|
|
const serviceInstance = {
|
|
id,
|
|
version,
|
|
instance
|
|
}
|
|
|
|
// push to pool
|
|
instancePool.push(serviceInstance)
|
|
|
|
// if is NODE_ENV to development, start a file watcher for hot-reload
|
|
if (process.env.NODE_ENV === "development") {
|
|
const ignored = [
|
|
...await getIgnoredFiles(cwd),
|
|
"**/node_modules/**",
|
|
"**/dist/**",
|
|
"**/build/**",
|
|
]
|
|
|
|
chokidar.watch(cwd, {
|
|
ignored: ignored,
|
|
persistent: true,
|
|
ignoreInitial: true,
|
|
}).on("all", (event, path) => {
|
|
// find instance from pool
|
|
const instanceIndex = instancePool.findIndex((instance) => instance.id === id)
|
|
|
|
console.log(event, path, instanceIndex)
|
|
|
|
// reload
|
|
instancePool[instanceIndex].instance.reload()
|
|
})
|
|
}
|
|
}
|
|
|
|
// create repl
|
|
repl.start({
|
|
prompt: "> ",
|
|
useGlobal: true,
|
|
eval: (input, context, filename, callback) => {
|
|
let inputs = input.split(" ")
|
|
|
|
// remove last \n from input
|
|
inputs[inputs.length - 1] = inputs[inputs.length - 1].replace(/\n/g, "")
|
|
|
|
// find relp command
|
|
const command = inputs[0]
|
|
const args = inputs.slice(1)
|
|
|
|
const command_fn = relp_commands.find((relp_command) => {
|
|
let exising = false
|
|
|
|
if (Array.isArray(relp_command.aliases)) {
|
|
exising = relp_command.aliases.includes(command)
|
|
}
|
|
|
|
if (relp_command.cmd === command) {
|
|
exising = true
|
|
}
|
|
|
|
return exising
|
|
})
|
|
|
|
if (!command_fn) {
|
|
return callback(`Command not found: ${command}`)
|
|
}
|
|
|
|
return command_fn.fn(callback, ...args)
|
|
}
|
|
}).on("exit", () => {
|
|
process.exit(0)
|
|
})
|
|
}
|
|
|
|
process.on("exit", handleProcessExit)
|
|
process.on("SIGINT", handleProcessExit)
|
|
process.on("uncaughtException", handleProcessExit)
|
|
process.on("unhandledRejection", handleProcessExit)
|
|
|
|
main() |