Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Jan 29, 2025

Summary by CodeRabbit

  • Configuration Updates

    • Enhanced logging configuration for the application
    • Added process termination timeout
    • Removed ip-regex dependency
  • Code Refactoring

    • Streamlined PM2 module imports
    • Simplified report generation process
    • Updated PM2 state management approach
  • Cleanup

    • Removed GraphQL-related data fetching logic
    • Simplified configuration reading methods
    • Removed unit tests for the anonymiseOrigins function

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2025

Walkthrough

The pull request introduces several modifications to the Unraid API project, focusing on changes in configuration management, dependency handling, and code structure. Key updates include enhancing logging and process management in the ecosystem.config.json, removing the ip-regex dependency, and refactoring the PM2-related functionality. Additionally, the report command has shifted from GraphQL-based data retrieval to a configuration file-based approach, streamlining the application's structure and reducing complexity.

Changes

File Change Summary
api/ecosystem.config.json - Replaced single log entry with separate out_file and error_file log configurations
- Added kill_timeout parameter set to 10000 milliseconds
api/package.json - Removed ip-regex dependency
api/src/core/utils/pm2/unraid-api-running.ts - Updated PM2 module import to use dynamic import syntax and adjusted function calls accordingly
api/src/index.ts - Updated exit handling to use asynchronous methods and replaced direct exit calls with gracefulExit
api/src/unraid-api/cli/report.command.ts - Removed GraphQL-related types and functions
- Added getBothMyServersConfigsWithoutError method
- Simplified report and run methods
api/src/unraid-api/cli/start.command.ts - Removed configurePm2 method
- Added cleanupPM2State method for PM2 process management
api/src/unraid-api/rest/rest.service.ts - Simplified saveApiReport method call without parameters
api/src/unraid-api/cli/report.spec.ts - Deleted unit tests for anonymiseOrigins function

Possibly related PRs

Suggested reviewers

  • pujitm
  • mdatelle

Poem

🐰 A Rabbit's Ode to Code Refactoring 🔧

Logs now split, config refined,
GraphQL queries left behind,
PM2 dancing to a new beat,
Simplicity makes the code complete!

Hop, hop, hooray! 🎉

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@elibosley elibosley requested review from mdatelle and pujitm January 29, 2025 15:57
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
api/src/unraid-api/cli/start.command.ts (1)

19-25: Consider using a consistent async/await error-handling style.

In cleanupPM2State, you're mixing await with .then(...).catch(...) patterns. For clarity and consistency, you might refactor to a single try/catch approach for each execa call. This improves code readability and maintains uniform error management.

async cleanupPM2State() {
-   await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH])
-       .then(({ stdout }) => this.logger.debug(stdout))
-       .catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr));
+   try {
+       const { stdout } = await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH]);
+       this.logger.debug(stdout);
+   } catch (error: any) {
+       this.logger.error('PM2 Stop Error: ' + error.stderr);
+   }

-   await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
-       .then(({ stdout }) => this.logger.debug(stdout))
-       .catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr));
+   try {
+       const { stdout } = await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH]);
+       this.logger.debug(stdout);
+   } catch (error: any) {
+       this.logger.error('PM2 Delete API Error: ' + error.stderr);
+   }

    // Update PM2
    ...
}
api/src/unraid-api/cli/report.command.ts (2)

34-48: Consider handling partial or malformed config data.

getBothMyServersConfigsWithoutError returns null if neither config is found. If one of the INI files is malformed, the parse call might throw. Currently, you catch only the file read error, not parse errors. You might wrap the parse in a try/catch to avoid unexpected runtime exceptions.

try {
    return MyServersConfigMemorySchema.parse(ini.parse(diskConfig));
} catch (parseError) {
    this.logger.debug('Failed to parse diskConfig: ' + parseError);
    return null;
}

51-51: Refine the return type to avoid confusion with 'void'.

TypeScript lint tools warn that void in a union can be confusing. Consider using Promise<string | undefined> instead.

