From b3c53688bc1a5ae49bab4eb77a9378c590507abb Mon Sep 17 00:00:00 2001 From: trevorjtclarke Date: Wed, 15 Dec 2021 14:34:21 -0800 Subject: [PATCH] setup systemctl file generation --- .env.example | 12 ++++++---- README.md | 50 +++++++++++++++++++++++++++++++++++++----- bin/croncat.js | 17 ++++++++++++++ package.json | 2 +- src/actions.js | 12 ++++++---- src/configuration.js | 24 +++++++++++++------- src/createSystemctl.js | 36 ++++++++++++++++++++++++++++++ src/slack.js | 2 +- 8 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 src/createSystemctl.js diff --git a/.env.example b/.env.example index cd88656..2a9e2d1 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ AGENT_ACCOUNT_ID=croncat-agent.near AGENT_MIN_TASK_BALANCE=1 #NOTE: This is really only useful if the payout account is the same as the agent account AGENT_AUTO_REFILL=false +AGENT_AUTO_RE_REGISTER=false # Period between executing standard tasks, needs to be less than 60 seconds to be effective WAIT_INTERVAL_MS=25000 @@ -29,7 +30,10 @@ HEARTBEAT_URL= ## Configure the following as CSV, in priority order, for RPC Failover ## ------------------------------------------------------------------- # Example: RPC_MAINNET_PROVIDERS="https://rpc.mainnet.near.org,http://localhost:3030" -RPC_MAINNET_PROVIDERS="https://rpc.mainnet.near.org" -RPC_TESTNET_PROVIDERS="https://rpc.testnet.near.org" -RPC_GUILDNET_PROVIDERS="https://rpc.openshards.io" -RPC_BETANET_PROVIDERS="https://rpc.betanet.near.org" \ No newline at end of file +RPC_MAINNET_PROVIDERS="https://mainnet-rpc.openshards.io,https://rpc.mainnet.near.org" +RPC_TESTNET_PROVIDERS="https://rpc.testnet.near.org,https://testnet-rpc.openshards.io" +RPC_GUILDNET_PROVIDERS="https://rpc.openshards.io,https://guildnet-rpc.openshards.io" +RPC_BETANET_PROVIDERS="https://rpc.betanet.near.org" + +## RPC API KEY for providers that require it +RPC_API_KEY= \ No newline at end of file diff --git a/README.md b/README.md index 73ea9d6..e33779c 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,14 @@ For a list of up-to-date commands, run `croncat --help` in your terminal. Usage: croncat [options] Commands: - croncat register Add your agent to cron known agents - croncat update Update your agent to cron known agents + croncat register [payable_account_id] Add your agent to cron known agents + croncat update [payable_account_id] Update your agent to cron known agents croncat unregister Account to remove from list of active agents. - croncat withdraw Withdraw all rewards earned for this account - croncat status Check agent status and balance for this account + croncat withdraw [account_id] Withdraw all rewards earned for this account + croncat status [account_id] Check agent status and balance for this account croncat tasks Check how many tasks are currently available - croncat go Run tasks that are available, if agent is registered and has balance + croncat go [account_id] Run tasks that are available, if agent is registered and has balance + croncat daemon [near_env] Generate a network specific croncat daemon service ``` ## Docker Installation & Setup @@ -86,6 +87,8 @@ AGENT_ACCOUNT_ID=YOUR_ACCOUNT.near AGENT_MIN_TASK_BALANCE=1 # When balance is empty, will auto-withdraw rewards to cover signing txns, withdraws the payout account. AGENT_AUTO_REFILL=true +# Helpful if your agent gets kicked after being inactive for any reason. Will attempt to re-register and become a pending agent upon next start. +AGENT_AUTO_RE_REGISTER=false # The interval to wait between checking for tasks. Good intervals are below 60 seconds and above 10 seconds. WAIT_INTERVAL_MS=450000 @@ -97,6 +100,43 @@ SLACK_CHANNEL=general # If you have an external heartbeat service that just needs a ping (GET request) HEARTBEAT=false HEARTBEAT_URL=GET_REQUEST_URL_FOR_STATUS_SERVICE + +## ------------------------------------------------------------------- +## RPC Providers +## Configure the following as CSV, in priority order, for RPC Failover +## ------------------------------------------------------------------- +# Example: RPC_MAINNET_PROVIDERS="https://rpc.mainnet.near.org,http://localhost:3030" +RPC_MAINNET_PROVIDERS="https://mainnet-rpc.openshards.io,https://rpc.mainnet.near.org" +RPC_TESTNET_PROVIDERS="https://rpc.testnet.near.org,https://testnet-rpc.openshards.io" +RPC_GUILDNET_PROVIDERS="https://rpc.openshards.io" +RPC_BETANET_PROVIDERS="https://rpc.betanet.near.org" + +## RPC API KEY for providers that require it +RPC_API_KEY= +``` + +## Croncat Agent DAEMON + +To setup an agent that has auto reboot capability, do the following steps: + +```bash +# 1. create a service file via daemon command: Example for guildnet, use your desired network +croncat daemon guildnet + +# 2. create the service symlink and then enable the service +sudo systemctl link ~/croncat/testnet/croncat_testnet.service +sudo systemctl enable croncat_testnet.service + +# 3. reload systemctl +sudo systemctl daemon-reload + +# 4. start the service +sudo systemctl start croncat_testnet.service + +# 5. for accessing logs, you can use these commands, just make sure to use the right network name +journalctl -f -u croncat_testnet.service +tail -f /var/log/croncat_testnet.log +tail -f /var/log/croncaterror.log ``` ## Development & Local Testing diff --git a/bin/croncat.js b/bin/croncat.js index b1899f2..7354396 100644 --- a/bin/croncat.js +++ b/bin/croncat.js @@ -3,6 +3,7 @@ const chalk = require('chalk') const yargs = require('yargs') import { utils } from 'near-api-js' import getConfig from '../src/configuration' +import { createDaemonFile } from '../src/createSystemctl' const { agentFunction, bootstrapAgent, runAgentTick, registerAgent } = require('../src/actions') const AGENT_ACCOUNT_ID = process.env.AGENT_ACCOUNT_ID @@ -117,6 +118,21 @@ const go = { } }; +const daemon = { + command: 'daemon [near_env]', + desc: 'Generate a network specific croncat daemon service', + builder: (yargs) => yargs + .option('near_env', { + desc: 'NEAR_ENV', + type: 'string', + required: false + }), + handler: async options => { + const env = options.near_env || 'testnet' + await createDaemonFile(env) + } +}; + const config = getConfig(process.env.NODE_ENV || 'development') yargs // eslint-disable-line .strict() @@ -159,6 +175,7 @@ yargs // eslint-disable-line .command(status) .command(tasks) .command(go) + .command(daemon) .config(config) .showHelpOnFail(true) .recommendCommands() diff --git a/package.json b/package.json index f144075..a1f7d4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "croncat", - "version": "1.6.1", + "version": "1.6.3", "description": "cron.cat CLI and Agent Runner", "main": "src/index.js", "scripts": { diff --git a/src/actions.js b/src/actions.js index 7e852d8..0701c28 100644 --- a/src/actions.js +++ b/src/actions.js @@ -15,15 +15,17 @@ export const WAIT_INTERVAL_MS = process.env.WAIT_INTERVAL_MS ? parseInt(`${proce export const AGENT_ACCOUNT_ID = process.env.AGENT_ACCOUNT_ID || 'croncat-agent' export const AGENT_MIN_TASK_BALANCE = utils.format.parseNearAmount(`${process.env.AGENT_MIN_TASK_BALANCE || '1'}`) // Default: 1_000_000_000_000_000_000_000_000 (1 NEAR) export const AGENT_AUTO_REFILL = process.env.AGENT_AUTO_REFILL === 'true' ? true : false +export const AGENT_AUTO_RE_REGISTER = process.env.AGENT_AUTO_RE_REGISTER === 'true' ? true : false export const BASE_GAS_FEE = 300000000000000 export const BASE_ATTACHED_PAYMENT = 0 export const BASE_REGISTER_AGENT_FEE = '4840000000000000000000' let agentSettings = {} let croncatSettings = {} -const slackProvider = new slack({ 'slackToken': process.env.SLACK_TOKEN }) +const slackToken = process.env.SLACK_TOKEN || null +const slackProvider = new slack({ slackToken }) const notifySlack = text => { - if (process.env.SLACK_TOKEN) return slackProvider.send({ + if (slackToken) return slackProvider.send({ slackChannel: process.env.SLACK_CHANNEL, text }) @@ -391,7 +393,9 @@ export async function bootstrapAgent(agentId, options) { process.exit(0); } } catch (e) { - log(`No Agent: ${chalk.gray('Please register')}`) - // await registerAgent(agentId) + if (AGENT_AUTO_RE_REGISTER) { + log(`No Agent: ${chalk.gray('Attempting to register...')}`) + await registerAgent(agentId) + } else log(`No Agent: ${chalk.gray('Please register')}`) } } \ No newline at end of file diff --git a/src/configuration.js b/src/configuration.js index e76343e..1ec10a6 100644 --- a/src/configuration.js +++ b/src/configuration.js @@ -1,23 +1,31 @@ require('dotenv').config() -export const RPC_MAINNET = process.env.RPC_MAINNET_PROVIDERS ? process.env.RPC_MAINNET_PROVIDERS.split(',') : 'https://rpc.mainnet.near.org' -export const RPC_TESTNET = process.env.RPC_TESTNET_PROVIDERS ? process.env.RPC_TESTNET_PROVIDERS.split(',') : 'https://rpc.testnet.near.org' -export const RPC_GUILDNET = process.env.RPC_GUILDNET_PROVIDERS ? process.env.RPC_GUILDNET_PROVIDERS.split(',') : 'https://rpc.openshards.io' -export const RPC_BETANET = process.env.RPC_BETANET_PROVIDERS ? process.env.RPC_BETANET_PROVIDERS.split(',') : 'https://rpc.betanet.near.org' +export const RPC_MAINNET = process.env.RPC_MAINNET_PROVIDERS ? process.env.RPC_MAINNET_PROVIDERS.split(',') : ['https://rpc.mainnet.near.org'] +export const RPC_TESTNET = process.env.RPC_TESTNET_PROVIDERS ? process.env.RPC_TESTNET_PROVIDERS.split(',') : ['https://rpc.testnet.near.org'] +export const RPC_GUILDNET = process.env.RPC_GUILDNET_PROVIDERS ? process.env.RPC_GUILDNET_PROVIDERS.split(',') : ['https://rpc.openshards.io'] +export const RPC_BETANET = process.env.RPC_BETANET_PROVIDERS ? process.env.RPC_BETANET_PROVIDERS.split(',') : ['https://rpc.betanet.near.org'] +export const RPC_API_KEY = process.env.RPC_API_KEY ? process.env.RPC_API_KEY : null -const failoverRpcs = { +const rpcs = { mainnet: RPC_MAINNET, testnet: RPC_TESTNET, guildnet: RPC_GUILDNET, betanet: RPC_BETANET, } +const headers = {} +if (RPC_API_KEY) headers['x-api-key'] = RPC_API_KEY + +// allows configuration with defaults +const getRpcByNetworkId = id => { + return rpcs[id] && rpcs[id].length > 1 ? rpcs[id][0] : rpcs[id] +} + function getConfigByType(networkId, config) { return { - // Cache of available RPC nodes for failover - // rpcNodes: failoverRpcs[networkId] || [], networkId, - nodeUrl: networkId !== 'guildnet' ? `https://rpc.${networkId}.near.org` : 'https://rpc.openshards.io', + headers, + nodeUrl: getRpcByNetworkId(networkId), explorerUrl: `https://explorer.${networkId === 'mainnet' ? '' : networkId + '.'}near.org`, walletUrl: `https://wallet.${networkId === 'mainnet' ? '' : networkId + '.'}near.org`, helperUrl: `https://helper.${networkId}.near.org`, diff --git a/src/createSystemctl.js b/src/createSystemctl.js new file mode 100644 index 0000000..75f4f56 --- /dev/null +++ b/src/createSystemctl.js @@ -0,0 +1,36 @@ +const path = require("path"); +const { writeFileSync } = require("fs"); + +const generateDaemon = (env = 'testnet', user = 'near') => { + return `Description=CronCat ${env.toUpperCase()} Agent +After=multi-user.target + +[Service] +Type=simple +User=${user} +WorkingDirectory=/home/${user}/croncat/${env} +ExecStart=/usr/bin/croncat go +StandardOutput=append:/var/log/croncat_${env}.log +StandardError=append:/var/log/croncat_${env}error.log +Restart=on-failure +RestartSec=60 +KillSignal=SIGINT +TimeoutStopSec=45 +KillMode=mixed + +[Install] +WantedBy=multi-user.target` +} + +export const createDaemonFile = async (env) => { + const _env = env || process.env.NEAR_ENV || 'testnet' + const user = require("os").userInfo().username + const daemon = generateDaemon(_env, user) + + await writeFileSync(path.join(process.cwd(), `croncat_${_env}.service`), daemon) +} + +// // NOTE: for testing +// ;(async () => { +// await createDaemonFile() +// })() \ No newline at end of file diff --git a/src/slack.js b/src/slack.js index 3087971..3a7ec94 100644 --- a/src/slack.js +++ b/src/slack.js @@ -7,7 +7,7 @@ class Slack { } getHookUrl(options) { - if (!options || !options.slackToken) return + if (!options || !options.slackToken && !this.slackToken) return const id = options.slackToken || this.slackToken return `https://hooks.slack.com/services/${id}` }