Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed Mar 30, 2024
1 parent e9f5bad commit 9b5c561
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 91 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# @cross/service

Service is a JavaScript/TypeScript module for managing system services. It offers a convenient way to install, uninstall, and generate service configurations for various service managers.
Service is a JavaScript/TypeScript module for managing system services. It offers a convenient way to install, uninstall, and generate service configurations for various service managers. It can be
used as either a command line tool, or a typescript library for direct integration in other programs.

Part of the @cross suite - check out our growing collection of cross-runtime tools at [github.com/cross-org](https://github.com/cross-org).

## Features

- Easy-to-use CLI for managing services
- Library usage through `mod.ts` for custom integrations
- Library usage for custom integrations
- Install, uninstall, and generate service configurations
- Compatible with systemd, sysvinit, docker-init, upstart (Linux), launchd (macOS) and SCM (Windows) service managers
- Installs any script as service on any system.
Expand All @@ -17,11 +18,10 @@ Part of the @cross suite - check out our growing collection of cross-runtime too
To use Service as a CLI program, you can install or upgrade it using Deno:

```sh
deno install -frA --name cross-service jsr:@cross/[email protected].1/install
deno install -frA --name cross-service jsr:@cross/[email protected].2/install
```

For library usage in Node, Deno or Bun - install according to the instructions at [jsr.io/@cross/service](https://jsr.io/@cross/service) and simply import the `installService()` function from the
`mod.ts` file:
For library usage in Node, Deno or Bun - install according to the instructions at [jsr.io/@cross/service](https://jsr.io/@cross/service) and simply import the `installService()` function from the:

```ts
import { installService } from "@cross/service";
Expand Down
6 changes: 3 additions & 3 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
},
"imports": {
"@cross/env": "jsr:@cross/env@^1.0.0",
"@cross/fs": "jsr:@cross/fs@^0.0.4",
"@cross/fs": "jsr:@cross/fs@^0.0.7",
"@cross/runtime": "jsr:@cross/runtime@^1.0.0",
"@cross/test": "jsr:@cross/test@^0.0.9",
"@cross/utils": "jsr:@cross/utils@^0.8.2",
"@std/assert": "jsr:@std/assert@^0.220.1",
"@std/path": "jsr:@std/path@^0.220.1"
"@std/assert": "jsr:@std/assert@^0.221.0",
"@std/path": "jsr:@std/path@^0.221.0"
},
"publish": {
"exclude": [".github", "*.test.ts"]
Expand Down
20 changes: 18 additions & 2 deletions lib/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,17 @@ async function main(inputArgs: string[]) {
*/
if (baseArgument === "install" || baseArgument === "generate") {
try {
await installService({ system, name, cmd, cwd, user, home, path, env }, baseArgument === "generate", force);
const result = await installService({ system, name, cmd, cwd, user, home, path, env }, baseArgument === "generate", force);
if (baseArgument === "generate") {
console.log(result.serviceFileContent);
} else {
if (result.manualSteps) {
console.log("To complete the installation, carry out these manual steps:");
console.log(result.manualSteps);
} else {
console.log(`Service ´${name}' successfully installed at '${result.servicePath}'.`);
}
}
exit(0);
} catch (e) {
console.error(`Could not install service, error: ${e.message}`);
Expand All @@ -75,7 +85,13 @@ async function main(inputArgs: string[]) {
*/
} else if (baseArgument === "uninstall") {
try {
await uninstallService({ system, name, home });
const result = await uninstallService({ system, name, home });
if (result.manualSteps) {
console.log(`Carry out these manual steps to complete the unistallation of '${name}'`);
console.log(result.manualSteps);
} else {
console.log(`Service '${name}' at '${result.servicePath}' is now uninstalled.`);
}
exit(0);
} catch (e) {
console.error(`Could not install service, error: ${e.message}`);
Expand Down
61 changes: 40 additions & 21 deletions lib/managers/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import { exists, mktempdir, writeFile } from "@cross/fs";
import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts";
import { getEnv } from "@cross/env";
import { join } from "@std/path";
import { ServiceInstallResult, ServiceUninstallResult } from "../result.ts";
import { ServiceManualStep } from "../result.ts";

const initScriptTemplate = `#!/bin/sh
### BEGIN INIT INFO
Expand Down Expand Up @@ -68,8 +68,9 @@ class InitService {
* @returns {string} - The configuration file content.
*/
generateConfig(config: InstallServiceOptions): string {
const denoPath = Deno.execPath();
const command = config.cmd;
const servicePath = `${config.path?.join(":")}:${getEnv("PATH")}`;
const servicePath = `${config.path?.join(":")}:${denoPath}:${config.home}/.deno/bin`;

let initScriptContent = initScriptTemplate.replace(/{{name}}/g, config.name);
initScriptContent = initScriptContent.replace("{{command}}", command);
Expand Down Expand Up @@ -109,16 +110,26 @@ class InitService {
const tempFilePathDir = await mktempdir("svcinstall");
const tempFilePath = join(tempFilePathDir, "svc-init");
await writeFile(tempFilePath, initScriptContent);
let manualSteps = "";
manualSteps += "\nThe service installer does not have (and should not have) root permissions, so the next steps have to be carried out manually.";
manualSteps += `\nStep 1: The init script has been saved to a temporary file, copy this file to the correct location using the following command:`;
manualSteps += `\n sudo cp ${tempFilePath} ${initScriptPath}`;
manualSteps += `\nStep 2: Make the script executable:`;
manualSteps += `\n sudo chmod +x ${initScriptPath}`;
manualSteps += `\nStep 3: Enable the service to start at boot:`;
manualSteps += `\n sudo update-rc.d ${config.name} defaults`;
manualSteps += `\nStep 4: Start the service now`;
manualSteps += `\n sudo service ${config.name} start`;
const manualSteps: ServiceManualStep[] = [];
manualSteps.push({
text: "The service installer does not have (and should not have) root permissions, so the next steps have to be carried out manually.",
});
manualSteps.push({
text: "Step 1: The init script has been saved to a temporary file, copy this file to the correct location using the following command:",
command: `sudo cp ${tempFilePath} ${initScriptPath}`,
});
manualSteps.push({
text: "Step 2: Make the script executable:",
command: `sudo chmod +x ${initScriptPath}`,
});
manualSteps.push({
text: "Step 3: Enable the service to start at boot:",
command: `sudo update-rc.d ${config.name} defaults`,
});
manualSteps.push({
text: "Step 4: Start the service now:",
command: `sudo service ${config.name} start`,
});
return {
servicePath: tempFilePath,
serviceFileContent: initScriptContent,
Expand All @@ -134,15 +145,23 @@ class InitService {
throw new Error(`Service '${config.name}' does not exist in '${initScriptPath}'.`);
}

let manualSteps = "";
manualSteps += "The uninstaller does not have (and should not have) root permissions, so the next steps have to be carried out manually.";
manualSteps += `\nStep 1: Stop the service (if it's running):`;
manualSteps += `\n sudo service ${config.name} stop`;
manualSteps += `\nStep 2: Disable the service from starting at boot:`;
manualSteps += `\n sudo update-rc.d -f ${config.name} remove`;
manualSteps += `\nStep 3: Remove the init script:`;
manualSteps += `\n sudo rm ${initScriptPath}`;

const manualSteps: ServiceManualStep[] = [
{
text: "The uninstaller does not have (and should not have) root permissions, so the next steps have to be carried out manually.",
},
{
text: "Step 1: Stop the service (if it's running):",
command: `sudo service ${config.name} stop`,
},
{
text: "Step 2: Disable the service from starting at boot:",
command: `sudo update-rc.d -f ${config.name} remove`,
},
{
text: `Step 3: Remove the init script:`,
command: `sudo rm ${initScriptPath}`,
},
];
return {
servicePath: initScriptPath,
manualSteps,
Expand Down
36 changes: 22 additions & 14 deletions lib/managers/launchd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { exists, mkdir, unlink, writeFile } from "@cross/fs";
import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts";
import { dirname } from "@std/path";
import { cwd } from "@cross/utils";
import { getEnv } from "@cross/env";
import { ServiceInstallResult, ServiceUninstallResult } from "../result.ts";
import { ServiceInstallResult, ServiceManualStep, ServiceUninstallResult } from "../result.ts";

const plistTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
Expand Down Expand Up @@ -41,8 +40,9 @@ class LaunchdService {
* @returns {string} The generated Launchd plist configuration file content as a string.
*/
generateConfig(options: InstallServiceOptions): string {
const denoPath = Deno.execPath();
const commandArgs = options.cmd.split(" ");
const servicePath = `${options.path?.join(":")}:${getEnv("PATH")}`;
const servicePath = `${options.path?.join(":")}:${denoPath}:${options.home}/.deno/bin`;
const workingDirectory = options.cwd ? options.cwd : cwd();

let plistContent = plistTemplate.replace(/{{name}}/g, options.name);
Expand Down Expand Up @@ -99,15 +99,19 @@ class LaunchdService {
await writeFile(plistPath, plistContent);

// ToDo: Actually run the service and verify that it works, if not - use the rollback function
let manualSteps = "";
// Manual Step Generation
const manualSteps: ServiceManualStep[] = [];
if (config.system) {
manualSteps += "Please run the following command as root to load the service:";
manualSteps += `sudo launchctl load ${plistPath}`;
manualSteps.push({
text: "Please run the following command as root to load the service:",
command: `sudo launchctl load ${plistPath}`,
});
} else {
manualSteps += "Please run the following command to load the service:";
manualSteps += `launchctl load ${plistPath}`;
manualSteps.push({
text: "Please run the following command to load the service:",
command: `launchctl load ${plistPath}`,
});
}

return {
servicePath: plistPath,
serviceFileContent: plistContent,
Expand Down Expand Up @@ -152,13 +156,17 @@ class LaunchdService {
await unlink(plistPath);

// Unload the service
let manualSteps = "";
const manualSteps: ServiceManualStep[] = [];
if (config.system) {
manualSteps += "Please run the following command as root to unload the service (if it's running):";
manualSteps += `sudo launchctl unload ${plistPath}`;
manualSteps.push({
text: "Please run the following command as root to unload the service (if it's running):",
command: `sudo launchctl unload ${plistPath}`,
});
} else {
manualSteps += "Please run the following command to unload the service (if it's running):";
manualSteps += `launchctl unload ${plistPath}`;
manualSteps.push({
text: "Please run the following command to unload the service (if it's running):",
command: `launchctl unload ${plistPath}`,
});
}

return {
Expand Down
54 changes: 30 additions & 24 deletions lib/managers/systemd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { exists, mkdir, mktempdir, unlink, writeFile } from "@cross/fs";
import { InstallServiceOptions, UninstallServiceOptions } from "../service.ts";
import { dirname, join } from "@std/path";
import { cwd, spawn } from "@cross/utils";
import { getEnv } from "@cross/env";
import { ServiceInstallResult, ServiceUninstallResult } from "../result.ts";
import { ServiceInstallResult, ServiceManualStep, ServiceUninstallResult } from "../result.ts";

const serviceFileTemplate = `[Unit]
Description={{name}} (Deno Service)
Expand Down Expand Up @@ -76,16 +75,23 @@ class SystemdService {
// Store temporary file
const tempFilePath = await mktempdir("svcinstall");
await writeFile(join(tempFilePath, "cfg"), serviceFileContent);
let manualSteps = "";
manualSteps += "\Service installer do not have (and should not have) root permissions, so the next steps have to be carried out manually.";
manualSteps += `\nStep 1: The systemd configuration has been saved to a temporary file, copy this file to the correct location using the following command:`;
manualSteps += `\n sudo cp ${tempFilePath} ${servicePath}`;
manualSteps += `\nStep 2: Reload systemd configuration`;
manualSteps += `\n sudo systemctl daemon-reload`;
manualSteps += `\nStep 3: Enable the service`;
manualSteps += `\n sudo systemctl enable ${config.name}`;
manualSteps += `\nStep 4: Start the service now`;
manualSteps += `\n sudo systemctl start ${config.name}\n`;
const manualSteps: ServiceManualStep[] = [];
manualSteps.push({
text: "The systemd configuration has been saved to a temporary file. Copy this file to the correct location using the following command:",
command: `sudo cp ${tempFilePath} ${servicePath}`,
});
manualSteps.push({
text: "Reload the systemd configuration:",
command: `sudo systemctl daemon-reload`,
});
manualSteps.push({
text: "Enable the service:",
command: `sudo systemctl enable ${config.name}`,
});
manualSteps.push({
text: "Start the service now:",
command: `sudo systemctl start ${config.name}`,
});
return {
servicePath: tempFilePath,
serviceFileContent,
Expand Down Expand Up @@ -150,17 +156,16 @@ class SystemdService {

try {
await unlink(servicePath);
if (config.system) {
return {
servicePath,
manualSteps: "Please run the following command as root to reload the systemctl daemon:\nsudo systemctl daemon-reload",
};
} else {
return {
servicePath,
manualSteps: "Please run the following command as root to reload the systemctl daemon:\nsudo systemctl --user daemon-reload",
};
}
const manualSteps: ServiceManualStep[] = [];
const reloadCommand = config.system ? "sudo systemctl daemon-reload" : "systemctl --user daemon-reload";
manualSteps.push({
text: `Please run the following command to reload the systemctl daemon:`,
command: reloadCommand,
});
return {
servicePath,
manualSteps: manualSteps,
};
} catch (error) {
throw new Error(`Failed to uninstall service: Could not remove '${servicePath}'. Error: '${error.message}'`);
}
Expand All @@ -173,7 +178,8 @@ class SystemdService {
* @returns {string} The generated systemd service configuration file content as a string.
*/
generateConfig(options: InstallServiceOptions): string {
const defaultPath = `PATH=${getEnv("PATH")}`;
const denoPath = Deno.execPath();
const defaultPath = `PATH=${denoPath}:${options.home}/.deno/bin`;
const envPath = options.path ? `${defaultPath}:${options.path.join(":")}` : defaultPath;
const workingDirectory = options.cwd ? options.cwd : cwd();

Expand Down
Loading

0 comments on commit 9b5c561

Please sign in to comment.