- async report(): Promise<string | void> {
+ async report(): Promise<string | undefined> {
🧰 Tools
🪛 Biome (1.9.4)

[error] 51-51: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

api/src/core/utils/pm2/unraid-api-running.ts (1)

4-4: Use a structured logger for consistency.

The direct console.log(err) calls differ from the logger usage in other modules. Consider using a central logging mechanism to standardize log formatting and levels across the codebase.

- console.error(err);
+ logger.error(err);

- console.log(false);
+ logger.debug('PM2 process not found. Returning false.');

- console.log(isOnline);
+ logger.debug(`Unraid API Online: ${isOnline}`);

Also applies to: 10-10, 21-21

api/ecosystem.config.json (1)

21-21: Document the kill_timeout parameter.

The addition of kill_timeout: 3000 is good for process management, but consider adding a comment explaining why 3 seconds was chosen as the timeout value.

             "ignore_watch": [
                 "node_modules",
                 "src",
                 ".env.*",
                 "myservers.cfg"
             ],
             "out_file": "/var/log/unraid-api/unraid-api.log",
             "error_file": "/var/log/unraid-api/unraid-api.error.log",
+            // Allow 3 seconds for graceful shutdown before force killing the process
             "kill_timeout": 3000
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between c58f7a7 and 76970ed.

⛔ Files ignored due to path filters (1)
  • api/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • api/ecosystem.config.json (1 hunks)
  • api/package.json (0 hunks)
  • api/src/core/utils/pm2/unraid-api-running.ts (2 hunks)
  • api/src/index.ts (0 hunks)
  • api/src/unraid-api/cli/report.command.ts (2 hunks)
  • api/src/unraid-api/cli/start.command.ts (1 hunks)
  • api/src/unraid-api/rest/rest.service.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • api/src/index.ts
  • api/package.json
🧰 Additional context used
🪛 Biome (1.9.4)
api/src/unraid-api/cli/report.command.ts

[error] 51-51: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build Web App
  • GitHub Check: Build and Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (8)
api/src/unraid-api/cli/start.command.ts (1)

35-35: Great step ordering.

Cleaning up the PM2 state before starting the Unraid API helps ensure a fresh environment and can prevent stale process conflicts.

api/src/unraid-api/cli/report.command.ts (4)

1-2: Good use of modern file system promises.

Using readFile from 'fs/promises' is a clean approach and keeps the codebase aligned with modern async patterns.


5-7: Imports are consistent with the new configuration approach.

Importing MyServersConfigMemory and its schema helps maintain a strongly typed interface for configuration data. This guards against schema drift.


53-75: Lazy importing the PM2 helper is a good approach.

Dynamically importing isUnraidApiRunning keeps dependencies lightweight for users of this CLI command. Validate PM2 usage within this code block is thoroughly tested to confirm it handles all PM2 states (e.g., if PM2 is not installed).


79-80: Method signature simplifies the usage of CLI commands.

Removing parameters from run() and delegating the logic to report() creates a more streamlined flow. This is a concise improvement.

api/src/core/utils/pm2/unraid-api-running.ts (1)

2-2: Dynamic import of PM2 streamlines dependencies.

This approach loads PM2 functions only when needed. However, ensure that environment constraints allow dynamic imports at runtime, especially in containerized or server environments.

api/src/unraid-api/rest/rest.service.ts (1)

22-22: Verify the impact of removing report parameters.

The removal of parameters ({ json: true, verbose: 2, raw: false }) from reportCommand.report() could affect the report format. Please ensure this change:

  1. Aligns with the new implementation in ReportCommand
  2. Maintains backward compatibility if other parts of the system expect specific report formats

Let's verify the usage of this method:

✅ Verification successful

The parameter removal is correct and safe

The ReportCommand.report() method always returns JSON-formatted output regardless of the parameters, making the previous parameters ({ json: true, verbose: 2, raw: false }) redundant. This change aligns with the actual implementation and removes unused parameters.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other callers of saveApiReport to ensure they can handle the new format
rg "saveApiReport" -A 5

# Search for consumers of the report.json file
rg "report\.json" -A 5

Length of output: 1545


Script:

#!/bin/bash
# Check ReportCommand implementation
ast-grep --pattern 'class ReportCommand {
  $$$
  report($$$) {
    $$$
  }
  $$$
}'

# Look for related files
fd "report.command" --type f

# Check git history for context
git log --grep="report" -n 5 --oneline

Length of output: 156


Script:

#!/bin/bash
# View ReportCommand implementation
cat api/src/unraid-api/cli/report.command.ts

# Check recent changes to this file
git log -p api/src/unraid-api/cli/report.command.ts

Length of output: 2773

api/ecosystem.config.json (1)

19-20: LGTM! Improved log separation.

Splitting logs into separate files for standard output and errors will improve debugging capabilities.

Let's verify the log directory exists and has proper permissions:

pujitm
pujitm previously approved these changes Jan 29, 2025
Copy link
Member

@pujitm pujitm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanity check on exit hook, lgtm otherwise

mdatelle
mdatelle previously approved these changes Jan 29, 2025
@elibosley elibosley dismissed stale reviews from mdatelle and pujitm via 45f02f6 January 29, 2025 16:47
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
api/src/index.ts (1)

99-99: Replace console.log with logger.

Use the already imported logger instead of console.log for consistent logging throughout the application.

-            console.log('exithook', signal);
+            logger.info('Initiating exit hook', { signal });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 5446672 and 45f02f6.

📒 Files selected for processing (2)
  • api/src/index.ts (3 hunks)
  • api/src/unraid-api/cli/report.spec.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • api/src/unraid-api/cli/report.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build Web App
  • GitHub Check: Build and Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
api/src/index.ts (3)

12-12: LGTM! Import changes align with async exit handling.

The change from exitHook to { asyncExitHook, gracefulExit } provides better control over the application shutdown process.


123-123: LGTM! Consistent error handling exit.

The change to gracefulExit(1) maintains the error status code while ensuring graceful shutdown, consistent with the new exit handling approach.


97-109: Verify wait time alignment with PM2 kill_timeout.

The async exit hook implementation looks good and addresses the previous review comment. However, we should verify that the wait time (9999ms) aligns with the PM2 kill_timeout configuration to ensure proper shutdown coordination.

✅ Verification successful

✅ PM2 kill_timeout and asyncExitHook wait time are properly aligned

The asyncExitHook wait time (9999ms) is correctly set to be slightly under PM2's kill_timeout (10000ms), ensuring proper shutdown coordination.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check PM2 ecosystem config for kill_timeout setting

# Search for kill_timeout in ecosystem config
rg -A 5 "kill_timeout" ecosystem.config.json

Length of output: 106


Script:

#!/bin/bash
# Search for potential PM2 config files and kill_timeout settings

# Look for ecosystem config files
echo "=== Searching for PM2 config files ==="
fd -e js -e json -e yaml -e yml ecosystem

# Search for PM2 related settings in package.json
echo -e "\n=== Checking package.json for PM2 config ==="
fd package.json --exec cat {} | jq -r 'select(.pm2 != null) | .pm2'

# Search for kill_timeout pattern in all files
echo -e "\n=== Searching for kill_timeout pattern ==="
rg -i "kill_timeout|pm2.*timeout|graceful.*timeout" --type ts --type js --type json

Length of output: 580

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL: https://preview.dl.unraid.net/unraid-api/pr/1081/dynamix.unraid.net.staging.plg

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
api/src/unraid-api/cli/report.command.ts (1)

Line range hint 18-33: Remove or implement unused command options.

The parseRaw and parseJson options are defined but not utilized in the current implementation. They always return true regardless of the input, and the report method doesn't use these values.

Either:

  1. Remove these unused options, or
  2. Implement the JSON/raw output formatting in the report method based on these options.
-    @Option({
-        flags: '-r, --raw',
-        description: 'whether to enable raw command output',
-        defaultValue: false,
-    })
-    parseRaw(): boolean {
-        return true;
-    }
-
-    @Option({
-        flags: '-j, --json',
-        description: 'Display JSON output for this command',
-        defaultValue: false,
-    })
-    parseJson(): boolean {
-        return true;
-    }
🧰 Tools
🪛 Biome (1.9.4)

[error] 51-51: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

🧹 Nitpick comments (1)
api/src/unraid-api/cli/report.command.ts (1)

79-81: Consider handling the return value from report.

The run method ignores the return value from report, which could be useful for testing or scripting purposes.

Consider this improvement:

-async run(): Promise<void> {
-    await this.report();
+async run(): Promise<string | undefined> {
+    return this.report();
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 45f02f6 and fda7c7a.

⛔ Files ignored due to path filters (1)
  • api/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • api/ecosystem.config.json (1 hunks)
  • api/package.json (0 hunks)
  • api/src/core/utils/pm2/unraid-api-running.ts (2 hunks)
  • api/src/index.ts (3 hunks)
  • api/src/unraid-api/cli/report.command.ts (2 hunks)
  • api/src/unraid-api/cli/report.spec.ts (0 hunks)
  • api/src/unraid-api/cli/start.command.ts (1 hunks)
  • api/src/unraid-api/rest/rest.service.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • api/package.json
  • api/src/unraid-api/cli/report.spec.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • api/src/unraid-api/rest/rest.service.ts
  • api/ecosystem.config.json
  • api/src/index.ts
  • api/src/core/utils/pm2/unraid-api-running.ts
🧰 Additional context used
🪛 Biome (1.9.4)
api/src/unraid-api/cli/report.command.ts

[error] 51-51: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build Web App
  • GitHub Check: Build and Test API
  • GitHub Check: Cloudflare Pages

Comment on lines 34 to 36
this.logger.info('Starting the Unraid API');
await this.configurePm2();

await this.cleanupPM2State();
const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve startup robustness and graceful shutdown handling.

The current implementation has several potential issues:

  1. No validation of PM2_PATH and ECOSYSTEM_PATH
  2. Direct process.exit calls could prevent cleanup
  3. No graceful shutdown handling

Consider this improved implementation:

 async run(_: string[], options: StartCommandOptions): Promise<void> {
     this.logger.info('Starting the Unraid API');
+    // Validate paths
+    if (!PM2_PATH || !ECOSYSTEM_PATH) {
+        throw new Error('PM2_PATH and ECOSYSTEM_PATH must be defined');
+    }
+
+    // Register cleanup handler
+    const cleanup = async (signal?: string) => {
+        if (signal) {
+            this.logger.info(`Received ${signal}, cleaning up...`);
+        }
+        try {
+            await this.cleanupPM2State();
+            process.exit(0);
+        } catch (error) {
+            this.logger.error('Cleanup failed:', error);
+            process.exit(1);
+        }
+    };
+
+    process.on('SIGTERM', () => cleanup('SIGTERM'));
+    process.on('SIGINT', () => cleanup('SIGINT'));
+
     await this.cleanupPM2State();
     const envLog = options['log-level'] ? `LOG_LEVEL=${options['log-level']}` : '';
-    const { stderr, stdout } = await execa(`${envLog} ${PM2_PATH}`.trim(), [
-        'start',
-        ECOSYSTEM_PATH,
-        '--update-env',
-    ]);
-    if (stdout) {
-        this.logger.log(stdout);
-    }
-    if (stderr) {
-        this.logger.error(stderr);
-        process.exit(1);
+    try {
+        const { stderr, stdout } = await execa(`${envLog} ${PM2_PATH}`.trim(), [
+            'start',
+            ECOSYSTEM_PATH,
+            '--update-env',
+        ]);
+        if (stdout) {
+            this.logger.log(stdout);
+        }
+        if (stderr) {
+            this.logger.error(stderr);
+            await cleanup();
+        }
+    } catch (error) {
+        this.logger.error('Failed to start PM2:', error);
+        await cleanup();
     }
-    process.exit(0);
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +19 to 30
async cleanupPM2State() {
await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Logrotate Set Error: ' + stderr));
await execa(PM2_PATH, ['set', 'pm2-logrotate:compress', 'true'])
.catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr));
await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Logrotate Compress Error: ' + stderr));
await execa(PM2_PATH, ['set', 'pm2-logrotate:max_size', '1M'])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Logrotate Max Size Error: ' + stderr));

// PM2 Save Settings
await execa(PM2_PATH, ['set', 'pm2:autodump', 'true'])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Autodump Error: ' + stderr));
.catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr));

// Update PM2
await execa(PM2_PATH, ['update'])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Update Error: ' + stderr));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling and process management in cleanupPM2State.

The current implementation has several areas for improvement:

  1. Error handling continues execution even if critical operations fail
  2. No timeout handling for PM2 operations
  3. Potential race conditions between PM2 operations

Consider this improved implementation:

 async cleanupPM2State() {
+    const PM2_TIMEOUT = 10000; // 10 seconds
+    try {
         await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH], {
+            timeout: PM2_TIMEOUT,
+            stripFinalNewline: true,
         })
-            .then(({ stdout }) => this.logger.debug(stdout))
-            .catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr));
+            .then(({ stdout }) => this.logger.debug('PM2 Stop Success:', stdout));
+    } catch (error) {
+        const err = error as { stderr?: string, message: string };
+        this.logger.error('PM2 Stop Error:', err.stderr || err.message);
+        throw new Error('Failed to stop PM2 process');
+    }
+
+    try {
         await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
-            .then(({ stdout }) => this.logger.debug(stdout))
-            .catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr));
+            .then(({ stdout }) => this.logger.debug('PM2 Delete Success:', stdout));
+    } catch (error) {
+        const err = error as { stderr?: string, message: string };
+        this.logger.error('PM2 Delete Error:', err.stderr || err.message);
+        throw new Error('Failed to delete PM2 process');
+    }

