Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/config/Categories.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@
"Find / Replace",
"Regular expression",
"Fuzzy Match",
"Generate Systemd Unit",
"Offset checker",
"Hamming Distance",
"Levenshtein Distance",
Expand Down
126 changes: 126 additions & 0 deletions src/core/operations/GenerateSystemdUnit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* @author 0xff1ce [github.com/0xff1ce]
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/

import Operation from "../Operation.mjs";

const RESTART_OPTIONS = ["no", "on-sucess", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"];
const TYPE_OPTIONS = ["simple", "exec", "forking", "oneshot", "dbus", "notify", "idle"];

/**
* GenerateSystemdUnit operation
*/
class GenerateSystemdUnit extends Operation {

/**
* GenerateSystemdUnit constructor
*/
constructor() {
super();

this.name = "Generate Systemd Unit";
this.module = "Default";
this.description = "Generates a systemd unit file based on provided inputs";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
"name": "Description",
"type": "string",
"value": "",
},
{
"name": "After",
"type": "string",
"value": "network.target",
},
{
"name": "Wants",
"type": "string",
"value": "network-online.target",
},
{
"name": "Restart",
"type": "option",
"value": RESTART_OPTIONS,
},
{
"name": "Type",
"type": "option",
"value": TYPE_OPTIONS,
},
{
"name": "ExecStart",
"type": "string",
"value": "",
},
{
"name": "Environment",
"type": "string",
"value": "KEY=VALUE",
},
{
"name": "User",
"type": "string",
"value": "",
},
{
"name": "WorkingDirectory",
"type": "string",
"value": "",
},
{
"name": "WantedBy",
"type": "string",
"value": "multi-user.target",
},
];
}

/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(_, args) {
const description = args[0];
const after = args[1];
const wants = args[2];
const restart = args[3];
const type = args[4];
const execStart = args[5];
const environment = args[6];
const user = args[7];
const workingDirectory = args[8];
const wantedBy = args[9];

const lines = [];

// [Unit] section
lines.push("[Unit]");
if (description) lines.push(`Description=${description}`);
if (after) lines.push(`After=${after}`);
if (wants) lines.push(`Wants=${wants}`);

// [Service] section
lines.push("");
lines.push("[Service]");
if (restart) lines.push(`Restart=${restart}`);
if (type) lines.push(`Type=${type}`);
if (execStart) lines.push(`ExecStart=${execStart}`);
if (environment) lines.push(`Environment=${environment}`);
if (user) lines.push(`User=${user}`);
if (workingDirectory) lines.push(`WorkingDirectory=${workingDirectory}`);

// [Install] section
lines.push("");
lines.push("[Install]");
if (wantedBy) lines.push(`WantedBy=${wantedBy}`);

return lines.join("\n");
}
}

export default GenerateSystemdUnit;
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import "./tests/GenerateAllChecksums.mjs";
import "./tests/GenerateAllHashes.mjs";
import "./tests/GenerateDeBruijnSequence.mjs";
import "./tests/GenerateQRCode.mjs";
import "./tests/GenerateSystemdUnit.mjs";
import "./tests/GetAllCasings.mjs";
import "./tests/GOST.mjs";
import "./tests/Gunzip.mjs";
Expand Down
100 changes: 100 additions & 0 deletions tests/operations/tests/GenerateSystemdUnit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* GenerateSystemdUnit tests.
*
* @author 0xff1ce
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/

import TestRegister from "../../lib/TestRegister.mjs";

TestRegister.addTests([
{
name: "Generate minimal systemd unit with required defaults",
input: "",
expectedMatch: /\[Unit\]\nAfter=network\.target\nWants=network-online\.target\n\n\[Service\]\nEnvironment=KEY=VALUE\n\n\[Install\]\nWantedBy=multi-user\.target/,
recipeConfig: [
{
op: "Generate Systemd Unit",
args: [
"", // Description
"network.target", // After
"network-online.target", // Wants
"", // Restart
"", // Type
"", // ExecStart
"KEY=VALUE", // Environment
"", // User
"", // WorkingDirectory
"multi-user.target" // WantedBy
]
},
],
},
{
name: "Generate full systemd unit with all fields populated",
input: "",
expectedMatch: /\[Unit\]\nDescription=My Service\nAfter=network\.target\nWants=network-online\.target\n\n\[Service\]\nRestart=always\nType=simple\nExecStart=\/usr\/bin\/node app\.js\nEnvironment=NODE_ENV=production\nUser=www-data\nWorkingDirectory=\/var\/www\n\n\[Install\]\nWantedBy=multi-user\.target/,
recipeConfig: [
{
op: "Generate Systemd Unit",
args: [
"My Service",
"network.target",
"network-online.target",
"always",
"simple",
"/usr/bin/node app.js",
"NODE_ENV=production",
"www-data",
"/var/www",
"multi-user.target"
]
},
],
},
{
name: "Generate unit without optional Service fields",
input: "",
expectedMatch: /\[Unit\]\nDescription=Test\nAfter=network\.target\nWants=network-online\.target\n\n\[Service\]\nEnvironment=KEY=VALUE\n\n\[Install\]\nWantedBy=multi-user\.target/,
recipeConfig: [
{
op: "Generate Systemd Unit",
args: [
"Test",
"network.target",
"network-online.target",
"", // Restart
"", // Type
"", // ExecStart
"KEY=VALUE",
"", // User
"", // WorkingDirectory
"multi-user.target"
]
},
],
},
{
name: "Generate unit with custom dependencies and install target",
input: "",
expectedMatch: /\[Unit\]\nDescription=Custom\nAfter=docker\.service\nWants=network\.target\n\n\[Service\]\nRestart=on-failure\nType=exec\nExecStart=\/bin\/bash start\.sh\nEnvironment=ENV=dev\n\n\[Install\]\nWantedBy=graphical\.target/,
recipeConfig: [
{
op: "Generate Systemd Unit",
args: [
"Custom",
"docker.service",
"network.target",
"on-failure",
"exec",
"/bin/bash start.sh",
"ENV=dev",
"", // User
"", // WorkingDirectory
"graphical.target"
]
},
],
}
]);