puppyperpetual 0.0.1-security → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1 +1,57 @@
1
- WIP, coming soon
1
+ # puppyperpetual
2
+
3
+ node script to run things... perpetually. adapted from [legacyshell](https://github.com/onlypuppy7/LegacyShell)
4
+
5
+ [On npm @ https://www.npmjs.com/package/puppyperpetual](https://www.npmjs.com/package/puppyperpetual)
6
+
7
+ [And GitHub @ https://github.com/onlypuppy7/perpetual](https://github.com/onlypuppy7/perpetual)
8
+
9
+ ## usage (as an npm package)
10
+
11
+ you can use it to run your node stuff perpetually:
12
+
13
+ ```js
14
+ import Perpetual from '../index.js';
15
+
16
+ const perpetual = new Perpetual('test_process', {
17
+ process_cmd: 'node ./src/test/test_process.js',
18
+ });
19
+ await perpetual.run();
20
+ ```
21
+
22
+ or anything really:
23
+
24
+ ```js
25
+ import Perpetual from '../index.js';
26
+
27
+ const perpetual = new Perpetual('test_process', {
28
+ process_cmd: 'cd . && git pull && date',
29
+ });
30
+ await perpetual.run();
31
+ ```
32
+
33
+ ## usage (as a node app thing)
34
+
35
+ as per tradition for my projects, simple installation!
36
+
37
+ 1. `npm i`
38
+
39
+ then run once to create config
40
+
41
+ - `npm run start`
42
+
43
+ then customise it in `store/config.yaml`, making a new entry for every thing
44
+
45
+ then run like this:
46
+
47
+ - `node .\perpetual.js --default`
48
+
49
+ alternatively, just pass in your command:
50
+
51
+ - `node .\perpetual.js "cd . && echo date"`
52
+
53
+ then you dont need to mess around with the horrible yaml. dont worry, i hate doing it too.
54
+
55
+ ## it doesnt work?
56
+
57
+ i dont care. i use this for my own stuff. i know that it works on linux and thats all i need.
package/package.json CHANGED
@@ -1,5 +1,27 @@
1
1
  {
2
- "name": "puppyperpetual",
3
- "description": "coming soon!",
4
- "version": "0.0.1-security"
5
- }
2
+ "name": "puppyperpetual",
3
+ "type": "module",
4
+ "version": "v1.0.1",
5
+ "description": "Run stuff FOREVER! PERPETUALLY!",
6
+ "main": "perpetual.js",
7
+ "keywords": [
8
+ "perpetual",
9
+ "process-manager",
10
+ "logger",
11
+ "automation",
12
+ "puppy"
13
+ ],
14
+ "author": "onlypuppy7",
15
+ "license": "MIT",
16
+ "scripts": {
17
+ "start": "node perpetual.js",
18
+ "test": "node src/test/test.js"
19
+ },
20
+ "dependencies": {
21
+ "chalk": "^5.3.0",
22
+ "js-yaml": "^4.1.0",
23
+ "puppylog": "^1.0.5",
24
+ "puppymisc": "^1.0.4",
25
+ "puppywebhook": "^1.0.1"
26
+ }
27
+ }
package/perpetual.js ADDED
@@ -0,0 +1,22 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { dirname } from 'node:path';
3
+ import Perpetual from './src/index.js';
4
+
5
+ let rootDir = import.meta.dirname;
6
+
7
+ if (!rootDir) {
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ rootDir = dirname(__filename);
10
+ console.log("(Using fallback mechanism for rootDir)");
11
+ };
12
+
13
+ console.log(process.argv, rootDir);
14
+
15
+ const serverName = process.argv[2].replace("--","");
16
+ const isDirect = !process.argv[2].startsWith("--");
17
+
18
+ const perpetual = new Perpetual(serverName, {
19
+ process_cmd: isDirect ? process.argv.slice(2).join(" ") : null,
20
+ rootDir,
21
+ });
22
+ await perpetual.run();
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+
5
+ export class ConfigManager {
6
+ constructor(rootDir, serverName) {
7
+ this.rootDir = rootDir;
8
+ this.serverName = serverName;
9
+
10
+ this.defaultConfigPath = path.join(rootDir, 'src', 'defaultconfig.yaml');
11
+ this.configPath = path.join(rootDir, 'store', 'config.yaml');
12
+
13
+ this.ensureConfigExists();
14
+ this.config = yaml.load(fs.readFileSync(this.configPath, 'utf8'));
15
+ }
16
+
17
+ ensureConfigExists() {
18
+ if (!fs.existsSync(this.configPath)) {
19
+ fs.mkdirSync(path.dirname(this.configPath), { recursive: true });
20
+ fs.copyFileSync(this.defaultConfigPath, this.configPath);
21
+ }
22
+ }
23
+
24
+ getServerOptions(options) {
25
+ let passed = options || {};
26
+ Object.apply(passed, this.config.servers?.[this.serverName] || {});
27
+
28
+ if (!passed.dir) {
29
+ if (passed.process_cmd.includes("cd ")) {
30
+ console.log("Using custom process command:", passed.process_cmd);
31
+ //detect via regex if cd is used
32
+ const cdMatch = passed.process_cmd.match(/cd\s+([^\s]+)\s*&&\s*(.*)/);
33
+ if (cdMatch) {
34
+ console.log("Detected 'cd' command in process_cmd:", cdMatch[1]);
35
+ passed.dir = cdMatch[1];
36
+ };
37
+ };
38
+ }
39
+
40
+ return {
41
+ //process
42
+ process_cmd: passed.process_cmd || "idk lol",
43
+ dir: passed.dir || "",
44
+ //daily restart
45
+ dailyrestart_enable: passed.dailyrestart_enable || false,
46
+ dailyrestart_time: passed.dailyrestart_time || "4:00",
47
+ dailyrestart_quickpull: passed.dailyrestart_quickpull,
48
+ //file logging
49
+ logfile_enable: passed.logfile_enable,
50
+ logfile_location: passed.logfile_location || path.join(this.rootDir, "store", "logs", this.serverName), //no editing kek
51
+ //webhook logging
52
+ webhook_url: passed.webhook_url || "", //false or empty is disabled
53
+ webhook_username: passed.webhook_username || "Webhook", //eg "LegacyShell: Client Server"
54
+ webhook_avatar: passed.webhook_avatar || "https://cdn.onlypuppy7.online/legacyshell/client.png", //eg "https://cdn.onlypuppy7.online/legacyshell/client.png"
55
+ webhook_ping_user: passed.webhook_ping_user || false, //this might flood your shit
56
+ webhook_ping_role: passed.webhook_ping_role || false, //this might flood EVERYONE'S shit
57
+ //pulling
58
+ is_puller: this.config.pullers?.includes(this.serverName) || false,
59
+ };
60
+ }
61
+ }
package/src/Logger.js ADDED
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import fetch from 'node-fetch';
3
+ import { getTimestamp, stripAnsi, divideString, ensureDirExists } from 'puppymisc';
4
+ import log from 'puppylog';
5
+
6
+ export class Logger {
7
+ constructor(options) {
8
+ this.options = options;
9
+ this.logQueue = [];
10
+ this.queuedChunks = [];
11
+ this.maxMessageLength = 1900;
12
+ this.messagesSent = 0;
13
+
14
+ ensureDirExists(options.logfile_location);
15
+ this.logFilePath = `${options.logfile_location}/${getTimestamp(true)}.log`;
16
+ ensureDirExists(this.logFilePath);
17
+ }
18
+
19
+ appendLog(msg, noSend = false) {
20
+ msg = stripAnsi(msg);
21
+ if (!noSend) this.logQueue.push(msg);
22
+ fs.appendFile(this.logFilePath, `${msg}\n`, () => {});
23
+ }
24
+
25
+ logSend(msg) {
26
+ const line = `#${getTimestamp()} ${msg}`;
27
+ log.muted(line);
28
+ this.appendLog(line);
29
+ }
30
+
31
+ logNoSend(msg) {
32
+ const line = `#${getTimestamp()} ${msg}`;
33
+ log.muted(line);
34
+ this.appendLog(line, true);
35
+ }
36
+
37
+ async sendToWebhook() {
38
+ while (this.logQueue.length) {
39
+ let msg = this.logQueue.shift();
40
+ if (msg.length > this.maxMessageLength) {
41
+ this.logQueue.unshift(...divideString(msg, this.maxMessageLength));
42
+ } else {
43
+ const lastChunk = this.queuedChunks[0] || "";
44
+ const newChunk = lastChunk + `\n${msg}`;
45
+ if (newChunk.length > this.maxMessageLength) {
46
+ this.queuedChunks.unshift(`\n${msg}`);
47
+ } else {
48
+ this.queuedChunks[0] = newChunk;
49
+ }
50
+ }
51
+ }
52
+
53
+ if (this.queuedChunks.length && this.options.webhook_url) {
54
+ try {
55
+ const response = await fetch(this.options.webhook_url, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({
59
+ username: this.options.webhook_username,
60
+ avatar_url: this.options.webhook_avatar,
61
+ content: this.queuedChunks[this.queuedChunks.length - 1].slice(0, 2000),
62
+ })
63
+ });
64
+ if (!response.ok) throw new Error(response.statusText);
65
+ this.logNoSend(`Logs sent to webhook (${this.messagesSent})`);
66
+ this.queuedChunks.pop();
67
+ this.messagesSent++;
68
+ } catch (err) {
69
+ this.logNoSend(`Error sending webhook: ${err.message}`);
70
+ }
71
+ }
72
+ }
73
+
74
+ startWebhookInterval(interval = 15000) {
75
+ this.webhookInterval = setInterval(() => this.sendToWebhook(), interval);
76
+ }
77
+ }
@@ -0,0 +1,193 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { Worker } from 'node:worker_threads';
5
+ import { getTimestamp } from 'puppymisc';
6
+
7
+ export class ProcessManager {
8
+ constructor(options, logger, rootDir) {
9
+ this.options = options;
10
+ this.logger = logger;
11
+ this.rootDir = rootDir;
12
+
13
+ this.runningProcess = null;
14
+ this.restartScheduled = false;
15
+ }
16
+
17
+ async startProcess(purposefulStop = false) {
18
+ if (this.runningProcess) {
19
+ this.logger.logSend(`Stopping previous process...`);
20
+ this.runningProcess.purposefulStop = purposefulStop;
21
+
22
+ if (this.runningProcess instanceof Worker) {
23
+ try {
24
+ await this.runningProcess.terminate();
25
+ } catch (err) {
26
+ this.logger.logSend(`Failed to stop previous process via .terminate: ${err.message} ${this.runningProcess}`);
27
+ }
28
+ } else {
29
+ try {
30
+ this.runningProcess.kill('SIGINT');
31
+ this.logger.logSend(`Stopped previous process via .kill: ${this.runningProcess.pid}`);
32
+ } catch (err) {
33
+ this.logger.logSend(`Failed to stop previous process via .kill: ${err.message} ${this.runningProcess}`);
34
+ }
35
+
36
+ try {
37
+ process.kill(-this.runningProcess.pid, 'SIGINT');
38
+ this.logger.logSend(`Stopped previous process via process.kill: ${this.runningProcess.pid}`);
39
+ } catch (err) {
40
+ this.logger.logSend(`Failed to stop previous process via process.kill: ${err.message} ${this.runningProcess}`);
41
+ }
42
+ }
43
+ } else {
44
+ this.logger.logSend(`Starting process: ${this.options.process_cmd}`);
45
+
46
+ const useWorkerThreads = true;
47
+ const isNodeScript = this.options.process_cmd.startsWith('node ');
48
+ let scriptPath = this.options.process_cmd;
49
+
50
+ if (isNodeScript && useWorkerThreads) {
51
+ // Use Worker Threads for Node.js scripts
52
+ scriptPath = scriptPath.slice(5).trim();
53
+ scriptPath = path.isAbsolute(scriptPath) ? scriptPath : path.join(this.rootDir, scriptPath);
54
+
55
+ if (!fs.existsSync(scriptPath)) {
56
+ this.logger.logSend(`Script file does not exist: ${scriptPath}`);
57
+ return;
58
+ }
59
+
60
+ this.logger.logSend(`Using Worker Threads for script: ${scriptPath}`);
61
+ this.runningProcess = new Worker(scriptPath, { workerData: {}, stdout: true, stderr: true });
62
+
63
+ this.runningProcess.stdout.on('data', (data) => {
64
+ const lines = data.toString().split('\n').filter(Boolean);
65
+ lines.forEach(line => {
66
+ const logLine = `${getTimestamp()} ${line}`;
67
+ process.stdout.write(`${logLine}\n`);
68
+ this.logger.appendLog(logLine);
69
+ });
70
+ });
71
+
72
+ this.runningProcess.stderr.on('data', (data) => {
73
+ const lines = data.toString().split('\n').filter(Boolean);
74
+ lines.forEach(line => {
75
+ const logLine = `${getTimestamp()} ERROR: ${line}`;
76
+ process.stderr.write(`${logLine}\n`);
77
+ this.logger.appendLog(logLine);
78
+ });
79
+ });
80
+
81
+ this.runningProcess.on('error', (err) => {
82
+ const logLine = `${getTimestamp()} ERROR: ${err.message}`;
83
+ process.stderr.write(`${logLine}\n`);
84
+ this.logger.appendLog(logLine);
85
+ });
86
+
87
+ this.runningProcess.on('exit', (code, signal) => {
88
+ code = code === 57 ? 1337 : code;
89
+ if (signal === 'SIGINT') {
90
+ this.logger.logSend(`Process terminated manually.`);
91
+ return;
92
+ }
93
+
94
+ const pingUser = this.options.webhook_ping_user ? ` <@${this.options.webhook_ping_user}>` : "";
95
+ const pingRole = this.options.webhook_ping_role ? ` <@&${this.options.webhook_ping_role}>` : "";
96
+
97
+ this.logger.logSend(`Process exited with code ${code}, signal ${signal}. ${(code === 1337 || this.runningProcess.purposefulStop) ? "No ping, intended restart" : `Restarting...${pingUser}${pingRole}`}`);
98
+
99
+ setTimeout(() => {
100
+ this.runningProcess = null;
101
+ this.startProcess();
102
+ }, (code === 1337 || this.runningProcess.purposefulStop) ? 1000 : 5000);
103
+ });
104
+
105
+ } else {
106
+ this.runningProcess = spawn('bash', ['-c', scriptPath], {
107
+ stdio: ['inherit', 'pipe', 'pipe'],
108
+ env: { ...process.env, FORCE_COLOR: 'true' },
109
+ detached: process.platform !== 'win32',
110
+ });
111
+
112
+ const handleOutput = (data, isError = false) => {
113
+ const lines = data.toString().split('\n').filter(Boolean);
114
+ lines.forEach(line => {
115
+ const logLine = `${getTimestamp()}${isError ? " ERROR:" : ""} ${line}`;
116
+ if (isError) process.stderr.write(`${logLine}\n`);
117
+ else process.stdout.write(`${logLine}\n`);
118
+ this.logger.appendLog(logLine);
119
+ });
120
+ };
121
+
122
+ this.runningProcess.stdout.on('data', d => handleOutput(d));
123
+ this.runningProcess.stderr.on('data', d => handleOutput(d, true));
124
+
125
+ const onExit = (code, signal) => {
126
+ code = code === 57 ? 1337 : code;
127
+ const pingUser = this.options.webhook_ping_user ? ` <@${this.options.webhook_ping_user}>` : "";
128
+ const pingRole = this.options.webhook_ping_role ? ` <@&${this.options.webhook_ping_role}>` : "";
129
+ this.logger.logSend(`Process exited with code ${code}. ${code === 1337 ? "No ping, intended restart" : `Restarting...${pingUser}${pingRole}`}`);
130
+ setTimeout(() => {
131
+ this.runningProcess = null;
132
+ this.startProcess();
133
+ }, (code === 1337 || signal === 'SIGINT') ? 1000 : 5000);
134
+ };
135
+
136
+ this.runningProcess.on('exit', (code, signal) => {
137
+ this.logger.logSend(`Process exited with code ${code}, signal ${signal}.`);
138
+ onExit(code, signal);
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ autoRestart() {
145
+ if (!this.options.dailyrestart_enable) return;
146
+ if (this.restartScheduled) {
147
+ this.logger.logSend("Restart already scheduled.");
148
+ return;
149
+ }
150
+
151
+ const now = new Date();
152
+ const [hour, minute] = this.options.dailyrestart_time.split(':').map(Number);
153
+ const nextRestart = new Date();
154
+ nextRestart.setHours(hour, minute, 0, 0);
155
+ if (nextRestart < now) nextRestart.setDate(nextRestart.getDate() + 1);
156
+
157
+ let timeUntilRestart = nextRestart - now;
158
+ if (timeUntilRestart <= 0) timeUntilRestart += 24*60*60*1000;
159
+
160
+ this.logger.logSend(`Scheduled restart in ${Math.floor(timeUntilRestart / 1000 / 60)} minutes.`);
161
+ this.restartScheduled = true;
162
+
163
+ setTimeout(async () => {
164
+ if (this.options.dailyrestart_quickpull) {
165
+ this.logger.logSend("Quick-pulling before restart.");
166
+ this.executeCommand('git', ['pull'], 'ignore');
167
+ }
168
+ this.logger.logSend("Auto-restarting process.");
169
+ await this.startProcess();
170
+ this.restartScheduled = false;
171
+ this.autoRestart();
172
+ }, timeUntilRestart);
173
+ }
174
+
175
+ executeCommand(command, args, stdio = "inherit") {
176
+ let dir = this.options.dir || "";
177
+
178
+ console.log(dir, this.options.dir);
179
+
180
+ const cmdProcess = spawn(command, args, {
181
+ stdio,
182
+ cwd: this.options.dir && this.options.dir !== "" && dir,
183
+ });
184
+
185
+ cmdProcess.on('exit', (code) => {
186
+ if (stdio !== 'ignore') console.log(`${command} exited with code: ${code}`);
187
+ });
188
+
189
+ cmdProcess.on('error', (err) => {
190
+ console.error(`Failed to start ${command}:`, err);
191
+ });
192
+ }
193
+ }
@@ -0,0 +1,23 @@
1
+ #which should be the ones to do pulls?
2
+ pullers: ["default"] #default: ["default"]
3
+
4
+ # config for how you want the files to continually run. setup for error notifications on discord!
5
+ # the options are the same for all, but you can configure each differently.
6
+ default: #example
7
+ process_cmd: "node src/test.js" #dont touch unless u changed the path
8
+ dir: ""
9
+
10
+ #daily restarts, at specified time every day
11
+ dailyrestart_enable: true
12
+ dailyrestart_time: "4:00" # HH:MM (24 hours)
13
+ dailyrestart_quickpull: false #if true, will pull before restarting
14
+
15
+ #logfiles
16
+ logfile_enable: true
17
+
18
+ #webhook logging
19
+ webhook_url: "" #empty disables it
20
+ webhook_username: "Test!"
21
+ webhook_avatar: "https://cdn.onlypuppy7.online/legacyshell/client.png"
22
+ webhook_ping_user: "514778439018872842" #ENTER THE USER ID. for when there is an error. empty = no ping.
23
+ webhook_ping_role: "1221557010651418705" #ENTER THE ROLE ID. for when there is an error. empty = no ping.
package/src/index.js ADDED
@@ -0,0 +1,47 @@
1
+ import path from 'node:path';
2
+ import { ConfigManager } from './ConfigManager.js';
3
+ import { Logger } from './Logger.js';
4
+ import { ProcessManager } from './ProcessManager.js';
5
+ import readline from 'node:readline';
6
+
7
+ export class Perpetual {
8
+ constructor(serverName, options) {
9
+ const rootDir = options.rootDir || path.resolve('./');
10
+ this.configManager = new ConfigManager(rootDir, serverName);
11
+ this.options = this.configManager.getServerOptions(options);
12
+ this.logger = new Logger(this.options);
13
+ this.processManager = new ProcessManager(this.options, this.logger, rootDir);
14
+ }
15
+
16
+ async run() {
17
+ if (this.options.webhook_url) this.logger.startWebhookInterval();
18
+ await this.processManager.startProcess();
19
+
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ prompt: '> '
24
+ });
25
+
26
+ rl.prompt();
27
+ rl.on('line', async (line) => {
28
+ let cmd = line.trim();
29
+ if (cmd === "r" || cmd === "restart") {
30
+ await this.processManager.startProcess(true);
31
+ } else if (cmd === "p" || cmd === "pull") {
32
+ this.processManager.executeCommand('git', ['pull']);
33
+ } else if (cmd === "pr") { // pull and restart
34
+ this.processManager.executeCommand('git', ['pull']);
35
+ setTimeout(() => {
36
+ this.processManager.startProcess(true);
37
+ }, 5e3);
38
+ };
39
+ rl.prompt();
40
+ }).on('close', () => {
41
+ console.log('Exiting interactive mode!');
42
+ process.exit(0);
43
+ });
44
+ }
45
+ }
46
+
47
+ export default Perpetual;
@@ -0,0 +1,7 @@
1
+ import Perpetual from '../index.js';
2
+
3
+ const perpetual = new Perpetual('test_process', {
4
+ // process_cmd: 'node ./src/test/test_process.js',
5
+ process_cmd: 'date',
6
+ })
7
+ await perpetual.run();
@@ -0,0 +1,10 @@
1
+ import log from 'puppylog';
2
+
3
+ (async () => {
4
+ let count = 0;
5
+ while (true) {
6
+ log.success("Hello, world!", count++);
7
+ if (count == 10) throw new Error("Test error to check error handling");
8
+ await new Promise(resolve => setTimeout(resolve, 1000));
9
+ };
10
+ })();
package/.gitattributes DELETED
@@ -1,2 +0,0 @@
1
- # Auto detect text files and perform LF normalization
2
- * text=auto