-        // Update PM2
-        await execa(PM2_PATH, ['update'])
-            .then(({ stdout }) => this.logger.debug(stdout))
-            .catch(({ stderr }) => this.logger.error('PM2 Update Error: ' + stderr));
+    // Only update PM2 if necessary (could be moved to a separate method)
+    if (process.env.UPDATE_PM2 === 'true') {
+        try {
+            await execa(PM2_PATH, ['update'], { timeout: PM2_TIMEOUT })
+                .then(({ stdout }) => this.logger.debug('PM2 Update Success:', stdout));
+        } catch (error) {
+            const err = error as { stderr?: string, message: string };
+            this.logger.warn('PM2 Update Warning:', err.stderr || err.message);
+            // Don't throw on update failure as it's not critical
+        }
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async cleanupPM2State() {
await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Logrotate Set Error: ' + stderr));
await execa(PM2_PATH, ['set', 'pm2-logrotate:compress', 'true'])
.catch(({ stderr }) => this.logger.error('PM2 Stop Error: ' + stderr));
await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Logrotate Compress Error: ' + stderr));
await execa(PM2_PATH, ['set', 'pm2-logrotate:max_size', '1M'])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Logrotate Max Size Error: ' + stderr));
// PM2 Save Settings
await execa(PM2_PATH, ['set', 'pm2:autodump', 'true'])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Autodump Error: ' + stderr));
.catch(({ stderr }) => this.logger.error('PM2 Delete API Error: ' + stderr));
// Update PM2
await execa(PM2_PATH, ['update'])
.then(({ stdout }) => this.logger.debug(stdout))
.catch(({ stderr }) => this.logger.error('PM2 Update Error: ' + stderr));
async cleanupPM2State() {
const PM2_TIMEOUT = 10000; // 10 seconds
try {
await execa(PM2_PATH, ['stop', ECOSYSTEM_PATH], {
timeout: PM2_TIMEOUT,
stripFinalNewline: true,
})
.then(({ stdout }) => this.logger.debug('PM2 Stop Success:', stdout));
} catch (error) {
const err = error as { stderr?: string, message: string };
this.logger.error('PM2 Stop Error:', err.stderr || err.message);
throw new Error('Failed to stop PM2 process');
}
try {
await execa(PM2_PATH, ['delete', ECOSYSTEM_PATH])
.then(({ stdout }) => this.logger.debug('PM2 Delete Success:', stdout));
} catch (error) {
const err = error as { stderr?: string, message: string };
this.logger.error('PM2 Delete Error:', err.stderr || err.message);
throw new Error('Failed to delete PM2 process');
}
// Only update PM2 if necessary (could be moved to a separate method)
if (process.env.UPDATE_PM2 === 'true') {
try {
await execa(PM2_PATH, ['update'], { timeout: PM2_TIMEOUT })
.then(({ stdout }) => this.logger.debug('PM2 Update Success:', stdout));
} catch (error) {
const err = error as { stderr?: string, message: string };
this.logger.warn('PM2 Update Warning:', err.stderr || err.message);
// Don't throw on update failure as it's not critical
}
}
}

Comment on lines +34 to +48
async getBothMyServersConfigsWithoutError(): Promise<MyServersConfigMemory | null> {
const ini = await import('ini');
const diskConfig = await readFile(getters.paths()['myservers-config'], 'utf-8').catch(
(_) => null
);
const memoryConfig = await readFile(getters.paths()['myservers-config-states'], 'utf-8').catch(
(_) => null
);

if (memoryConfig) {
return MyServersConfigMemorySchema.parse(ini.parse(memoryConfig));
} else if (diskConfig) {
return MyServersConfigMemorySchema.parse(ini.parse(diskConfig));
}
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and imports in config retrieval.

Several improvements can be made to this method:

  1. The dynamic import of 'ini' could be moved to top-level imports
  2. Error handling is too permissive, silently returning null
  3. Missing type assertion for diskConfig parse case
  4. No explicit handling of parse errors

Consider this improved implementation:

+import ini from 'ini';
+
 async getBothMyServersConfigsWithoutError(): Promise<MyServersConfigMemory | null> {
-    const ini = await import('ini');
     const diskConfig = await readFile(getters.paths()['myservers-config'], 'utf-8').catch(
-        (_) => null
+        (error) => {
+            this.logger.debug(`Failed to read disk config: ${error.message}`);
+            return null;
+        }
     );
     const memoryConfig = await readFile(getters.paths()['myservers-config-states'], 'utf-8').catch(
-        (_) => null
+        (error) => {
+            this.logger.debug(`Failed to read memory config: ${error.message}`);
+            return null;
+        }
     );

     if (memoryConfig) {
-        return MyServersConfigMemorySchema.parse(ini.parse(memoryConfig));
+        try {
+            return MyServersConfigMemorySchema.parse(ini.parse(memoryConfig));
+        } catch (error) {
+            this.logger.debug(`Failed to parse memory config: ${error.message}`);
+            return null;
+        }
     } else if (diskConfig) {
-        return MyServersConfigMemorySchema.parse(ini.parse(diskConfig));
+        try {
+            return MyServersConfigMemorySchema.parse(ini.parse(diskConfig));
+        } catch (error) {
+            this.logger.debug(`Failed to parse disk config: ${error.message}`);
+            return null;
+        }
     }
     return null;
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +51 to +75
async report(): Promise<string | void> {
try {
// Show loading message
if (isInteractive) {
this.logger.info('Generating report please wait…');
}

const jsonReport = options?.json ?? false;

// Find all processes called "unraid-api" which aren't this process
const unraidApiRunning = await isUnraidApiRunning();

// Load my servers config file into store
await store.dispatch(loadConfigFile());
await store.dispatch(loadStateFiles());

const { config, emhttp } = store.getState();

const client = getApiApolloClient({ localApiKey: config.remote.localApiKey || '' });
// Fetch the cloud endpoint
const cloud = await getCloudData(client)
.then((data) => {
this.logger.debug('Cloud Data', data);
return data;
})
.catch((error) => {
this.logger.debug(
'Failed fetching cloud from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
return null;
});

// Query local graphql using upc's API key
// Get the servers array
const servers = await getServersData({ client, verbosity: options.verbose });

// Check if the API key is valid
const isApiKeyValid = cloud?.apiKey.valid ?? false;

const reportObject: ReportObject = {
os: {
serverName: emhttp.var.name,
version: emhttp.var.version,
},
api: {
version: API_VERSION,
status: unraidApiRunning ? 'running' : 'stopped',
environment: process.env.ENVIRONMENT ?? 'THIS_WILL_BE_REPLACED_WHEN_BUILT',
nodeVersion: process.version,
},
apiKey: isApiKeyValid ? 'valid' : (cloud?.apiKey.error ?? 'invalid'),
...(servers ? { servers } : {}),
myServers: {
status: config?.remote?.username ? 'authenticated' : 'signed out',
...(config?.remote?.username
? {
myServersUsername: config?.remote?.username?.includes('@')
? 'REDACTED'
: config?.remote.username,
}
: {}),
},
minigraph: {
status: cloud?.minigraphql.status ?? MinigraphStatus.PRE_INIT,
timeout: cloud?.minigraphql.timeout ?? null,
error:
(cloud?.minigraphql.error ?? !cloud?.minigraphql.status)
? 'API Disconnected'
: null,
},
cloud: {
status: cloud?.cloud.status ?? 'error',
...(cloud?.cloud.error ? { error: cloud.cloud.error } : {}),
...(cloud?.cloud.status === 'ok' ? { ip: cloud.cloud.ip ?? 'NO_IP' } : {}),
...(getAllowedOrigins(cloud, options.verbose)
? { allowedOrigins: getAllowedOrigins(cloud, options.verbose) }
: {}),
},
};

if (jsonReport) {
this.logger.clear();
this.logger.info(JSON.stringify(reportObject) + '\n');
return reportObject;
} else {
// Generate the actual report
const report = `
<-----UNRAID-API-REPORT----->
SERVER_NAME: ${reportObject.os.serverName}
ENVIRONMENT: ${reportObject.api.environment}
UNRAID_VERSION: ${reportObject.os.version}
UNRAID_API_VERSION: ${reportObject.api.version}
UNRAID_API_STATUS: ${reportObject.api.status}
API_KEY: ${reportObject.apiKey}
MY_SERVERS: ${reportObject.myServers.status}${
reportObject.myServers.myServersUsername
? `\nMY_SERVERS_USERNAME: ${reportObject.myServers.myServersUsername}`
: ''
}
CLOUD: ${getReadableCloudDetails(reportObject, options.verbose)}
MINI-GRAPH: ${getReadableMinigraphDetails(reportObject)}${getReadableServerDetails(
reportObject,
options.verbose
)}${getReadableAllowedOrigins(reportObject)}
</----UNRAID-API-REPORT----->
`;
this.logger.clear();

this.logger.info(report);
return report;
}
} catch (error: unknown) {
if (error instanceof Error) {
this.logger.debug(error);
this.logger.error(`\nFailed generating report with "${error.message}"\n`);
return;
} else {
this.logger.error('Failed generating report');
return;
}
const { isUnraidApiRunning } = await import('@app/core/utils/pm2/unraid-api-running');

const apiRunning = await isUnraidApiRunning().catch((err) => {
this.logger.debug('failed to get PM2 state with error: ' + err);
return false;
});

const config =
(await this.getBothMyServersConfigsWithoutError()) as MyServersConfigMemory & {
connectionStatus: { running: 'yes' | 'no' };
};
config.connectionStatus.running = apiRunning ? 'yes' : 'no';
this.logger.clear();
this.logger.info(JSON.stringify(config, null, 2));
} catch (error) {
this.logger.debug('Error Generating Config: ' + error);
this.logger.warn(
JSON.stringify(
{ error: 'Please ensure the API is configured before attempting to run a report' },
null,
2
)
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety and error handling in report method.

Several improvements can be made:

  1. Return type includes void in union (flagged by static analysis)
  2. Unsafe type assertion for config
  3. Dynamic import could be moved to top-level
  4. Generic error message

Consider this improved implementation:

-import { readFile } from 'fs/promises';
+import { readFile } from 'fs/promises';
+import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running';

-async report(): Promise<string | void> {
+async report(): Promise<string | undefined> {
     try {
-        const { isUnraidApiRunning } = await import('@app/core/utils/pm2/unraid-api-running');
-
         const apiRunning = await isUnraidApiRunning().catch((err) => {
             this.logger.debug('failed to get PM2 state with error: ' + err);
             return false;
         });

-        const config =
-            (await this.getBothMyServersConfigsWithoutError()) as MyServersConfigMemory & {
-                connectionStatus: { running: 'yes' | 'no' };
-            };
+        const config = await this.getBothMyServersConfigsWithoutError();
+        if (!config) {
+            throw new Error('Failed to retrieve configuration');
+        }
+
+        const configWithStatus = {
+            ...config,
+            connectionStatus: {
+                running: apiRunning ? 'yes' : 'no' as const
+            }
+        };

-        config.connectionStatus.running = apiRunning ? 'yes' : 'no';
         this.logger.clear();
-        this.logger.info(JSON.stringify(config, null, 2));
+        const output = JSON.stringify(configWithStatus, null, 2);
+        this.logger.info(output);
+        return output;
     } catch (error) {
-        this.logger.debug('Error Generating Config: ' + error);
+        this.logger.debug(`Failed to generate report: ${error instanceof Error ? error.message : String(error)}`);
         this.logger.warn(
             JSON.stringify(
                 { error: 'Please ensure the API is configured before attempting to run a report' },
                 null,
                 2
             )
         );
+        return undefined;
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async report(): Promise<string | void> {
try {
// Show loading message
if (isInteractive) {
this.logger.info('Generating report please wait…');
}
const jsonReport = options?.json ?? false;
// Find all processes called "unraid-api" which aren't this process
const unraidApiRunning = await isUnraidApiRunning();
// Load my servers config file into store
await store.dispatch(loadConfigFile());
await store.dispatch(loadStateFiles());
const { config, emhttp } = store.getState();
const client = getApiApolloClient({ localApiKey: config.remote.localApiKey || '' });
// Fetch the cloud endpoint
const cloud = await getCloudData(client)
.then((data) => {
this.logger.debug('Cloud Data', data);
return data;
})
.catch((error) => {
this.logger.debug(
'Failed fetching cloud from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
return null;
});
// Query local graphql using upc's API key
// Get the servers array
const servers = await getServersData({ client, verbosity: options.verbose });
// Check if the API key is valid
const isApiKeyValid = cloud?.apiKey.valid ?? false;
const reportObject: ReportObject = {
os: {
serverName: emhttp.var.name,
version: emhttp.var.version,
},
api: {
version: API_VERSION,
status: unraidApiRunning ? 'running' : 'stopped',
environment: process.env.ENVIRONMENT ?? 'THIS_WILL_BE_REPLACED_WHEN_BUILT',
nodeVersion: process.version,
},
apiKey: isApiKeyValid ? 'valid' : (cloud?.apiKey.error ?? 'invalid'),
...(servers ? { servers } : {}),
myServers: {
status: config?.remote?.username ? 'authenticated' : 'signed out',
...(config?.remote?.username
? {
myServersUsername: config?.remote?.username?.includes('@')
? 'REDACTED'
: config?.remote.username,
}
: {}),
},
minigraph: {
status: cloud?.minigraphql.status ?? MinigraphStatus.PRE_INIT,
timeout: cloud?.minigraphql.timeout ?? null,
error:
(cloud?.minigraphql.error ?? !cloud?.minigraphql.status)
? 'API Disconnected'
: null,
},
cloud: {
status: cloud?.cloud.status ?? 'error',
...(cloud?.cloud.error ? { error: cloud.cloud.error } : {}),
...(cloud?.cloud.status === 'ok' ? { ip: cloud.cloud.ip ?? 'NO_IP' } : {}),
...(getAllowedOrigins(cloud, options.verbose)
? { allowedOrigins: getAllowedOrigins(cloud, options.verbose) }
: {}),
},
};
if (jsonReport) {
this.logger.clear();
this.logger.info(JSON.stringify(reportObject) + '\n');
return reportObject;
} else {
// Generate the actual report
const report = `
<-----UNRAID-API-REPORT----->
SERVER_NAME: ${reportObject.os.serverName}
ENVIRONMENT: ${reportObject.api.environment}
UNRAID_VERSION: ${reportObject.os.version}
UNRAID_API_VERSION: ${reportObject.api.version}
UNRAID_API_STATUS: ${reportObject.api.status}
API_KEY: ${reportObject.apiKey}
MY_SERVERS: ${reportObject.myServers.status}${
reportObject.myServers.myServersUsername
? `\nMY_SERVERS_USERNAME: ${reportObject.myServers.myServersUsername}`
: ''
}
CLOUD: ${getReadableCloudDetails(reportObject, options.verbose)}
MINI-GRAPH: ${getReadableMinigraphDetails(reportObject)}${getReadableServerDetails(
reportObject,
options.verbose
)}${getReadableAllowedOrigins(reportObject)}
</----UNRAID-API-REPORT----->
`;
this.logger.clear();
this.logger.info(report);
return report;
}
} catch (error: unknown) {
if (error instanceof Error) {
this.logger.debug(error);
this.logger.error(`\nFailed generating report with "${error.message}"\n`);
return;
} else {
this.logger.error('Failed generating report');
return;
}
const { isUnraidApiRunning } = await import('@app/core/utils/pm2/unraid-api-running');
const apiRunning = await isUnraidApiRunning().catch((err) => {
this.logger.debug('failed to get PM2 state with error: ' + err);
return false;
});
const config =
(await this.getBothMyServersConfigsWithoutError()) as MyServersConfigMemory & {
connectionStatus: { running: 'yes' | 'no' };
};
config.connectionStatus.running = apiRunning ? 'yes' : 'no';
this.logger.clear();
this.logger.info(JSON.stringify(config, null, 2));
} catch (error) {
this.logger.debug('Error Generating Config: ' + error);
this.logger.warn(
JSON.stringify(
{ error: 'Please ensure the API is configured before attempting to run a report' },
null,
2
)
);
async report(): Promise<string | undefined> {
try {
const apiRunning = await isUnraidApiRunning().catch((err) => {
this.logger.debug('failed to get PM2 state with error: ' + err);
return false;
});
const config = await this.getBothMyServersConfigsWithoutError();
if (!config) {
throw new Error('Failed to retrieve configuration');
}
const configWithStatus = {
...config,
connectionStatus: {
running: apiRunning ? 'yes' : 'no' as const
}
};
this.logger.clear();
const output = JSON.stringify(configWithStatus, null, 2);
this.logger.info(output);
return output;
} catch (error) {
this.logger.debug(`Failed to generate report: ${error instanceof Error ? error.message : String(error)}`);
this.logger.warn(
JSON.stringify(
{ error: 'Please ensure the API is configured before attempting to run a report' },
null,
2
)
);
return undefined;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 51-51: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL: https://preview.dl.unraid.net/unraid-api/pr/1081/dynamix.unraid.net.staging.plg

@elibosley elibosley merged commit 4302f31 into main Jan 29, 2025
10 checks passed
@elibosley elibosley deleted the feat/report-refactor branch January 29, 2025 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants