Wizulus Redikulus 1 år sedan
incheckning
d4b1c9b99a
7 ändrade filer med 893 tillägg och 0 borttagningar
  1. 175 0
      .gitignore
  2. 15 0
      README.md
  3. BIN
      bun.lockb
  4. 17 0
      main.ts
  5. 17 0
      package.json
  6. 642 0
      service.ts
  7. 27 0
      tsconfig.json

+ 175 - 0
.gitignore

@@ -0,0 +1,175 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+# mdns-ssh
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run main.ts
+```
+
+This project was created using `bun init` in bun v1.1.34. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

BIN
bun.lockb


+ 17 - 0
main.ts

@@ -0,0 +1,17 @@
+import service from './service';
+
+service({
+  name: 'my-service',
+  description: 'This is my service',
+  runner ({logger}) {
+    logger.log('Running my service');
+    const timer = setInterval(() => {
+      logger.log('Still running');
+    }, 5000);
+
+    return () => {
+      clearInterval(timer);
+      logger.log('Stopped running');
+    }
+  }
+})

+ 17 - 0
package.json

@@ -0,0 +1,17 @@
+{
+  "name": "service",
+  "module": "main.ts",
+  "devDependencies": {
+    "@types/bun": "latest"
+  },
+  "peerDependencies": {
+    "typescript": "^5.0.0"
+  },
+  "scripts": {
+    "build-linux-x64": "bun build --compile --target=bun-linux-x64-baseline ./main.ts --outfile dist/service",
+    "build-linux-arm64": "bun build --compile --target=bun-linux-arm64 ./main.ts --outfile dist/service-arm64",
+    "build-windows-x64": "bun build --compile --target=bun-windows-x64 ./main.ts --outfile dist/service.exe",
+    "build": "npm run build-linux-x64 && npm run build-linux-arm64 && npm run build-windows-x64"
+  },
+  "type": "module"
+}

+ 642 - 0
service.ts

@@ -0,0 +1,642 @@
+#!/usr/bin/env bun
+
+import { execSync, spawn } from 'child_process';
+import * as fs from 'fs';
+import { dirname, basename, join, resolve } from 'path';
+import * as zlib from 'zlib';
+import * as readline from 'readline';
+import type { Readable } from 'stream';
+
+type RunnerFunction = (options: RunnerOptions) => (() => void) | void;
+
+type Logger = {
+  log: (...args: any[]) => void;
+  trace: (...args: any[]) => void;
+  debug: (...args: any[]) => void;
+  info: (...args: any[]) => void;
+  warn: (...args: any[]) => void;
+  error: (...args: any[]) => void;
+};
+
+type RunnerOptions = {
+  logger: Logger;
+};
+
+type ServiceOptions = {
+  name: string;
+  description: string;
+  runner: RunnerFunction;
+};
+
+export default function service(options: ServiceOptions) {
+  const serviceName = options.name;
+  const serviceDescription = options.description;
+  const runner = options.runner;
+
+  // Paths
+  const lockFile = `/tmp/${serviceName}.pid`;
+  const logDir = `/tmp/`;
+  const logFile = join(logDir, `${serviceName}.log`);
+  const maxLogSize = 5 * 1024 * 1024; // 5 MB per log file
+  const maxTotalLogSize = 20 * 1024 * 1024; // 20 MB total for all logs
+  const serviceConfig = `/etc/systemd/system/${serviceName}.service`;
+
+  // Get the absolute path to the current executable
+  const selfFile = resolve(process.argv0);
+  const selfDir = dirname(selfFile);
+  const selfFilename = basename(selfFile);
+
+  // Function to check if systemd is available
+  function hasSystemd() {
+    try {
+      execSync('which systemctl', { stdio: 'ignore' });
+      return fs.existsSync('/run/systemd/system');
+    } catch {
+      return false;
+    }
+  }
+
+  // Function to check if the systemd service file exists
+  function hasSystemdService() {
+    return fs.existsSync(serviceConfig);
+  }
+
+  // Log stream variable
+  let logStream: fs.WriteStream | null = null;
+
+  // Function to create the logger
+  function createLogger(): Logger {
+    // Determine if running under systemd
+    const useSystemdLogging = hasSystemdService();
+
+    // If not using systemd, set up log file
+    if (!useSystemdLogging) {
+      // Open the log file for appending
+      logStream = fs.createWriteStream(logFile, { flags: 'a' });
+    }
+
+    // Function to write logs and handle rotation
+    function writeLog(message: string, severity: string = 'INFO') {
+      const formattedMessage = useSystemdLogging
+        // Systemd includes its own timestamp
+        ? `[${severity}] ${message}\n`
+        // Custom timestamp
+        : `${new Date().toISOString()} [${severity}] ${message}\n`;
+
+      if (useSystemdLogging) {
+        // Write to stdout or stderr based on severity
+        if (severity === 'ERROR' || severity === 'WARN') {
+          process.stderr.write(formattedMessage);
+        } else {
+          process.stdout.write(formattedMessage);
+        }
+      } else {
+        rotateLogs();
+        if (logStream) {
+          try {
+            logStream.write(formattedMessage);
+          } catch {
+            // If writing fails, fallback to stdout/stderr
+            process.stdout.write(formattedMessage);
+          }
+        } else {
+          // If logStream is closed, write to stdout/stderr
+          process.stdout.write(formattedMessage);
+        }
+      }
+    }
+
+    // Return the logger object
+    const logger: Logger = {
+      log: (...args: any[]) => {
+        writeLog(args.join(' '), 'INFO');
+      },
+      trace: (...args: any[]) => {
+        writeLog(args.join(' '), 'TRACE');
+      },
+      debug: (...args: any[]) => {
+        writeLog(args.join(' '), 'DEBUG');
+      },
+      info: (...args: any[]) => {
+        writeLog(args.join(' '), 'INFO');
+      },
+      warn: (...args: any[]) => {
+        writeLog(args.join(' '), 'WARN');
+      },
+      error: (...args: any[]) => {
+        writeLog(args.join(' '), 'ERROR');
+      },
+    };
+
+    return logger;
+  }
+
+  // Function to close the logger
+  function closeLogger() {
+    if (logStream) {
+      logStream.end();
+      logStream = null;
+    }
+  }
+
+  // Function to rotate logs
+  function rotateLogs() {
+    if (fs.existsSync(logFile)) {
+      const stats = fs.statSync(logFile);
+      if (stats.size >= maxLogSize) {
+        // Close the existing log stream before renaming
+        if (logStream) {
+          logStream.end(); // Close the stream
+          logStream = null;
+        }
+
+        // Determine the rotated log file name
+        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
+        const rotatedLogFile = `${logFile}.${timestamp}`;
+
+        // Rename the current log file
+        fs.renameSync(logFile, rotatedLogFile);
+
+        // Compress the rotated log file
+        try {
+          const gzip = zlib.createGzip();
+          const source = fs.createReadStream(rotatedLogFile);
+          const destination = fs.createWriteStream(`${rotatedLogFile}.gz`);
+
+          source.pipe(gzip).pipe(destination);
+
+          source.on('end', () => {
+            fs.unlinkSync(rotatedLogFile);
+          });
+        } catch (error) {
+          console.error('Error compressing log file:', error);
+        }
+
+        // Manage total log size
+        manageTotalLogSize();
+
+        // Re-open the log stream
+        logStream = fs.createWriteStream(logFile, { flags: 'a' });
+      }
+    }
+  }
+
+  // Function to manage total log size
+  function manageTotalLogSize() {
+    try {
+      const files = fs.readdirSync(logDir);
+      const logFiles = files
+        .filter((file) => file.startsWith(`${serviceName}.log`))
+        .map((file) => {
+          const filePath = join(logDir, file);
+          const stats = fs.lstatSync(filePath);
+          return {
+            name: file,
+            path: filePath,
+            time: stats.mtime.getTime(),
+            size: stats.size,
+          };
+        });
+
+      logFiles.sort((a, b) => b.time - a.time); // Sort by most recent
+
+      // Calculate total size
+      let totalSize = logFiles.reduce((acc, file) => acc + file.size, 0);
+
+      // Delete oldest logs until total size is under maxTotalLogSize
+      for (let i = logFiles.length - 1; i >= 0 && totalSize > maxTotalLogSize; i--) {
+        try {
+          fs.unlinkSync(logFiles[i].path);
+          totalSize -= logFiles[i].size;
+        } catch (error) {
+          console.error('Error deleting log file:', error);
+        }
+      }
+    } catch (err) {
+      console.error('Error managing total log size:', err);
+    }
+  }
+
+  // Daemon function
+  function daemon() {
+    // Create the logger
+    const logger = createLogger();
+
+    logger.info('Daemon started...');
+
+    // Run the user-provided runner function
+    let cleanupFunction: (() => void) | void;
+    try {
+      cleanupFunction = runner({ logger });
+    } catch (err) {
+      logger.error('Error in runner function:', err);
+      process.exit(1);
+    }
+
+    // Signal handling
+    function cleanUp() {
+      logger.info('Daemon is stopping...');
+      try {
+        if (cleanupFunction) {
+          cleanupFunction();
+        }
+      } catch (err) {
+        logger.error('Error in cleanup function:', err);
+      }
+
+      closeLogger();
+
+      try {
+        fs.unlinkSync(lockFile);
+      } catch {
+        // Ignore errors
+      }
+
+      process.exit(0);
+    }
+
+    process.on('SIGTERM', cleanUp);
+    process.on('SIGINT', cleanUp);
+    process.on('SIGQUIT', cleanUp);
+    process.on('uncaughtException', (err) => {
+      logger.error('Uncaught exception:', err);
+      cleanUp();
+    });
+
+    // Keep the process running
+    process.stdin.resume();
+  }
+
+  // Install function
+  function install() {
+    console.log(`Installing ${serviceName}...`);
+    if (!hasSystemd()) {
+      console.error('Systemd is not available on this system.');
+      process.exit(1);
+    }
+
+    const serviceConfigContent = `
+[Unit]
+Description=${serviceDescription}
+After=network.target
+
+[Service]
+ExecStart=${selfFile} daemon
+Environment="NODE_ENV=production" "_=${selfFile}"
+WorkingDirectory=${selfDir}
+User=root
+Restart=always
+KillSignal=SIGTERM
+TimeoutStopSec=5
+SyslogIdentifier=${serviceName}
+
+[Install]
+WantedBy=multi-user.target
+`;
+
+    try {
+      fs.writeFileSync(`/tmp/${serviceName}.service`, serviceConfigContent);
+      execSync(`sudo mv /tmp/${serviceName}.service ${serviceConfig}`, {
+        stdio: 'inherit',
+      });
+      execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
+      execSync(`sudo systemctl enable ${serviceName}`, { stdio: 'inherit' });
+      start();
+    } catch (error) {
+      console.error('Error during installation:', error);
+      process.exit(1);
+    }
+  }
+
+  // Uninstall function
+  function uninstall() {
+    console.log(`Uninstalling ${serviceName}...`);
+    if (!hasSystemdService()) {
+      console.error('Service is not installed via systemd.');
+      process.exit(1);
+    }
+
+    try {
+      execSync(`sudo systemctl stop ${serviceName}`, { stdio: 'inherit' });
+      execSync(`sudo systemctl disable ${serviceName}`, { stdio: 'inherit' });
+      execSync(`sudo rm ${serviceConfig}`, { stdio: 'inherit' });
+      execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
+    } catch (error) {
+      console.error('Error during uninstallation:', error);
+      process.exit(1);
+    }
+  }
+
+  // Start function
+  function start() {
+    if (hasSystemdService()) {
+      console.log(`Starting ${serviceName} via systemd...`);
+      try {
+        execSync(`sudo systemctl start ${serviceName}`, { stdio: 'inherit' });
+      } catch (error) {
+        console.error('Error starting service via systemd:', error);
+        process.exit(1);
+      }
+    } else {
+      console.log(`Starting ${serviceName} manually...`);
+      if (isRunning()) {
+        console.log(`${serviceName} is already running.`);
+        process.exit(0);
+      }
+
+      const child = spawn(selfFile, ['daemon'], {
+        detached: true,
+        cwd: selfDir,
+        stdio: ['ignore', 'ignore', 'ignore'],
+      });
+
+      if (!child.pid) {
+        console.error('Failed to start the service.');
+        process.exit(1);
+      }
+
+      fs.writeFileSync(lockFile, child.pid.toString());
+      console.log(`${serviceName} started with PID ${child.pid}`);
+      child.unref();
+    }
+  }
+
+  // Stop function
+  function stop() {
+    if (hasSystemdService()) {
+      console.log(`Stopping ${serviceName} via systemd...`);
+      try {
+        execSync(`sudo systemctl stop ${serviceName}`, { stdio: 'inherit' });
+      } catch (error) {
+        console.error('Error stopping service via systemd:', error);
+        process.exit(1);
+      }
+    } else {
+      console.log(`Stopping ${serviceName} manually...`);
+      if (!fs.existsSync(lockFile)) {
+        console.log(`${serviceName} is not running.`);
+        process.exit(0);
+      }
+
+      const pidStr = fs.readFileSync(lockFile, 'utf-8');
+      const pid = parseInt(pidStr, 10);
+      try {
+        process.kill(pid, 'SIGTERM');
+        console.log(`Stopped ${serviceName} with PID ${pid}.`);
+      } catch (err) {
+        console.error(`Failed to stop ${serviceName}:`, err);
+      }
+
+      try {
+        fs.unlinkSync(lockFile);
+      } catch {
+        // Ignore errors
+      }
+    }
+  }
+
+  // Restart function
+  function restart() {
+    if (hasSystemdService()) {
+      console.log(`Restarting ${serviceName} via systemd...`);
+      try {
+        execSync(`sudo systemctl restart ${serviceName}`, { stdio: 'inherit' });
+      } catch (error) {
+        console.error('Error restarting service via systemd:', error);
+        process.exit(1);
+      }
+    } else {
+      console.log(`Restarting ${serviceName} manually...`);
+      stop();
+      // Give some time for the process to stop
+      setTimeout(() => {
+        start();
+      }, 1000);
+    }
+  }
+
+  // Status function
+  function status() {
+    if (hasSystemdService()) {
+      try {
+        execSync(`systemctl status ${serviceName}`, { stdio: 'inherit' });
+      } catch {
+        console.log(`${serviceName} is not running via systemd.`);
+      }
+    } else {
+      if (isRunning()) {
+        const pid = fs.readFileSync(lockFile, 'utf-8');
+        console.log(`${serviceName} is running with PID ${pid}.`);
+      } else {
+        console.log(`${serviceName} is not running.`);
+      }
+    }
+  }
+
+  // Check if the process is running
+  function isRunning() {
+    try {
+      const pidStr = fs.readFileSync(lockFile, 'utf-8');
+      const pid = parseInt(pidStr, 10);
+      process.kill(pid, 0);
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  // Function to parse command-line options
+  function parseLogOptions(args?: string): { follow: boolean; lines: number } {
+    const options = { follow: false, lines: 10 }; // Default values
+    const argsArray = args ? args.split(' ') : [];
+
+    argsArray.forEach((arg, index) => {
+      if (arg === '-f' || arg === '--follow') {
+        options.follow = true;
+      } else if (arg === '-n' || arg === '--lines') {
+        const value = argsArray[index + 1];
+        if (value && !isNaN(parseInt(value, 10))) {
+          options.lines = parseInt(value, 10);
+        }
+      }
+    });
+
+    return options;
+  }
+
+  // Custom log viewer function
+  function logs(args?: string) {
+    const options = parseLogOptions(args);
+
+    if (hasSystemdService()) {
+      try {
+        execSync(`journalctl -u ${serviceName} ${args || ''}`, {
+          stdio: 'inherit',
+        });
+      } catch (error) {
+        console.error('Error fetching logs via systemd:', error);
+        process.exit(1);
+      }
+    } else {
+      console.log(`Fetching logs manually...`);
+
+      // Get all log files (including compressed ones)
+      try {
+        const files = fs.readdirSync(logDir);
+        const logFiles = files
+          .filter((file) => file.startsWith(`${serviceName}.log`))
+          .map((file) => {
+            const filePath = join(logDir, file);
+            const stats = fs.lstatSync(filePath);
+            return {
+              name: file,
+              path: filePath,
+              time: stats.mtime.getTime(),
+            };
+          });
+
+        logFiles.sort((a, b) => a.time - b.time); // Sort by oldest first
+
+        if (logFiles.length > 0) {
+          if (options.follow) {
+            // Real-time log following
+            followLogs();
+          } else {
+            // Display historical logs
+            displayLogs(logFiles, options.lines);
+          }
+        } else {
+          console.log('No logs found.');
+        }
+      } catch (error) {
+        console.error('Error fetching logs:', error);
+      }
+    }
+  }
+
+  // Function to display historical logs
+  function displayLogs(logFiles: { path: string }[], lines: number) {
+    const allLines: string[] = [];
+
+    for (const file of logFiles) {
+      let fileStream: Readable;
+      if (file.path.endsWith('.gz')) {
+        fileStream = fs.createReadStream(file.path).pipe(zlib.createGunzip());
+      } else {
+        fileStream = fs.createReadStream(file.path);
+      }
+
+      const rl = readline.createInterface({
+        input: fileStream,
+        crlfDelay: Infinity,
+      });
+
+      rl.on('line', (line) => {
+        allLines.push(line);
+      });
+
+      rl.on('close', () => {
+        // After reading all lines, display the last 'n' lines
+        const start = Math.max(allLines.length - lines, 0);
+        for (let i = start; i < allLines.length; i++) {
+          console.log(allLines[i]);
+        }
+      });
+    }
+  }
+
+  // Function to follow logs in real-time
+  function followLogs() {
+    let currentLogFile = logFile;
+    let position = 0;
+
+    const readNewData = () => {
+      fs.stat(currentLogFile, (err, stats) => {
+        if (err) {
+          // File might not exist yet
+          return;
+        }
+
+        if (stats.size < position) {
+          // Log file was rotated
+          position = 0;
+        }
+
+        const stream = fs.createReadStream(currentLogFile, {
+          encoding: 'utf-8',
+          flags: 'r',
+          start: position,
+        });
+
+        stream.on('data', (chunk) => {
+          process.stdout.write(chunk as string);
+          position += Buffer.byteLength(chunk, 'utf-8');
+        });
+
+        stream.on('error', (err) => {
+          console.error('Error reading log file:', err);
+        });
+      });
+    };
+
+    // Initial read to get any existing data
+    readNewData();
+
+    // Watch the current log file
+    let watcher = fs.watch(currentLogFile, (eventType) => {
+      if (eventType === 'change') {
+        readNewData();
+      } else if (eventType === 'rename') {
+        // Log file was rotated
+        watcher.close();
+        position = 0;
+        currentLogFile = logFile; // The new log file
+        // Start watching the new log file
+        watcher = fs.watch(currentLogFile, (eventType) => {
+          if (eventType === 'change') {
+            readNewData();
+          }
+        });
+      }
+    });
+
+    // Keep the process running
+    process.stdin.resume();
+  }
+
+  // Command-line interface
+  const action = process.argv[2];
+  const args = process.argv.slice(3).join(' ');
+
+  switch (action) {
+    case 'install':
+      install();
+      break;
+    case 'uninstall':
+      uninstall();
+      break;
+    case 'start':
+      start();
+      break;
+    case 'stop':
+      stop();
+      break;
+    case 'restart':
+      restart();
+      break;
+    case 'status':
+      status();
+      break;
+    case 'logs':
+      logs(args);
+      break;
+    case 'daemon':
+      daemon();
+      break;
+    default:
+      console.log(
+        `Usage: ${selfFilename} {install|uninstall|start|stop|restart|status|logs [args]}`
+      );
+      console.log(`Description: ${serviceDescription}`);
+  }
+}

+ 27 - 0
tsconfig.json

@@ -0,0 +1,27 @@
+{
+  "compilerOptions": {
+    // Enable latest features
+    "lib": ["ESNext", "DOM"],
+    "target": "ESNext",
+    "module": "ESNext",
+    "moduleDetection": "force",
+    "jsx": "react-jsx",
+    "allowJs": true,
+
+    // Bundler mode
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "noEmit": true,
+
+    // Best practices
+    "strict": true,
+    "skipLibCheck": true,
+    "noFallthroughCasesInSwitch": true,
+
+    // Some stricter flags (disabled by default)
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noPropertyAccessFromIndexSignature": false
+  }
+}