Skip to content

Commit

Permalink
Merge pull request #56 from fluentci-io/feat/support-for-services
Browse files Browse the repository at this point in the history
feat: add commands for managing services (up, down, status, ps)
  • Loading branch information
tsirysndr authored Jul 23, 2024
2 parents 2b92602 + 6c47292 commit e124e11
Show file tree
Hide file tree
Showing 13 changed files with 439 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
jobs:
build:
name: release
runs-on: ubuntu-latest
runs-on: macos-latest
strategy:
matrix:
target:
Expand Down
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ Requirements:

**Latest (Desktop):**

- `Mac`: arm64: [fluentci-studio_v0.1.6_arm64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.6/fluentci-studio_v0.1.6_arm64.dmg) intel: [fluentci-studio_v0.1.6_x64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.6/fluentci-studio_v0.1.6_x64.dmg)
- `Linux`: [fluentci-studio_v0.1.6.AppImage](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.6/fluentci-studio_v0.1.6.AppImage)
- `Mac`: arm64: [fluentci-studio_v0.1.7_arm64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.7/fluentci-studio_v0.1.7_arm64.dmg) intel: [fluentci-studio_v0.1.7_x64.dmg](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.7/fluentci-studio_v0.1.7_x64.dmg)
- `Linux`: [fluentci-studio_v0.1.7.AppImage](https://github.com/fluentci-io/fluentci-studio/releases/download/v0.1.7/fluentci-studio_v0.1.7.AppImage)

**Latest (CLI):**

Expand Down Expand Up @@ -110,7 +110,7 @@ fluentci studio
fluentci --help

Usage: fluentci [pipeline] [jobs...]
Version: 0.15.2
Version: 0.15.3

Description:

Expand Down Expand Up @@ -148,8 +148,16 @@ Commands:
whoami - Show current logged in user
repl [pipelines...] - Start FluentCI REPL
studio - Start FluentCI Studio, a web-based user interface
project - Manage projects
server - Start FluentCI GraphQL Server
project - Manage projects
server - Start FluentCI GraphQL Server
up - Start services
down - Stop services
ps - List services
status <service> - Show status of a service
start <service> - Start a service
restart <service> - Restart a service
stop <service> - Stop a service
echo <service> - Stream logs of a service
```
## 📚 Documentation
Expand Down
2 changes: 2 additions & 0 deletions deps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as semver from "jsr:@std/semver@0.224.0";
export { semver };
import procfile from "npm:procfile";
export { procfile };
export {
bold,
brightGreen,
Expand Down
44 changes: 44 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import repl from "./src/cmd/repl.ts";
import studio from "./src/cmd/studio.ts";
import * as projects from "./src/cmd/project.ts";
import server from "./src/cmd/server.ts";
import down from "./src/cmd/down.ts";
import up from "./src/cmd/up.ts";
import listServices from "./src/cmd/ps.ts";
import status from "./src/cmd/status.ts";
import restart from "./src/cmd/restart.ts";
import stop from "./src/cmd/stop.ts";
import echo from "./src/cmd/echo.ts";

export async function main() {
Deno.env.set(
Expand Down Expand Up @@ -205,6 +212,43 @@ export async function main() {
.action(function (options) {
server(options);
})
.command("up", "Start services")
.action(async function () {
await up();
})
.command("down", "Stop services")
.action(async function () {
await down();
})
.command("ps", "List services")
.action(async function () {
await listServices();
})
.command("status", "Show status of a service")
.arguments("<service:string>")
.action(async function (_, service) {
await status(service);
})
.command("start", "Start a service")
.arguments("<service:string>")
.action(async function (_, service) {
await restart(service);
})
.command("restart", "Restart a service")
.arguments("<service:string>")
.action(async function (_, service) {
await restart(service);
})
.command("stop", "Stop a service")
.arguments("<service:string>")
.action(async function (_, service) {
await stop(service);
})
.command("echo", "Stream logs of a service")
.arguments("<service:string>")
.action(async function (_, service) {
await echo(service);
})
.globalOption("--check-update <checkUpdate:boolean>", "check for update", {
default: true,
})
Expand Down
34 changes: 34 additions & 0 deletions src/cmd/down.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { green, procfile } from "../../deps.ts";
import { getProcfiles } from "../utils.ts";

export default async function down() {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo stop | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { success } = await process.output();
if (!success) {
console.log(`Failed to stop ${green(service)}`);
continue;
}
console.log(`Successfully stopped ${green(service)}`);
}
}
}
41 changes: 41 additions & 0 deletions src/cmd/echo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { green, procfile } from "../../deps.ts";
import { getProcfiles } from "../utils.ts";

export default async function echo(name: string) {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
}
}

if (!infos[name]) {
console.log("Service not found in Procfile");
Deno.exit(1);
}

const socket = infos[name].socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo echo | nc -U ${socket}`],
stdout: "inherit",
stderr: "inherit",
});
const process = await command.spawn();
const { success } = await process.output();
if (!success) {
console.log(`Failed to stream logs for ${green(name)}`);
Deno.exit(1);
}
}
52 changes: 52 additions & 0 deletions src/cmd/ps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { procfile, Table } from "../../deps.ts";
import { getProcfiles, getServicePid } from "../utils.ts";

export default async function listServices() {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo status | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { stdout, success } = await process.output();
if (!success) {
infos[service].status = "Stopped";
continue;
}
const decoder = new TextDecoder();
infos[service].status = decoder.decode(stdout).includes("running")
? "Up"
: "Stopped";
}
}

services.sort();

const table = new Table();
table.header(["PROCESS", "PID", "STATUS", "COMMAND"]);
for (const service of services) {
const pid = await getServicePid(service, infos[service].socket);
table.push([
service,
pid,
infos[service].status,
infos[service].command + " " + infos[service].options.join(" "),
]);
}
table.render();
}
41 changes: 41 additions & 0 deletions src/cmd/restart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { green, procfile } from "../../deps.ts";
import { getProcfiles } from "../utils.ts";

export default async function restart(name: string) {
const files = await getProcfiles();
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
}
}

if (!infos[name]) {
console.log("Service not found in Procfile");
Deno.exit(1);
}

const socket = infos[name].socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo restart | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { success } = await process.output();
if (!success) {
console.log(`Failed to restart ${green(name)}`);
return;
}
console.log(`Successfully restarted ${green(name)}`);
}
83 changes: 83 additions & 0 deletions src/cmd/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { brightGreen, gray, bold, procfile, Table, Cell } from "../../deps.ts";
import { getServicePid } from "../utils.ts";

export default async function status(name: string) {
const command = new Deno.Command("bash", {
args: ["-c", "ls .fluentci/*/Procfile"],
stdout: "piped",
});
const process = await command.spawn();
const { stdout, success } = await process.output();
if (!success) {
console.log("No services running");
Deno.exit(0);
}
const decoder = new TextDecoder();
const files = decoder.decode(stdout).trim().split("\n");
const services = [];
// deno-lint-ignore no-explicit-any
let infos: Record<string, any> = {};

for (const file of files) {
const manifest = procfile.parse(Deno.readTextFileSync(file));
services.push(...Object.keys(manifest));
infos = {
...infos,
...manifest,
};

for (const service of Object.keys(manifest)) {
infos[service].procfile = file;
const socket = file.replace("Procfile", ".overmind.sock");
infos[service].socket = socket;
const command = new Deno.Command("sh", {
args: ["-c", `echo status | nc -U -w 1 ${socket}`],
stdout: "piped",
});
const process = await command.spawn();
const { stdout, success } = await process.output();
if (!success) {
infos[service].status = "Stopped";
continue;
}
const decoder = new TextDecoder();
infos[service].status = decoder.decode(stdout).includes("running")
? "Up"
: "Stopped";
}
}

if (!infos[name]) {
console.log("Service not found in Procfile");
Deno.exit(1);
}

const pid = await getServicePid(name, infos[name].socket);

console.log(
`${infos[name].status === "Up" ? brightGreen("●") : "○"} ${name}`
);

const table = new Table().body([
[
new Cell("Procfile:").align("right"),
`${infos[name].procfile}\n└─ ${gray(
infos[name].command + " " + infos[name].options.join(" ")
)}`,
],
[
new Cell("Active:").align("right"),
infos[name].status === "Up"
? bold(brightGreen("active (running)"))
: "inactive (dead)",
],
[new Cell("Socket:").align("right"), infos[name].socket],
[new Cell("Main PID:").align("right"), pid],
[
new Cell("WorkDir:").align("right"),
infos[name].socket.replace("/.overmind.sock", ""),
],
]);
table.render();
console.log("");
}
Loading

0 comments on commit e124e11

Please sign in to comment.